diff --git a/src/lib/json-ui/component-renderer.tsx b/src/lib/json-ui/component-renderer.tsx index 672539c..6a10850 100644 --- a/src/lib/json-ui/component-renderer.tsx +++ b/src/lib/json-ui/component-renderer.tsx @@ -205,6 +205,11 @@ export function ComponentRenderer({ component, data, context = {}, state, onEven return null } + const resolvedChildren = component.children ?? resolvedProps.children + if (resolvedChildren !== undefined && resolvedChildren !== component.children) { + delete resolvedProps.children + } + if (component.loop) { const items = resolveDataBinding(component.loop.source, data, context, { state, bindings: context }) || [] const loopChildren = items.map((item: unknown, index: number) => { @@ -232,7 +237,7 @@ export function ComponentRenderer({ component, data, context = {}, state, onEven return ( - {renderChildren(component.children, loopContext)} + {renderChildren(resolvedChildren as UIComponent[] | string | undefined, loopContext)} ) }) @@ -254,5 +259,9 @@ export function ComponentRenderer({ component, data, context = {}, state, onEven } } - return createElement(Component, resolvedProps, renderChildren(component.children, context)) + return createElement( + Component, + resolvedProps, + renderChildren(resolvedChildren as UIComponent[] | string | undefined, context) + ) } diff --git a/src/lib/json-ui/wrappers/ComponentBindingDialogWrapper.tsx b/src/lib/json-ui/wrappers/ComponentBindingDialogWrapper.tsx index d741482..1adfe31 100644 --- a/src/lib/json-ui/wrappers/ComponentBindingDialogWrapper.tsx +++ b/src/lib/json-ui/wrappers/ComponentBindingDialogWrapper.tsx @@ -1,8 +1,7 @@ -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' +import type { ChangeEvent } from 'react' +import { ComponentRenderer } from '@/lib/json-ui/component-renderer' import { cn } from '@/lib/utils' +import { componentBindingDialogDefinition } from './definitions' import type { ComponentBindingDialogWrapperProps } from './interfaces' export function ComponentBindingDialogWrapper({ @@ -18,58 +17,31 @@ export function ComponentBindingDialogWrapper({ onOpenChange, className, }: ComponentBindingDialogWrapperProps) { + const handleBindingFieldChange = (event: ChangeEvent) => { + const fieldId = event.currentTarget?.dataset?.fieldId || event.target?.dataset?.fieldId + if (!fieldId) return + onBindingChange?.(fieldId, event.target.value) + } + return ( - - - - {title} - {description} - - - {(componentType || componentId) && ( - - {componentType && ( - - Component: - {componentType} - - )} - {componentId && ( - - ID: - {componentId} - - )} - - )} - - - {bindings.length === 0 ? ( - No bindings configured. - ) : ( - bindings.map((binding) => ( - - {binding.label} - onBindingChange?.(binding.id, event.target.value)} - /> - - )) - )} - - - - - Cancel - - - Save - - - - + ) } diff --git a/src/lib/json-ui/wrappers/DataSourceEditorDialogWrapper.tsx b/src/lib/json-ui/wrappers/DataSourceEditorDialogWrapper.tsx index eedd4bb..6bab82e 100644 --- a/src/lib/json-ui/wrappers/DataSourceEditorDialogWrapper.tsx +++ b/src/lib/json-ui/wrappers/DataSourceEditorDialogWrapper.tsx @@ -1,8 +1,7 @@ -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' +import type { ChangeEvent } from 'react' +import { ComponentRenderer } from '@/lib/json-ui/component-renderer' import { cn } from '@/lib/utils' +import { dataSourceEditorDialogDefinition } from './definitions' import type { DataSourceEditorDialogWrapperProps } from './interfaces' export function DataSourceEditorDialogWrapper({ @@ -16,44 +15,29 @@ export function DataSourceEditorDialogWrapper({ onOpenChange, className, }: DataSourceEditorDialogWrapperProps) { + const handleFieldChange = (event: ChangeEvent) => { + const fieldId = event.currentTarget?.dataset?.fieldId || event.target?.dataset?.fieldId + if (!fieldId) return + onFieldChange?.(fieldId, event.target.value) + } + return ( - - - - {title} - {description} - - - - {fields.length === 0 ? ( - No fields configured. - ) : ( - fields.map((field) => ( - - {field.label} - onFieldChange?.(field.id, event.target.value)} - /> - {field.helperText && ( - {field.helperText} - )} - - )) - )} - - - - - Cancel - - - Save - - - - + ) } diff --git a/src/lib/json-ui/wrappers/GitHubBuildStatusWrapper.tsx b/src/lib/json-ui/wrappers/GitHubBuildStatusWrapper.tsx index 5343004..faee8a0 100644 --- a/src/lib/json-ui/wrappers/GitHubBuildStatusWrapper.tsx +++ b/src/lib/json-ui/wrappers/GitHubBuildStatusWrapper.tsx @@ -1,50 +1,25 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' +import { ComponentRenderer } from '@/lib/json-ui/component-renderer' import { cn } from '@/lib/utils' -import { - ArrowSquareOut, - CheckCircle, - Clock, - WarningCircle, - XCircle, -} from '@phosphor-icons/react' +import { gitHubBuildStatusDefinition } from './definitions' import type { GitHubBuildStatusWrapperProps, GitHubBuildStatusWorkflowItem } from './interfaces' -const getStatusBadge = (workflow: GitHubBuildStatusWorkflowItem) => { +const getWorkflowStatus = (workflow: GitHubBuildStatusWorkflowItem) => { if (workflow.status === 'completed') { if (workflow.conclusion === 'success') { - return Success + return { + label: 'Success', + className: 'bg-green-500/10 text-green-600 border-green-500/20', + } } if (workflow.conclusion === 'failure') { - return Failed + return { label: 'Failed', className: 'bg-red-500/10 text-red-600 border-red-500/20' } } if (workflow.conclusion === 'cancelled') { - return Cancelled + return { label: 'Cancelled', className: 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20' } } } - return ( - - Running - - ) -} - -const getStatusIcon = (workflow: GitHubBuildStatusWorkflowItem) => { - if (workflow.status === 'completed') { - if (workflow.conclusion === 'success') { - return - } - if (workflow.conclusion === 'failure') { - return - } - if (workflow.conclusion === 'cancelled') { - return - } - } - - return + return { label: 'Running', className: 'border-blue-500/50 text-blue-500' } } export function GitHubBuildStatusWrapper({ @@ -58,70 +33,32 @@ export function GitHubBuildStatusWrapper({ footerLinkUrl, className, }: GitHubBuildStatusWrapperProps) { + const normalizedWorkflows = workflows.map((workflow) => { + const status = getWorkflowStatus(workflow) + return { + ...workflow, + statusLabel: status.label, + statusClass: status.className, + summaryLine: [workflow.branch, workflow.updatedAt, workflow.event].filter(Boolean).join(' • '), + } + }) + return ( - - - - - {title} - - {description} - - - {isLoading && Loading workflows…} - - {!isLoading && errorMessage && ( - - - {errorMessage} - - )} - - {!isLoading && !errorMessage && workflows.length === 0 && ( - {emptyMessage} - )} - - {!isLoading && !errorMessage && workflows.length > 0 && ( - - {workflows.map((workflow) => ( - - - {getStatusIcon(workflow)} - - - {workflow.name} - {getStatusBadge(workflow)} - - - {[workflow.branch, workflow.updatedAt, workflow.event] - .filter(Boolean) - .join(' • ')} - - - - {workflow.url && ( - - - - - - )} - - ))} - - )} - - {footerLinkUrl && ( - - - {footerLinkLabel} - - - )} - - + 0, + footerLinkLabel, + footerLinkUrl, + className: cn(className), + }} + /> ) } diff --git a/src/lib/json-ui/wrappers/definitions.ts b/src/lib/json-ui/wrappers/definitions.ts new file mode 100644 index 0000000..1a7d422 --- /dev/null +++ b/src/lib/json-ui/wrappers/definitions.ts @@ -0,0 +1,555 @@ +import type { UIComponent } from '@/types/json-ui' + +export const componentBindingDialogDefinition: UIComponent = { + id: 'component-binding-dialog', + type: 'Dialog', + bindings: { + open: 'open', + onOpenChange: 'onOpenChange', + }, + children: [ + { + id: 'component-binding-dialog-content', + type: 'DialogContent', + bindings: { + className: 'contentClassName', + }, + children: [ + { + id: 'component-binding-dialog-header', + type: 'DialogHeader', + children: [ + { + id: 'component-binding-dialog-title', + type: 'DialogTitle', + bindings: { + children: 'title', + }, + }, + { + id: 'component-binding-dialog-description', + type: 'DialogDescription', + bindings: { + children: 'description', + }, + }, + ], + }, + { + id: 'component-binding-dialog-info', + type: 'div', + props: { + className: 'rounded-md border border-border bg-muted/30 p-3 text-sm', + }, + conditional: { + if: 'componentType || componentId', + }, + children: [ + { + id: 'component-binding-dialog-type', + type: 'div', + props: { + className: 'flex items-center gap-2', + }, + conditional: { + if: 'componentType', + }, + children: [ + { + id: 'component-binding-dialog-type-label', + type: 'span', + props: { + className: 'text-muted-foreground', + }, + children: 'Component:', + }, + { + id: 'component-binding-dialog-type-value', + type: 'span', + props: { + className: 'font-mono font-medium', + }, + bindings: { + children: 'componentType', + }, + }, + ], + }, + { + id: 'component-binding-dialog-id', + type: 'div', + props: { + className: 'flex items-center gap-2', + }, + conditional: { + if: 'componentId', + }, + children: [ + { + id: 'component-binding-dialog-id-label', + type: 'span', + props: { + className: 'text-muted-foreground', + }, + children: 'ID:', + }, + { + id: 'component-binding-dialog-id-value', + type: 'span', + props: { + className: 'font-mono text-xs', + }, + bindings: { + children: 'componentId', + }, + }, + ], + }, + ], + }, + { + id: 'component-binding-dialog-body', + type: 'div', + props: { + className: 'space-y-4', + }, + children: [ + { + id: 'component-binding-dialog-empty', + type: 'p', + props: { + className: 'text-sm text-muted-foreground', + }, + bindings: { + children: 'emptyMessage', + }, + conditional: { + if: '!bindingFields || bindingFields.length === 0', + }, + }, + { + id: 'component-binding-dialog-fields', + type: 'div', + props: { + className: 'space-y-4', + }, + conditional: { + if: 'bindingFields && bindingFields.length > 0', + }, + loop: { + source: 'bindingFields', + itemVar: 'field', + }, + children: [ + { + id: 'component-binding-dialog-field', + type: 'div', + props: { + className: 'space-y-2', + }, + children: [ + { + id: 'component-binding-dialog-field-label', + type: 'Label', + bindings: { + children: 'field.label', + }, + }, + { + id: 'component-binding-dialog-field-input', + type: 'Input', + bindings: { + value: 'field.value', + placeholder: 'field.placeholder', + onChange: 'onBindingFieldChange', + 'data-field-id': 'field.id', + }, + }, + ], + }, + ], + }, + ], + }, + { + id: 'component-binding-dialog-footer', + type: 'DialogFooter', + children: [ + { + id: 'component-binding-dialog-cancel', + type: 'Button', + props: { + variant: 'outline', + }, + bindings: { + onClick: 'onCancel', + children: 'cancelLabel', + }, + }, + { + id: 'component-binding-dialog-save', + type: 'Button', + bindings: { + onClick: 'onSave', + children: 'saveLabel', + }, + }, + ], + }, + ], + }, + ], +} + +export const dataSourceEditorDialogDefinition: UIComponent = { + id: 'data-source-editor-dialog', + type: 'Dialog', + bindings: { + open: 'open', + onOpenChange: 'onOpenChange', + }, + children: [ + { + id: 'data-source-editor-dialog-content', + type: 'DialogContent', + bindings: { + className: 'contentClassName', + }, + children: [ + { + id: 'data-source-editor-dialog-header', + type: 'DialogHeader', + children: [ + { + id: 'data-source-editor-dialog-title', + type: 'DialogTitle', + bindings: { + children: 'title', + }, + }, + { + id: 'data-source-editor-dialog-description', + type: 'DialogDescription', + bindings: { + children: 'description', + }, + }, + ], + }, + { + id: 'data-source-editor-dialog-body', + type: 'div', + props: { + className: 'space-y-4', + }, + children: [ + { + id: 'data-source-editor-dialog-empty', + type: 'p', + props: { + className: 'text-sm text-muted-foreground', + }, + bindings: { + children: 'emptyMessage', + }, + conditional: { + if: '!fields || fields.length === 0', + }, + }, + { + id: 'data-source-editor-dialog-fields', + type: 'div', + props: { + className: 'space-y-4', + }, + conditional: { + if: 'fields && fields.length > 0', + }, + loop: { + source: 'fields', + itemVar: 'field', + }, + children: [ + { + id: 'data-source-editor-dialog-field', + type: 'div', + props: { + className: 'space-y-2', + }, + children: [ + { + id: 'data-source-editor-dialog-field-label', + type: 'Label', + bindings: { + children: 'field.label', + }, + }, + { + id: 'data-source-editor-dialog-field-input', + type: 'Input', + bindings: { + value: 'field.value', + placeholder: 'field.placeholder', + onChange: 'onFieldChange', + 'data-field-id': 'field.id', + }, + }, + { + id: 'data-source-editor-dialog-field-helper', + type: 'p', + props: { + className: 'text-xs text-muted-foreground', + }, + bindings: { + children: 'field.helperText', + }, + conditional: { + if: 'field.helperText', + }, + }, + ], + }, + ], + }, + ], + }, + { + id: 'data-source-editor-dialog-footer', + type: 'DialogFooter', + children: [ + { + id: 'data-source-editor-dialog-cancel', + type: 'Button', + props: { + variant: 'outline', + }, + bindings: { + onClick: 'onCancel', + children: 'cancelLabel', + }, + }, + { + id: 'data-source-editor-dialog-save', + type: 'Button', + bindings: { + onClick: 'onSave', + children: 'saveLabel', + }, + }, + ], + }, + ], + }, + ], +} + +export const gitHubBuildStatusDefinition: UIComponent = { + id: 'github-build-status-card', + type: 'Card', + bindings: { + className: 'className', + }, + children: [ + { + id: 'github-build-status-header', + type: 'CardHeader', + children: [ + { + id: 'github-build-status-title', + type: 'CardTitle', + props: { + className: 'flex items-center gap-2', + }, + bindings: { + children: 'title', + }, + }, + { + id: 'github-build-status-description', + type: 'CardDescription', + bindings: { + children: 'description', + }, + }, + ], + }, + { + id: 'github-build-status-content', + type: 'CardContent', + props: { + className: 'space-y-4', + }, + children: [ + { + id: 'github-build-status-loading', + type: 'p', + props: { + className: 'text-sm text-muted-foreground', + }, + bindings: { + children: 'loadingMessage', + }, + conditional: { + if: 'isLoading', + }, + }, + { + id: 'github-build-status-error', + type: 'div', + props: { + className: 'flex items-center gap-2 text-sm text-red-500', + }, + conditional: { + if: 'errorMessage', + }, + children: [ + { + id: 'github-build-status-error-text', + type: 'span', + bindings: { + children: 'errorMessage', + }, + }, + ], + }, + { + id: 'github-build-status-empty', + type: 'p', + props: { + className: 'text-sm text-muted-foreground', + }, + bindings: { + children: 'emptyMessage', + }, + conditional: { + if: '!isLoading && !errorMessage && !hasWorkflows', + }, + }, + { + id: 'github-build-status-list', + type: 'div', + props: { + className: 'space-y-3', + }, + conditional: { + if: 'hasWorkflows', + }, + loop: { + source: 'workflows', + itemVar: 'workflow', + }, + children: [ + { + id: 'github-build-status-item', + type: 'div', + props: { + className: 'flex items-center justify-between gap-3 rounded-lg border border-border p-3', + }, + children: [ + { + id: 'github-build-status-item-info', + type: 'div', + props: { + className: 'min-w-0', + }, + children: [ + { + id: 'github-build-status-item-row', + type: 'div', + props: { + className: 'flex items-center gap-2', + }, + children: [ + { + id: 'github-build-status-item-name', + type: 'p', + props: { + className: 'text-sm font-medium truncate', + }, + bindings: { + children: 'workflow.name', + }, + }, + { + id: 'github-build-status-item-badge', + type: 'Badge', + bindings: { + className: 'workflow.statusClass', + children: 'workflow.statusLabel', + }, + }, + ], + }, + { + id: 'github-build-status-item-meta', + type: 'div', + props: { + className: 'text-xs text-muted-foreground truncate', + }, + bindings: { + children: 'workflow.summaryLine', + }, + }, + ], + }, + { + id: 'github-build-status-item-link', + type: 'Button', + props: { + variant: 'ghost', + size: 'sm', + asChild: true, + }, + conditional: { + if: 'workflow.url', + }, + children: [ + { + id: 'github-build-status-item-anchor', + type: 'a', + bindings: { + href: 'workflow.url', + }, + props: { + target: '_blank', + rel: 'noopener noreferrer', + }, + children: 'View', + }, + ], + }, + ], + }, + ], + }, + { + id: 'github-build-status-footer', + type: 'Button', + props: { + variant: 'outline', + size: 'sm', + asChild: true, + className: 'w-full', + }, + conditional: { + if: 'footerLinkUrl', + }, + children: [ + { + id: 'github-build-status-footer-anchor', + type: 'a', + bindings: { + href: 'footerLinkUrl', + children: 'footerLinkLabel', + }, + props: { + target: '_blank', + rel: 'noopener noreferrer', + }, + }, + ], + }, + ], + }, + ], +}
No bindings configured.
No fields configured.
{field.helperText}
Loading workflows…
{emptyMessage}
{workflow.name}