@questpie/admin 3.2.7 → 3.3.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.
Files changed (79) hide show
  1. package/dist/client/components/blocks/block-editor-provider.mjs +13 -0
  2. package/dist/client/components/fields/array-field.mjs +105 -122
  3. package/dist/client/components/fields/asset-preview-field.mjs +1 -1
  4. package/dist/client/components/fields/blocks-field/blocks-field.mjs +1 -1
  5. package/dist/client/components/fields/boolean-field.mjs +1 -1
  6. package/dist/client/components/fields/date-field.mjs +1 -1
  7. package/dist/client/components/fields/datetime-field.mjs +1 -1
  8. package/dist/client/components/fields/email-field.mjs +1 -1
  9. package/dist/client/components/fields/field-wrapper.mjs +44 -15
  10. package/dist/client/components/fields/number-field.mjs +1 -1
  11. package/dist/client/components/fields/object-array-field.mjs +179 -149
  12. package/dist/client/components/fields/object-field.mjs +96 -87
  13. package/dist/client/components/fields/relation-picker.mjs +1 -1
  14. package/dist/client/components/fields/relation-select.mjs +1 -1
  15. package/dist/client/components/fields/rich-text-editor/index.mjs +1 -1
  16. package/dist/client/components/fields/select-field.mjs +1 -1
  17. package/dist/client/components/fields/text-field.mjs +1 -1
  18. package/dist/client/components/fields/textarea-field.mjs +1 -1
  19. package/dist/client/components/fields/time-field.mjs +1 -1
  20. package/dist/client/components/fields/upload-field.mjs +1 -1
  21. package/dist/client/components/history-sidebar.mjs +10 -4
  22. package/dist/client/components/structured-diff.mjs +367 -0
  23. package/dist/client/components/ui/sidebar.mjs +1 -1
  24. package/dist/client/hooks/use-field-options.mjs +34 -15
  25. package/dist/client/hooks/use-transition-stage.mjs +2 -2
  26. package/dist/client/utils/auto-expand-fields.mjs +1 -1
  27. package/dist/client/views/collection/auto-form-fields.mjs +23 -19
  28. package/dist/client/views/collection/cells/complex-cells.mjs +1 -1
  29. package/dist/client/views/collection/columns/build-columns.mjs +1 -1
  30. package/dist/client/views/collection/columns/column-defaults.mjs +17 -4
  31. package/dist/client/views/collection/field-renderer.mjs +19 -7
  32. package/dist/client/views/collection/form-view.mjs +10 -6
  33. package/dist/client/views/collection/table-view.mjs +19 -13
  34. package/dist/client/views/globals/global-form-view.mjs +47 -27
  35. package/dist/client/views/layout/admin-sidebar.mjs +2 -2
  36. package/dist/client.mjs +1 -1
  37. package/dist/index.mjs +1 -1
  38. package/dist/server/i18n/messages/cs.mjs +8 -0
  39. package/dist/server/i18n/messages/de.mjs +8 -0
  40. package/dist/server/i18n/messages/en.mjs +8 -0
  41. package/dist/server/i18n/messages/es.mjs +8 -0
  42. package/dist/server/i18n/messages/fr.mjs +8 -0
  43. package/dist/server/i18n/messages/pl.mjs +8 -0
  44. package/dist/server/i18n/messages/pt.mjs +8 -0
  45. package/dist/server/i18n/messages/sk.mjs +8 -0
  46. package/dist/server/modules/admin/collections/account.d.mts +50 -50
  47. package/dist/server/modules/admin/collections/admin-locks.d.mts +54 -54
  48. package/dist/server/modules/admin/collections/admin-preferences.d.mts +39 -39
  49. package/dist/server/modules/admin/collections/admin-saved-views.d.mts +47 -47
  50. package/dist/server/modules/admin/collections/apikey.d.mts +68 -68
  51. package/dist/server/modules/admin/collections/assets.d.mts +39 -39
  52. package/dist/server/modules/admin/collections/session.d.mts +42 -42
  53. package/dist/server/modules/admin/collections/user.d.mts +63 -63
  54. package/dist/server/modules/admin/collections/verification.d.mts +36 -36
  55. package/dist/server/modules/admin/routes/admin-config.d.mts +2 -2
  56. package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
  57. package/dist/server/modules/admin/routes/locales.d.mts +2 -2
  58. package/dist/server/modules/admin/routes/preview.d.mts +24 -19
  59. package/dist/server/modules/admin/routes/preview.mjs +83 -62
  60. package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
  61. package/dist/server/modules/admin/routes/setup.d.mts +7 -7
  62. package/dist/server/modules/admin/routes/translations.d.mts +4 -4
  63. package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
  64. package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +41 -41
  65. package/dist/server/modules/audit/.generated/module.d.mts +6 -6
  66. package/dist/server/modules/audit/collections/audit-log.d.mts +80 -78
  67. package/dist/server/modules/audit/collections/audit-log.mjs +7 -2
  68. package/dist/server/modules/audit/config/localize-title.mjs +67 -0
  69. package/dist/server/modules/audit/index.d.mts +2 -1
  70. package/dist/server/modules/audit/jobs/audit-cleanup.d.mts +2 -2
  71. package/dist/server/modules/audit/log-audit-entry.d.mts +85 -0
  72. package/dist/server/modules/audit/log-audit-entry.mjs +125 -0
  73. package/dist/server/plugin.mjs +4 -4
  74. package/dist/server.d.mts +2 -1
  75. package/dist/server.mjs +2 -1
  76. package/dist/shared/preview-utils.d.mts +4 -4
  77. package/dist/shared/preview-utils.mjs +5 -7
  78. package/package.json +3 -3
  79. package/dist/client/hooks/use-audit-history.mjs +0 -38
@@ -3,7 +3,7 @@ import { cn } from "../../lib/utils.mjs";
3
3
  import { selectAdmin, useAdminStore } from "../../runtime/provider.mjs";
4
4
  import { configureField } from "../../builder/field/field.mjs";
5
5
  import { Button } from "../ui/button.mjs";
6
- import { LocaleBadge } from "./locale-badge.mjs";
6
+ import { FieldCountAccessory, FieldWrapper } from "./field-wrapper.mjs";
7
7
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog.mjs";
8
8
  import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "../ui/sheet.mjs";
9
9
  import { gridColumnClasses } from "./field-utils.mjs";
@@ -19,7 +19,7 @@ import { useFieldArray, useFormContext, useWatch } from "react-hook-form";
19
19
  * Manages arrays of objects with inline, modal, or drawer editing.
20
20
  * Similar to EmbeddedCollectionField but with inline field definitions.
21
21
  */
22
- function ItemFieldRenderer({ fieldName, fieldDef, parentName, disabled }) {
22
+ const ItemFieldRenderer = React.memo(function ItemFieldRenderer$1({ fieldName, fieldDef, parentName, disabled }) {
23
23
  const resolveText = useResolveText();
24
24
  const fullName = `${parentName}.${fieldName}`;
25
25
  const options = fieldDef["~options"] || {};
@@ -41,8 +41,8 @@ function ItemFieldRenderer({ fieldName, fieldDef, parentName, disabled }) {
41
41
  locale,
42
42
  ...fieldSpecificOptions
43
43
  });
44
- }
45
- function ObjectArrayItemFields({ fieldEntries, layout, columns, name, index, disabled }) {
44
+ });
45
+ const ObjectArrayItemFields = React.memo(function ObjectArrayItemFields$1({ fieldEntries, layout, columns, name, index, disabled }) {
46
46
  if (fieldEntries.length === 0) return /* @__PURE__ */ jsx("div", {
47
47
  className: "py-2",
48
48
  children: /* @__PURE__ */ jsx("p", {
@@ -68,8 +68,108 @@ function ObjectArrayItemFields({ fieldEntries, layout, columns, name, index, dis
68
68
  className: "space-y-4",
69
69
  children: fieldElements
70
70
  });
71
+ });
72
+ function ObjectArrayItemLabel({ name, index, fallbackLabel, resolveItemLabel }) {
73
+ const { control } = useFormContext();
74
+ return /* @__PURE__ */ jsx(Fragment, { children: resolveItemLabel(useWatch({
75
+ control,
76
+ name: `${name}.${index}`
77
+ }), index) || fallbackLabel });
71
78
  }
72
- function ObjectArrayField({ name, label, description, placeholder, required, disabled, localized, locale, item: itemProp, mode = "inline", layout = "stack", columns = 2, itemLabel: itemLabelProp, orderable = false, minItems, maxItems }) {
79
+ const ObjectArrayItemRow = React.memo(function ObjectArrayItemRow$1({ index, totalCount, fieldEntries, layout, columns, name, mode, orderable, canRemove, disabled, itemFieldsDisabled, resolveItemLabel, onEdit, onMove, onRemove, t }) {
80
+ const canMoveUp = orderable && index > 0;
81
+ const canMoveDown = orderable && index < totalCount - 1;
82
+ return /* @__PURE__ */ jsxs("div", {
83
+ className: "panel-surface overflow-hidden",
84
+ children: [/* @__PURE__ */ jsxs("div", {
85
+ className: "border-border-subtle bg-surface-low flex items-center justify-between border-b px-3 py-2",
86
+ children: [/* @__PURE__ */ jsxs("div", {
87
+ className: "flex min-w-0 items-center gap-2",
88
+ children: [/* @__PURE__ */ jsxs("span", {
89
+ className: "text-muted-foreground shrink-0 text-xs tabular-nums",
90
+ children: ["#", index + 1]
91
+ }), /* @__PURE__ */ jsx("span", {
92
+ className: "min-w-0 truncate text-sm font-medium",
93
+ children: /* @__PURE__ */ jsx(ObjectArrayItemLabel, {
94
+ name,
95
+ index,
96
+ resolveItemLabel
97
+ })
98
+ })]
99
+ }), /* @__PURE__ */ jsxs("div", {
100
+ className: "flex shrink-0 items-center gap-1",
101
+ children: [
102
+ orderable && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
103
+ type: "button",
104
+ variant: "ghost",
105
+ size: "icon-sm",
106
+ className: "relative after:absolute after:-inset-1",
107
+ onClick: () => onMove(index, index - 1),
108
+ disabled: !canMoveUp || disabled,
109
+ title: t("field.moveUp"),
110
+ "aria-label": t("field.moveUp"),
111
+ children: /* @__PURE__ */ jsx(Icon, {
112
+ icon: "ph:caret-up",
113
+ className: "size-3.5"
114
+ })
115
+ }), /* @__PURE__ */ jsx(Button, {
116
+ type: "button",
117
+ variant: "ghost",
118
+ size: "icon-sm",
119
+ className: "relative after:absolute after:-inset-1",
120
+ onClick: () => onMove(index, index + 1),
121
+ disabled: !canMoveDown || disabled,
122
+ title: t("field.moveDown"),
123
+ "aria-label": t("field.moveDown"),
124
+ children: /* @__PURE__ */ jsx(Icon, {
125
+ icon: "ph:caret-down",
126
+ className: "size-3.5"
127
+ })
128
+ })] }),
129
+ mode !== "inline" && /* @__PURE__ */ jsx(Button, {
130
+ type: "button",
131
+ variant: "ghost",
132
+ size: "icon-sm",
133
+ className: "relative after:absolute after:-inset-1",
134
+ onClick: () => onEdit(index),
135
+ disabled,
136
+ title: t("common.edit"),
137
+ "aria-label": t("common.edit"),
138
+ children: /* @__PURE__ */ jsx(Icon, {
139
+ icon: "ph:pencil",
140
+ className: "size-3.5"
141
+ })
142
+ }),
143
+ canRemove && /* @__PURE__ */ jsx(Button, {
144
+ type: "button",
145
+ variant: "ghost",
146
+ size: "icon-sm",
147
+ className: "relative after:absolute after:-inset-1",
148
+ onClick: () => onRemove(index),
149
+ disabled,
150
+ title: t("common.remove"),
151
+ "aria-label": t("common.remove"),
152
+ children: /* @__PURE__ */ jsx(Icon, {
153
+ icon: "ph:trash",
154
+ className: "size-3.5"
155
+ })
156
+ })
157
+ ]
158
+ })]
159
+ }), mode === "inline" && /* @__PURE__ */ jsx("div", {
160
+ className: "p-3",
161
+ children: /* @__PURE__ */ jsx(ObjectArrayItemFields, {
162
+ fieldEntries,
163
+ layout,
164
+ columns,
165
+ name,
166
+ index,
167
+ disabled: itemFieldsDisabled
168
+ })
169
+ })]
170
+ });
171
+ });
172
+ function ObjectArrayField({ name, label, description, placeholder, required, disabled, readOnly, error, localized, locale, item: itemProp, mode = "inline", layout = "stack", columns = 2, itemLabel: itemLabelProp, orderable = false, minItems, maxItems }) {
73
173
  const { t } = useTranslation();
74
174
  const resolveText = useResolveText();
75
175
  const resolvedPlaceholder = placeholder ? resolveText(placeholder) : void 0;
@@ -79,10 +179,6 @@ function ObjectArrayField({ name, label, description, placeholder, required, dis
79
179
  control,
80
180
  name
81
181
  });
82
- const values = useWatch({
83
- control,
84
- name
85
- });
86
182
  const admin = useAdminStore(selectAdmin);
87
183
  const [activeIndex, setActiveIndex] = React.useState(null);
88
184
  const [isOpen, setIsOpen] = React.useState(false);
@@ -96,9 +192,10 @@ function ObjectArrayField({ name, label, description, placeholder, required, dis
96
192
  }
97
193
  return itemProp;
98
194
  }, [itemProp, admin]);
99
- const fieldEntries = Object.entries(itemFields);
100
- const canAddMore = !maxItems || fields.length < maxItems;
101
- const canRemove = !minItems || fields.length > minItems;
195
+ const fieldEntries = React.useMemo(() => Object.entries(itemFields), [itemFields]);
196
+ const withinMaxItems = !maxItems || fields.length < maxItems;
197
+ const canAddMore = !readOnly && withinMaxItems;
198
+ const canRemove = !readOnly && (!minItems || fields.length > minItems);
102
199
  const fallbackLabel = resolvedLabel || "Item";
103
200
  const emptyLabel = t("array.empty", { name: fallbackLabel });
104
201
  const addLabel = t("array.addItem", { name: fallbackLabel });
@@ -117,15 +214,23 @@ function ObjectArrayField({ name, label, description, placeholder, required, dis
117
214
  itemLabelProp,
118
215
  resolveText
119
216
  ]);
120
- const handleAdd = () => {
121
- if (disabled || !canAddMore) return;
217
+ const handleAdd = React.useCallback(() => {
218
+ if (disabled || readOnly || !canAddMore) return;
122
219
  append(Object.fromEntries(fieldEntries.map(([key]) => [key, void 0])));
123
220
  if (mode !== "inline") {
124
221
  setActiveIndex(fields.length);
125
222
  setIsOpen(true);
126
223
  }
127
- };
128
- const handleRemove = (index) => {
224
+ }, [
225
+ append,
226
+ canAddMore,
227
+ disabled,
228
+ readOnly,
229
+ fieldEntries,
230
+ fields.length,
231
+ mode
232
+ ]);
233
+ const handleRemove = React.useCallback((index) => {
129
234
  if (!canRemove) return;
130
235
  remove(index);
131
236
  if (activeIndex === null) return;
@@ -133,20 +238,32 @@ function ObjectArrayField({ name, label, description, placeholder, required, dis
133
238
  setIsOpen(false);
134
239
  setActiveIndex(null);
135
240
  } else if (index < activeIndex) setActiveIndex((prev) => prev !== null ? prev - 1 : null);
136
- };
137
- const handleMove = (from, to) => {
241
+ }, [
242
+ activeIndex,
243
+ canRemove,
244
+ remove
245
+ ]);
246
+ const handleMove = React.useCallback((from, to) => {
138
247
  if (to < 0 || to >= fields.length) return;
139
248
  move(from, to);
140
249
  if (activeIndex === null) return;
141
250
  if (activeIndex === from) setActiveIndex(to);
142
251
  else if (activeIndex === to) setActiveIndex(from);
143
- };
144
- const handleOpenChange = (open) => {
252
+ }, [
253
+ activeIndex,
254
+ fields.length,
255
+ move
256
+ ]);
257
+ const handleEdit = React.useCallback((index) => {
258
+ setActiveIndex(index);
259
+ setIsOpen(true);
260
+ }, []);
261
+ const handleOpenChange = React.useCallback((open) => {
145
262
  setIsOpen(open);
146
263
  if (!open) setActiveIndex(null);
147
- };
264
+ }, []);
148
265
  const emptyState = /* @__PURE__ */ jsx("div", {
149
- className: "py-2",
266
+ className: "panel-surface border-border-subtle border-dashed px-3 py-3",
150
267
  children: /* @__PURE__ */ jsx("p", {
151
268
  className: "text-muted-foreground text-sm text-pretty",
152
269
  children: resolvedPlaceholder || emptyLabel
@@ -158,137 +275,50 @@ function ObjectArrayField({ name, label, description, placeholder, required, dis
158
275
  columns,
159
276
  name,
160
277
  index: activeIndex,
161
- disabled
278
+ disabled: disabled || readOnly
162
279
  }) : null;
163
- const editorTitle = activeIndex !== null ? resolveItemLabel(values?.[activeIndex], activeIndex) : fallbackLabel;
280
+ const editorTitle = activeIndex !== null ? /* @__PURE__ */ jsx(ObjectArrayItemLabel, {
281
+ name,
282
+ index: activeIndex,
283
+ fallbackLabel,
284
+ resolveItemLabel
285
+ }) : fallbackLabel;
164
286
  const showEditor = mode === "modal" || mode === "drawer";
165
- return /* @__PURE__ */ jsxs("div", {
166
- className: "qa-object-array-field space-y-2",
287
+ return /* @__PURE__ */ jsxs(FieldWrapper, {
288
+ name,
289
+ label,
290
+ description,
291
+ required,
292
+ disabled,
293
+ readOnly,
294
+ error,
295
+ localized,
296
+ locale,
297
+ labelAccessory: /* @__PURE__ */ jsx(FieldCountAccessory, {
298
+ count: fields.length,
299
+ max: maxItems
300
+ }),
167
301
  children: [
168
- label && /* @__PURE__ */ jsxs("div", {
169
- className: "flex items-center gap-2",
170
- children: [/* @__PURE__ */ jsxs("label", {
171
- htmlFor: name,
172
- className: "text-sm font-medium",
173
- children: [
174
- resolvedLabel,
175
- required && /* @__PURE__ */ jsx("span", {
176
- className: "text-destructive",
177
- children: "*"
178
- }),
179
- maxItems && /* @__PURE__ */ jsxs("span", {
180
- className: "text-muted-foreground ml-2 text-xs tabular-nums",
181
- children: [
182
- "(",
183
- fields.length,
184
- "/",
185
- maxItems,
186
- ")"
187
- ]
188
- })
189
- ]
190
- }), localized && /* @__PURE__ */ jsx(LocaleBadge, { locale: locale || "i18n" })]
191
- }),
192
- description && /* @__PURE__ */ jsx("p", {
193
- className: "text-muted-foreground text-sm text-pretty",
194
- children: resolveText(description)
195
- }),
196
302
  /* @__PURE__ */ jsx("div", {
197
- className: "space-y-3",
198
- children: fields.length === 0 ? emptyState : fields.map((field, index) => {
199
- const itemValue = values?.[index];
200
- const itemLabel = resolveItemLabel(itemValue, index);
201
- const canMoveUp = orderable && index > 0;
202
- const canMoveDown = orderable && index < fields.length - 1;
203
- return /* @__PURE__ */ jsxs("div", {
204
- className: "panel-surface overflow-hidden",
205
- children: [/* @__PURE__ */ jsxs("div", {
206
- className: "border-border-subtle bg-surface-low flex items-center justify-between border-b px-3 py-2",
207
- children: [/* @__PURE__ */ jsxs("div", {
208
- className: "flex items-center gap-2",
209
- children: [/* @__PURE__ */ jsxs("span", {
210
- className: "text-muted-foreground text-xs tabular-nums",
211
- children: ["#", index + 1]
212
- }), /* @__PURE__ */ jsx("span", {
213
- className: "text-sm font-medium",
214
- children: itemLabel
215
- })]
216
- }), /* @__PURE__ */ jsxs("div", {
217
- className: "flex items-center gap-1",
218
- children: [
219
- orderable && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
220
- type: "button",
221
- variant: "ghost",
222
- size: "icon-sm",
223
- className: "relative after:absolute after:-inset-1",
224
- onClick: () => handleMove(index, index - 1),
225
- disabled: !canMoveUp || disabled,
226
- title: t("field.moveUp"),
227
- "aria-label": t("field.moveUp"),
228
- children: /* @__PURE__ */ jsx(Icon, {
229
- icon: "ph:caret-up",
230
- className: "size-3.5"
231
- })
232
- }), /* @__PURE__ */ jsx(Button, {
233
- type: "button",
234
- variant: "ghost",
235
- size: "icon-sm",
236
- className: "relative after:absolute after:-inset-1",
237
- onClick: () => handleMove(index, index + 1),
238
- disabled: !canMoveDown || disabled,
239
- title: t("field.moveDown"),
240
- "aria-label": t("field.moveDown"),
241
- children: /* @__PURE__ */ jsx(Icon, {
242
- icon: "ph:caret-down",
243
- className: "size-3.5"
244
- })
245
- })] }),
246
- mode !== "inline" && /* @__PURE__ */ jsx(Button, {
247
- type: "button",
248
- variant: "ghost",
249
- size: "icon-sm",
250
- className: "relative after:absolute after:-inset-1",
251
- onClick: () => {
252
- setActiveIndex(index);
253
- setIsOpen(true);
254
- },
255
- disabled,
256
- title: t("common.edit"),
257
- "aria-label": t("common.edit"),
258
- children: /* @__PURE__ */ jsx(Icon, {
259
- icon: "ph:pencil",
260
- className: "size-3.5"
261
- })
262
- }),
263
- canRemove && /* @__PURE__ */ jsx(Button, {
264
- type: "button",
265
- variant: "ghost",
266
- size: "icon-sm",
267
- className: "relative after:absolute after:-inset-1",
268
- onClick: () => handleRemove(index),
269
- disabled,
270
- title: t("common.remove"),
271
- "aria-label": t("common.remove"),
272
- children: /* @__PURE__ */ jsx(Icon, {
273
- icon: "ph:trash",
274
- className: "size-3.5"
275
- })
276
- })
277
- ]
278
- })]
279
- }), mode === "inline" && /* @__PURE__ */ jsx("div", {
280
- className: "p-3",
281
- children: /* @__PURE__ */ jsx(ObjectArrayItemFields, {
282
- fieldEntries,
283
- layout,
284
- columns,
285
- name,
286
- index,
287
- disabled
288
- })
289
- })]
290
- }, field.id);
291
- })
303
+ className: "qa-object-array-field space-y-3",
304
+ children: fields.length === 0 ? emptyState : fields.map((field, index) => /* @__PURE__ */ jsx(ObjectArrayItemRow, {
305
+ index,
306
+ totalCount: fields.length,
307
+ fieldEntries,
308
+ layout,
309
+ columns,
310
+ name,
311
+ mode,
312
+ orderable: orderable && !readOnly,
313
+ canRemove,
314
+ disabled,
315
+ itemFieldsDisabled: disabled || readOnly,
316
+ resolveItemLabel,
317
+ onEdit: handleEdit,
318
+ onMove: handleMove,
319
+ onRemove: handleRemove,
320
+ t
321
+ }, field.id))
292
322
  }),
293
323
  canAddMore && /* @__PURE__ */ jsxs(Button, {
294
324
  type: "button",
@@ -2,8 +2,9 @@ import { useResolveText } from "../../i18n/hooks.mjs";
2
2
  import { cn } from "../../lib/utils.mjs";
3
3
  import { selectAdmin, useAdminStore } from "../../runtime/provider.mjs";
4
4
  import { configureField } from "../../builder/field/field.mjs";
5
+ import { Button } from "../ui/button.mjs";
6
+ import { FieldLocaleIndicator, FieldWrapper } from "./field-wrapper.mjs";
5
7
  import { gridColumnClasses } from "./field-utils.mjs";
6
- import { FieldWrapper } from "./field-wrapper.mjs";
7
8
  import { FieldLayoutRenderer } from "../layout/field-layout-renderer.mjs";
8
9
  import { Icon } from "@iconify/react";
9
10
  import * as React from "react";
@@ -16,6 +17,46 @@ import { jsx, jsxs } from "react/jsx-runtime";
16
17
  * Renders nested fields for JSON object structures.
17
18
  * Supports wrapper modes (flat, collapsible) and layout modes (stack, inline, grid).
18
19
  */
20
+ function ObjectFieldPanel({ name, label, description, required, disabled, localized, locale, className, isCollapsed, onToggle, children }) {
21
+ const resolveText = useResolveText();
22
+ const resolvedLabel = resolveText(label ?? name);
23
+ return /* @__PURE__ */ jsxs("div", {
24
+ className: cn("qa-object-field panel-surface overflow-hidden", className),
25
+ children: [/* @__PURE__ */ jsxs(Button, {
26
+ type: "button",
27
+ variant: "ghost",
28
+ onClick: onToggle,
29
+ className: "h-auto min-h-10 w-full justify-between rounded-none px-3 py-2 text-left",
30
+ disabled,
31
+ children: [/* @__PURE__ */ jsxs("span", {
32
+ className: "flex min-w-0 items-center gap-2",
33
+ children: [
34
+ /* @__PURE__ */ jsx(Icon, {
35
+ icon: isCollapsed ? "ph:caret-right" : "ph:caret-down",
36
+ className: "size-4 shrink-0"
37
+ }),
38
+ /* @__PURE__ */ jsx("span", {
39
+ className: "min-w-0 truncate font-medium",
40
+ children: resolvedLabel
41
+ }),
42
+ required && /* @__PURE__ */ jsx("span", {
43
+ className: "text-destructive shrink-0",
44
+ children: "*"
45
+ })
46
+ ]
47
+ }), /* @__PURE__ */ jsx(FieldLocaleIndicator, {
48
+ localized,
49
+ locale
50
+ })]
51
+ }), !isCollapsed && /* @__PURE__ */ jsxs("div", {
52
+ className: "border-border-subtle space-y-4 border-t p-4",
53
+ children: [description && /* @__PURE__ */ jsx("p", {
54
+ className: "text-muted-foreground text-sm text-pretty",
55
+ children: resolveText(description)
56
+ }), children]
57
+ })]
58
+ });
59
+ }
19
60
  function NestedFieldRenderer({ fieldName, fieldDef, parentName, disabled }) {
20
61
  const resolveText = useResolveText();
21
62
  const fullName = `${parentName}.${fieldName}`;
@@ -25,7 +66,7 @@ function NestedFieldRenderer({ fieldName, fieldDef, parentName, disabled }) {
25
66
  className: "text-destructive text-sm",
26
67
  children: ["No component for field type: ", fieldDef.name]
27
68
  });
28
- const { label, description, placeholder, required, disabled: optionsDisabled, readOnly, hidden, localized, locale, ...fieldSpecificOptions } = options;
69
+ const { label, description, placeholder, required, disabled: optionsDisabled, readOnly, hidden: _hidden, localized, locale, ...fieldSpecificOptions } = options;
29
70
  return /* @__PURE__ */ jsx(Component, {
30
71
  name: fullName,
31
72
  label: resolveText(label),
@@ -43,6 +84,9 @@ function ObjectField({ name, label, description, required, disabled, localized,
43
84
  const resolveText = useResolveText();
44
85
  const admin = useAdminStore(selectAdmin);
45
86
  const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed);
87
+ const toggleCollapsed = React.useCallback(() => {
88
+ setIsCollapsed((current) => !current);
89
+ }, []);
46
90
  const nestedFields = React.useMemo(() => {
47
91
  if (!fieldsProp) return {};
48
92
  if (typeof fieldsProp === "function") {
@@ -53,56 +97,43 @@ function ObjectField({ name, label, description, required, disabled, localized,
53
97
  }
54
98
  return fieldsProp;
55
99
  }, [fieldsProp, admin]);
56
- const fieldEntries = Object.entries(nestedFields);
100
+ const fieldEntries = React.useMemo(() => Object.entries(nestedFields), [nestedFields]);
101
+ const layoutCtx = React.useMemo(() => ({
102
+ renderField: (fieldName) => {
103
+ const fieldDef = nestedFields[fieldName];
104
+ if (!fieldDef) return null;
105
+ return /* @__PURE__ */ jsx(NestedFieldRenderer, {
106
+ fieldName,
107
+ fieldDef,
108
+ parentName: name,
109
+ disabled
110
+ }, fieldName);
111
+ },
112
+ resolveText: (text, fallback) => resolveText(text, fallback)
113
+ }), [
114
+ disabled,
115
+ name,
116
+ nestedFields,
117
+ resolveText
118
+ ]);
57
119
  if (fieldEntries.length === 0) return null;
58
120
  if (formProp?.fields?.length) {
59
121
  const content = /* @__PURE__ */ jsx(FieldLayoutRenderer, {
60
122
  items: formProp.fields,
61
- ctx: {
62
- renderField: (fieldName, opts) => {
63
- const fieldDef = nestedFields[fieldName];
64
- if (!fieldDef) return null;
65
- return /* @__PURE__ */ jsx(NestedFieldRenderer, {
66
- fieldName,
67
- fieldDef,
68
- parentName: name,
69
- disabled
70
- }, fieldName);
71
- },
72
- resolveText: (text, fallback) => resolveText(text, fallback)
73
- }
123
+ ctx: layoutCtx
74
124
  });
75
- if (wrapper === "collapsible") return /* @__PURE__ */ jsxs("div", {
76
- className: cn("qa-object-field panel-surface", className),
77
- children: [/* @__PURE__ */ jsx("button", {
78
- type: "button",
79
- onClick: () => setIsCollapsed(!isCollapsed),
80
- className: "hover:bg-muted flex min-h-10 w-full items-center justify-between p-3 text-left transition-colors active:scale-[0.96]",
81
- disabled,
82
- children: /* @__PURE__ */ jsxs("div", {
83
- className: "flex items-center gap-2",
84
- children: [
85
- isCollapsed ? /* @__PURE__ */ jsx(Icon, {
86
- icon: "ph:caret-right",
87
- className: "h-4 w-4"
88
- }) : /* @__PURE__ */ jsx(Icon, {
89
- icon: "ph:caret-down",
90
- className: "h-4 w-4"
91
- }),
92
- /* @__PURE__ */ jsx("span", {
93
- className: "font-medium",
94
- children: resolveText(label ?? name)
95
- }),
96
- required && /* @__PURE__ */ jsx("span", {
97
- className: "text-destructive",
98
- children: "*"
99
- })
100
- ]
101
- })
102
- }), !isCollapsed && /* @__PURE__ */ jsx("div", {
103
- className: "border-t p-4",
104
- children: content
105
- })]
125
+ if (wrapper === "collapsible") return /* @__PURE__ */ jsx(ObjectFieldPanel, {
126
+ name,
127
+ label,
128
+ description,
129
+ required,
130
+ disabled,
131
+ localized,
132
+ locale,
133
+ className,
134
+ isCollapsed,
135
+ onToggle: toggleCollapsed,
136
+ children: content
106
137
  });
107
138
  if (label) return /* @__PURE__ */ jsx(FieldWrapper, {
108
139
  name,
@@ -122,46 +153,24 @@ function ObjectField({ name, label, description, required, disabled, localized,
122
153
  children: content
123
154
  });
124
155
  }
125
- if (wrapper === "collapsible" || layout === "collapsible") return /* @__PURE__ */ jsxs("div", {
126
- className: cn("qa-object-field panel-surface", className),
127
- children: [/* @__PURE__ */ jsx("button", {
128
- type: "button",
129
- onClick: () => setIsCollapsed(!isCollapsed),
130
- className: "hover:bg-muted flex min-h-10 w-full items-center justify-between p-3 text-left transition-colors active:scale-[0.96]",
131
- disabled,
132
- children: /* @__PURE__ */ jsxs("div", {
133
- className: "flex items-center gap-2",
134
- children: [
135
- isCollapsed ? /* @__PURE__ */ jsx(Icon, {
136
- icon: "ph:caret-right",
137
- className: "h-4 w-4"
138
- }) : /* @__PURE__ */ jsx(Icon, {
139
- icon: "ph:caret-down",
140
- className: "h-4 w-4"
141
- }),
142
- /* @__PURE__ */ jsx("span", {
143
- className: "font-medium",
144
- children: resolveText(label ?? name)
145
- }),
146
- required && /* @__PURE__ */ jsx("span", {
147
- className: "text-destructive",
148
- children: "*"
149
- })
150
- ]
151
- })
152
- }), !isCollapsed && /* @__PURE__ */ jsxs("div", {
153
- className: "border-t p-4",
154
- children: [description && /* @__PURE__ */ jsx("p", {
155
- className: "text-muted-foreground mb-4 text-sm text-pretty",
156
- children: resolveText(description)
157
- }), /* @__PURE__ */ jsx(NestedFieldsLayout, {
158
- fieldEntries,
159
- layout,
160
- columns,
161
- name,
162
- disabled
163
- })]
164
- })]
156
+ if (wrapper === "collapsible" || layout === "collapsible") return /* @__PURE__ */ jsx(ObjectFieldPanel, {
157
+ name,
158
+ label,
159
+ description,
160
+ required,
161
+ disabled,
162
+ localized,
163
+ locale,
164
+ className,
165
+ isCollapsed,
166
+ onToggle: toggleCollapsed,
167
+ children: /* @__PURE__ */ jsx(NestedFieldsLayout, {
168
+ fieldEntries,
169
+ layout,
170
+ columns,
171
+ name,
172
+ disabled
173
+ })
165
174
  });
166
175
  if (label) return /* @__PURE__ */ jsx(FieldWrapper, {
167
176
  name,
@@ -2,11 +2,11 @@ import { useResolveText, useTranslation } from "../../i18n/hooks.mjs";
2
2
  import { selectClient, useAdminStore } from "../../runtime/provider.mjs";
3
3
  import { resolveIconElement } from "../component-renderer.mjs";
4
4
  import { Button } from "../ui/button.mjs";
5
- import { LocaleBadge } from "./locale-badge.mjs";
6
5
  import { getAutoColumns } from "./field-utils.mjs";
7
6
  import { useAdminConfig } from "../../hooks/use-admin-config.mjs";
8
7
  import { SelectSingle } from "../primitives/select-single.mjs";
9
8
  import { ResourceSheet } from "../sheets/resource-sheet.mjs";
9
+ import { LocaleBadge } from "./locale-badge.mjs";
10
10
  import { RelationItemsDisplay } from "./relation/relation-items-display.mjs";
11
11
  import { Icon } from "@iconify/react";
12
12
  import * as React from "react";
@@ -2,13 +2,13 @@ import { useResolveText, useTranslation } from "../../i18n/hooks.mjs";
2
2
  import { cn } from "../../lib/utils.mjs";
3
3
  import { selectClient, useAdminStore } from "../../runtime/provider.mjs";
4
4
  import { resolveIconElement } from "../component-renderer.mjs";
5
- import { LocaleBadge } from "./locale-badge.mjs";
6
5
  import { useAdminConfig } from "../../hooks/use-admin-config.mjs";
7
6
  import { InputGroupAddon, InputGroupButton } from "../ui/input-group.mjs";
8
7
  import { FieldSelectActionGroup } from "../primitives/field-select-control.mjs";
9
8
  import { SelectSingle } from "../primitives/select-single.mjs";
10
9
  import { useCollectionItem } from "../../hooks/use-collection.mjs";
11
10
  import { ResourceSheet } from "../sheets/resource-sheet.mjs";
11
+ import { LocaleBadge } from "./locale-badge.mjs";
12
12
  import { Icon } from "@iconify/react";
13
13
  import * as React from "react";
14
14
  import { useQueryClient } from "@tanstack/react-query";
@@ -1,9 +1,9 @@
1
1
  import { useResolveText, useTranslation } from "../../../i18n/hooks.mjs";
2
2
  import { cn } from "../../../lib/utils.mjs";
3
- import { LocaleBadge } from "../locale-badge.mjs";
4
3
  import { Label } from "../../ui/label.mjs";
5
4
  import { Skeleton } from "../../ui/skeleton.mjs";
6
5
  import { isModifierShortcut } from "../../../utils/keyboard-shortcuts.mjs";
6
+ import { LocaleBadge } from "../locale-badge.mjs";
7
7
  import { createLinkAttributes, isLikelyLinkHref } from "./link-utils.mjs";
8
8
  import { RichTextToolbar } from "./toolbar.mjs";
9
9
  import { RichTextBubbleMenu } from "./bubble-menu.mjs";