@elytracms/studio 0.0.25 → 0.0.27
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/convex/cms.ts +49 -7
- package/convex/delivery.ts +14 -16
- package/convex/references.ts +38 -2
- package/convex/schema.ts +6 -1
- package/dist/components/assets/assets-store.js +41 -30
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/auth-context.d.ts +4 -2
- package/dist/components/auth-context.js +19 -19
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/cms/cms-store.js +5 -4
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +10 -3
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +36 -9
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +2 -1
- package/dist/components/cms/field-control.js +161 -73
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/versions-pane.js +28 -16
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/project-session.d.ts +11 -0
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/routes/routes-shell.js +22 -22
- package/dist/components/routes/routes-shell.js.map +1 -1
- package/dist/components/studio-context.js +5 -1
- package/dist/components/studio-context.js.map +1 -1
- package/dist/lib/cms/published-documents.d.ts +6 -6
- package/dist/lib/cms/published-documents.js +21 -58
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +7 -3
- package/dist/lib/cms-views/view-model.js +11 -5
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.js +7 -5
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/data/data-store.js +34 -24
- package/dist/lib/data/data-store.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +22 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.js +6 -2
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/query/index.d.ts +6 -0
- package/dist/lib/query/index.js +6 -0
- package/dist/lib/query/index.js.map +1 -0
- package/dist/lib/query/operations-query.d.ts +85 -0
- package/dist/lib/query/operations-query.js +124 -0
- package/dist/lib/query/operations-query.js.map +1 -0
- package/dist/lib/references/reference-model.js +7 -1
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +5 -10
- package/dist/lib/references/references-store.js +55 -77
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/ui/collapsible-section.d.ts +20 -0
- package/dist/ui/collapsible-section.js +18 -0
- package/dist/ui/collapsible-section.js.map +1 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/package.json +7 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { AlertTriangle, ChevronDown, ChevronUp, GripVertical, ImagePlus, Link2, Pencil, Plus, RotateCcw, SlidersHorizontal, Trash2, Upload, X } from 'lucide-react';
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { AlertTriangle, ChevronDown, ChevronUp, GripVertical, ImagePlus, Link2, MoreHorizontal, Pencil, Plus, RotateCcw, SlidersHorizontal, Trash2, Upload, X } from 'lucide-react';
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
5
5
|
import { BUILTIN_FIELD_CONTROLS, assetAcceptAttr, assetMatchesAccept, assetNode, assetSubfieldsOf, describeAccept, descendantIds, isFieldHidden, parentRelationField, readAssetId, } from '@elytracms/core/cms-core';
|
|
6
|
-
import { Badge, Button, Card, Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Field, Input, Row, Select, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
6
|
+
import { Badge, Button, Card, Code, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, Input, Row, Select, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
7
7
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
8
8
|
import { applyAssetSelection, assetIdsOf, singleAssetIdOf } from '../assets/asset-selection';
|
|
9
9
|
import { useOptionalAssets } from '../assets/assets-store';
|
|
@@ -148,45 +148,36 @@ export function objectItemSummary(fields, item, index) {
|
|
|
148
148
|
function truncateSummary(s, max = 60) {
|
|
149
149
|
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}) }));
|
|
151
|
+
// EC-305: nested-array depth, propagated via context so each array level can encode
|
|
152
|
+
// its depth (rail colour + tint) without prop-drilling through FieldControl's many
|
|
153
|
+
// branches. The detail editor renders top-level fields with the default depth 0.
|
|
154
|
+
const ArrayDepthContext = createContext(0);
|
|
155
|
+
const useArrayDepth = () => useContext(ArrayDepthContext);
|
|
156
|
+
// (B) Depth-keyed visuals: a coloured left rail + subtle tint so three levels deep
|
|
157
|
+
// no longer read as four identical borders. Literal classes so Tailwind emits them.
|
|
158
|
+
const DEPTH_RAIL = ['border-l-border', 'border-l-primary/60', 'border-l-info/60', 'border-l-warning/60'];
|
|
159
|
+
const DEPTH_TINT = ['', 'bg-muted/20', 'bg-muted/30', 'bg-muted/40'];
|
|
160
|
+
const depthRail = (d) => DEPTH_RAIL[Math.min(d, DEPTH_RAIL.length - 1)];
|
|
161
|
+
const depthTint = (d) => DEPTH_TINT[Math.min(d, DEPTH_TINT.length - 1)];
|
|
162
|
+
// (C) An object array renders as a compact TABLE when every subfield is a scalar (a
|
|
163
|
+
// flat row) and there are few enough columns to read; richer/nested object types stay
|
|
164
|
+
// as cards with depth encoding (B). Conditional (function-hidden) subfields force the
|
|
165
|
+
// card path — a table has no place to show/hide a column per row.
|
|
166
|
+
const TABULAR_FIELD_TYPES = new Set(['text', 'number', 'boolean', 'select', 'date']);
|
|
167
|
+
const MAX_TABLE_COLUMNS = 4;
|
|
168
|
+
export function isTabularObjectType(field) {
|
|
169
|
+
return (field.fields.length > 0 &&
|
|
170
|
+
field.fields.length <= MAX_TABLE_COLUMNS &&
|
|
171
|
+
field.fields.every((f) => TABULAR_FIELD_TYPES.has(f.type) && typeof f.form?.hidden !== 'function'));
|
|
173
172
|
}
|
|
174
173
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
174
|
+
* Shared array state for an object repeater (EC-253/EC-294/EC-305): the value-only
|
|
175
|
+
* items + a parallel stable-key list kept in lockstep through every structural op
|
|
176
|
+
* (the EC-253 C2 key↔value lock), add/remove/move, and native drag-reorder. Used by
|
|
177
|
+
* both the card list (B) and the table (C) so they behave identically.
|
|
179
178
|
*/
|
|
180
|
-
function
|
|
181
|
-
if (field.cardinality !== 'many') {
|
|
182
|
-
return (_jsx(Card, { className: "border-l-2 border-l-border p-3", children: _jsx(ObjectItemForm, { fields: field.fields, value: asRecord(value), readOnly: readOnly, idPrefix: id, onChange: onChange }) }));
|
|
183
|
-
}
|
|
179
|
+
function useObjectArray(field, value, onChange) {
|
|
184
180
|
const items = Array.isArray(value) ? value : [];
|
|
185
|
-
// Stable React keys for the value-only items (they carry no intrinsic id). Index
|
|
186
|
-
// keys would mis-associate a row's child/DOM state (an open relation/asset picker,
|
|
187
|
-
// focus) when rows are reordered or removed; we keep a parallel id list in lockstep
|
|
188
|
-
// with structural ops, reconciled by length when the array is replaced wholesale
|
|
189
|
-
// (e.g. switching documents). Edits keep the keys — only structure changes them.
|
|
190
181
|
const seq = useRef(0);
|
|
191
182
|
const keysRef = useRef([]);
|
|
192
183
|
if (keysRef.current.length !== items.length) {
|
|
@@ -197,8 +188,6 @@ function ObjectField({ field, value, onChange, readOnly, id, }) {
|
|
|
197
188
|
keysRef.current = nextKeys;
|
|
198
189
|
onChange(nextItems);
|
|
199
190
|
};
|
|
200
|
-
// The same (from,to) drives both the value array AND the parallel key array, so
|
|
201
|
-
// they stay aligned through a reorder (EC-253 C2 / EC-294).
|
|
202
191
|
const move = (from, to) => commit(reorder(items, from, to), reorder(keys, from, to));
|
|
203
192
|
const removeAt = (i) => commit(items.filter((_, j) => j !== i), keys.filter((_, j) => j !== i));
|
|
204
193
|
const addItem = () => {
|
|
@@ -206,25 +195,15 @@ function ObjectField({ field, value, onChange, readOnly, id, }) {
|
|
|
206
195
|
for (const sub of field.fields)
|
|
207
196
|
if (sub.default !== undefined)
|
|
208
197
|
seed[sub.name] = sub.default;
|
|
209
|
-
|
|
198
|
+
const key = seq.current++;
|
|
199
|
+
commit([...items, seed], [...keys, key]);
|
|
200
|
+
return key;
|
|
210
201
|
};
|
|
211
202
|
const setItem = (i, updated) => {
|
|
212
203
|
const nextItems = items.slice();
|
|
213
204
|
nextItems[i] = updated;
|
|
214
205
|
commit(nextItems, keys);
|
|
215
206
|
};
|
|
216
|
-
// EC-294: per-item collapse (ephemeral disclosure state keyed by the stable key —
|
|
217
|
-
// not persisted) + native drag-to-reorder. ▲/▼ stay as the keyboard/a11y fallback.
|
|
218
|
-
const [collapsedKeys, setCollapsedKeys] = useState(() => new Set());
|
|
219
|
-
const toggleCollapsed = (key) => setCollapsedKeys((prev) => {
|
|
220
|
-
const next = new Set(prev);
|
|
221
|
-
if (next.has(key))
|
|
222
|
-
next.delete(key);
|
|
223
|
-
else
|
|
224
|
-
next.add(key);
|
|
225
|
-
return next;
|
|
226
|
-
});
|
|
227
|
-
const allCollapsed = items.length > 0 && keys.every((k) => collapsedKeys.has(k));
|
|
228
207
|
const [dragIndex, setDragIndex] = useState(null);
|
|
229
208
|
const [overIndex, setOverIndex] = useState(null);
|
|
230
209
|
const endDrag = () => {
|
|
@@ -236,26 +215,135 @@ function ObjectField({ field, value, onChange, readOnly, id, }) {
|
|
|
236
215
|
move(dragIndex, to);
|
|
237
216
|
endDrag();
|
|
238
217
|
};
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
218
|
+
return { items, keys, move, removeAt, addItem, setItem, dragIndex, overIndex, setDragIndex, setOverIndex, endDrag, drop };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* (E) Chrome reduction: the per-row move/remove controls collapse into one hover/
|
|
222
|
+
* focus overflow menu, so a row reads as content, not a six-button toolbar. Drag is
|
|
223
|
+
* the primary reorder; the arrows stay here for keyboard/a11y.
|
|
224
|
+
*/
|
|
225
|
+
function ItemOverflowMenu({ index, count, onMove, onRemove, }) {
|
|
226
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", "aria-label": "Item actions", children: _jsx(MoreHorizontal, { className: "size-3.5" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { disabled: index === 0, onClick: () => onMove(index, index - 1), children: [_jsx(ChevronUp, {}), " Move up"] }), _jsxs(DropdownMenuItem, { disabled: index === count - 1, onClick: () => onMove(index, index + 1), children: [_jsx(ChevronDown, {}), " Move down"] }), _jsxs(DropdownMenuItem, { onClick: onRemove, children: [_jsx(Trash2, {}), " Remove"] })] })] }));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The nested subfield form for one object item (EC-253). Reuses {@link FieldControl}
|
|
230
|
+
* recursively, so any subfield type — including a nested object/repeater — works for
|
|
231
|
+
* free. Writing a subfield merges it back into the item and bubbles the new item up.
|
|
232
|
+
*/
|
|
233
|
+
function ObjectItemForm({ fields, value, onChange, readOnly, idPrefix, }) {
|
|
234
|
+
return (_jsx(Stack, { gap: "3", className: "mt-2", children: fields.map((sub) => {
|
|
235
|
+
// EC-273: a conditionally-hidden subfield (its predicate keys off the item's
|
|
236
|
+
// own sibling values) is omitted — e.g. a discriminated link's `href` shows
|
|
237
|
+
// only when `linkType === 'href'`. Re-evaluated as the item is edited.
|
|
238
|
+
if (isFieldHidden(sub, { values: value }))
|
|
239
|
+
return null;
|
|
240
|
+
const subId = `${idPrefix}-${sub.name}`;
|
|
241
|
+
return (_jsx(Field, { htmlFor: subId, hint: sub.form?.description, label: _jsxs(Row, { gap: "2", children: [_jsx("span", { children: fieldLabel(sub) }), sub.validation?.required ? _jsx("span", { className: "text-destructive", children: "*" }) : null] }), children: _jsx(FieldControl, { id: subId, field: sub, value: value[sub.name], readOnly: readOnly, onChange: (next) => {
|
|
242
|
+
const updated = { ...value };
|
|
243
|
+
if (next === undefined)
|
|
244
|
+
delete updated[sub.name];
|
|
245
|
+
else
|
|
246
|
+
updated[sub.name] = next;
|
|
247
|
+
onChange(updated);
|
|
248
|
+
} }) }, sub.name));
|
|
249
|
+
}) }));
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Object / repeater field control (EC-253), legibility-rebuilt (EC-305). A single
|
|
253
|
+
* object (`cardinality: 'one'`) renders a depth-encoded card; an array (`'many'`)
|
|
254
|
+
* dispatches by shape: a **flat** object type (all scalar subfields) → a compact
|
|
255
|
+
* editable TABLE (C); a richer/nested type → a card list with depth encoding (B),
|
|
256
|
+
* summary-first collapse (D), and reduced per-row chrome (E). Editing stays in the
|
|
257
|
+
* shared field form — no canvas escape (unlike `blocks`) — so props and document
|
|
258
|
+
* fields share this control. Depth flows via {@link ArrayDepthContext}.
|
|
259
|
+
*/
|
|
260
|
+
function ObjectField({ field, value, onChange, readOnly, id, }) {
|
|
261
|
+
const depth = useArrayDepth();
|
|
262
|
+
if (field.cardinality !== 'many') {
|
|
263
|
+
return (_jsx(Card, { className: cn('border-l-2 p-3', depthRail(depth), depthTint(depth)), children: _jsx(ArrayDepthContext.Provider, { value: depth + 1, children: _jsx(ObjectItemForm, { fields: field.fields, value: asRecord(value), readOnly: readOnly, idPrefix: id, onChange: onChange }) }) }));
|
|
264
|
+
}
|
|
265
|
+
const props = { field, value, onChange, readOnly, id, depth };
|
|
266
|
+
return isTabularObjectType(field) ? _jsx(ObjectTableField, { ...props }) : _jsx(ObjectCardList, { ...props });
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* (B + D + E) The card list for a nested/rich object array: depth-encoded cards
|
|
270
|
+
* (coloured rail + tint), items collapsed by default to a scannable summary outline
|
|
271
|
+
* (D), accordion expansion (open one → collapse siblings), and the per-row controls
|
|
272
|
+
* collapsed into one overflow menu (E). Drag-to-reorder; arrows live in the menu for
|
|
273
|
+
* a11y. Nested arrays render one depth deeper via the context provider.
|
|
274
|
+
*/
|
|
275
|
+
function ObjectCardList({ field, value, onChange, readOnly, id, depth }) {
|
|
276
|
+
const arr = useObjectArray(field, value, onChange);
|
|
277
|
+
const { items, keys } = arr;
|
|
278
|
+
// (D) Default COLLAPSED: track which keys are EXPANDED (empty = all collapsed).
|
|
279
|
+
// (B) Accordion: expanding one collapses the siblings.
|
|
280
|
+
const [expandedKeys, setExpandedKeys] = useState(() => new Set());
|
|
281
|
+
const toggle = (key) => setExpandedKeys((prev) => {
|
|
282
|
+
if (prev.has(key)) {
|
|
283
|
+
const next = new Set(prev);
|
|
284
|
+
next.delete(key);
|
|
285
|
+
return next;
|
|
286
|
+
}
|
|
287
|
+
return new Set([key]);
|
|
288
|
+
});
|
|
289
|
+
const allExpanded = items.length > 0 && keys.every((k) => expandedKeys.has(k));
|
|
290
|
+
const onAdd = () => setExpandedKeys(new Set([arr.addItem()]));
|
|
291
|
+
return (_jsx(ArrayDepthContext.Provider, { value: depth + 1, children: _jsxs(Stack, { gap: "2", children: [items.length > 1 && !readOnly ? (_jsx(Row, { justify: "end", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setExpandedKeys(allExpanded ? new Set() : new Set(keys)), children: allExpanded ? 'Collapse all' : 'Expand all' }) })) : null, items.length === 0 ? (_jsx(Text, { size: "sm", tone: "subtle", children: "No items" })) : (items.map((item, i) => {
|
|
292
|
+
// keysRef is reconciled to items.length, so keys[i] is always present;
|
|
293
|
+
// `?? i` only satisfies noUncheckedIndexedAccess (never taken at runtime).
|
|
294
|
+
const key = keys[i] ?? i;
|
|
295
|
+
const record = asRecord(item);
|
|
296
|
+
const isExpanded = expandedKeys.has(key);
|
|
297
|
+
return (_jsxs(Card, { onDragOver: (e) => {
|
|
298
|
+
if (arr.dragIndex === null)
|
|
299
|
+
return;
|
|
300
|
+
e.preventDefault();
|
|
301
|
+
if (arr.overIndex !== i)
|
|
302
|
+
arr.setOverIndex(i);
|
|
303
|
+
}, onDrop: () => arr.drop(i), className: cn('border-l-2 p-0', depthRail(depth), depthTint(depth), arr.dragIndex === i ? 'opacity-50' : '', arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i
|
|
304
|
+
? 'border-l-primary ring-1 ring-primary/40'
|
|
305
|
+
: ''), children: [_jsxs(Row, { className: "select-none border-b border-border/60 bg-muted/20 px-2 py-1.5", children: [!readOnly ? (_jsx("span", { draggable: true, onDragStart: (e) => {
|
|
306
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
307
|
+
e.dataTransfer.setData('text/plain', String(i));
|
|
308
|
+
arr.setDragIndex(i);
|
|
309
|
+
}, onDragEnd: arr.endDrag, "aria-label": "Drag to reorder", className: "cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing", children: _jsx(GripVertical, { className: "size-3.5" }) })) : null, _jsxs("button", { type: "button", onClick: () => toggle(key), "aria-expanded": isExpanded, className: "flex min-w-0 flex-1 items-center gap-1.5 text-left", children: [_jsx(ChevronDown, { className: cn('size-3.5 shrink-0 text-muted-foreground transition-transform', isExpanded ? '' : '-rotate-90'), "aria-hidden": true }), _jsx(Text, { size: "sm", className: "min-w-0 truncate font-medium", children: objectItemSummary(field.fields, record, i) })] }), !readOnly ? (_jsx(ItemOverflowMenu, { index: i, count: items.length, onMove: arr.move, onRemove: () => arr.removeAt(i) })) : null] }), isExpanded ? (_jsx("div", { className: "px-3 pb-3", children: _jsx(ObjectItemForm, { fields: field.fields, value: record, readOnly: readOnly, idPrefix: `${id}-${i}`, onChange: (updated) => arr.setItem(i, updated) }) })) : null] }, key));
|
|
310
|
+
})), !readOnly ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: onAdd, children: [_jsx(Plus, { className: "size-3" }), " Add item"] })) : null] }) }));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* (C) A compact editable table for a FLAT object array — columns are the scalar
|
|
314
|
+
* subfields, one row per item. Kills the card-stack sprawl for the commonest case
|
|
315
|
+
* (option lists, opening hours, social links). Drag-reorder + an overflow menu per
|
|
316
|
+
* row; cells reuse {@link FieldControl} so each subfield keeps its native control.
|
|
317
|
+
*/
|
|
318
|
+
function ObjectTableField({ field, value, onChange, readOnly, id, depth }) {
|
|
319
|
+
const arr = useObjectArray(field, value, onChange);
|
|
320
|
+
const { items, keys } = arr;
|
|
321
|
+
const cols = field.fields;
|
|
322
|
+
const span = cols.length + (readOnly ? 0 : 2);
|
|
323
|
+
return (_jsxs("div", { className: cn('overflow-hidden rounded-md border border-border border-l-2', depthRail(depth), depthTint(depth)), children: [_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border/60 bg-muted/30 text-left", children: [!readOnly ? _jsx("th", { className: "w-7" }) : null, cols.map((c) => (_jsxs("th", { className: "px-2 py-1.5 text-xs font-medium text-muted-foreground", children: [fieldLabel(c), c.validation?.required ? _jsx("span", { className: "text-destructive", children: " *" }) : null] }, c.name))), !readOnly ? _jsx("th", { className: "w-9" }) : null] }) }), _jsx("tbody", { children: items.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: span, className: "px-2 py-2 text-muted-foreground", children: _jsx(Text, { size: "sm", tone: "subtle", children: "No items" }) }) })) : (items.map((item, i) => {
|
|
324
|
+
const key = keys[i] ?? i;
|
|
325
|
+
const record = asRecord(item);
|
|
326
|
+
return (_jsxs("tr", { onDragOver: (e) => {
|
|
327
|
+
if (arr.dragIndex === null)
|
|
328
|
+
return;
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
if (arr.overIndex !== i)
|
|
331
|
+
arr.setOverIndex(i);
|
|
332
|
+
}, onDrop: () => arr.drop(i), className: cn('border-b border-border/40 align-top last:border-0', arr.dragIndex === i ? 'opacity-50' : '', arr.overIndex === i && arr.dragIndex !== null && arr.dragIndex !== i
|
|
333
|
+
? 'bg-primary/5'
|
|
334
|
+
: ''), children: [!readOnly ? (_jsx("td", { className: "px-1 py-1", children: _jsx("span", { draggable: true, onDragStart: (e) => {
|
|
335
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
336
|
+
e.dataTransfer.setData('text/plain', String(i));
|
|
337
|
+
arr.setDragIndex(i);
|
|
338
|
+
}, onDragEnd: arr.endDrag, "aria-label": "Drag to reorder", className: "inline-flex cursor-grab text-muted-foreground/50 hover:text-muted-foreground active:cursor-grabbing", children: _jsx(GripVertical, { className: "size-3.5" }) }) })) : null, cols.map((c) => (_jsx("td", { className: "px-2 py-1", children: _jsx(FieldControl, { id: `${id}-${i}-${c.name}`, field: c, value: record[c.name], readOnly: readOnly, onChange: (next) => {
|
|
339
|
+
const updated = { ...record };
|
|
340
|
+
if (next === undefined)
|
|
341
|
+
delete updated[c.name];
|
|
342
|
+
else
|
|
343
|
+
updated[c.name] = next;
|
|
344
|
+
arr.setItem(i, updated);
|
|
345
|
+
} }) }, c.name))), !readOnly ? (_jsx("td", { className: "px-1 py-1", children: _jsx(ItemOverflowMenu, { index: i, count: items.length, onMove: arr.move, onRemove: () => arr.removeAt(i) }) })) : null] }, key));
|
|
346
|
+
})) })] }), !readOnly ? (_jsx("div", { className: "border-t border-border/60 p-1.5", children: _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => arr.addItem(), children: [_jsx(Plus, { className: "size-3" }), " Add"] }) })) : null] }));
|
|
259
347
|
}
|
|
260
348
|
function RelationField({ field, value, onChange, readOnly, }) {
|
|
261
349
|
const cms = useCms();
|