@opensaas/stack-ui 0.20.1 → 0.22.0
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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +59 -0
- package/dist/components/ItemFormClient.d.ts +5 -2
- package/dist/components/ItemFormClient.d.ts.map +1 -1
- package/dist/components/ItemFormClient.js +46 -119
- package/dist/components/fields/FieldRenderer.d.ts.map +1 -1
- package/dist/components/fields/FieldRenderer.js +24 -20
- package/dist/components/fields/FileField.d.ts +1 -1
- package/dist/components/fields/FileField.d.ts.map +1 -1
- package/dist/components/fields/ImageField.d.ts +1 -1
- package/dist/components/fields/ImageField.d.ts.map +1 -1
- package/dist/components/standalone/ItemCreateForm.d.ts.map +1 -1
- package/dist/components/standalone/ItemCreateForm.js +13 -64
- package/dist/components/standalone/ItemEditForm.d.ts.map +1 -1
- package/dist/components/standalone/ItemEditForm.js +15 -83
- package/dist/lib/theme.d.ts +1 -1
- package/dist/lib/theme.d.ts.map +1 -1
- package/dist/lib/useItemForm.d.ts +85 -0
- package/dist/lib/useItemForm.d.ts.map +1 -0
- package/dist/lib/useItemForm.js +122 -0
- package/dist/server/types.d.ts +1 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/styles/globals.css +5 -0
- package/package.json +5 -5
- package/src/components/ItemFormClient.tsx +61 -131
- package/src/components/fields/FieldRenderer.tsx +38 -27
- package/src/components/fields/FileField.tsx +1 -1
- package/src/components/fields/ImageField.tsx +1 -1
- package/src/components/standalone/ItemCreateForm.tsx +21 -69
- package/src/components/standalone/ItemEditForm.tsx +26 -93
- package/src/lib/theme.ts +1 -1
- package/src/lib/useItemForm.ts +194 -0
- package/src/server/types.ts +1 -1
- package/tests/lib/useItemForm.test.ts +107 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
> @opensaas/stack-ui@0.
|
|
2
|
+
> @opensaas/stack-ui@0.22.0 build /home/runner/work/stack/stack/packages/ui
|
|
3
3
|
> tsc && npm run build:css
|
|
4
4
|
|
|
5
|
-
npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm.
|
|
6
|
-
npm warn Unknown env config "npm-globalconfig". This will stop working in the next major version of npm.
|
|
7
|
-
npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
|
|
5
|
+
npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
|
|
6
|
+
npm warn Unknown env config "npm-globalconfig". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
|
|
7
|
+
npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
|
|
8
8
|
|
|
9
|
-
> @opensaas/stack-ui@0.
|
|
9
|
+
> @opensaas/stack-ui@0.22.0 build:css
|
|
10
10
|
> mkdir -p dist/styles && postcss ./src/styles/globals.css -o ./dist/styles/globals.css
|
|
11
11
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# @opensaas/stack-ui
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
## 0.21.0
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- [#417](https://github.com/OpenSaasAU/stack/pull/417) [`ed1c9f5`](https://github.com/OpenSaasAU/stack/commit/ed1c9f532b77ef59d7a845731e6a6116904a859e) Thanks [@borisno2](https://github.com/borisno2)! - Unify the item-form logic behind a shared `useItemForm` engine
|
|
10
|
+
|
|
11
|
+
The AdminUI form (`ItemFormClient`) and the standalone `ItemCreateForm`/
|
|
12
|
+
`ItemEditForm` each carried their own near-identical copy of the form state,
|
|
13
|
+
the relationship-to-`connect` submit transform, the clear-error-on-change
|
|
14
|
+
behaviour, and the error/pending handling. That logic now lives once in a
|
|
15
|
+
`useItemForm` hook (with pure, exported `transformItemFormData`,
|
|
16
|
+
`transformInitialData`, and `getEditableFields` helpers); each form supplies
|
|
17
|
+
only an `onSubmit` adapter and renders the returned state.
|
|
18
|
+
|
|
19
|
+
Behaviour is unified to the superset: every form now applies the relationship
|
|
20
|
+
transform, the password `{ isSet }` skip for unchanged passwords, and
|
|
21
|
+
system-field filtering. The transform logic is covered by unit tests for the
|
|
22
|
+
first time.
|
|
23
|
+
|
|
24
|
+
No public API change — `ItemCreateForm`, `ItemEditForm`, and the AdminUI form
|
|
25
|
+
keep their existing props.
|
|
26
|
+
|
|
27
|
+
- [#415](https://github.com/OpenSaasAU/stack/pull/415) [`8980ff3`](https://github.com/OpenSaasAU/stack/commit/8980ff36ffb0879d8f4409740493dd940572cc9d) Thanks [@borisno2](https://github.com/borisno2)! - Curate the `@opensaas/stack-core` public surface into clearly-scoped entry points
|
|
28
|
+
|
|
29
|
+
The root entry point now exposes only the everyday consumer surface — `config`,
|
|
30
|
+
`list`, `getContext`, the naming helpers (`getDbKey`, `getUrlKey`,
|
|
31
|
+
`getListKeyFromUrl`), `ValidationError`, and the config/access types you annotate
|
|
32
|
+
with. Plugin and field authoring contracts move to a new `/extend` path, and the
|
|
33
|
+
plumbing shared with sibling packages and generated code moves to `/internal`.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Everyday usage (unchanged)
|
|
37
|
+
import { config, list, getContext } from '@opensaas/stack-core'
|
|
38
|
+
|
|
39
|
+
// Authoring a plugin or a third-party field package
|
|
40
|
+
import type { Plugin, BaseFieldConfig, TypeInfo } from '@opensaas/stack-core/extend'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`@opensaas/stack-core/internal` carries no semver guarantees; application code
|
|
44
|
+
should never import from it. `Session` stays on the root entry point because it is
|
|
45
|
+
the module-augmentation target.
|
|
46
|
+
|
|
47
|
+
Removed from the public surface (zero callers): the nine `*HookArgs` types and the
|
|
48
|
+
callerless typed-query runtime types. The other `@opensaas/*` packages and the CLI
|
|
49
|
+
generator are updated to import from the new paths.
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- [#412](https://github.com/OpenSaasAU/stack/pull/412) [`9696f98`](https://github.com/OpenSaasAU/stack/commit/9696f9800284f94e21e14c31a716de4b48d736e5) Thanks [@borisno2](https://github.com/borisno2)! - Refactor `FieldRenderer` to use data-presence checks instead of `fieldConfig.type` comparisons
|
|
54
|
+
|
|
55
|
+
`FieldRenderer` no longer checks `fieldConfig.type` to decide which props to pass to field
|
|
56
|
+
components. Field-specific UI props (select options, relationship items/key/many) are now derived
|
|
57
|
+
from the serialised field config using data-presence checks (`fieldConfig.options`, `fieldConfig.ref`)
|
|
58
|
+
— the same self-contained pattern used for Prisma and TypeScript generation.
|
|
59
|
+
|
|
60
|
+
**For users:** no changes required. Field rendering behaviour is unchanged.
|
|
61
|
+
|
|
3
62
|
## 0.20.1
|
|
4
63
|
|
|
5
64
|
## 0.20.0
|
|
@@ -15,8 +15,11 @@ export interface ItemFormClientProps {
|
|
|
15
15
|
}>>;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Client component for
|
|
19
|
-
*
|
|
18
|
+
* Client component for the AdminUI item form.
|
|
19
|
+
*
|
|
20
|
+
* Shares the form state/transform/error/pending logic with the standalone
|
|
21
|
+
* forms via `useItemForm`; this component only adapts submission to the
|
|
22
|
+
* AdminUI server action + router navigation, and adds the delete flow.
|
|
20
23
|
*/
|
|
21
24
|
export declare function ItemFormClient({ listKey, urlKey, mode, fields, initialData, itemId, basePath, serverAction, relationshipData, }: ItemFormClientProps): import("react/jsx-runtime").JSX.Element;
|
|
22
25
|
//# sourceMappingURL=ItemFormClient.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ItemFormClient.d.ts","sourceRoot":"","sources":["../../src/components/ItemFormClient.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;
|
|
1
|
+
{"version":3,"file":"ItemFormClient.d.ts","sourceRoot":"","sources":["../../src/components/ItemFormClient.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAG7E,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACxE;AAoBD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,IAAI,EACJ,MAAM,EACN,WAAgB,EAChB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,gBAAqB,GACtB,EAAE,mBAAmB,2CAkIrB"}
|
|
@@ -6,142 +6,69 @@ import { FieldRenderer } from './fields/FieldRenderer.js';
|
|
|
6
6
|
import { ConfirmDialog } from './ConfirmDialog.js';
|
|
7
7
|
import { LoadingSpinner } from './LoadingSpinner.js';
|
|
8
8
|
import { Button } from '../primitives/button.js';
|
|
9
|
+
import { useItemForm } from '../lib/useItemForm.js';
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Normalise a server action's response into the form engine's result shape.
|
|
12
|
+
* Supports both the `{ success, error, fieldErrors }` envelope and the legacy
|
|
13
|
+
* "truthy data = success, null = access denied" convention.
|
|
14
|
+
*/
|
|
15
|
+
function toSubmitResult(result, deniedMessage) {
|
|
16
|
+
if (result && typeof result === 'object' && 'success' in result) {
|
|
17
|
+
const r = result;
|
|
18
|
+
return r.success
|
|
19
|
+
? { success: true }
|
|
20
|
+
: { success: false, error: r.error, fieldErrors: r.fieldErrors };
|
|
21
|
+
}
|
|
22
|
+
if (result)
|
|
23
|
+
return { success: true };
|
|
24
|
+
return { success: false, error: deniedMessage };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Client component for the AdminUI item form.
|
|
28
|
+
*
|
|
29
|
+
* Shares the form state/transform/error/pending logic with the standalone
|
|
30
|
+
* forms via `useItemForm`; this component only adapts submission to the
|
|
31
|
+
* AdminUI server action + router navigation, and adds the delete flow.
|
|
12
32
|
*/
|
|
13
33
|
export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}, itemId, basePath, serverAction, relationshipData = {}, }) {
|
|
14
34
|
const router = useRouter();
|
|
15
|
-
const [isPending, startTransition] = useTransition();
|
|
16
|
-
const [formData, setFormData] = useState(initialData);
|
|
17
|
-
const [errors, setErrors] = useState({});
|
|
18
|
-
const [generalError, setGeneralError] = useState(null);
|
|
19
35
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
setErrors({});
|
|
34
|
-
setGeneralError(null);
|
|
35
|
-
startTransition(async () => {
|
|
36
|
-
// Transform relationship fields to Prisma format
|
|
37
|
-
// Filter out password fields with isSet objects (unchanged passwords)
|
|
38
|
-
// File/Image fields: pass File objects through (Next.js will serialize them)
|
|
39
|
-
const transformedData = {};
|
|
40
|
-
for (const [fieldName, value] of Object.entries(formData)) {
|
|
41
|
-
const fieldConfig = fields[fieldName];
|
|
42
|
-
// Skip password fields that have { isSet: boolean } value (not being changed)
|
|
43
|
-
if (typeof value === 'object' && value !== null && 'isSet' in value) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
// Transform relationship fields - check discriminated union type
|
|
47
|
-
const fieldAny = fieldConfig;
|
|
48
|
-
if (fieldAny?.type === 'relationship') {
|
|
49
|
-
if (fieldAny.many) {
|
|
50
|
-
// Many relationship: use connect format
|
|
51
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
52
|
-
transformedData[fieldName] = {
|
|
53
|
-
connect: value.map((id) => ({ id })),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
// Single relationship: use connect format
|
|
59
|
-
if (value) {
|
|
60
|
-
transformedData[fieldName] = {
|
|
61
|
-
connect: { id: value },
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
// Non-relationship field: pass through (including File objects for file/image fields)
|
|
68
|
-
// File objects will be serialized by Next.js server action
|
|
69
|
-
transformedData[fieldName] = value;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const result = mode === 'create'
|
|
73
|
-
? await serverAction({
|
|
74
|
-
listKey,
|
|
75
|
-
action: 'create',
|
|
76
|
-
data: transformedData,
|
|
77
|
-
})
|
|
78
|
-
: await serverAction({
|
|
79
|
-
listKey,
|
|
80
|
-
action: 'update',
|
|
81
|
-
id: itemId,
|
|
82
|
-
data: transformedData,
|
|
83
|
-
});
|
|
84
|
-
// Check if result has the new format with success/error fields
|
|
85
|
-
if (result && typeof result === 'object' && 'success' in result) {
|
|
86
|
-
const actionResult = result;
|
|
87
|
-
if (actionResult.success) {
|
|
88
|
-
// Navigate back to list view
|
|
89
|
-
router.push(`${basePath}/${urlKey}`);
|
|
90
|
-
router.refresh();
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
// Handle error response
|
|
94
|
-
if (actionResult.fieldErrors) {
|
|
95
|
-
setErrors(actionResult.fieldErrors);
|
|
96
|
-
}
|
|
97
|
-
setGeneralError(actionResult.error);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else if (result) {
|
|
101
|
-
// Legacy format: result is the data itself
|
|
36
|
+
// Separate transition for delete (not a form submit, so outside the engine).
|
|
37
|
+
const [isDeleting, startDeleteTransition] = useTransition();
|
|
38
|
+
const { formData, errors, generalError, isPending, editableFields, handleFieldChange, handleSubmit, setGeneralError, } = useItemForm({
|
|
39
|
+
fields,
|
|
40
|
+
initialData,
|
|
41
|
+
mode: mode === 'create' ? 'create' : 'update',
|
|
42
|
+
errorFallback: 'Access denied or operation failed',
|
|
43
|
+
onSubmit: async (data, action) => {
|
|
44
|
+
const result = action === 'create'
|
|
45
|
+
? await serverAction({ listKey, action: 'create', data })
|
|
46
|
+
: await serverAction({ listKey, action: 'update', id: itemId, data });
|
|
47
|
+
const normalized = toSubmitResult(result, 'Access denied or operation failed');
|
|
48
|
+
if (normalized.success) {
|
|
102
49
|
router.push(`${basePath}/${urlKey}`);
|
|
103
50
|
router.refresh();
|
|
104
51
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
-
};
|
|
111
|
-
const handleDelete = async () => {
|
|
52
|
+
return normalized;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const handleDelete = () => {
|
|
112
56
|
if (!itemId)
|
|
113
57
|
return;
|
|
114
58
|
setGeneralError(null);
|
|
115
59
|
setShowDeleteConfirm(false);
|
|
116
|
-
|
|
117
|
-
const result = await serverAction({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
id: itemId,
|
|
121
|
-
});
|
|
122
|
-
// Check if result has the new format with success/error fields
|
|
123
|
-
if (result && typeof result === 'object' && 'success' in result) {
|
|
124
|
-
const actionResult = result;
|
|
125
|
-
if (actionResult.success) {
|
|
126
|
-
router.push(`${basePath}/${urlKey}`);
|
|
127
|
-
router.refresh();
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
setGeneralError(actionResult.error);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else if (result) {
|
|
134
|
-
// Legacy format: result is the data itself
|
|
60
|
+
startDeleteTransition(async () => {
|
|
61
|
+
const result = await serverAction({ listKey, action: 'delete', id: itemId });
|
|
62
|
+
const normalized = toSubmitResult(result, 'Access denied or failed to delete item');
|
|
63
|
+
if (normalized.success) {
|
|
135
64
|
router.push(`${basePath}/${urlKey}`);
|
|
136
65
|
router.refresh();
|
|
137
66
|
}
|
|
138
67
|
else {
|
|
139
|
-
|
|
140
|
-
setGeneralError('Access denied or failed to delete item');
|
|
68
|
+
setGeneralError(normalized.error);
|
|
141
69
|
}
|
|
142
70
|
});
|
|
143
71
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-6", children: [generalError && (_jsx("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-4", children: _jsx("p", { className: "text-sm font-medium", children: generalError }) })), _jsx("div", { className: "space-y-6", children: editableFields.map(([fieldName, fieldConfig]) => (_jsx(FieldRenderer, { fieldName: fieldName, fieldConfig: fieldConfig, value: formData[fieldName], onChange: (value) => handleFieldChange(fieldName, value), error: errors[fieldName], disabled: isPending, mode: "edit", relationshipItems: relationshipData[fieldName] || [], relationshipLoading: false, basePath: basePath }, fieldName))) }), _jsxs("div", { className: "flex items-center justify-between pt-6 border-t border-border", children: [_jsxs("div", { className: "flex gap-3", children: [_jsxs(Button, { type: "submit", disabled: isPending, className: "gap-2", children: [isPending && (_jsx(LoadingSpinner, { size: "sm", className: "border-primary-foreground border-t-transparent" })), isPending ? 'Saving...' : mode === 'create' ? 'Create' : 'Save'] }), _jsx(Button, { type: "button", variant: "secondary", onClick: () => router.push(`${basePath}/${urlKey}`), disabled: isPending, children: "Cancel" })] }), mode === 'edit' && itemId && (_jsx(Button, { type: "button", variant: "destructive", onClick: () => setShowDeleteConfirm(true), disabled: isPending, children: "Delete" }))] }), _jsx(ConfirmDialog, { isOpen: showDeleteConfirm, title: "Delete Item", message: "Are you sure you want to delete this item? This action cannot be undone.", confirmLabel: "Delete", cancelLabel: "Cancel", variant: "danger", onConfirm: handleDelete, onCancel: () => setShowDeleteConfirm(false) })] }));
|
|
72
|
+
const busy = isPending || isDeleting;
|
|
73
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-6", children: [generalError && (_jsx("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-4", children: _jsx("p", { className: "text-sm font-medium", children: generalError }) })), _jsx("div", { className: "space-y-6", children: editableFields.map(([fieldName, fieldConfig]) => (_jsx(FieldRenderer, { fieldName: fieldName, fieldConfig: fieldConfig, value: formData[fieldName], onChange: (value) => handleFieldChange(fieldName, value), error: errors[fieldName], disabled: busy, mode: "edit", relationshipItems: relationshipData[fieldName] || [], relationshipLoading: false, basePath: basePath }, fieldName))) }), _jsxs("div", { className: "flex items-center justify-between pt-6 border-t border-border", children: [_jsxs("div", { className: "flex gap-3", children: [_jsxs(Button, { type: "submit", disabled: busy, className: "gap-2", children: [isPending && (_jsx(LoadingSpinner, { size: "sm", className: "border-primary-foreground border-t-transparent" })), isPending ? 'Saving...' : mode === 'create' ? 'Create' : 'Save'] }), _jsx(Button, { type: "button", variant: "secondary", onClick: () => router.push(`${basePath}/${urlKey}`), disabled: busy, children: "Cancel" })] }), mode === 'edit' && itemId && (_jsx(Button, { type: "button", variant: "destructive", onClick: () => setShowDeleteConfirm(true), disabled: busy, children: "Delete" }))] }), _jsx(ConfirmDialog, { isOpen: showDeleteConfirm, title: "Delete Item", message: "Are you sure you want to delete this item? This action cannot be undone.", confirmLabel: "Delete", cancelLabel: "Cancel", variant: "danger", onConfirm: handleDelete, onCancel: () => setShowDeleteConfirm(false) })] }));
|
|
147
74
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FieldRenderer.d.ts","sourceRoot":"","sources":["../../../src/components/fields/FieldRenderer.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAA;AAEhF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,uBAAuB,CAAA;IACpC,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;
|
|
1
|
+
{"version":3,"file":"FieldRenderer.d.ts","sourceRoot":"","sources":["../../../src/components/fields/FieldRenderer.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAA;AAEhF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,uBAAuB,CAAA;IACpC,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAwFD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,kDA+BtD"}
|
|
@@ -27,33 +27,37 @@ function FieldRendererInner({ Component, fieldName, fieldConfig, value, onChange
|
|
|
27
27
|
required: isRequired,
|
|
28
28
|
mode,
|
|
29
29
|
};
|
|
30
|
-
//
|
|
31
|
-
const specificProps =
|
|
32
|
-
if (fieldConfig.type === 'select' && 'options' in fieldConfig && fieldConfig.options) {
|
|
33
|
-
specificProps.options = fieldConfig.options.map((opt) => typeof opt === 'string' ? { label: opt, value: opt } : opt);
|
|
34
|
-
}
|
|
35
|
-
if (fieldConfig.type === 'password') {
|
|
36
|
-
specificProps.showConfirm = mode === 'edit';
|
|
37
|
-
}
|
|
38
|
-
if (fieldConfig.type === 'relationship') {
|
|
39
|
-
specificProps.items = relationshipItems;
|
|
40
|
-
specificProps.isLoading = relationshipLoading;
|
|
41
|
-
specificProps.many = fieldConfig.many || false;
|
|
42
|
-
// Extract related list key from ref (format: 'ListName.fieldName')
|
|
43
|
-
if (fieldConfig.ref) {
|
|
44
|
-
const [relatedListName] = fieldConfig.ref.split('.');
|
|
45
|
-
specificProps.relatedListKey = getUrlKey(relatedListName);
|
|
46
|
-
specificProps.basePath = basePath;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
30
|
+
// Derive field-type-specific props from data-presence checks — no branching on fieldConfig.type.
|
|
31
|
+
const specificProps = buildFallbackUIProps(fieldConfig, relationshipItems, relationshipLoading, basePath);
|
|
49
32
|
// Pass through any UI options from fieldConfig.ui (excluding component and fieldType)
|
|
50
33
|
if (fieldConfig.ui) {
|
|
51
|
-
const { _component, _fieldType, ...uiOptions } = fieldConfig.ui;
|
|
34
|
+
const { component: _component, fieldType: _fieldType, ...uiOptions } = fieldConfig.ui;
|
|
52
35
|
Object.assign(specificProps, uiOptions);
|
|
53
36
|
}
|
|
54
37
|
const allProps = { ...baseProps, ...specificProps };
|
|
55
38
|
return _jsx(Component, { ...allProps });
|
|
56
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Derive field-specific UI props from the serialised field config using
|
|
42
|
+
* data-presence checks rather than `fieldConfig.type` comparisons.
|
|
43
|
+
*/
|
|
44
|
+
function buildFallbackUIProps(fieldConfig, relationshipItems, relationshipLoading, basePath) {
|
|
45
|
+
const props = {};
|
|
46
|
+
// Select options — only present on select fields
|
|
47
|
+
if (fieldConfig.options) {
|
|
48
|
+
props.options = fieldConfig.options;
|
|
49
|
+
}
|
|
50
|
+
// Relationship props — only present on relationship fields
|
|
51
|
+
if (fieldConfig.ref) {
|
|
52
|
+
const [relatedListName] = fieldConfig.ref.split('.');
|
|
53
|
+
props.relatedListKey = getUrlKey(relatedListName ?? '');
|
|
54
|
+
props.many = fieldConfig.many || false;
|
|
55
|
+
props.items = relationshipItems;
|
|
56
|
+
props.isLoading = relationshipLoading;
|
|
57
|
+
props.basePath = basePath;
|
|
58
|
+
}
|
|
59
|
+
return props;
|
|
60
|
+
}
|
|
57
61
|
/**
|
|
58
62
|
* Factory component that renders the appropriate field type
|
|
59
63
|
* based on the field configuration and component registry
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/FileField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"FileField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/FileField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAMjE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,IAAI,GAAG,YAAY,GAAG,IAAI,CAAA;IACjC,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,YAAY,GAAG,IAAI,KAAK,IAAI,CAAA;IACrD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,QAAQ,EACR,WAA8C,GAC/C,EAAE,cAAc,2CA+KhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/ImageField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ImageField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/ImageField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAOlE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,IAAI,GAAG,aAAa,GAAG,IAAI,CAAA;IAClC,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,aAAa,GAAG,IAAI,KAAK,IAAI,CAAA;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,QAAQ,EACR,WAAgD,EAChD,WAAkB,EAClB,WAAiB,GAClB,EAAE,eAAe,2CAmRjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ItemCreateForm.d.ts","sourceRoot":"","sources":["../../../src/components/standalone/ItemCreateForm.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"ItemCreateForm.d.ts","sourceRoot":"","sources":["../../../src/components/standalone/ItemCreateForm.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAIvD,MAAM,WAAW,mBAAmB,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAClE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC9D,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,gBAAqB,EACrB,WAAsB,EACtB,WAAsB,EACtB,SAAS,GACV,EAAE,mBAAmB,CAAC,KAAK,CAAC,2CAmE5B"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
4
|
import { FieldRenderer } from '../fields/FieldRenderer.js';
|
|
5
5
|
import { LoadingSpinner } from '../LoadingSpinner.js';
|
|
6
6
|
import { Button } from '../../primitives/button.js';
|
|
7
7
|
import { serializeFieldConfigs } from '../../lib/serializeFieldConfig.js';
|
|
8
|
+
import { useItemForm } from '../../lib/useItemForm.js';
|
|
8
9
|
/**
|
|
9
10
|
* Standalone form component for creating items
|
|
10
11
|
* Can be embedded in any custom page
|
|
@@ -24,68 +25,16 @@ import { serializeFieldConfigs } from '../../lib/serializeFieldConfig.js';
|
|
|
24
25
|
export function ItemCreateForm({ fields, onSubmit, onCancel, relationshipData = {}, submitLabel = 'Create', cancelLabel = 'Cancel', className, }) {
|
|
25
26
|
// Serialize field configs to remove non-serializable properties
|
|
26
27
|
const serializedFields = useMemo(() => serializeFieldConfigs(fields), [fields]);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return newErrors;
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
const handleSubmit = async (e) => {
|
|
43
|
-
e.preventDefault();
|
|
44
|
-
setErrors({});
|
|
45
|
-
setGeneralError(null);
|
|
46
|
-
setIsPending(true);
|
|
47
|
-
try {
|
|
48
|
-
// Transform relationship fields to Prisma format
|
|
49
|
-
const transformedData = {};
|
|
50
|
-
for (const [fieldName, value] of Object.entries(formData)) {
|
|
51
|
-
const fieldConfig = serializedFields[fieldName];
|
|
52
|
-
// Transform relationship fields
|
|
53
|
-
if (fieldConfig?.type === 'relationship') {
|
|
54
|
-
if (fieldConfig.many) {
|
|
55
|
-
// Many relationship: use connect format
|
|
56
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
57
|
-
transformedData[fieldName] = {
|
|
58
|
-
connect: value.map((id) => ({ id })),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
// Single relationship: use connect format
|
|
64
|
-
if (value) {
|
|
65
|
-
transformedData[fieldName] = {
|
|
66
|
-
connect: { id: value },
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
// Non-relationship field: pass through
|
|
73
|
-
transformedData[fieldName] = value;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const result = await onSubmit(transformedData);
|
|
77
|
-
if (!result.success) {
|
|
78
|
-
setGeneralError(result.error || 'Failed to create item');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
setGeneralError(error.message || 'Failed to create item');
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
setIsPending(false);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
// Filter out system fields
|
|
89
|
-
const editableFields = Object.entries(serializedFields).filter(([key]) => !['id', 'createdAt', 'updatedAt'].includes(key));
|
|
28
|
+
const { formData, errors, generalError, isPending, editableFields, handleFieldChange, handleSubmit, } = useItemForm({
|
|
29
|
+
fields: serializedFields,
|
|
30
|
+
mode: 'create',
|
|
31
|
+
errorFallback: 'Failed to create item',
|
|
32
|
+
onSubmit: async (data) => {
|
|
33
|
+
const result = await onSubmit(data);
|
|
34
|
+
return result.success
|
|
35
|
+
? { success: true }
|
|
36
|
+
: { success: false, error: result.error || 'Failed to create item' };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
90
39
|
return (_jsxs("form", { onSubmit: handleSubmit, className: className, children: [generalError && (_jsx("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-4 mb-6", children: _jsx("p", { className: "text-sm font-medium", children: generalError }) })), _jsx("div", { className: "space-y-6", children: editableFields.map(([fieldName, fieldConfig]) => (_jsx(FieldRenderer, { fieldName: fieldName, fieldConfig: fieldConfig, value: formData[fieldName], onChange: (value) => handleFieldChange(fieldName, value), error: errors[fieldName], disabled: isPending, mode: "edit", relationshipItems: relationshipData[fieldName] || [], relationshipLoading: false }, fieldName))) }), _jsxs("div", { className: "flex gap-3 pt-6 mt-6 border-t border-border", children: [_jsxs(Button, { type: "submit", disabled: isPending, className: "gap-2", children: [isPending && (_jsx(LoadingSpinner, { size: "sm", className: "border-primary-foreground border-t-transparent" })), isPending ? 'Creating...' : submitLabel] }), onCancel && (_jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: isPending, children: cancelLabel }))] })] }));
|
|
91
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ItemEditForm.d.ts","sourceRoot":"","sources":["../../../src/components/standalone/ItemEditForm.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"ItemEditForm.d.ts","sourceRoot":"","sources":["../../../src/components/standalone/ItemEditForm.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAIvD,MAAM,WAAW,iBAAiB,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAChE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACnC,WAAW,EAAE,KAAK,CAAA;IAClB,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC5D,MAAM,EACN,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,gBAAqB,EACrB,WAAoB,EACpB,WAAsB,EACtB,SAAS,EACT,QAAmB,GACpB,EAAE,iBAAiB,CAAC,KAAK,CAAC,2CA2E1B"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
4
|
import { FieldRenderer } from '../fields/FieldRenderer.js';
|
|
5
5
|
import { LoadingSpinner } from '../LoadingSpinner.js';
|
|
6
6
|
import { Button } from '../../primitives/button.js';
|
|
7
7
|
import { serializeFieldConfigs } from '../../lib/serializeFieldConfig.js';
|
|
8
|
+
import { useItemForm, transformInitialData } from '../../lib/useItemForm.js';
|
|
8
9
|
/**
|
|
9
10
|
* Standalone form component for editing items
|
|
10
11
|
* Can be embedded in any custom page
|
|
@@ -26,87 +27,18 @@ export function ItemEditForm({ fields, initialData, onSubmit, onCancel, relation
|
|
|
26
27
|
// Serialize field configs to remove non-serializable properties
|
|
27
28
|
const serializedFields = useMemo(() => serializeFieldConfigs(fields), [fields]);
|
|
28
29
|
// Apply valueForClientSerialization transformations to initial data
|
|
29
|
-
const transformedInitialData = useMemo(() =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
}, [initialData, fields]);
|
|
43
|
-
const [isPending, setIsPending] = useState(false);
|
|
44
|
-
const [formData, setFormData] = useState(transformedInitialData);
|
|
45
|
-
const [errors, setErrors] = useState({});
|
|
46
|
-
const [generalError, setGeneralError] = useState(null);
|
|
47
|
-
const handleFieldChange = (fieldName, value) => {
|
|
48
|
-
setFormData((prev) => ({ ...prev, [fieldName]: value }));
|
|
49
|
-
// Clear error for this field when user starts typing
|
|
50
|
-
if (errors[fieldName]) {
|
|
51
|
-
setErrors((prev) => {
|
|
52
|
-
const newErrors = { ...prev };
|
|
53
|
-
delete newErrors[fieldName];
|
|
54
|
-
return newErrors;
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
const handleSubmit = async (e) => {
|
|
59
|
-
e.preventDefault();
|
|
60
|
-
setErrors({});
|
|
61
|
-
setGeneralError(null);
|
|
62
|
-
setIsPending(true);
|
|
63
|
-
try {
|
|
64
|
-
// Transform relationship fields to Prisma format
|
|
65
|
-
// Filter out password fields with isSet objects (unchanged passwords)
|
|
66
|
-
const transformedData = {};
|
|
67
|
-
for (const [fieldName, value] of Object.entries(formData)) {
|
|
68
|
-
const fieldConfig = serializedFields[fieldName];
|
|
69
|
-
// Skip password fields that have { isSet: boolean } value (not being changed)
|
|
70
|
-
if (typeof value === 'object' && value !== null && 'isSet' in value) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
// Transform relationship fields
|
|
74
|
-
if (fieldConfig?.type === 'relationship') {
|
|
75
|
-
if (fieldConfig.many) {
|
|
76
|
-
// Many relationship: use connect format
|
|
77
|
-
if (Array.isArray(value) && value.length > 0) {
|
|
78
|
-
transformedData[fieldName] = {
|
|
79
|
-
connect: value.map((id) => ({ id })),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
// Single relationship: use connect format
|
|
85
|
-
if (value) {
|
|
86
|
-
transformedData[fieldName] = {
|
|
87
|
-
connect: { id: value },
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
// Non-relationship field: pass through
|
|
94
|
-
transformedData[fieldName] = value;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const result = await onSubmit(transformedData);
|
|
98
|
-
if (!result.success) {
|
|
99
|
-
setGeneralError(result.error || 'Failed to update item');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
catch (error) {
|
|
103
|
-
setGeneralError(error.message || 'Failed to update item');
|
|
104
|
-
}
|
|
105
|
-
finally {
|
|
106
|
-
setIsPending(false);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
// Filter out system fields
|
|
110
|
-
const editableFields = Object.entries(serializedFields).filter(([key]) => !['id', 'createdAt', 'updatedAt'].includes(key));
|
|
30
|
+
const transformedInitialData = useMemo(() => transformInitialData(fields, initialData), [initialData, fields]);
|
|
31
|
+
const { formData, errors, generalError, isPending, editableFields, handleFieldChange, handleSubmit, } = useItemForm({
|
|
32
|
+
fields: serializedFields,
|
|
33
|
+
initialData: transformedInitialData,
|
|
34
|
+
mode: 'update',
|
|
35
|
+
errorFallback: 'Failed to update item',
|
|
36
|
+
onSubmit: async (data) => {
|
|
37
|
+
const result = await onSubmit(data);
|
|
38
|
+
return result.success
|
|
39
|
+
? { success: true }
|
|
40
|
+
: { success: false, error: result.error || 'Failed to update item' };
|
|
41
|
+
},
|
|
42
|
+
});
|
|
111
43
|
return (_jsxs("form", { onSubmit: handleSubmit, className: className, children: [generalError && (_jsx("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-4 mb-6", children: _jsx("p", { className: "text-sm font-medium", children: generalError }) })), _jsx("div", { className: "space-y-6", children: editableFields.map(([fieldName, fieldConfig]) => (_jsx(FieldRenderer, { fieldName: fieldName, fieldConfig: fieldConfig, value: formData[fieldName], onChange: (value) => handleFieldChange(fieldName, value), error: errors[fieldName], disabled: isPending, mode: "edit", relationshipItems: relationshipData[fieldName] || [], relationshipLoading: false, basePath: basePath }, fieldName))) }), _jsxs("div", { className: "flex gap-3 pt-6 mt-6 border-t border-border", children: [_jsxs(Button, { type: "submit", disabled: isPending, className: "gap-2", children: [isPending && (_jsx(LoadingSpinner, { size: "sm", className: "border-primary-foreground border-t-transparent" })), isPending ? 'Saving...' : submitLabel] }), onCancel && (_jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: isPending, children: cancelLabel }))] })] }));
|
|
112
44
|
}
|
package/dist/lib/theme.d.ts
CHANGED