@rakun-kit/manager-react 1.4.3 → 1.4.4

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.
@@ -1,91 +1,91 @@
1
- .EditorTheme__code {
2
- background-color: transparent;
3
- font-family: Menlo, Consolas, Monaco, monospace;
4
- display: block;
5
- padding: 8px 8px 8px 52px;
6
- line-height: 1.53;
7
- font-size: 13px;
8
- margin: 0;
9
- margin-top: 8px;
10
- margin-bottom: 8px;
11
- overflow-x: auto;
12
- border: 1px solid #ccc;
13
- position: relative;
14
- border-radius: 8px;
15
- tab-size: 2;
16
- }
17
- .EditorTheme__code:before {
18
- content: attr(data-gutter);
19
- position: absolute;
20
- background-color: transparent;
21
- border-right: 1px solid #ccc;
22
- left: 0;
23
- top: 0;
24
- padding: 8px;
25
- color: #777;
26
- white-space: pre-wrap;
27
- text-align: right;
28
- min-width: 25px;
29
- }
30
- .EditorTheme__table {
31
- border-collapse: collapse;
32
- border-spacing: 0;
33
- overflow-y: scroll;
34
- overflow-x: scroll;
35
- table-layout: fixed;
36
- width: fit-content;
37
- width: 100%;
38
- margin: 0px 0px 30px 0px;
39
- }
40
- .EditorTheme__tokenComment {
41
- color: slategray;
42
- }
43
- .EditorTheme__tokenPunctuation {
44
- color: #999;
45
- }
46
- .EditorTheme__tokenProperty {
47
- color: #905;
48
- }
49
- .EditorTheme__tokenSelector {
50
- color: #690;
51
- }
52
- .EditorTheme__tokenOperator {
53
- color: #9a6e3a;
54
- }
55
- .EditorTheme__tokenAttr {
56
- color: #07a;
57
- }
58
- .EditorTheme__tokenVariable {
59
- color: #e90;
60
- }
61
- .EditorTheme__tokenFunction {
62
- color: #dd4a68;
63
- }
64
-
65
- .Collapsible__container {
66
- background-color: var(--background);
67
- border: 1px solid #ccc;
68
- border-radius: 0.5rem;
69
- margin-bottom: 0.5rem;
70
- }
71
-
72
- .Collapsible__title{
73
- padding: 0.25rem;
74
- padding-left: 1rem;
75
- position: relative;
76
- font-weight: bold;
77
- outline: none;
78
- cursor: pointer;
79
- list-style-type: disclosure-closed;
80
- list-style-position: inside;
81
- }
82
-
83
- .Collapsible__title p{
84
- display: inline-flex;
85
- }
86
- .Collapsible__title::marker{
87
- color: lightgray;
88
- }
89
- .Collapsible__container[open] >.Collapsible__title {
90
- list-style-type: disclosure-open;
91
- }
1
+ .EditorTheme__code {
2
+ background-color: transparent;
3
+ font-family: Menlo, Consolas, Monaco, monospace;
4
+ display: block;
5
+ padding: 8px 8px 8px 52px;
6
+ line-height: 1.53;
7
+ font-size: 13px;
8
+ margin: 0;
9
+ margin-top: 8px;
10
+ margin-bottom: 8px;
11
+ overflow-x: auto;
12
+ border: 1px solid #ccc;
13
+ position: relative;
14
+ border-radius: 8px;
15
+ tab-size: 2;
16
+ }
17
+ .EditorTheme__code:before {
18
+ content: attr(data-gutter);
19
+ position: absolute;
20
+ background-color: transparent;
21
+ border-right: 1px solid #ccc;
22
+ left: 0;
23
+ top: 0;
24
+ padding: 8px;
25
+ color: #777;
26
+ white-space: pre-wrap;
27
+ text-align: right;
28
+ min-width: 25px;
29
+ }
30
+ .EditorTheme__table {
31
+ border-collapse: collapse;
32
+ border-spacing: 0;
33
+ overflow-y: scroll;
34
+ overflow-x: scroll;
35
+ table-layout: fixed;
36
+ width: fit-content;
37
+ width: 100%;
38
+ margin: 0px 0px 30px 0px;
39
+ }
40
+ .EditorTheme__tokenComment {
41
+ color: slategray;
42
+ }
43
+ .EditorTheme__tokenPunctuation {
44
+ color: #999;
45
+ }
46
+ .EditorTheme__tokenProperty {
47
+ color: #905;
48
+ }
49
+ .EditorTheme__tokenSelector {
50
+ color: #690;
51
+ }
52
+ .EditorTheme__tokenOperator {
53
+ color: #9a6e3a;
54
+ }
55
+ .EditorTheme__tokenAttr {
56
+ color: #07a;
57
+ }
58
+ .EditorTheme__tokenVariable {
59
+ color: #e90;
60
+ }
61
+ .EditorTheme__tokenFunction {
62
+ color: #dd4a68;
63
+ }
64
+
65
+ .Collapsible__container {
66
+ background-color: var(--background);
67
+ border: 1px solid #ccc;
68
+ border-radius: 0.5rem;
69
+ margin-bottom: 0.5rem;
70
+ }
71
+
72
+ .Collapsible__title{
73
+ padding: 0.25rem;
74
+ padding-left: 1rem;
75
+ position: relative;
76
+ font-weight: bold;
77
+ outline: none;
78
+ cursor: pointer;
79
+ list-style-type: disclosure-closed;
80
+ list-style-position: inside;
81
+ }
82
+
83
+ .Collapsible__title p{
84
+ display: inline-flex;
85
+ }
86
+ .Collapsible__title::marker{
87
+ color: lightgray;
88
+ }
89
+ .Collapsible__container[open] >.Collapsible__title {
90
+ list-style-type: disclosure-open;
91
+ }
@@ -127,10 +127,10 @@ function Sortable(props) {
127
127
  },
128
128
  }), [value]);
129
129
  const screenReaderInstructions = React.useMemo(() => ({
130
- draggable: `
131
- To pick up a sortable item, press space or enter.
132
- While dragging, use the ${orientation === "vertical" ? "up and down" : orientation === "horizontal" ? "left and right" : "arrow"} keys to move the item.
133
- Press space or enter again to drop the item in its new position, or press escape to cancel.
130
+ draggable: `
131
+ To pick up a sortable item, press space or enter.
132
+ While dragging, use the ${orientation === "vertical" ? "up and down" : orientation === "horizontal" ? "left and right" : "arrow"} keys to move the item.
133
+ Press space or enter again to drop the item in its new position, or press escape to cancel.
134
134
  `,
135
135
  }), [orientation]);
136
136
  const contextValue = React.useMemo(() => ({
@@ -1 +1 @@
1
- {"version":3,"file":"DynamicDataControl.d.ts","sourceRoot":"","sources":["../../../../../../../src/router/dashboard/[contentType]/[edit]/_fields/DynamicDataControl.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,uBAAuB,EAEvB,kBAAkB,EAClB,mBAAmB,EAGpB,MAAM,wBAAwB,CAAA;AA4F/B,eAAO,MAAM,qBAAqB,GAChC,aAAa,kBAAkB,EAC/B,OAAO,mBAAmB,YAM3B,CAAA;AAqvBD,eAAO,MAAM,kBAAkB,GAAI,qHAWhC;IACD,WAAW,EAAE,kBAAkB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,kBAAkB,CAAA;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,mBAAmB,CAAA;IAC1B,YAAY,EAAE,kBAAkB,EAAE,CAAA;IAClC,QAAQ,EAAE,uBAAuB,GAAG,SAAS,CAAA;IAC7C,QAAQ,EAAE,CAAC,QAAQ,EAAE,uBAAuB,GAAG,SAAS,KAAK,IAAI,CAAA;IACjE,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAA;CAC/C,mDAiOA,CAAA"}
1
+ {"version":3,"file":"DynamicDataControl.d.ts","sourceRoot":"","sources":["../../../../../../../src/router/dashboard/[contentType]/[edit]/_fields/DynamicDataControl.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,uBAAuB,EAIvB,kBAAkB,EAClB,mBAAmB,EAIpB,MAAM,wBAAwB,CAAA;AAuG/B,eAAO,MAAM,qBAAqB,GAChC,aAAa,kBAAkB,EAC/B,OAAO,mBAAmB,YAM3B,CAAA;AAwxCD,eAAO,MAAM,kBAAkB,GAAI,qHAWhC;IACD,WAAW,EAAE,kBAAkB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,kBAAkB,CAAA;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,mBAAmB,CAAA;IAC1B,YAAY,EAAE,kBAAkB,EAAE,CAAA;IAClC,QAAQ,EAAE,uBAAuB,GAAG,SAAS,CAAA;IAC7C,QAAQ,EAAE,CAAC,QAAQ,EAAE,uBAAuB,GAAG,SAAS,KAAK,IAAI,CAAA;IACjE,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAA;CAC/C,mDAiOA,CAAA"}
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { getListField, isDynamicDataSourceContentTypeAllowed, isTranslatableObject, } from '@rakun-kit/core/client';
4
- import { Cable, HelpCircle, Link2, ListFilter, X } from 'lucide-react';
4
+ import { Cable, ChevronRight, HelpCircle, Link2, ListFilter, X } from 'lucide-react';
5
5
  import { useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { Badge } from '../../../../../components/ui/badge.js';
7
7
  import { Button } from '../../../../../components/ui/button.js';
8
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '../../../../../components/ui/collapsible.js';
8
9
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../../../../components/ui/dialog.js';
9
10
  import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '../../../../../components/ui/select.js';
10
11
  import { Tooltip, TooltipContent, TooltipTrigger, } from '../../../../../components/ui/tooltip.js';
@@ -27,6 +28,7 @@ const cleanDynamicBindings = (bindings) => {
27
28
  };
28
29
  const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
29
30
  const isListField = (field) => field.config.ui === 'List' || field.config.ui === 'Iterator';
31
+ const isRelatedCollectionSource = (source) => !!source && 'kind' in source && source.kind === 'relatedCollection';
30
32
  const isDynamicVisibleField = (field) => (field.visibility ?? 'all') === 'all' && field.isDynamic !== false;
31
33
  export const isDynamicFieldEnabled = (contentType, field) => {
32
34
  return (contentType.dynamicData !== false &&
@@ -56,6 +58,8 @@ const getFieldKind = (field) => {
56
58
  }
57
59
  if (field.config.type === 'Relation')
58
60
  return 'object';
61
+ if (field.config.ui === 'SimpleList')
62
+ return 'array';
59
63
  if (isListField(field))
60
64
  return 'array';
61
65
  return 'unknown';
@@ -65,11 +69,31 @@ const isCompatibleSourceKind = (sourceKind, targetField) => {
65
69
  return sourceKind !== 'object' && sourceKind !== 'array';
66
70
  return sourceKind === getFieldKind(targetField);
67
71
  };
72
+ const getMultipleFileField = (field) => field.config.type === 'File' &&
73
+ field.isMultiple
74
+ ? field
75
+ : undefined;
76
+ const areFieldKindsCompatible = (sourceField, targetField) => {
77
+ const sourceKind = getFieldKind(sourceField);
78
+ if (!targetField)
79
+ return isCompatibleSourceKind(sourceKind);
80
+ const targetKind = getFieldKind(targetField);
81
+ if (sourceKind !== targetKind)
82
+ return false;
83
+ if (sourceKind !== 'array')
84
+ return true;
85
+ const sourceFile = getMultipleFileField(sourceField);
86
+ const targetFile = getMultipleFileField(targetField);
87
+ if (!sourceFile && !targetFile)
88
+ return true;
89
+ if (!sourceFile || !targetFile)
90
+ return false;
91
+ return (sourceFile.mediaType === 'Any' ||
92
+ targetFile.mediaType === 'Any' ||
93
+ sourceFile.mediaType === targetFile.mediaType);
94
+ };
68
95
  const fieldLabel = (path) => path.startsWith('_seo.') ? `seo.${path.slice('_seo.'.length)}` : path;
69
- const isSeoPath = (path) => path === '_seo' ||
70
- path === 'seo' ||
71
- path.startsWith('_seo.') ||
72
- path.startsWith('seo.');
96
+ const isSeoPath = (path) => path.split('.').some((segment) => segment === '_seo' || segment === 'seo');
73
97
  const fileFieldOptions = (path, targetField) => {
74
98
  const options = [
75
99
  { label: fieldLabel(`${path}.url`), value: `${path}.url`, kind: 'string' },
@@ -117,10 +141,22 @@ const nestedSourceFieldOptions = ({ contentType, prefix = '', targetField, depth
117
141
  depth: depth + 1,
118
142
  });
119
143
  }
144
+ if (field.config.type === 'File' &&
145
+ field.isMultiple) {
146
+ if (!areFieldKindsCompatible(field, targetField))
147
+ return [];
148
+ return [
149
+ {
150
+ label: fieldLabel(path),
151
+ value: path,
152
+ kind,
153
+ },
154
+ ];
155
+ }
120
156
  if (field.config.type === 'File') {
121
157
  return fileFieldOptions(path, targetField);
122
158
  }
123
- if (!isCompatibleSourceKind(kind, targetField))
159
+ if (!areFieldKindsCompatible(field, targetField))
124
160
  return [];
125
161
  return [
126
162
  {
@@ -145,6 +181,26 @@ const createSource = (contentType, value, id) => value === '$href'
145
181
  : { contentType, id, path: value };
146
182
  const sourceValue = (source) => source?.virtual === 'href' ? '$href' : source?.path || '';
147
183
  const sourceLabel = (source) => source?.virtual === 'href' ? 'href' : fieldLabel(source?.path || '');
184
+ const mappingSourceSummary = (source, currentSourceName) => {
185
+ if (!source) {
186
+ return {
187
+ mode: 'Not configured',
188
+ detail: 'Choose a mapping source',
189
+ };
190
+ }
191
+ if (!isRelatedCollectionSource(source)) {
192
+ return {
193
+ mode: 'Direct field',
194
+ detail: `${currentSourceName}.${sourceLabel(source)}`,
195
+ };
196
+ }
197
+ const sort = Object.entries(source.sort ?? {})[0];
198
+ const sortSummary = sort ? ` · sort ${sort[0]} ${sort[1]}` : '';
199
+ return {
200
+ mode: 'Related collection',
201
+ detail: `${source.contentType}.${source.relation} → ${currentSourceName} · collect ${source.path} · limit ${source.limit}${sortSummary}`,
202
+ };
203
+ };
148
204
  const sourceContentTypeLabel = (source, documentContentTypeName) => source.contentType === documentContentTypeName && !source.id
149
205
  ? 'Current document'
150
206
  : source.contentType;
@@ -212,6 +268,7 @@ const bindingSummary = ({ list, fieldBinding, listBinding, documentContentTypeNa
212
268
  return `${sourceContentTypeLabel(fieldBinding, documentContentTypeName)} -> ${sourceLabel(fieldBinding)}`;
213
269
  };
214
270
  const PanelSection = ({ title, children, }) => (_jsxs("div", { className: 'grid gap-3 rounded-md border border-border bg-muted/20 p-3', children: [_jsx("div", { className: 'text-xs font-semibold uppercase text-muted-foreground', children: title }), children] }));
271
+ const ControlLabel = ({ children, help, }) => (_jsxs("div", { className: 'flex items-center gap-1.5', children: [_jsx(Label, { children: children }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: 'button', className: 'rounded-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring', "aria-label": help, children: _jsx(HelpCircle, { className: 'h-3.5 w-3.5' }) }) }), _jsx(TooltipContent, { side: 'top', sideOffset: 6, className: 'max-w-xs', children: help })] })] }));
215
272
  const FieldBindingEditor = ({ contentTypes, documentContentType, currentDocumentSourceEnabled, targetField, binding, onChange, }) => {
216
273
  const { language } = useLanguage();
217
274
  const [sourceType, setSourceType] = useState(binding?.contentType || '');
@@ -241,6 +298,9 @@ const FieldBindingEditor = ({ contentTypes, documentContentType, currentDocument
241
298
  selectedContentType?.dynamicDataSource === true,
242
299
  });
243
300
  const items = (sourceItemsQuery.data?.items ?? []);
301
+ const hasCurrentDocumentItem = currentDocumentSourceEnabled && sourceType === documentContentType.name;
302
+ const hasItemOptions = hasCurrentDocumentItem || items.length > 0;
303
+ const fieldOptions = sourceFieldOptions(selectedContentType, targetField);
244
304
  const emit = (nextType = sourceType, nextId = sourceId, nextPath = fieldPath) => {
245
305
  const usesCurrentDocument = currentDocumentSourceEnabled &&
246
306
  nextType === documentContentType.name &&
@@ -251,7 +311,7 @@ const FieldBindingEditor = ({ contentTypes, documentContentType, currentDocument
251
311
  }
252
312
  onChange(createSource(nextType, nextPath, usesCurrentDocument ? undefined : nextId));
253
313
  };
254
- return (_jsx(PanelSection, { title: 'Source value', children: _jsxs("div", { className: 'grid gap-3 md:grid-cols-[1fr_1.4fr_1fr]', children: [_jsxs(Label, { className: 'grid gap-1.5', children: ["Source", _jsxs(Select, { value: sourceType, onValueChange: (value) => {
314
+ return (_jsx(PanelSection, { title: 'Source value', children: _jsxs("div", { className: 'grid gap-3 md:grid-cols-[1fr_1.4fr_1fr]', children: [_jsxs(Label, { className: 'grid gap-1.5', children: ["Source", _jsxs(Select, { disabled: contentTypes.length === 0, value: sourceType, onValueChange: (value) => {
255
315
  const nextId = currentDocumentSourceEnabled &&
256
316
  value === documentContentType.name
257
317
  ? CURRENT_DOCUMENT_ID
@@ -260,24 +320,23 @@ const FieldBindingEditor = ({ contentTypes, documentContentType, currentDocument
260
320
  setSourceId(nextId);
261
321
  setFieldPath('');
262
322
  onChange(undefined);
263
- }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Content type' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: contentTypes.map((ct) => (_jsx(SelectItem, { value: ct.name, children: currentDocumentSourceEnabled &&
323
+ }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: contentTypes.length > 0 ? 'Content type' : 'No content types' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: contentTypes.map((ct) => (_jsx(SelectItem, { value: ct.name, children: currentDocumentSourceEnabled &&
264
324
  ct.name === documentContentType.name
265
325
  ? `${ct.name} (current document)`
266
- : ct.name }, ct.name))) }) })] })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Item", _jsxs(Select, { disabled: !sourceType, value: sourceId, onValueChange: (value) => {
326
+ : ct.name }, ct.name))) }) })] })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Item", _jsxs(Select, { disabled: !sourceType || !hasItemOptions, value: sourceId, onValueChange: (value) => {
267
327
  setSourceId(value);
268
328
  emit(sourceType, value, fieldPath);
269
- }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Item' }) }), _jsx(SelectContent, { children: _jsxs(SelectGroup, { children: [currentDocumentSourceEnabled &&
270
- sourceType === documentContentType.name ? (_jsx(SelectItem, { value: CURRENT_DOCUMENT_ID, children: "Current document" })) : null, items.map((item) => {
329
+ }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: hasItemOptions ? 'Item' : 'No items available' }) }), _jsx(SelectContent, { children: _jsxs(SelectGroup, { children: [hasCurrentDocumentItem ? (_jsx(SelectItem, { value: CURRENT_DOCUMENT_ID, children: "Current document" })) : null, items.map((item) => {
271
330
  const label = selectedContentType
272
331
  ? getListField(item, selectedContentType.listFields ?? [])
273
332
  : item._id;
274
333
  return (_jsx(SelectItem, { value: item._id, children: isTranslatableObject(label)
275
334
  ? label[language.code]
276
335
  : String(label || item._id) }, item._id));
277
- })] }) })] })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Field", _jsxs(Select, { disabled: !sourceType || !sourceId, value: fieldPath, onValueChange: (value) => {
336
+ })] }) })] })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Field", _jsxs(Select, { disabled: !sourceType || !sourceId || fieldOptions.length === 0, value: fieldPath, onValueChange: (value) => {
278
337
  setFieldPath(value);
279
338
  emit(sourceType, sourceId, value);
280
- }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Field' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: sourceFieldOptions(selectedContentType, targetField).map((field) => (_jsx(SelectItem, { value: field.value, children: field.label }, field.value))) }) })] })] })] }) }));
339
+ }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: fieldOptions.length > 0 ? 'Field' : 'No compatible fields' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: fieldOptions.map((field) => (_jsx(SelectItem, { value: field.value, children: field.label }, field.value))) }) })] })] })] }) }));
281
340
  };
282
341
  const getListTargetContentType = (field, itemName) => {
283
342
  const entry = field.fields.find((item) => item.name === itemName);
@@ -285,15 +344,140 @@ const getListTargetContentType = (field, itemName) => {
285
344
  return undefined;
286
345
  return entry.field.contentType;
287
346
  };
347
+ const getRelationContentType = (field) => {
348
+ if (field.isTranslatable)
349
+ return undefined;
350
+ if (field.config.type === 'Relation') {
351
+ return field.contentType;
352
+ }
353
+ if (field.config.ui === 'SimpleList') {
354
+ const itemField = field.field;
355
+ if (itemField.config.type === 'Relation') {
356
+ return itemField.contentType;
357
+ }
358
+ }
359
+ return undefined;
360
+ };
361
+ const getRelatedRelationOptions = (contentType, currentSource) => Object.entries(contentType.fields).flatMap(([name, field]) => {
362
+ if (!isDynamicVisibleField(field))
363
+ return [];
364
+ const relationContentType = getRelationContentType(field);
365
+ return relationContentType?.name === currentSource.name ? [name] : [];
366
+ });
367
+ const getRelatedPathOptions = (contentType, targetField) => sourceFieldOptions(contentType, targetField).filter((option) => option.kind === 'array' && option.value !== '$href');
368
+ const getSortFieldOptions = (contentType) => Object.entries(contentType.fields).flatMap(([name, field]) => {
369
+ if (!isDynamicVisibleField(field) ||
370
+ field.isTranslatable ||
371
+ name.startsWith('_') ||
372
+ isSeoPath(name)) {
373
+ return [];
374
+ }
375
+ const kind = getFieldKind(field);
376
+ const isSingleSelect = field.config.type === 'Select' &&
377
+ !field.isMultiple;
378
+ const isSortable = (field.config.type === 'String' && field.config.ui !== 'RichText') ||
379
+ field.config.type === 'Number' ||
380
+ field.config.type === 'Boolean' ||
381
+ field.config.type === 'Date' ||
382
+ isSingleSelect;
383
+ return isSortable ? [{ label: fieldLabel(name), value: name, kind }] : [];
384
+ });
385
+ const createRelatedCollectionSource = ({ contentType, currentSource, targetField, }) => {
386
+ const relation = getRelatedRelationOptions(contentType, currentSource)[0];
387
+ const path = getRelatedPathOptions(contentType, targetField)[0]?.value;
388
+ if (!relation || !path)
389
+ return undefined;
390
+ return {
391
+ kind: 'relatedCollection',
392
+ contentType: contentType.name,
393
+ relation,
394
+ path,
395
+ limit: 10,
396
+ };
397
+ };
398
+ const MappingSourceEditor = ({ contentTypes, currentSource, targetField, source, onChange, }) => {
399
+ const relatedSource = isRelatedCollectionSource(source) ? source : undefined;
400
+ const directSource = source && !isRelatedCollectionSource(source) ? source : undefined;
401
+ const directFieldOptions = sourceFieldOptions(currentSource, targetField);
402
+ const relatedContentTypes = contentTypes.filter((contentType) => getRelatedRelationOptions(contentType, currentSource).length > 0 &&
403
+ getRelatedPathOptions(contentType, targetField).length > 0);
404
+ const selectedRelatedContentType = relatedSource
405
+ ? relatedContentTypes.find((contentType) => contentType.name === relatedSource.contentType)
406
+ : undefined;
407
+ const relationOptions = selectedRelatedContentType
408
+ ? getRelatedRelationOptions(selectedRelatedContentType, currentSource)
409
+ : [];
410
+ const pathOptions = selectedRelatedContentType
411
+ ? getRelatedPathOptions(selectedRelatedContentType, targetField)
412
+ : [];
413
+ const sortOptions = selectedRelatedContentType
414
+ ? getSortFieldOptions(selectedRelatedContentType)
415
+ : [];
416
+ const sortEntry = Object.entries(relatedSource?.sort ?? {})[0];
417
+ const sortField = sortEntry?.[0] ?? '';
418
+ const sortDirection = sortEntry?.[1] ?? 'desc';
419
+ if (!relatedSource) {
420
+ return (_jsxs("div", { className: 'grid gap-3 md:grid-cols-[0.7fr_1.3fr]', children: [_jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsx(ControlLabel, { help: 'Choose a direct field or query a related collection.', children: "Mapping mode" }), _jsxs(Select, { value: 'field', onValueChange: (value) => {
421
+ if (value !== 'relatedCollection')
422
+ return;
423
+ const next = relatedContentTypes
424
+ .map((contentType) => createRelatedCollectionSource({
425
+ contentType,
426
+ currentSource,
427
+ targetField,
428
+ }))
429
+ .find(Boolean);
430
+ onChange(next);
431
+ }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: 'field', children: "Direct field" }), relatedContentTypes.length > 0 ? (_jsx(SelectItem, { value: 'relatedCollection', children: "Related collection" })) : null] })] })] }), _jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsxs(ControlLabel, { help: 'Its value will be assigned to this mapped property.', children: ["Field on ", currentSource.name] }), _jsxs(Select, { disabled: directFieldOptions.length === 0, value: sourceValue(directSource), onValueChange: (value) => onChange(createSource(currentSource.name, value)), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: directFieldOptions.length > 0
432
+ ? 'Select a source field'
433
+ : 'No compatible fields' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: directFieldOptions.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) }) })] })] })] }));
434
+ }
435
+ return (_jsxs("div", { className: 'grid gap-3', children: [_jsxs("div", { className: 'grid gap-3 sm:grid-cols-2 lg:grid-cols-4', children: [_jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsx(ControlLabel, { help: 'Query records related to the current source item.', children: "Mapping mode" }), _jsxs(Select, { value: 'relatedCollection', onValueChange: () => onChange(undefined), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: 'field', children: "Direct field" }), _jsx(SelectItem, { value: 'relatedCollection', children: "Related collection" })] })] })] }), _jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsx(ControlLabel, { help: 'Content type whose records will be searched.', children: "Collection to query" }), _jsxs(Select, { disabled: relatedContentTypes.length === 0, value: relatedSource.contentType, onValueChange: (value) => {
436
+ const contentType = relatedContentTypes.find((item) => item.name === value);
437
+ if (!contentType)
438
+ return;
439
+ onChange(createRelatedCollectionSource({
440
+ contentType,
441
+ currentSource,
442
+ targetField,
443
+ }));
444
+ }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: relatedContentTypes.length > 0
445
+ ? 'Select a content type'
446
+ : 'No compatible collections' }) }), _jsx(SelectContent, { children: relatedContentTypes.map((contentType) => (_jsx(SelectItem, { value: contentType.name, children: contentType.name }, contentType.name))) })] })] }), _jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsxs(ControlLabel, { help: `Field on ${relatedSource.contentType} that points to ${currentSource.name}.`, children: ["Relation to ", currentSource.name] }), _jsxs(Select, { disabled: relationOptions.length === 0, value: relatedSource.relation, onValueChange: (relation) => onChange({ ...relatedSource, relation }), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: relationOptions.length > 0
447
+ ? 'Select a relation field'
448
+ : 'No compatible relations' }) }), _jsx(SelectContent, { children: relationOptions.map((relation) => (_jsx(SelectItem, { value: relation, children: relation }, relation))) })] })] }), _jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsx(ControlLabel, { help: 'Arrays from matching records are flattened into one result.', children: "Array field to collect" }), _jsxs(Select, { disabled: pathOptions.length === 0, value: relatedSource.path, onValueChange: (path) => onChange({ ...relatedSource, path }), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: pathOptions.length > 0
449
+ ? 'Select an array field'
450
+ : 'No compatible array fields' }) }), _jsx(SelectContent, { children: pathOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] })] })] }), _jsxs("div", { className: 'grid gap-3 sm:grid-cols-[0.7fr_1fr_0.7fr]', children: [_jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsx(ControlLabel, { help: `Number of ${relatedSource.contentType} records queried per ${currentSource.name}. Minimum 1, maximum 100.`, children: "Maximum related records" }), _jsx(Input, { type: 'number', min: 1, max: 100, value: String(relatedSource.limit), onChange: (event) => onChange({
451
+ ...relatedSource,
452
+ limit: Math.min(100, Math.max(1, Number(event.target.value || 10))),
453
+ }) })] }), _jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsx(ControlLabel, { help: 'Optional. Only direct scalar fields can be used.', children: "Sort related records by" }), _jsxs(Select, { value: sortField || '__none__', onValueChange: (value) => onChange({
454
+ ...relatedSource,
455
+ sort: value === '__none__'
456
+ ? undefined
457
+ : { [value]: sortDirection },
458
+ }), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'No sort' }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: '__none__', children: "No sort" }), sortOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value)))] })] })] }), _jsxs("div", { className: 'grid content-start gap-1.5', children: [_jsx(ControlLabel, { help: 'Applied before collecting and flattening the arrays.', children: "Sort direction" }), _jsxs(Select, { disabled: !sortField, value: sortDirection, onValueChange: (direction) => sortField
459
+ ? onChange({
460
+ ...relatedSource,
461
+ sort: { [sortField]: direction },
462
+ })
463
+ : undefined, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: 'asc', children: "Ascending" }), _jsx(SelectItem, { value: 'desc', children: "Descending" })] })] })] })] })] }));
464
+ };
288
465
  const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
289
466
  const [sourceType, setSourceType] = useState(binding?.contentType || '');
290
467
  const [itemName, setItemName] = useState(binding?.itemName || field.fields[0]?.name || '');
291
468
  const [filterState, setFilterState] = useState(readFilterState(binding?.query?.filter));
469
+ const [openMappingField, setOpenMappingField] = useState(null);
292
470
  const selectedSource = contentTypes.find((ct) => ct.name === sourceType);
293
471
  const targetContentType = getListTargetContentType(field, itemName);
294
472
  const targetFields = useMemo(() => targetContentType
295
473
  ? Object.entries(targetContentType.fields).filter(([, targetField]) => isDynamicVisibleField(targetField))
296
474
  : [], [targetContentType]);
475
+ const sortFieldOptions = selectedSource
476
+ ? getSortFieldOptions(selectedSource)
477
+ : [];
478
+ const filterFieldOptions = selectedSource
479
+ ? sourceFieldOptions(selectedSource).filter((item) => item.value !== '$href')
480
+ : [];
297
481
  const emit = (patch) => {
298
482
  const next = {
299
483
  contentType: sourceType,
@@ -317,13 +501,13 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
317
501
  },
318
502
  });
319
503
  };
320
- return (_jsxs("div", { className: 'grid gap-3', children: [_jsx(PanelSection, { title: 'Collection', children: _jsxs("div", { className: 'grid gap-3 md:grid-cols-2', children: [_jsxs(Label, { className: 'grid gap-1.5', children: ["Source", _jsxs(Select, { value: sourceType, onValueChange: (value) => {
504
+ return (_jsxs("div", { className: 'grid gap-3', children: [_jsx(PanelSection, { title: 'Collection', children: _jsxs("div", { className: 'grid gap-3 md:grid-cols-2', children: [_jsxs(Label, { className: 'grid gap-1.5', children: ["Source", _jsxs(Select, { disabled: contentTypes.length === 0, value: sourceType, onValueChange: (value) => {
321
505
  setSourceType(value);
322
506
  emit({ contentType: value, map: {} });
323
- }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Content type' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: contentTypes.map((ct) => (_jsx(SelectItem, { value: ct.name, children: ct.name }, ct.name))) }) })] })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Item", _jsxs(Select, { disabled: !sourceType, value: itemName, onValueChange: (value) => {
507
+ }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: contentTypes.length > 0 ? 'Content type' : 'No content types' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: contentTypes.map((ct) => (_jsx(SelectItem, { value: ct.name, children: ct.name }, ct.name))) }) })] })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Item", _jsxs(Select, { disabled: !sourceType || field.fields.length === 0, value: itemName, onValueChange: (value) => {
324
508
  setItemName(value);
325
509
  emit({ itemName: value, map: {} });
326
- }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Item type' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: field.fields.map((item) => (_jsx(SelectItem, { value: item.name, children: item.name }, item.name))) }) })] })] })] }) }), _jsx(PanelSection, { title: 'Query', children: _jsxs("div", { className: 'grid gap-3 md:grid-cols-[0.7fr_1fr_1.1fr]', children: [_jsxs(Label, { className: 'grid gap-1.5', children: ["Limit", _jsx(Input, { type: 'number', min: 1, value: String(binding?.query?.options?.limit ?? 10), onChange: (event) => emit({
510
+ }, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: field.fields.length > 0 ? 'Item type' : 'No item types' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: field.fields.map((item) => (_jsx(SelectItem, { value: item.name, children: item.name }, item.name))) }) })] })] })] }) }), _jsx(PanelSection, { title: 'Query', children: _jsxs("div", { className: 'grid gap-3 md:grid-cols-[0.7fr_1fr_1.1fr]', children: [_jsxs(Label, { className: 'grid gap-1.5', children: ["Limit", _jsx(Input, { type: 'number', min: 1, value: String(binding?.query?.options?.limit ?? 10), onChange: (event) => emit({
327
511
  query: {
328
512
  ...binding?.query,
329
513
  options: {
@@ -331,7 +515,7 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
331
515
  limit: Number(event.target.value || 10),
332
516
  },
333
517
  },
334
- }) })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Sort", _jsxs(Select, { disabled: !selectedSource, value: Object.keys(binding?.query?.options?.sort ?? {})[0] ?? '', onValueChange: (value) => emit({
518
+ }) })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Sort", _jsxs(Select, { disabled: sortFieldOptions.length === 0, value: Object.keys(binding?.query?.options?.sort ?? {})[0] ?? '', onValueChange: (value) => emit({
335
519
  query: {
336
520
  ...binding?.query,
337
521
  options: {
@@ -339,17 +523,17 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
339
523
  sort: value ? { [value]: 'desc' } : undefined,
340
524
  },
341
525
  },
342
- }), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Field' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: sourceFieldOptions(selectedSource)
343
- .filter((item) => item.value !== '$href')
344
- .map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) }) })] })] }), _jsxs("div", { className: 'grid gap-1.5', children: [_jsxs("div", { className: 'flex items-center justify-between gap-2', children: [_jsx("span", { className: 'text-sm font-medium', children: "Filter" }), _jsxs(Button, { type: 'button', size: 'icon', variant: 'ghost', className: 'size-7', onClick: () => updateFilter(undefined), children: [_jsx(X, { className: 'h-4 w-4' }), _jsx("span", { className: 'sr-only', children: "Clear filter" })] })] }), _jsxs("div", { className: 'grid gap-2 sm:grid-cols-[1fr_0.9fr_1fr]', children: [_jsxs(Select, { disabled: !selectedSource, value: filterState?.field || '', onValueChange: (value) => updateFilter(value
526
+ }), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: sortFieldOptions.length > 0
527
+ ? 'Field'
528
+ : 'No sortable fields' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: sortFieldOptions.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) }) })] })] }), _jsxs("div", { className: 'grid gap-1.5', children: [_jsxs("div", { className: 'flex items-center justify-between gap-2', children: [_jsx("span", { className: 'text-sm font-medium', children: "Filter" }), _jsxs(Button, { type: 'button', size: 'icon', variant: 'ghost', className: 'size-7', onClick: () => updateFilter(undefined), children: [_jsx(X, { className: 'h-4 w-4' }), _jsx("span", { className: 'sr-only', children: "Clear filter" })] })] }), _jsxs("div", { className: 'grid gap-2 sm:grid-cols-[1fr_0.9fr_1fr]', children: [_jsxs(Select, { disabled: filterFieldOptions.length === 0, value: filterState?.field || '', onValueChange: (value) => updateFilter(value
345
529
  ? {
346
530
  field: value,
347
531
  operator: filterState?.operator ?? 'equals',
348
532
  value: filterState?.value ?? '',
349
533
  }
350
- : undefined), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Field' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: sourceFieldOptions(selectedSource)
351
- .filter((item) => item.value !== '$href')
352
- .map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) }) })] }), _jsxs(Select, { disabled: !filterState?.field, value: filterState?.operator || 'equals', onValueChange: (value) => filterState
534
+ : undefined), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: filterFieldOptions.length > 0
535
+ ? 'Field'
536
+ : 'No filterable fields' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: filterFieldOptions.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) }) })] }), _jsxs(Select, { disabled: !filterState?.field, value: filterState?.operator || 'equals', onValueChange: (value) => filterState
353
537
  ? updateFilter({
354
538
  ...filterState,
355
539
  operator: value,
@@ -365,12 +549,18 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
365
549
  })
366
550
  : undefined }))] })] })] }) }), _jsx(PanelSection, { title: 'Mapping', children: _jsx("div", { className: 'grid gap-2', children: targetFields.map(([targetField, targetFieldConfig]) => {
367
551
  const source = binding?.map?.[targetField];
368
- return (_jsxs("div", { className: 'grid items-center gap-2 rounded-md border border-border bg-background/70 px-3 py-2 md:grid-cols-[minmax(8rem,0.7fr)_auto_minmax(12rem,1.3fr)]', children: [_jsx("span", { className: 'truncate text-sm font-medium', children: targetField }), _jsx("span", { className: 'hidden text-xs text-muted-foreground md:block', children: '->' }), _jsxs(Select, { disabled: !selectedSource, value: sourceValue(source), onValueChange: (value) => emit({
369
- map: {
370
- ...binding?.map,
371
- [targetField]: createSource(sourceType, value),
372
- },
373
- }), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Source field' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: sourceFieldOptions(selectedSource, targetFieldConfig).map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) }) })] })] }, targetField));
552
+ const summary = mappingSourceSummary(source, selectedSource?.name ?? 'Source');
553
+ const isOpen = openMappingField === targetField;
554
+ return (_jsx(Collapsible, { open: isOpen, onOpenChange: (open) => setOpenMappingField(open ? targetField : null), children: _jsxs("div", { className: 'overflow-hidden rounded-md border border-border bg-background/70 transition-colors hover:border-foreground/20', children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: 'button', disabled: !selectedSource, className: 'group grid w-full grid-cols-[minmax(6rem,0.45fr)_auto_minmax(0,1fr)_auto] items-center gap-3 px-3 py-2.5 text-left outline-none transition-colors hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', children: [_jsx("span", { className: 'truncate text-sm font-semibold', children: targetField }), _jsx(Badge, { variant: source ? 'secondary' : 'outline', className: 'max-w-40 truncate', children: summary.mode }), _jsx("span", { className: 'truncate text-sm text-muted-foreground', children: selectedSource
555
+ ? summary.detail
556
+ : 'Select a source collection first' }), _jsx(ChevronRight, { className: 'h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-90' }), _jsxs("span", { className: 'sr-only', children: ["Configure ", targetField, " mapping"] })] }) }), _jsx(CollapsibleContent, { children: _jsx("div", { className: 'border-t border-border bg-muted/20 p-3', children: selectedSource ? (_jsx(MappingSourceEditor, { contentTypes: contentTypes, currentSource: selectedSource, targetField: targetFieldConfig, source: source, onChange: (nextSource) => {
557
+ const map = { ...(binding?.map ?? {}) };
558
+ if (nextSource)
559
+ map[targetField] = nextSource;
560
+ else
561
+ delete map[targetField];
562
+ emit({ map });
563
+ } })) : null }) })] }) }, targetField));
374
564
  }) }) })] }));
375
565
  };
376
566
  export const DynamicDataControl = ({ contentType, documentContentType, fieldName, field, contentTypes, bindings, onChange, open, onOpenChange, mode = 'full', }) => {