@dyrected/admin 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +1 -0
- package/dist/admin.css +2 -0
- package/dist/components/auth/auth-gate.d.ts +13 -0
- package/dist/components/error-boundary.d.ts +16 -0
- package/dist/components/forms/field-renderer.d.ts +22 -0
- package/dist/components/forms/fields/block-builder.d.ts +9 -0
- package/dist/components/forms/fields/date-picker.d.ts +8 -0
- package/dist/components/forms/fields/json-editor.d.ts +8 -0
- package/dist/components/forms/fields/media-picker.d.ts +12 -0
- package/dist/components/forms/fields/multi-select.d.ts +19 -0
- package/dist/components/forms/fields/radio-field.d.ts +8 -0
- package/dist/components/forms/fields/relationship-picker.d.ts +10 -0
- package/dist/components/forms/fields/rich-text-editor.d.ts +9 -0
- package/dist/components/forms/fields/select-field.d.ts +8 -0
- package/dist/components/forms/fields/switch-field.d.ts +6 -0
- package/dist/components/forms/fields/text-area-field.d.ts +8 -0
- package/dist/components/forms/fields/text-field.d.ts +8 -0
- package/dist/components/forms/form-engine.d.ts +14 -0
- package/dist/components/forms/form-field-renderer.d.ts +20 -0
- package/dist/components/forms/utils.d.ts +11 -0
- package/dist/components/layout/admin-shell.d.ts +5 -0
- package/dist/components/layout/branding-provider.d.ts +4 -0
- package/dist/components/live-preview/LivePreviewPane.d.ts +7 -0
- package/dist/components/media/focal-point-picker.d.ts +12 -0
- package/dist/components/media/media-card.d.ts +8 -0
- package/dist/components/media/media-grid.d.ts +8 -0
- package/dist/components/media/media-library-dialog.d.ts +11 -0
- package/dist/components/ui/aspect-ratio.d.ts +3 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/calendar.d.ts +8 -0
- package/dist/components/ui/card.d.ts +8 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/command.d.ts +80 -0
- package/dist/components/ui/data-table.d.ts +14 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dropdown-menu.d.ts +27 -0
- package/dist/components/ui/form.d.ts +23 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/page-header.d.ts +10 -0
- package/dist/components/ui/pagination.d.ts +11 -0
- package/dist/components/ui/popover.d.ts +6 -0
- package/dist/components/ui/progress.d.ts +4 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/render-cell.d.ts +8 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/select.d.ts +13 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/sheet.d.ts +25 -0
- package/dist/components/ui/sidebar.d.ts +65 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/sonner.d.ts +4 -0
- package/dist/components/ui/switch.d.ts +4 -0
- package/dist/components/ui/table.d.ts +10 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/toggle.d.ts +12 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-preferences.d.ts +6 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.mjs +69091 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/main.d.ts +0 -0
- package/dist/pages/auth/first-user-page.d.ts +4 -0
- package/dist/pages/auth/login-page.d.ts +4 -0
- package/dist/pages/collections/edit-page.d.ts +1 -0
- package/dist/pages/collections/list-page.d.ts +5 -0
- package/dist/pages/dashboard/dashboard.d.ts +1 -0
- package/dist/pages/globals/editor-page.d.ts +1 -0
- package/dist/pages/media/media-page.d.ts +4 -0
- package/dist/pages/setup/setup-prompt.d.ts +6 -0
- package/dist/providers/dyrected-provider.d.ts +29 -0
- package/dist/providers/query-provider.d.ts +3 -0
- package/package.json +6 -3
- package/CHANGELOG.md +0 -153
- package/components.json +0 -17
- package/eslint.config.js +0 -22
- package/index.html +0 -13
- package/postcss.config.js +0 -6
- package/scripts/prefix-tailwind-precision.py +0 -98
- package/scripts/prefix-tailwind.py +0 -67
- package/src/App.css +0 -184
- package/src/App.tsx +0 -25
- package/src/assets/dyrected.svg +0 -155
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/assets/vite.svg +0 -1
- package/src/components/auth/auth-gate.tsx +0 -64
- package/src/components/error-boundary.tsx +0 -45
- package/src/components/forms/field-renderer.tsx +0 -111
- package/src/components/forms/fields/block-builder.tsx +0 -213
- package/src/components/forms/fields/date-picker.tsx +0 -60
- package/src/components/forms/fields/json-editor.tsx +0 -62
- package/src/components/forms/fields/media-picker.tsx +0 -286
- package/src/components/forms/fields/multi-select.tsx +0 -145
- package/src/components/forms/fields/radio-field.tsx +0 -51
- package/src/components/forms/fields/relationship-picker.tsx +0 -143
- package/src/components/forms/fields/rich-text-editor.tsx +0 -224
- package/src/components/forms/fields/select-field.tsx +0 -35
- package/src/components/forms/fields/switch-field.tsx +0 -16
- package/src/components/forms/fields/text-area-field.tsx +0 -15
- package/src/components/forms/fields/text-field.tsx +0 -24
- package/src/components/forms/form-engine.tsx +0 -87
- package/src/components/forms/form-field-renderer.tsx +0 -269
- package/src/components/forms/utils.ts +0 -97
- package/src/components/layout/admin-shell.tsx +0 -479
- package/src/components/layout/branding-provider.tsx +0 -112
- package/src/components/live-preview/LivePreviewPane.tsx +0 -128
- package/src/components/media/focal-point-picker.tsx +0 -66
- package/src/components/media/media-card.tsx +0 -44
- package/src/components/media/media-grid.tsx +0 -32
- package/src/components/media/media-library-dialog.tsx +0 -465
- package/src/components/ui/aspect-ratio.tsx +0 -7
- package/src/components/ui/badge.tsx +0 -36
- package/src/components/ui/button.tsx +0 -56
- package/src/components/ui/calendar.tsx +0 -214
- package/src/components/ui/card.tsx +0 -79
- package/src/components/ui/checkbox.tsx +0 -28
- package/src/components/ui/command.tsx +0 -151
- package/src/components/ui/data-table.tsx +0 -219
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -200
- package/src/components/ui/form.tsx +0 -178
- package/src/components/ui/input.tsx +0 -24
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/page-header.tsx +0 -30
- package/src/components/ui/pagination.tsx +0 -57
- package/src/components/ui/popover.tsx +0 -29
- package/src/components/ui/progress.tsx +0 -26
- package/src/components/ui/radio-group.tsx +0 -42
- package/src/components/ui/render-cell.tsx +0 -110
- package/src/components/ui/scroll-area.tsx +0 -46
- package/src/components/ui/select.tsx +0 -160
- package/src/components/ui/separator.tsx +0 -29
- package/src/components/ui/sheet.tsx +0 -140
- package/src/components/ui/sidebar.tsx +0 -771
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/sonner.tsx +0 -27
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/table.tsx +0 -117
- package/src/components/ui/tabs.tsx +0 -53
- package/src/components/ui/textarea.tsx +0 -22
- package/src/components/ui/toggle.tsx +0 -43
- package/src/components/ui/tooltip.tsx +0 -28
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-preferences.ts +0 -56
- package/src/index.css +0 -111
- package/src/index.tsx +0 -198
- package/src/lib/utils.ts +0 -36
- package/src/main.tsx +0 -10
- package/src/pages/auth/first-user-page.tsx +0 -115
- package/src/pages/auth/login-page.tsx +0 -91
- package/src/pages/collections/edit-page.tsx +0 -280
- package/src/pages/collections/list-page.tsx +0 -343
- package/src/pages/dashboard/dashboard.tsx +0 -150
- package/src/pages/globals/editor-page.tsx +0 -122
- package/src/pages/media/media-page.tsx +0 -564
- package/src/pages/setup/setup-prompt.tsx +0 -181
- package/src/providers/dyrected-provider.tsx +0 -122
- package/src/providers/query-provider.tsx +0 -19
- package/src/types/jexl.d.ts +0 -11
- package/tailwind.config.ts +0 -103
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -12
- package/tsconfig.node.json +0 -25
- package/vite.config.ts +0 -39
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/icons.svg +0 -0
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { useEffect } from "react"
|
|
2
|
-
import { useForm, useWatch } from "react-hook-form"
|
|
3
|
-
import { zodResolver } from "@hookform/resolvers/zod"
|
|
4
|
-
import * as z from "zod"
|
|
5
|
-
import { Form } from "../ui/form"
|
|
6
|
-
import { Button } from "../ui/button"
|
|
7
|
-
import type { Field as FieldSchema, Block as BlockSchema } from "@dyrected/sdk"
|
|
8
|
-
import { buildSchemaShape, buildDefaultValues } from "./utils"
|
|
9
|
-
import { FormFieldRenderer } from "./form-field-renderer"
|
|
10
|
-
|
|
11
|
-
export type { FieldSchema, BlockSchema }
|
|
12
|
-
|
|
13
|
-
interface FormEngineProps {
|
|
14
|
-
collection: string
|
|
15
|
-
fields: FieldSchema[]
|
|
16
|
-
defaultValues?: Record<string, any>
|
|
17
|
-
onSubmit: (data: any) => void
|
|
18
|
-
onChange?: (isDirty: boolean) => void
|
|
19
|
-
isLoading?: boolean
|
|
20
|
-
submitLabel?: string
|
|
21
|
-
readOnly?: boolean
|
|
22
|
-
onDataChange?: (data: any) => void
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function FormEngine({
|
|
26
|
-
collection,
|
|
27
|
-
fields,
|
|
28
|
-
defaultValues = {},
|
|
29
|
-
onSubmit,
|
|
30
|
-
onChange,
|
|
31
|
-
isLoading,
|
|
32
|
-
submitLabel = "Save",
|
|
33
|
-
readOnly,
|
|
34
|
-
onDataChange
|
|
35
|
-
}: FormEngineProps) {
|
|
36
|
-
const schemaShape = buildSchemaShape(fields)
|
|
37
|
-
const formSchema = z.object(schemaShape)
|
|
38
|
-
|
|
39
|
-
const form = useForm<z.infer<typeof formSchema>>({
|
|
40
|
-
resolver: zodResolver(formSchema),
|
|
41
|
-
defaultValues: buildDefaultValues(fields, defaultValues),
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const { isDirty } = form.formState
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
onChange?.(isDirty)
|
|
48
|
-
}, [isDirty, onChange])
|
|
49
|
-
|
|
50
|
-
const watchedValues = useWatch({
|
|
51
|
-
control: form.control,
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (onDataChange) {
|
|
56
|
-
const handler = setTimeout(() => {
|
|
57
|
-
onDataChange(watchedValues)
|
|
58
|
-
}, 100)
|
|
59
|
-
return () => clearTimeout(handler)
|
|
60
|
-
}
|
|
61
|
-
}, [watchedValues, onDataChange])
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<Form {...form}>
|
|
65
|
-
<form onSubmit={form.handleSubmit(onSubmit)} className="dy-space-y-8">
|
|
66
|
-
<div className="dy-grid dy-gap-6">
|
|
67
|
-
{fields.filter(f => !f.admin?.hidden).map((field) => (
|
|
68
|
-
<FormFieldRenderer
|
|
69
|
-
key={field.name}
|
|
70
|
-
schema={field}
|
|
71
|
-
basePath=""
|
|
72
|
-
control={form.control}
|
|
73
|
-
collection={collection}
|
|
74
|
-
/>
|
|
75
|
-
))}
|
|
76
|
-
</div>
|
|
77
|
-
<div className="dy-flex dy-justify-end dy-gap-4">
|
|
78
|
-
{!readOnly && (
|
|
79
|
-
<Button type="submit" disabled={isLoading}>
|
|
80
|
-
{isLoading ? "Saving..." : submitLabel}
|
|
81
|
-
</Button>
|
|
82
|
-
)}
|
|
83
|
-
</div>
|
|
84
|
-
</form>
|
|
85
|
-
</Form>
|
|
86
|
-
)
|
|
87
|
-
}
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { useWatch, useFieldArray } from "react-hook-form"
|
|
3
|
-
import { useDyrected } from "../../providers/dyrected-provider"
|
|
4
|
-
import {
|
|
5
|
-
FormControl,
|
|
6
|
-
FormField,
|
|
7
|
-
FormItem,
|
|
8
|
-
FormLabel,
|
|
9
|
-
FormMessage,
|
|
10
|
-
} from "../ui/form"
|
|
11
|
-
import { Button } from "../ui/button"
|
|
12
|
-
import { Plus, Trash2, Layers } from "lucide-react"
|
|
13
|
-
import { MediaLibraryDialog } from "../media/media-library-dialog"
|
|
14
|
-
import { cn } from "../../lib/utils"
|
|
15
|
-
import jexl from 'jexl'
|
|
16
|
-
import type { Field as FieldSchema } from "@dyrected/sdk"
|
|
17
|
-
import { FieldRenderer } from "./field-renderer"
|
|
18
|
-
import { BlockBuilder } from "./fields/block-builder"
|
|
19
|
-
import { buildDefaultValues } from "./utils"
|
|
20
|
-
|
|
21
|
-
interface FormFieldRendererProps {
|
|
22
|
-
schema: FieldSchema
|
|
23
|
-
basePath: string
|
|
24
|
-
control: any
|
|
25
|
-
collection: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* FormFieldRenderer (Field Orchestrator)
|
|
30
|
-
*
|
|
31
|
-
* This component handles the high-level logic for a single form field, including:
|
|
32
|
-
* - Field Lifecycle: Manages registration and state via react-hook-form Controller.
|
|
33
|
-
* - Conditional Visibility: Evaluates 'admin.condition' to show/hide fields dynamically.
|
|
34
|
-
* - Layout & Presentation: Renders labels, descriptions, and error states.
|
|
35
|
-
* - Validation: Displays Zod validation errors.
|
|
36
|
-
*
|
|
37
|
-
* It delegates the actual rendering of the input UI to the FieldRenderer.
|
|
38
|
-
*/
|
|
39
|
-
export function FormFieldRenderer({ schema, basePath, control, collection }: FormFieldRendererProps) {
|
|
40
|
-
const { user, schemas } = useDyrected()
|
|
41
|
-
|
|
42
|
-
if (schema.admin?.hidden) return null
|
|
43
|
-
|
|
44
|
-
const formValues = useWatch({ control })
|
|
45
|
-
const siblingData = useWatch({ control, name: (basePath || undefined) as any }) || {}
|
|
46
|
-
const conditionData = basePath ? { ...formValues, ...siblingData } : formValues
|
|
47
|
-
|
|
48
|
-
let isVisible = true
|
|
49
|
-
const condition = schema.admin?.condition
|
|
50
|
-
|
|
51
|
-
if (typeof condition === 'function') {
|
|
52
|
-
isVisible = condition(conditionData, siblingData)
|
|
53
|
-
} else if (typeof condition === 'string') {
|
|
54
|
-
try {
|
|
55
|
-
const sanitizedCondition = condition.replace(/===/g, '==').replace(/!==/g, '!=')
|
|
56
|
-
isVisible = jexl.evalSync(sanitizedCondition, conditionData)
|
|
57
|
-
} catch (e) {
|
|
58
|
-
console.warn("Jexl eval failed:", e)
|
|
59
|
-
isVisible = true
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!isVisible) return null
|
|
64
|
-
|
|
65
|
-
const readAccess = (schema.access as any)?.read
|
|
66
|
-
let canRead = true
|
|
67
|
-
if (readAccess === false) {
|
|
68
|
-
canRead = false
|
|
69
|
-
} else if (typeof readAccess === 'string') {
|
|
70
|
-
try {
|
|
71
|
-
canRead = jexl.evalSync(readAccess, { user, ...conditionData })
|
|
72
|
-
} catch (e) {
|
|
73
|
-
console.warn("Read access eval failed:", e)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!canRead) return null
|
|
78
|
-
|
|
79
|
-
const fullPath = basePath ? `${basePath}.${schema.name}` : schema.name
|
|
80
|
-
|
|
81
|
-
if (schema.type === "object") {
|
|
82
|
-
return (
|
|
83
|
-
<div className="dy-left-accent dy-space-y-6">
|
|
84
|
-
<div className="dy-flex dy-items-center dy-gap-2 dy-mb-2">
|
|
85
|
-
<h4 className="dy-font-bold dy-text-sm dy-text-foreground/80 dy-tracking-tight">{schema.label || schema.name.charAt(0).toUpperCase() + schema.name.slice(1)}</h4>
|
|
86
|
-
{schema.admin?.description && (
|
|
87
|
-
<p className="dy-text-[10px] dy-text-muted-foreground/50 dy-italic">{schema.admin.description}</p>
|
|
88
|
-
)}
|
|
89
|
-
</div>
|
|
90
|
-
<div className="dy-space-y-6">
|
|
91
|
-
{schema.fields?.map(subField => (
|
|
92
|
-
<FormFieldRenderer key={subField.name} schema={subField} basePath={fullPath} control={control} collection={collection} />
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (schema.type === "array") {
|
|
100
|
-
return <ArrayFieldRenderer schema={schema} basePath={fullPath} control={control} collection={collection} />
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (schema.type === "blocks" && schema.blocks) {
|
|
104
|
-
return <BlockBuilder schema={schema} basePath={fullPath} control={control} collection={collection} />
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const isBoolean = schema.type === "boolean"
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<FormField
|
|
111
|
-
control={control}
|
|
112
|
-
name={fullPath}
|
|
113
|
-
render={({ field: formField }: { field: any }) => (
|
|
114
|
-
<FormItem className={cn(
|
|
115
|
-
isBoolean
|
|
116
|
-
? "dy-flex dy-flex-row dy-items-center dy-justify-between dy-rounded-xl dy-border dy-border-border/40 dy-p-4 dy-bg-white/50 dy-shadow-sm dy-space-y-0"
|
|
117
|
-
: "dy-space-y-3"
|
|
118
|
-
)}>
|
|
119
|
-
<div className={cn(isBoolean ? "dy-space-y-1" : "dy-flex dy-items-center dy-gap-2 dy-mb-1")}>
|
|
120
|
-
<FormLabel className="dy-text-sm dy-font-semibold dy-text-foreground/80 dy-cursor-pointer">
|
|
121
|
-
{schema.label || schema.name.charAt(0).toUpperCase() + schema.name.slice(1)}
|
|
122
|
-
{schema.required && <span className="dy-text-destructive dy-ml-1">*</span>}
|
|
123
|
-
</FormLabel>
|
|
124
|
-
{schema.admin?.description && (
|
|
125
|
-
<p className={cn(
|
|
126
|
-
"dy-text-muted-foreground/60 dy-italic",
|
|
127
|
-
isBoolean ? "dy-text-[11px] dy-leading-tight" : "dy-text-[11px] dy-leading-relaxed"
|
|
128
|
-
)}>
|
|
129
|
-
{schema.admin.description}
|
|
130
|
-
</p>
|
|
131
|
-
)}
|
|
132
|
-
{!isBoolean && schema.unique && (
|
|
133
|
-
<span className="dy-inline-flex dy-items-center dy-rounded-full dy-bg-primary/10 dy-px-1.5 dy-py-0.5 dy-text-[10px] dy-font-medium dy-text-primary dy-ring-1 dy-ring-inset dy-ring-primary/10">
|
|
134
|
-
Unique
|
|
135
|
-
</span>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
138
|
-
<FormControl>
|
|
139
|
-
<FieldRenderer schema={schema} field={formField} collection={collection} context={{ user, schemas, siblingData: conditionData }} />
|
|
140
|
-
</FormControl>
|
|
141
|
-
{!isBoolean && schema.admin?.description && (
|
|
142
|
-
<p className="dy-text-[11px] dy-text-muted-foreground/70 dy-leading-relaxed dy-italic">{schema.admin.description}</p>
|
|
143
|
-
)}
|
|
144
|
-
<FormMessage className="dy-text-xs dy-font-medium" />
|
|
145
|
-
</FormItem>
|
|
146
|
-
)}
|
|
147
|
-
/>
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function ArrayFieldRenderer({ schema, basePath, control, collection }: { schema: FieldSchema, basePath: string, control: any, collection: string }) {
|
|
152
|
-
const { fields, append, remove } = useFieldArray({ control, name: basePath })
|
|
153
|
-
const { schemas } = useDyrected()
|
|
154
|
-
const [isBulkOpen, setIsBulkOpen] = React.useState(false)
|
|
155
|
-
|
|
156
|
-
// Find if there is an image field or a relationship to an upload collection
|
|
157
|
-
const imageField = React.useMemo(() => {
|
|
158
|
-
return schema.fields?.find(f => {
|
|
159
|
-
if (f.type === 'image') return true
|
|
160
|
-
if (f.type === 'relationship' && f.relationTo) {
|
|
161
|
-
const relatedSchema = schemas?.collections?.find(s => s.slug === f.relationTo)
|
|
162
|
-
return relatedSchema?.upload === true
|
|
163
|
-
}
|
|
164
|
-
return false
|
|
165
|
-
})
|
|
166
|
-
}, [schema.fields, schemas])
|
|
167
|
-
|
|
168
|
-
const bulkCollection = (imageField?.type === 'relationship' ? imageField.relationTo : 'media') || 'media'
|
|
169
|
-
|
|
170
|
-
const handleBulkAdd = (ids: string[]) => {
|
|
171
|
-
ids.forEach(id => {
|
|
172
|
-
const newItem = buildDefaultValues(schema.fields || [], {})
|
|
173
|
-
if (imageField) {
|
|
174
|
-
newItem[imageField.name] = id
|
|
175
|
-
}
|
|
176
|
-
append(newItem)
|
|
177
|
-
})
|
|
178
|
-
setIsBulkOpen(false)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return (
|
|
182
|
-
<div className="dy-space-y-6 dy-transition-all dy-py-6">
|
|
183
|
-
<div className="dy-flex dy-justify-between dy-items-end dy-pb-2">
|
|
184
|
-
<div className="dy-space-y-1">
|
|
185
|
-
<div className="dy-flex dy-items-center dy-gap-2">
|
|
186
|
-
<Layers className="dy-h-4 dy-w-4 dy-text-primary" />
|
|
187
|
-
<h4 className="dy-font-serif dy-font-bold dy-text-base dy-text-foreground dy-tracking-tight">{schema.label || schema.name.charAt(0).toUpperCase() + schema.name.slice(1)}</h4>
|
|
188
|
-
</div>
|
|
189
|
-
{schema.admin?.description && (
|
|
190
|
-
<p className="dy-text-[11px] dy-text-muted-foreground/60 dy-italic dy-leading-relaxed">{schema.admin.description}</p>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
<div className="dy-flex dy-items-center dy-gap-2">
|
|
194
|
-
<Button
|
|
195
|
-
type="button"
|
|
196
|
-
variant="outline"
|
|
197
|
-
size="sm"
|
|
198
|
-
className="dy-h-9 dy-text-[11px] dy-font-bold dy-rounded-xl dy-border-primary/20 hover:dy-bg-primary/5 hover:dy-text-primary dy-transition-all dy-shadow-sm"
|
|
199
|
-
onClick={() => append(buildDefaultValues(schema.fields || [], {}))}
|
|
200
|
-
>
|
|
201
|
-
<Plus className="dy-w-3.5 dy-h-3.5 dy-mr-1.5" />
|
|
202
|
-
Add Item
|
|
203
|
-
</Button>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<div className="dy-space-y-8 dy-pl-0 dy-border-l dy-border-muted/30">
|
|
208
|
-
{fields.map((item, index) => (
|
|
209
|
-
<div key={item.id} className="dy-relative dy-group dy-animate-in dy-slide-in-from-left-2 dy-duration-300">
|
|
210
|
-
<div className="dy-bg-muted/5 dy-left-accent dy-transition-all dy-relative">
|
|
211
|
-
<Button
|
|
212
|
-
type="button"
|
|
213
|
-
variant="ghost"
|
|
214
|
-
size="icon"
|
|
215
|
-
className="dy-absolute dy-top-4 dy-right-4 dy-h-8 dy-w-8 dy-text-muted-foreground/20 hover:dy-text-destructive hover:dy-bg-destructive/10 dy-rounded-xl dy-opacity-0 dy-group-hover:dy-opacity-100 dy-transition-all"
|
|
216
|
-
onClick={() => remove(index)}
|
|
217
|
-
>
|
|
218
|
-
<Trash2 className="dy-w-4 dy-h-4" />
|
|
219
|
-
</Button>
|
|
220
|
-
<div className="dy-space-y-6">
|
|
221
|
-
{schema.fields?.map(subField => (
|
|
222
|
-
<FormFieldRenderer
|
|
223
|
-
key={subField.name}
|
|
224
|
-
schema={subField}
|
|
225
|
-
basePath={`${basePath}.${index}`}
|
|
226
|
-
control={control}
|
|
227
|
-
collection={collection}
|
|
228
|
-
/>
|
|
229
|
-
))}
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
))}
|
|
234
|
-
|
|
235
|
-
{fields.length === 0 && (
|
|
236
|
-
<div className="dy-flex dy-flex-col dy-items-center dy-justify-center dy-py-12 dy-border-2 dy-border-dashed dy-border-muted dy-rounded-3xl dy-bg-muted/5 dy-space-y-3">
|
|
237
|
-
<div className="dy-p-3 dy-bg-muted dy-rounded-full">
|
|
238
|
-
<Layers className="dy-h-6 dy-w-6 dy-text-muted-foreground/40" />
|
|
239
|
-
</div>
|
|
240
|
-
<p className="dy-text-xs dy-font-medium dy-text-muted-foreground/50">No items added yet</p>
|
|
241
|
-
</div>
|
|
242
|
-
)}
|
|
243
|
-
|
|
244
|
-
<Button
|
|
245
|
-
type="button"
|
|
246
|
-
variant="outline"
|
|
247
|
-
size="sm"
|
|
248
|
-
className="dy-w-full dy-h-10 dy-text-xs dy-font-bold dy-rounded-2xl dy-border-dashed dy-border-primary/20 hover:dy-bg-primary/5 hover:dy-text-primary dy-transition-all dy-shadow-sm"
|
|
249
|
-
onClick={() => append(buildDefaultValues(schema.fields || [], {}))}
|
|
250
|
-
>
|
|
251
|
-
<Plus className="dy-w-4 dy-h-4 dy-mr-2" />
|
|
252
|
-
Add Item
|
|
253
|
-
</Button>
|
|
254
|
-
</div>
|
|
255
|
-
|
|
256
|
-
{imageField && (
|
|
257
|
-
<MediaLibraryDialog
|
|
258
|
-
collection={bulkCollection}
|
|
259
|
-
isOpen={isBulkOpen}
|
|
260
|
-
onOpenChange={setIsBulkOpen}
|
|
261
|
-
selectedValues={[]}
|
|
262
|
-
multiple={true}
|
|
263
|
-
onSelect={() => { }}
|
|
264
|
-
onConfirm={(ids: string[]) => handleBulkAdd(ids)}
|
|
265
|
-
/>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
)
|
|
269
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import * as z from "zod"
|
|
2
|
-
import type { Field as FieldSchema } from "@dyrected/sdk"
|
|
3
|
-
|
|
4
|
-
export function normalizeOptions(options: string[] | { label: string; value: string }[] | undefined): { label: string; value: string }[] {
|
|
5
|
-
if (!options) return []
|
|
6
|
-
return options.map(opt => typeof opt === "string" ? { label: opt, value: opt } : opt)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function buildSchemaShape(fields: FieldSchema[]) {
|
|
10
|
-
const shape: Record<string, z.ZodTypeAny> = {}
|
|
11
|
-
fields.forEach((field) => {
|
|
12
|
-
let validator: any = z.any()
|
|
13
|
-
const label = field.label || field.name.charAt(0).toUpperCase() + field.name.slice(1)
|
|
14
|
-
|
|
15
|
-
if (field.type === "object" && field.fields) {
|
|
16
|
-
validator = z.object(buildSchemaShape(field.fields))
|
|
17
|
-
if (!field.required) validator = validator.optional()
|
|
18
|
-
shape[field.name] = validator
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (field.type === "blocks") {
|
|
23
|
-
validator = z.array(z.any())
|
|
24
|
-
if (!field.required) validator = validator.optional()
|
|
25
|
-
shape[field.name] = validator
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (field.type === "array" && field.fields) {
|
|
30
|
-
validator = z.array(z.object(buildSchemaShape(field.fields)))
|
|
31
|
-
if (!field.required) validator = validator.optional()
|
|
32
|
-
shape[field.name] = validator
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const fieldType = field.type as string
|
|
37
|
-
if (fieldType === "text" || fieldType === "textarea" || fieldType === "select" || fieldType === "image" || fieldType === "richText" || fieldType === "relationship" || fieldType === "date") {
|
|
38
|
-
validator = z.string()
|
|
39
|
-
if (field.required) validator = validator.min(1, `${label} is required`)
|
|
40
|
-
} else if (field.type === "email") {
|
|
41
|
-
validator = z.string().email(`${label} must be a valid email`)
|
|
42
|
-
if (field.required) validator = validator.min(1, `${label} is required`)
|
|
43
|
-
} else if (field.type === "url") {
|
|
44
|
-
validator = z.string().url(`${label} must be a valid URL`)
|
|
45
|
-
if (field.required) validator = validator.min(1, `${label} is required`)
|
|
46
|
-
} else if (field.type === "number") {
|
|
47
|
-
validator = z.coerce.number()
|
|
48
|
-
} else if (field.type === "boolean") {
|
|
49
|
-
validator = z.boolean()
|
|
50
|
-
} else if (field.type === "json") {
|
|
51
|
-
validator = z.any()
|
|
52
|
-
} else if (field.type === "multiSelect") {
|
|
53
|
-
validator = z.array(z.string())
|
|
54
|
-
if (field.required) validator = validator.min(1, `${label} requires at least one selection`)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!field.required && field.type !== "multiSelect") {
|
|
58
|
-
validator = validator.optional().or(z.literal(""))
|
|
59
|
-
} else if (!field.required && field.type === "multiSelect") {
|
|
60
|
-
validator = validator.optional()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
shape[field.name] = validator
|
|
64
|
-
})
|
|
65
|
-
return shape
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function buildDefaultValues(fields: FieldSchema[], defaults: any) {
|
|
69
|
-
return fields.reduce((acc, field) => {
|
|
70
|
-
let defaultVal = defaults[field.name] ?? field.defaultValue
|
|
71
|
-
|
|
72
|
-
if (field.type === "object" && field.fields) {
|
|
73
|
-
acc[field.name] = buildDefaultValues(field.fields, defaultVal || {})
|
|
74
|
-
return acc
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (field.type === "array") {
|
|
78
|
-
acc[field.name] = Array.isArray(defaultVal) ? defaultVal : []
|
|
79
|
-
return acc
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (field.type === "blocks") {
|
|
83
|
-
acc[field.name] = Array.isArray(defaultVal) ? defaultVal : []
|
|
84
|
-
return acc
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (defaultVal === undefined) {
|
|
88
|
-
if (field.type === "boolean") defaultVal = false
|
|
89
|
-
else if (field.type === "multiSelect") defaultVal = []
|
|
90
|
-
else if (field.type === "json") defaultVal = {}
|
|
91
|
-
else defaultVal = ""
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
acc[field.name] = defaultVal
|
|
95
|
-
return acc
|
|
96
|
-
}, {} as any)
|
|
97
|
-
}
|