mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Render JSON wrappers from schema definitions
This commit is contained in:
@@ -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 (
|
||||
<Fragment key={`${component.id}-${index}`}>
|
||||
{renderChildren(component.children, loopContext)}
|
||||
{renderChildren(resolvedChildren as UIComponent[] | string | undefined, loopContext)}
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
const fieldId = event.currentTarget?.dataset?.fieldId || event.target?.dataset?.fieldId
|
||||
if (!fieldId) return
|
||||
onBindingChange?.(fieldId, event.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={cn('max-w-2xl', className)}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{(componentType || componentId) && (
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3 text-sm">
|
||||
{componentType && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">Component:</span>
|
||||
<span className="font-mono font-medium">{componentType}</span>
|
||||
</div>
|
||||
)}
|
||||
{componentId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">ID:</span>
|
||||
<span className="font-mono text-xs">{componentId}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{bindings.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No bindings configured.</p>
|
||||
) : (
|
||||
bindings.map((binding) => (
|
||||
<div key={binding.id} className="space-y-2">
|
||||
<Label htmlFor={`binding-${binding.id}`}>{binding.label}</Label>
|
||||
<Input
|
||||
id={`binding-${binding.id}`}
|
||||
value={binding.value ?? ''}
|
||||
placeholder={binding.placeholder}
|
||||
onChange={(event) => onBindingChange?.(binding.id, event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ComponentRenderer
|
||||
component={componentBindingDialogDefinition}
|
||||
data={{
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
componentType,
|
||||
componentId,
|
||||
bindingFields: bindings,
|
||||
emptyMessage: 'No bindings configured.',
|
||||
contentClassName: cn('max-w-2xl', className),
|
||||
onBindingFieldChange: handleBindingFieldChange,
|
||||
onSave,
|
||||
onCancel,
|
||||
onOpenChange,
|
||||
cancelLabel: 'Cancel',
|
||||
saveLabel: 'Save',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
const fieldId = event.currentTarget?.dataset?.fieldId || event.target?.dataset?.fieldId
|
||||
if (!fieldId) return
|
||||
onFieldChange?.(fieldId, event.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={cn('max-w-2xl', className)}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{fields.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No fields configured.</p>
|
||||
) : (
|
||||
fields.map((field) => (
|
||||
<div key={field.id} className="space-y-2">
|
||||
<Label htmlFor={`field-${field.id}`}>{field.label}</Label>
|
||||
<Input
|
||||
id={`field-${field.id}`}
|
||||
value={field.value ?? ''}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(event) => onFieldChange?.(field.id, event.target.value)}
|
||||
/>
|
||||
{field.helperText && (
|
||||
<p className="text-xs text-muted-foreground">{field.helperText}</p>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ComponentRenderer
|
||||
component={dataSourceEditorDialogDefinition}
|
||||
data={{
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
fields,
|
||||
emptyMessage: 'No fields configured.',
|
||||
contentClassName: cn('max-w-2xl', className),
|
||||
onFieldChange: handleFieldChange,
|
||||
onSave,
|
||||
onCancel,
|
||||
onOpenChange,
|
||||
cancelLabel: 'Cancel',
|
||||
saveLabel: 'Save',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 <Badge className="bg-green-500/10 text-green-600 border-green-500/20">Success</Badge>
|
||||
return {
|
||||
label: 'Success',
|
||||
className: 'bg-green-500/10 text-green-600 border-green-500/20',
|
||||
}
|
||||
}
|
||||
if (workflow.conclusion === 'failure') {
|
||||
return <Badge variant="destructive">Failed</Badge>
|
||||
return { label: 'Failed', className: 'bg-red-500/10 text-red-600 border-red-500/20' }
|
||||
}
|
||||
if (workflow.conclusion === 'cancelled') {
|
||||
return <Badge variant="secondary">Cancelled</Badge>
|
||||
return { label: 'Cancelled', className: 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20' }
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className="border-blue-500/50 text-blue-500">
|
||||
Running
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
const getStatusIcon = (workflow: GitHubBuildStatusWorkflowItem) => {
|
||||
if (workflow.status === 'completed') {
|
||||
if (workflow.conclusion === 'success') {
|
||||
return <CheckCircle size={18} className="text-green-500" weight="fill" />
|
||||
}
|
||||
if (workflow.conclusion === 'failure') {
|
||||
return <XCircle size={18} className="text-red-500" weight="fill" />
|
||||
}
|
||||
if (workflow.conclusion === 'cancelled') {
|
||||
return <WarningCircle size={18} className="text-yellow-500" weight="fill" />
|
||||
}
|
||||
}
|
||||
|
||||
return <Clock size={18} className="text-blue-500" weight="duotone" />
|
||||
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 (
|
||||
<Card className={cn(className)}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ArrowSquareOut size={18} weight="duotone" />
|
||||
{title}
|
||||
</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading workflows…</p>}
|
||||
|
||||
{!isLoading && errorMessage && (
|
||||
<div className="flex items-center gap-2 text-sm text-red-500">
|
||||
<WarningCircle size={16} weight="fill" />
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !errorMessage && workflows.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{emptyMessage}</p>
|
||||
)}
|
||||
|
||||
{!isLoading && !errorMessage && workflows.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
{getStatusIcon(workflow)}
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium truncate">{workflow.name}</p>
|
||||
{getStatusBadge(workflow)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{[workflow.branch, workflow.updatedAt, workflow.event]
|
||||
.filter(Boolean)
|
||||
.join(' • ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{workflow.url && (
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<a href={workflow.url} target="_blank" rel="noopener noreferrer">
|
||||
<ArrowSquareOut size={14} />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{footerLinkUrl && (
|
||||
<Button variant="outline" size="sm" asChild className="w-full">
|
||||
<a href={footerLinkUrl} target="_blank" rel="noopener noreferrer">
|
||||
{footerLinkLabel}
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ComponentRenderer
|
||||
component={gitHubBuildStatusDefinition}
|
||||
data={{
|
||||
title,
|
||||
description,
|
||||
workflows: normalizedWorkflows,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
emptyMessage,
|
||||
loadingMessage: 'Loading workflows…',
|
||||
hasWorkflows: normalizedWorkflows.length > 0,
|
||||
footerLinkLabel,
|
||||
footerLinkUrl,
|
||||
className: cn(className),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
555
src/lib/json-ui/wrappers/definitions.ts
Normal file
555
src/lib/json-ui/wrappers/definitions.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user