Compare commits

..

1 Commits

Author SHA1 Message Date
52b27dd00c Load wrappers and icons from JSON registry 2026-01-18 18:16:20 +00:00
5 changed files with 295 additions and 278 deletions

View File

@@ -80,7 +80,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "ComponentBindingDialog"
"wrapperFor": "ComponentBindingDialog",
"loadFrom": {
"module": "wrappers",
"export": "ComponentBindingDialogWrapper"
}
},
{
"type": "Container",
@@ -122,7 +126,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "DataSourceEditorDialog"
"wrapperFor": "DataSourceEditorDialog",
"loadFrom": {
"module": "wrappers",
"export": "DataSourceEditorDialogWrapper"
}
},
{
"type": "Dialog",
@@ -724,7 +732,11 @@
"canHaveChildren": false,
"description": "ArrowLeft icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "ArrowLeft"
}
},
{
"type": "ArrowRight",
@@ -733,7 +745,11 @@
"canHaveChildren": false,
"description": "ArrowRight icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "ArrowRight"
}
},
{
"type": "Check",
@@ -742,7 +758,11 @@
"canHaveChildren": false,
"description": "Check icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Check"
}
},
{
"type": "X",
@@ -751,7 +771,11 @@
"canHaveChildren": false,
"description": "X icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "X"
}
},
{
"type": "Plus",
@@ -760,7 +784,11 @@
"canHaveChildren": false,
"description": "Plus icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Plus"
}
},
{
"type": "Minus",
@@ -769,7 +797,11 @@
"canHaveChildren": false,
"description": "Minus icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Minus"
}
},
{
"type": "Search",
@@ -778,7 +810,11 @@
"canHaveChildren": false,
"description": "Search icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "MagnifyingGlass"
}
},
{
"type": "Filter",
@@ -787,7 +823,11 @@
"canHaveChildren": false,
"description": "Filter icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Funnel"
}
},
{
"type": "Download",
@@ -796,7 +836,11 @@
"canHaveChildren": false,
"description": "Download icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Download"
}
},
{
"type": "Upload",
@@ -805,7 +849,11 @@
"canHaveChildren": false,
"description": "Upload icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Upload"
}
},
{
"type": "Edit",
@@ -814,7 +862,11 @@
"canHaveChildren": false,
"description": "Edit icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "PencilSimple"
}
},
{
"type": "Trash",
@@ -823,7 +875,11 @@
"canHaveChildren": false,
"description": "Trash icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Trash"
}
},
{
"type": "Eye",
@@ -832,7 +888,11 @@
"canHaveChildren": false,
"description": "Eye icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Eye"
}
},
{
"type": "EyeOff",
@@ -841,7 +901,11 @@
"canHaveChildren": false,
"description": "EyeOff icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "EyeClosed"
}
},
{
"type": "ChevronUp",
@@ -850,7 +914,11 @@
"canHaveChildren": false,
"description": "ChevronUp icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "CaretUp"
}
},
{
"type": "ChevronDown",
@@ -859,7 +927,11 @@
"canHaveChildren": false,
"description": "ChevronDown icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "CaretDown"
}
},
{
"type": "ChevronLeft",
@@ -868,7 +940,11 @@
"canHaveChildren": false,
"description": "ChevronLeft icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "CaretLeft"
}
},
{
"type": "ChevronRight",
@@ -877,7 +953,11 @@
"canHaveChildren": false,
"description": "ChevronRight icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "CaretRight"
}
},
{
"type": "Settings",
@@ -886,7 +966,11 @@
"canHaveChildren": false,
"description": "Settings icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Gear"
}
},
{
"type": "User",
@@ -895,7 +979,11 @@
"canHaveChildren": false,
"description": "User icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "User"
}
},
{
"type": "Bell",
@@ -904,7 +992,11 @@
"canHaveChildren": false,
"description": "Bell icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Bell"
}
},
{
"type": "Mail",
@@ -913,7 +1005,11 @@
"canHaveChildren": false,
"description": "Mail icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Envelope"
}
},
{
"type": "Calendar",
@@ -922,7 +1018,11 @@
"canHaveChildren": false,
"description": "Calendar icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Calendar"
}
},
{
"type": "Clock",
@@ -931,7 +1031,11 @@
"canHaveChildren": false,
"description": "Clock icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Clock"
}
},
{
"type": "Star",
@@ -940,7 +1044,11 @@
"canHaveChildren": false,
"description": "Star icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Star"
}
},
{
"type": "Heart",
@@ -949,7 +1057,11 @@
"canHaveChildren": false,
"description": "Heart icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Heart"
}
},
{
"type": "Share",
@@ -958,7 +1070,11 @@
"canHaveChildren": false,
"description": "Share icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "ShareNetwork"
}
},
{
"type": "Link",
@@ -967,7 +1083,11 @@
"canHaveChildren": false,
"description": "Link icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "LinkSimple"
}
},
{
"type": "Copy",
@@ -976,7 +1096,11 @@
"canHaveChildren": false,
"description": "Copy icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Copy"
}
},
{
"type": "Save",
@@ -985,7 +1109,11 @@
"canHaveChildren": false,
"description": "Save icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "FloppyDisk"
}
},
{
"type": "RefreshCw",
@@ -994,7 +1122,11 @@
"canHaveChildren": false,
"description": "RefreshCw icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "ArrowClockwise"
}
},
{
"type": "AlertCircle",
@@ -1003,7 +1135,11 @@
"canHaveChildren": false,
"description": "AlertCircle icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "WarningCircle"
}
},
{
"type": "Info",
@@ -1012,7 +1148,11 @@
"canHaveChildren": false,
"description": "Info icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Info"
}
},
{
"type": "HelpCircle",
@@ -1021,7 +1161,11 @@
"canHaveChildren": false,
"description": "HelpCircle icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "Question"
}
},
{
"type": "Home",
@@ -1030,7 +1174,11 @@
"canHaveChildren": false,
"description": "Home icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "House"
}
},
{
"type": "Menu",
@@ -1039,7 +1187,11 @@
"canHaveChildren": false,
"description": "Menu icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "List"
}
},
{
"type": "MoreVertical",
@@ -1048,7 +1200,11 @@
"canHaveChildren": false,
"description": "MoreVertical icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "DotsThreeVertical"
}
},
{
"type": "MoreHorizontal",
@@ -1057,7 +1213,11 @@
"canHaveChildren": false,
"description": "MoreHorizontal icon",
"status": "supported",
"source": "icons"
"source": "icons",
"loadFrom": {
"module": "icons",
"export": "DotsThree"
}
},
{
"type": "Breadcrumb",
@@ -1275,7 +1435,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "GitHubBuildStatus"
"wrapperFor": "GitHubBuildStatus",
"loadFrom": {
"module": "wrappers",
"export": "GitHubBuildStatusWrapper"
}
},
{
"type": "InfoBox",
@@ -1437,7 +1601,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "LazyBarChart"
"wrapperFor": "LazyBarChart",
"loadFrom": {
"module": "wrappers",
"export": "LazyBarChartWrapper"
}
},
{
"type": "LazyD3BarChart",
@@ -1460,7 +1628,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "LazyD3BarChart"
"wrapperFor": "LazyD3BarChart",
"loadFrom": {
"module": "wrappers",
"export": "LazyD3BarChartWrapper"
}
},
{
"type": "LazyLineChart",
@@ -1483,7 +1655,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "LazyLineChart"
"wrapperFor": "LazyLineChart",
"loadFrom": {
"module": "wrappers",
"export": "LazyLineChartWrapper"
}
},
{
"type": "List",
@@ -1542,7 +1718,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "SeedDataManager"
"wrapperFor": "SeedDataManager",
"loadFrom": {
"module": "wrappers",
"export": "SeedDataManagerWrapper"
}
},
{
"type": "StatCard",
@@ -1829,7 +2009,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "ComponentTree"
"wrapperFor": "ComponentTree",
"loadFrom": {
"module": "wrappers",
"export": "ComponentTreeWrapper"
}
},
{
"type": "ComponentTreeNode",
@@ -2072,7 +2256,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "SaveIndicator"
"wrapperFor": "SaveIndicator",
"loadFrom": {
"module": "wrappers",
"export": "SaveIndicatorWrapper"
}
},
{
"type": "SchemaEditorCanvas",
@@ -2236,7 +2424,11 @@
"status": "json-compatible",
"source": "wrappers",
"jsonCompatible": true,
"wrapperFor": "StorageSettings"
"wrapperFor": "StorageSettings",
"loadFrom": {
"module": "wrappers",
"export": "StorageSettingsWrapper"
}
},
{
"type": "Timestamp",

View File

@@ -73,6 +73,18 @@
"wrapperFor": {
"type": "string"
},
"loadFrom": {
"type": "object",
"properties": {
"module": {
"type": "string"
},
"export": {
"type": "string"
}
},
"additionalProperties": false
},
"deprecated": {
"type": "object",
"properties": {

View File

@@ -37,27 +37,9 @@ import { CircularProgress, Divider, ProgressBar } from '@/components/atoms'
import * as AtomComponents from '@/components/atoms'
import * as MoleculeComponents from '@/components/molecules'
import * as OrganismComponents from '@/components/organisms'
import {
ComponentBindingDialogWrapper,
ComponentTreeWrapper,
DataSourceEditorDialogWrapper,
GitHubBuildStatusWrapper,
LazyBarChartWrapper,
LazyD3BarChartWrapper,
LazyLineChartWrapper,
SaveIndicatorWrapper,
SeedDataManagerWrapper,
StorageSettingsWrapper,
} from '@/lib/json-ui/wrappers'
import * as WrapperComponents from '@/lib/json-ui/wrappers'
import jsonComponentsRegistry from '../../../json-components-registry.json'
import {
ArrowLeft, ArrowRight, Check, X, Plus, Minus, MagnifyingGlass,
Funnel, Download, Upload, PencilSimple, Trash, Eye, EyeClosed,
CaretUp, CaretDown, CaretLeft, CaretRight,
Gear, User, Bell, Envelope, Calendar, Clock, Star,
Heart, ShareNetwork, LinkSimple, Copy, FloppyDisk, ArrowClockwise, WarningCircle,
Info, Question, House, List as ListIcon, DotsThreeVertical, DotsThree
} from '@phosphor-icons/react'
import * as IconComponents from '@phosphor-icons/react'
export interface UIComponentRegistry {
[key: string]: ComponentType<any>
@@ -72,6 +54,10 @@ interface JsonRegistryEntry {
wrapperRequired?: boolean
wrapperComponent?: string
wrapperFor?: string
loadFrom?: {
module?: string
export?: string
}
deprecated?: DeprecatedComponentInfo
}
@@ -85,9 +71,13 @@ export interface DeprecatedComponentInfo {
}
const jsonRegistry = jsonComponentsRegistry as JsonComponentRegistry
const componentLoaders: Record<string, Record<string, ComponentType<any>>> = {
wrappers: WrapperComponents as Record<string, ComponentType<any>>,
icons: IconComponents as Record<string, ComponentType<any>>,
}
const getRegistryEntryName = (entry: JsonRegistryEntry): string | undefined =>
entry.export ?? entry.name ?? entry.type
entry.name ?? entry.type ?? entry.export
const buildRegistryFromNames = (
names: string[],
@@ -102,6 +92,27 @@ const buildRegistryFromNames = (
}, {})
}
const resolveLoadedComponent = (entry: JsonRegistryEntry): ComponentType<any> | null => {
const moduleKey = entry.loadFrom?.module ?? entry.source
const exportName = entry.loadFrom?.export ?? getRegistryEntryName(entry)
if (!moduleKey || !exportName) {
return null
}
const moduleComponents = componentLoaders[moduleKey]
return moduleComponents?.[exportName] ?? null
}
const buildRegistryFromEntries = (entries: JsonRegistryEntry[]): UIComponentRegistry => {
return entries.reduce<UIComponentRegistry>((registry, entry) => {
const registryName = getRegistryEntryName(entry)
const component = resolveLoadedComponent(entry)
if (registryName && component) {
registry[registryName] = component
}
return registry
}, {})
}
const jsonRegistryEntries = jsonRegistry.components ?? []
const registryEntryByType = new Map(
jsonRegistryEntries
@@ -141,14 +152,8 @@ const shadcnRegistryNames = jsonRegistryEntries
.filter((entry) => entry.source === 'ui')
.map((entry) => getRegistryEntryName(entry))
.filter((name): name is string => Boolean(name))
const wrapperRegistryNames = jsonRegistryEntries
.filter((entry) => entry.source === 'wrappers')
.map((entry) => getRegistryEntryName(entry))
.filter((name): name is string => Boolean(name))
const iconRegistryNames = jsonRegistryEntries
.filter((entry) => entry.source === 'icons')
.map((entry) => getRegistryEntryName(entry))
.filter((name): name is string => Boolean(name))
const wrapperRegistryEntries = jsonRegistryEntries.filter((entry) => entry.source === 'wrappers')
const iconRegistryEntries = jsonRegistryEntries.filter((entry) => entry.source === 'icons')
export const primitiveComponents: UIComponentRegistry = {
div: 'div' as any,
@@ -275,69 +280,11 @@ export const organismComponents: UIComponentRegistry = buildRegistryFromNames(
OrganismComponents as Record<string, ComponentType<any>>
)
const wrapperComponentMap: Record<string, ComponentType<any>> = {
ComponentBindingDialogWrapper,
ComponentTreeWrapper,
DataSourceEditorDialogWrapper,
GitHubBuildStatusWrapper,
SaveIndicatorWrapper,
LazyBarChartWrapper,
LazyLineChartWrapper,
LazyD3BarChartWrapper,
SeedDataManagerWrapper,
StorageSettingsWrapper,
}
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromNames(
wrapperRegistryNames,
wrapperComponentMap
export const jsonWrapperComponents: UIComponentRegistry = buildRegistryFromEntries(
wrapperRegistryEntries
)
const iconComponentMap: Record<string, ComponentType<any>> = {
ArrowLeft,
ArrowRight,
Check,
X,
Plus,
Minus,
Search: MagnifyingGlass,
Filter: Funnel,
Download,
Upload,
Edit: PencilSimple,
Trash,
Eye,
EyeOff: EyeClosed,
ChevronUp: CaretUp,
ChevronDown: CaretDown,
ChevronLeft: CaretLeft,
ChevronRight: CaretRight,
Settings: Gear,
User,
Bell,
Mail: Envelope,
Calendar,
Clock,
Star,
Heart,
Share: ShareNetwork,
Link: LinkSimple,
Copy,
Save: FloppyDisk,
RefreshCw: ArrowClockwise,
AlertCircle: WarningCircle,
Info,
HelpCircle: Question,
Home: House,
Menu: ListIcon,
MoreVertical: DotsThreeVertical,
MoreHorizontal: DotsThree,
}
export const iconComponents: UIComponentRegistry = buildRegistryFromNames(
iconRegistryNames,
iconComponentMap
)
export const iconComponents: UIComponentRegistry = buildRegistryFromEntries(iconRegistryEntries)
export const uiComponentRegistry: UIComponentRegistry = {
...primitiveComponents,

View File

@@ -1,112 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AutoSyncManager } from '../autoSyncMiddleware'
import { syncToFlaskBulk } from '../../slices/syncSlice'
vi.mock('../../slices/syncSlice', () => ({
syncToFlaskBulk: vi.fn(() => ({ type: 'sync/syncToFlaskBulk' })),
checkFlaskConnection: vi.fn(() => ({ type: 'sync/checkConnection' })),
}))
type Deferred<T> = {
promise: Promise<T>
resolve: (value: T) => void
reject: (error?: unknown) => void
}
const createDeferred = <T,>(): Deferred<T> => {
let resolve!: (value: T) => void
let reject!: (error?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
describe('AutoSyncManager', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('serializes syncs and runs one pending sync after completion', async () => {
const manager = new AutoSyncManager()
const deferreds = [createDeferred<void>(), createDeferred<void>()]
const dispatch = vi
.fn()
.mockImplementation(() => deferreds.shift()?.promise ?? Promise.resolve())
manager.setDispatch(dispatch)
const firstSync = manager.syncNow()
const secondSync = manager.syncNow()
expect(dispatch).toHaveBeenCalledTimes(1)
deferreds[0].resolve()
await Promise.resolve()
expect(dispatch).toHaveBeenCalledTimes(2)
deferreds[1].resolve()
await firstSync
await secondSync
})
it('resets changeCounter after a successful sync', async () => {
const manager = new AutoSyncManager()
const dispatch = vi.fn().mockResolvedValue(undefined)
manager.setDispatch(dispatch)
manager.trackChange()
manager.trackChange()
expect(manager.getStatus().changeCounter).toBe(2)
await manager.syncNow()
expect(manager.getStatus().changeCounter).toBe(0)
expect(dispatch).toHaveBeenCalledTimes(1)
})
it('coalesces multiple pending sync requests into one run', async () => {
const manager = new AutoSyncManager()
const deferreds = [createDeferred<void>(), createDeferred<void>()]
const dispatch = vi
.fn()
.mockImplementation(() => deferreds.shift()?.promise ?? Promise.resolve())
manager.setDispatch(dispatch)
const firstSync = manager.syncNow()
const secondSync = manager.syncNow()
const thirdSync = manager.syncNow()
expect(dispatch).toHaveBeenCalledTimes(1)
deferreds[0].resolve()
await Promise.resolve()
expect(dispatch).toHaveBeenCalledTimes(2)
deferreds[1].resolve()
await firstSync
await secondSync
await thirdSync
expect(dispatch).toHaveBeenCalledTimes(2)
})
it('dispatches the sync thunk when performing a sync', async () => {
const manager = new AutoSyncManager()
const dispatch = vi.fn().mockResolvedValue(undefined)
manager.setDispatch(dispatch)
await manager.syncNow()
expect(dispatch).toHaveBeenCalledWith(syncToFlaskBulk())
})
})

View File

@@ -9,7 +9,7 @@ interface AutoSyncConfig {
maxQueueSize: number
}
export class AutoSyncManager {
class AutoSyncManager {
private config: AutoSyncConfig = {
enabled: false,
intervalMs: 30000,
@@ -21,8 +21,6 @@ export class AutoSyncManager {
private lastSyncTime = 0
private changeCounter = 0
private dispatch: any = null
private syncInFlight: Promise<void> | null = null
private pendingSync = false
configure(config: Partial<AutoSyncConfig>) {
this.config = { ...this.config, ...config }
@@ -71,32 +69,12 @@ export class AutoSyncManager {
private async performSync() {
if (!this.dispatch) return
if (this.syncInFlight) {
this.pendingSync = true
return
}
const syncPromise = (async () => {
try {
await this.dispatch(syncToFlaskBulk())
this.lastSyncTime = Date.now()
this.changeCounter = 0
} catch (error) {
console.error('[AutoSync] Sync failed:', error)
}
})()
this.syncInFlight = syncPromise
try {
await syncPromise
} finally {
this.syncInFlight = null
}
if (this.pendingSync) {
this.pendingSync = false
await this.performSync()
await this.dispatch(syncToFlaskBulk())
this.lastSyncTime = Date.now()
this.changeCounter = 0
} catch (error) {
console.error('[AutoSync] Sync failed:', error)
}
}