@rakun-kit/manager-react 1.4.4 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/router/dashboard/[contentType]/[edit]/_fields/DynamicDataControl.js +369 -106
- package/dist/cjs/router/dashboard/[contentType]/[edit]/_fields/DynamicDataControl.js.map +1 -1
- package/dist/esm/router/dashboard/[contentType]/[edit]/_fields/DynamicDataControl.d.ts +21 -0
- package/dist/esm/router/dashboard/[contentType]/[edit]/_fields/DynamicDataControl.d.ts.map +1 -1
- package/dist/esm/router/dashboard/[contentType]/[edit]/_fields/DynamicDataControl.js +361 -98
- package/dist/esm/router/dashboard/[contentType]/[edit]/_fields/DynamicDataControl.js.map +1 -1
- package/dist/styles.css +11 -7
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { getListField, isDynamicDataSourceContentTypeAllowed, isTranslatableObject, } from '@rakun-kit/core/client';
|
|
4
|
-
import { Cable, ChevronRight, HelpCircle, Link2, ListFilter, X } from 'lucide-react';
|
|
3
|
+
import { DYNAMIC_QUERY_CURRENT_VALUE_KEY, getListField, isDynamicDataSourceContentTypeAllowed, isTranslatableObject, } from '@rakun-kit/core/client';
|
|
4
|
+
import { Cable, ChevronRight, HelpCircle, Link2, ListFilter, Plus, Trash2, 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';
|
|
@@ -69,10 +69,7 @@ const isCompatibleSourceKind = (sourceKind, targetField) => {
|
|
|
69
69
|
return sourceKind !== 'object' && sourceKind !== 'array';
|
|
70
70
|
return sourceKind === getFieldKind(targetField);
|
|
71
71
|
};
|
|
72
|
-
const
|
|
73
|
-
field.isMultiple
|
|
74
|
-
? field
|
|
75
|
-
: undefined;
|
|
72
|
+
const getFileField = (field) => field.config.type === 'File' ? field : undefined;
|
|
76
73
|
const areFieldKindsCompatible = (sourceField, targetField) => {
|
|
77
74
|
const sourceKind = getFieldKind(sourceField);
|
|
78
75
|
if (!targetField)
|
|
@@ -80,17 +77,16 @@ const areFieldKindsCompatible = (sourceField, targetField) => {
|
|
|
80
77
|
const targetKind = getFieldKind(targetField);
|
|
81
78
|
if (sourceKind !== targetKind)
|
|
82
79
|
return false;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const sourceFile = getMultipleFileField(sourceField);
|
|
86
|
-
const targetFile = getMultipleFileField(targetField);
|
|
80
|
+
const sourceFile = getFileField(sourceField);
|
|
81
|
+
const targetFile = getFileField(targetField);
|
|
87
82
|
if (!sourceFile && !targetFile)
|
|
88
83
|
return true;
|
|
89
84
|
if (!sourceFile || !targetFile)
|
|
90
85
|
return false;
|
|
91
|
-
return (sourceFile.
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
return (sourceFile.isMultiple === targetFile.isMultiple &&
|
|
87
|
+
(sourceFile.mediaType === 'Any' ||
|
|
88
|
+
targetFile.mediaType === 'Any' ||
|
|
89
|
+
sourceFile.mediaType === targetFile.mediaType));
|
|
94
90
|
};
|
|
95
91
|
const fieldLabel = (path) => path.startsWith('_seo.') ? `seo.${path.slice('_seo.'.length)}` : path;
|
|
96
92
|
const isSeoPath = (path) => path.split('.').some((segment) => segment === '_seo' || segment === 'seo');
|
|
@@ -134,27 +130,38 @@ const nestedSourceFieldOptions = ({ contentType, prefix = '', targetField, depth
|
|
|
134
130
|
return [];
|
|
135
131
|
if (field.config.type === 'Relation' && depth < 3) {
|
|
136
132
|
const relationContentType = field.contentType;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!areFieldKindsCompatible(field, targetField))
|
|
147
|
-
return [];
|
|
133
|
+
const idOption = isCompatibleSourceKind('string', targetField)
|
|
134
|
+
? [
|
|
135
|
+
{
|
|
136
|
+
label: fieldLabel(`${path}._id`),
|
|
137
|
+
value: `${path}._id`,
|
|
138
|
+
kind: 'string',
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
: [];
|
|
148
142
|
return [
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
143
|
+
...idOption,
|
|
144
|
+
...nestedSourceFieldOptions({
|
|
145
|
+
contentType: relationContentType,
|
|
146
|
+
prefix: path,
|
|
147
|
+
targetField,
|
|
148
|
+
depth: depth + 1,
|
|
149
|
+
}),
|
|
154
150
|
];
|
|
155
151
|
}
|
|
156
152
|
if (field.config.type === 'File') {
|
|
157
|
-
|
|
153
|
+
const fieldOption = areFieldKindsCompatible(field, targetField)
|
|
154
|
+
? [
|
|
155
|
+
{
|
|
156
|
+
label: fieldLabel(path),
|
|
157
|
+
value: path,
|
|
158
|
+
kind,
|
|
159
|
+
},
|
|
160
|
+
]
|
|
161
|
+
: [];
|
|
162
|
+
if (field.isMultiple)
|
|
163
|
+
return fieldOption;
|
|
164
|
+
return [...fieldOption, ...fileFieldOptions(path, targetField)];
|
|
158
165
|
}
|
|
159
166
|
if (!areFieldKindsCompatible(field, targetField))
|
|
160
167
|
return [];
|
|
@@ -166,7 +173,7 @@ const nestedSourceFieldOptions = ({ contentType, prefix = '', targetField, depth
|
|
|
166
173
|
},
|
|
167
174
|
];
|
|
168
175
|
});
|
|
169
|
-
const sourceFieldOptions = (contentType, targetField) => {
|
|
176
|
+
export const sourceFieldOptions = (contentType, targetField) => {
|
|
170
177
|
if (!contentType)
|
|
171
178
|
return [];
|
|
172
179
|
const fields = nestedSourceFieldOptions({ contentType, targetField });
|
|
@@ -205,17 +212,55 @@ const sourceContentTypeLabel = (source, documentContentTypeName) => source.conte
|
|
|
205
212
|
? 'Current document'
|
|
206
213
|
: source.contentType;
|
|
207
214
|
const getSourceContentTypes = (contentTypes) => contentTypes.filter((sourceContentType) => isDynamicDataSourceContentTypeAllowed(sourceContentType));
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
const operatorByMongoOperator = {
|
|
216
|
+
$eq: 'equals',
|
|
217
|
+
$ne: 'notEquals',
|
|
218
|
+
$contains: 'contains',
|
|
219
|
+
$in: 'in',
|
|
220
|
+
$nin: 'notIn',
|
|
221
|
+
$gt: 'greaterThan',
|
|
222
|
+
$gte: 'greaterThanOrEqual',
|
|
223
|
+
$lt: 'lessThan',
|
|
224
|
+
$lte: 'lessThanOrEqual',
|
|
225
|
+
};
|
|
226
|
+
const readFilterOperand = (value) => {
|
|
227
|
+
if (isRecord(value) &&
|
|
228
|
+
typeof value[DYNAMIC_QUERY_CURRENT_VALUE_KEY] === 'string') {
|
|
229
|
+
return {
|
|
230
|
+
value: value[DYNAMIC_QUERY_CURRENT_VALUE_KEY],
|
|
231
|
+
valueSource: 'current',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
value: Array.isArray(value) ? value.join(', ') : String(value ?? ''),
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
const filterConditionFromEntry = (field, value) => {
|
|
213
239
|
if (value === true)
|
|
214
240
|
return { field, operator: 'true', value: '' };
|
|
215
241
|
if (value === false)
|
|
216
242
|
return { field, operator: 'false', value: '' };
|
|
217
|
-
|
|
218
|
-
|
|
243
|
+
const directOperand = readFilterOperand(value);
|
|
244
|
+
if (directOperand.valueSource === 'current') {
|
|
245
|
+
return { field, operator: 'equals', ...directOperand };
|
|
246
|
+
}
|
|
247
|
+
if (isRecord(value)) {
|
|
248
|
+
if (typeof value.$exists === 'boolean') {
|
|
249
|
+
return {
|
|
250
|
+
field,
|
|
251
|
+
operator: value.$exists ? 'exists' : 'notExists',
|
|
252
|
+
value: '',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const operatorEntry = Object.entries(value).find(([operator]) => operator in operatorByMongoOperator);
|
|
256
|
+
if (operatorEntry) {
|
|
257
|
+
const [operator, operatorValue] = operatorEntry;
|
|
258
|
+
return {
|
|
259
|
+
field,
|
|
260
|
+
operator: operatorByMongoOperator[operator],
|
|
261
|
+
...readFilterOperand(operatorValue),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
219
264
|
}
|
|
220
265
|
return {
|
|
221
266
|
field,
|
|
@@ -223,31 +268,122 @@ const readFilterState = (filter) => {
|
|
|
223
268
|
value: typeof value === 'string' ? value : String(value ?? ''),
|
|
224
269
|
};
|
|
225
270
|
};
|
|
226
|
-
const
|
|
227
|
-
if (!
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
export const readFilterState = (filter) => {
|
|
272
|
+
if (!filter)
|
|
273
|
+
return { combinator: 'and', conditions: [] };
|
|
274
|
+
const logicalEntry = ['$and', '$or'].find((key) => Array.isArray(filter[key]));
|
|
275
|
+
if (logicalEntry) {
|
|
276
|
+
const conditions = filter[logicalEntry].flatMap((item) => isRecord(item)
|
|
277
|
+
? Object.entries(item)
|
|
278
|
+
.filter(([field]) => !field.startsWith('$'))
|
|
279
|
+
.map(([field, value]) => filterConditionFromEntry(field, value))
|
|
280
|
+
: []);
|
|
281
|
+
return {
|
|
282
|
+
combinator: logicalEntry === '$or' ? 'or' : 'and',
|
|
283
|
+
conditions,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
combinator: 'and',
|
|
288
|
+
conditions: Object.entries(filter)
|
|
289
|
+
.filter(([field]) => !field.startsWith('$'))
|
|
290
|
+
.map(([field, value]) => filterConditionFromEntry(field, value)),
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
const operatorNeedsValue = (operator) => !['true', 'false', 'exists', 'notExists'].includes(operator);
|
|
294
|
+
const parseFilterValue = (condition, fieldOptions) => {
|
|
295
|
+
const kind = fieldOptions.find((option) => option.value === condition.field)?.kind;
|
|
296
|
+
const parseValue = (value) => kind === 'number' && value.trim() !== '' && Number.isFinite(Number(value))
|
|
297
|
+
? Number(value)
|
|
298
|
+
: value.trim();
|
|
299
|
+
if (condition.operator === 'in' || condition.operator === 'notIn') {
|
|
300
|
+
return condition.value
|
|
301
|
+
.split(',')
|
|
302
|
+
.map((value) => value.trim())
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.map(parseValue);
|
|
305
|
+
}
|
|
306
|
+
return parseValue(condition.value);
|
|
307
|
+
};
|
|
308
|
+
const buildFilterCondition = (condition, fieldOptions) => {
|
|
309
|
+
if (!condition.field)
|
|
234
310
|
return undefined;
|
|
235
|
-
if (
|
|
236
|
-
return { [
|
|
311
|
+
if (condition.operator === 'true')
|
|
312
|
+
return { [condition.field]: true };
|
|
313
|
+
if (condition.operator === 'false')
|
|
314
|
+
return { [condition.field]: false };
|
|
315
|
+
if (condition.operator === 'exists') {
|
|
316
|
+
return { [condition.field]: { $exists: true } };
|
|
237
317
|
}
|
|
238
|
-
|
|
318
|
+
if (condition.operator === 'notExists') {
|
|
319
|
+
return { [condition.field]: { $exists: false } };
|
|
320
|
+
}
|
|
321
|
+
if (!condition.value.trim())
|
|
322
|
+
return undefined;
|
|
323
|
+
const value = condition.valueSource === 'current'
|
|
324
|
+
? { [DYNAMIC_QUERY_CURRENT_VALUE_KEY]: condition.value }
|
|
325
|
+
: parseFilterValue(condition, fieldOptions);
|
|
326
|
+
const mongoOperator = {
|
|
327
|
+
notEquals: '$ne',
|
|
328
|
+
contains: '$contains',
|
|
329
|
+
in: '$in',
|
|
330
|
+
notIn: '$nin',
|
|
331
|
+
greaterThan: '$gt',
|
|
332
|
+
greaterThanOrEqual: '$gte',
|
|
333
|
+
lessThan: '$lt',
|
|
334
|
+
lessThanOrEqual: '$lte',
|
|
335
|
+
};
|
|
336
|
+
const operator = mongoOperator[condition.operator];
|
|
337
|
+
return operator
|
|
338
|
+
? { [condition.field]: { [operator]: value } }
|
|
339
|
+
: { [condition.field]: value };
|
|
340
|
+
};
|
|
341
|
+
export const buildFilter = (state, fieldOptions = []) => {
|
|
342
|
+
const conditions = state.conditions.flatMap((condition) => {
|
|
343
|
+
const builtCondition = buildFilterCondition(condition, fieldOptions);
|
|
344
|
+
return builtCondition ? [builtCondition] : [];
|
|
345
|
+
});
|
|
346
|
+
if (conditions.length === 0)
|
|
347
|
+
return undefined;
|
|
348
|
+
if (conditions.length === 1)
|
|
349
|
+
return conditions[0];
|
|
350
|
+
return { [state.combinator === 'or' ? '$or' : '$and']: conditions };
|
|
239
351
|
};
|
|
240
352
|
const filterSummary = (filter) => {
|
|
241
353
|
const state = readFilterState(filter);
|
|
242
|
-
if (
|
|
354
|
+
if (state.conditions.length === 0)
|
|
243
355
|
return '';
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
356
|
+
const first = state.conditions[0];
|
|
357
|
+
const operatorLabels = {
|
|
358
|
+
equals: '=',
|
|
359
|
+
notEquals: '!=',
|
|
360
|
+
contains: 'contains',
|
|
361
|
+
in: 'in',
|
|
362
|
+
notIn: 'not in',
|
|
363
|
+
greaterThan: '>',
|
|
364
|
+
greaterThanOrEqual: '>=',
|
|
365
|
+
lessThan: '<',
|
|
366
|
+
lessThanOrEqual: '<=',
|
|
367
|
+
true: '= true',
|
|
368
|
+
false: '= false',
|
|
369
|
+
exists: 'is set',
|
|
370
|
+
notExists: 'is not set',
|
|
371
|
+
};
|
|
372
|
+
const firstSummary = [
|
|
373
|
+
first.field,
|
|
374
|
+
operatorLabels[first.operator],
|
|
375
|
+
operatorNeedsValue(first.operator)
|
|
376
|
+
? first.valueSource === 'current'
|
|
377
|
+
? `Current document.${first.value}`
|
|
378
|
+
: first.value
|
|
379
|
+
: '',
|
|
380
|
+
]
|
|
381
|
+
.filter(Boolean)
|
|
382
|
+
.join(' ');
|
|
383
|
+
const remaining = state.conditions.length - 1;
|
|
384
|
+
return remaining > 0
|
|
385
|
+
? `${firstSummary} + ${remaining} ${remaining === 1 ? 'condition' : 'conditions'}`
|
|
386
|
+
: firstSummary;
|
|
251
387
|
};
|
|
252
388
|
const bindingSummary = ({ list, fieldBinding, listBinding, documentContentTypeName, }) => {
|
|
253
389
|
if (list) {
|
|
@@ -382,6 +518,57 @@ const getSortFieldOptions = (contentType) => Object.entries(contentType.fields).
|
|
|
382
518
|
isSingleSelect;
|
|
383
519
|
return isSortable ? [{ label: fieldLabel(name), value: name, kind }] : [];
|
|
384
520
|
});
|
|
521
|
+
const filterOperatorOptions = {
|
|
522
|
+
equals: { label: 'Equals', value: 'equals' },
|
|
523
|
+
notEquals: { label: 'Does not equal', value: 'notEquals' },
|
|
524
|
+
contains: { label: 'Contains', value: 'contains' },
|
|
525
|
+
in: { label: 'Is one of', value: 'in' },
|
|
526
|
+
notIn: { label: 'Is not one of', value: 'notIn' },
|
|
527
|
+
greaterThan: { label: 'Greater than', value: 'greaterThan' },
|
|
528
|
+
greaterThanOrEqual: {
|
|
529
|
+
label: 'Greater than or equal',
|
|
530
|
+
value: 'greaterThanOrEqual',
|
|
531
|
+
},
|
|
532
|
+
lessThan: { label: 'Less than', value: 'lessThan' },
|
|
533
|
+
lessThanOrEqual: {
|
|
534
|
+
label: 'Less than or equal',
|
|
535
|
+
value: 'lessThanOrEqual',
|
|
536
|
+
},
|
|
537
|
+
true: { label: 'Is true', value: 'true' },
|
|
538
|
+
false: { label: 'Is false', value: 'false' },
|
|
539
|
+
exists: { label: 'Is set', value: 'exists' },
|
|
540
|
+
notExists: { label: 'Is not set', value: 'notExists' },
|
|
541
|
+
};
|
|
542
|
+
const getFilterOperatorOptions = (kind) => {
|
|
543
|
+
const operators = kind === 'boolean'
|
|
544
|
+
? ['true', 'false', 'exists', 'notExists']
|
|
545
|
+
: kind === 'number' || kind === 'date'
|
|
546
|
+
? [
|
|
547
|
+
'equals',
|
|
548
|
+
'notEquals',
|
|
549
|
+
'greaterThan',
|
|
550
|
+
'greaterThanOrEqual',
|
|
551
|
+
'lessThan',
|
|
552
|
+
'lessThanOrEqual',
|
|
553
|
+
'in',
|
|
554
|
+
'notIn',
|
|
555
|
+
'exists',
|
|
556
|
+
'notExists',
|
|
557
|
+
]
|
|
558
|
+
: kind === 'string' || kind === 'richText'
|
|
559
|
+
? [
|
|
560
|
+
'equals',
|
|
561
|
+
'notEquals',
|
|
562
|
+
'contains',
|
|
563
|
+
'in',
|
|
564
|
+
'notIn',
|
|
565
|
+
'exists',
|
|
566
|
+
'notExists',
|
|
567
|
+
]
|
|
568
|
+
: ['equals', 'notEquals', 'exists', 'notExists'];
|
|
569
|
+
return operators.map((operator) => filterOperatorOptions[operator]);
|
|
570
|
+
};
|
|
571
|
+
const defaultFilterOperator = (kind) => (kind === 'boolean' ? 'true' : 'equals');
|
|
385
572
|
const createRelatedCollectionSource = ({ contentType, currentSource, targetField, }) => {
|
|
386
573
|
const relation = getRelatedRelationOptions(contentType, currentSource)[0];
|
|
387
574
|
const path = getRelatedPathOptions(contentType, targetField)[0]?.value;
|
|
@@ -462,7 +649,7 @@ const MappingSourceEditor = ({ contentTypes, currentSource, targetField, source,
|
|
|
462
649
|
})
|
|
463
650
|
: 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
651
|
};
|
|
465
|
-
const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
|
|
652
|
+
const ListBindingEditor = ({ contentTypes, documentContentType, field, binding, onChange, }) => {
|
|
466
653
|
const [sourceType, setSourceType] = useState(binding?.contentType || '');
|
|
467
654
|
const [itemName, setItemName] = useState(binding?.itemName || field.fields[0]?.name || '');
|
|
468
655
|
const [filterState, setFilterState] = useState(readFilterState(binding?.query?.filter));
|
|
@@ -478,6 +665,15 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
|
|
|
478
665
|
const filterFieldOptions = selectedSource
|
|
479
666
|
? sourceFieldOptions(selectedSource).filter((item) => item.value !== '$href')
|
|
480
667
|
: [];
|
|
668
|
+
const currentDocumentFieldOptions = [
|
|
669
|
+
{ label: '_id', value: '_id', kind: 'string' },
|
|
670
|
+
...sourceFieldOptions(documentContentType).filter((item) => item.value !== '$href' &&
|
|
671
|
+
item.kind !== 'object' &&
|
|
672
|
+
item.kind !== 'array'),
|
|
673
|
+
];
|
|
674
|
+
const sortEntry = Object.entries(binding?.query?.options?.sort ?? {})[0];
|
|
675
|
+
const sortField = sortEntry?.[0] ?? '';
|
|
676
|
+
const sortDirection = sortEntry?.[1] ?? 'desc';
|
|
481
677
|
const emit = (patch) => {
|
|
482
678
|
const next = {
|
|
483
679
|
contentType: sourceType,
|
|
@@ -497,57 +693,124 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
|
|
|
497
693
|
emit({
|
|
498
694
|
query: {
|
|
499
695
|
...binding?.query,
|
|
500
|
-
filter: buildFilter(nextFilterState),
|
|
696
|
+
filter: buildFilter(nextFilterState, filterFieldOptions),
|
|
501
697
|
},
|
|
502
698
|
});
|
|
503
699
|
};
|
|
700
|
+
const updateFilterCondition = (index, nextCondition) => {
|
|
701
|
+
const conditions = [...filterState.conditions];
|
|
702
|
+
conditions[index] = nextCondition;
|
|
703
|
+
updateFilter({ ...filterState, conditions });
|
|
704
|
+
};
|
|
504
705
|
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) => {
|
|
505
706
|
setSourceType(value);
|
|
506
|
-
|
|
707
|
+
const nextFilterState = {
|
|
708
|
+
combinator: 'and',
|
|
709
|
+
conditions: [],
|
|
710
|
+
};
|
|
711
|
+
setFilterState(nextFilterState);
|
|
712
|
+
emit({
|
|
713
|
+
contentType: value,
|
|
714
|
+
map: {},
|
|
715
|
+
query: {
|
|
716
|
+
options: {
|
|
717
|
+
limit: binding?.query?.options?.limit ?? 10,
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
});
|
|
507
721
|
}, 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) => {
|
|
508
722
|
setItemName(value);
|
|
509
723
|
emit({ itemName: value, map: {} });
|
|
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))) }) })] })] })] }) }),
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
724
|
+
}, 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))) }) })] })] })] }) }), _jsxs(PanelSection, { title: 'Query', children: [_jsxs("div", { className: 'grid grid-cols-[minmax(5rem,0.4fr)_minmax(0,1.2fr)_minmax(8rem,0.6fr)] items-end gap-3', children: [_jsxs(Label, { className: 'grid gap-1.5', children: ["Limit", _jsx(Input, { type: 'number', min: 1, max: 100, value: String(binding?.query?.options?.limit ?? 10), onChange: (event) => emit({
|
|
725
|
+
query: {
|
|
726
|
+
...binding?.query,
|
|
727
|
+
options: {
|
|
728
|
+
...binding?.query?.options,
|
|
729
|
+
limit: Math.min(100, Math.max(1, Number(event.target.value || 10))),
|
|
730
|
+
},
|
|
516
731
|
},
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
732
|
+
}) })] }), _jsxs(Label, { className: 'grid min-w-0 gap-1.5', children: ["Sort by", _jsxs(Select, { disabled: sortFieldOptions.length === 0, value: sortField || '__none__', onValueChange: (value) => emit({
|
|
733
|
+
query: {
|
|
734
|
+
...binding?.query,
|
|
735
|
+
options: {
|
|
736
|
+
...binding?.query?.options,
|
|
737
|
+
sort: value === '__none__'
|
|
738
|
+
? undefined
|
|
739
|
+
: { [value]: sortDirection },
|
|
740
|
+
},
|
|
524
741
|
},
|
|
525
|
-
},
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
742
|
+
}), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: sortFieldOptions.length > 0
|
|
743
|
+
? 'Sort by field'
|
|
744
|
+
: 'No sortable fields' }) }), _jsx(SelectContent, { children: _jsxs(SelectGroup, { children: [_jsx(SelectItem, { value: '__none__', children: "No sort" }), sortFieldOptions.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value)))] }) })] })] }), _jsxs(Label, { className: 'grid gap-1.5', children: ["Direction", _jsxs(Select, { disabled: !sortField, value: sortDirection, onValueChange: (direction) => sortField
|
|
745
|
+
? emit({
|
|
746
|
+
query: {
|
|
747
|
+
...binding?.query,
|
|
748
|
+
options: {
|
|
749
|
+
...binding?.query?.options,
|
|
750
|
+
sort: {
|
|
751
|
+
[sortField]: direction,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
})
|
|
756
|
+
: 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" })] })] })] })] }), _jsxs("div", { className: 'grid gap-3 rounded-md border border-border bg-background/60 p-3', children: [_jsxs("div", { className: 'flex flex-wrap items-center justify-between gap-2', children: [_jsxs("div", { children: [_jsx("div", { className: 'text-sm font-medium', children: "Conditions" }), _jsx("div", { className: 'text-xs text-muted-foreground', children: "Filter the records used to build this list." })] }), _jsxs("div", { className: 'flex items-center gap-2', children: [filterState.conditions.length > 1 ? (_jsxs(Select, { value: filterState.combinator, onValueChange: (combinator) => updateFilter({
|
|
757
|
+
...filterState,
|
|
758
|
+
combinator: combinator,
|
|
759
|
+
}), children: [_jsx(SelectTrigger, { className: 'h-8 w-36', children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: 'and', children: "Match all" }), _jsx(SelectItem, { value: 'or', children: "Match any" })] })] })) : null, _jsxs(Button, { type: 'button', size: 'sm', variant: 'outline', disabled: filterFieldOptions.length === 0 ||
|
|
760
|
+
filterState.conditions.length >= 25, onClick: () => updateFilter({
|
|
538
761
|
...filterState,
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
})
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
762
|
+
conditions: [
|
|
763
|
+
...filterState.conditions,
|
|
764
|
+
{ field: '', operator: 'equals', value: '' },
|
|
765
|
+
],
|
|
766
|
+
}), children: [_jsx(Plus, { className: 'h-4 w-4' }), "Add condition"] })] })] }), filterState.conditions.length === 0 ? (_jsx("div", { className: 'rounded-md border border-dashed border-border px-3 py-5 text-center text-sm text-muted-foreground', children: "No conditions. All records from this collection will match." })) : (_jsx("div", { className: 'grid gap-2', children: filterState.conditions.map((condition, index) => {
|
|
767
|
+
const fieldOption = filterFieldOptions.find((option) => option.value === condition.field);
|
|
768
|
+
const operatorOptions = getFilterOperatorOptions(fieldOption?.kind);
|
|
769
|
+
const needsValue = operatorNeedsValue(condition.operator);
|
|
770
|
+
const currentValueOptions = currentDocumentFieldOptions.filter((option) => !fieldOption || option.kind === fieldOption.kind);
|
|
771
|
+
const allowsCurrentValue = condition.operator !== 'in' &&
|
|
772
|
+
condition.operator !== 'notIn' &&
|
|
773
|
+
currentValueOptions.length > 0;
|
|
774
|
+
return (_jsxs("div", { className: 'grid items-center gap-2 rounded-md border border-border bg-background p-2 sm:grid-cols-[1.2fr_1fr_1.8fr_auto]', children: [_jsxs(Select, { disabled: filterFieldOptions.length === 0, value: condition.field, onValueChange: (value) => {
|
|
775
|
+
const kind = filterFieldOptions.find((option) => option.value === value)?.kind;
|
|
776
|
+
updateFilterCondition(index, {
|
|
777
|
+
field: value,
|
|
778
|
+
operator: defaultFilterOperator(kind),
|
|
779
|
+
value: '',
|
|
780
|
+
});
|
|
781
|
+
}, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Field' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: filterFieldOptions.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) }) })] }), _jsxs(Select, { disabled: !condition.field, value: condition.operator, onValueChange: (operator) => updateFilterCondition(index, {
|
|
782
|
+
...condition,
|
|
783
|
+
operator: operator,
|
|
784
|
+
valueSource: operator === 'in' || operator === 'notIn'
|
|
785
|
+
? 'literal'
|
|
786
|
+
: condition.valueSource,
|
|
787
|
+
value: operatorNeedsValue(operator)
|
|
788
|
+
? operator === 'in' || operator === 'notIn'
|
|
789
|
+
? ''
|
|
790
|
+
: condition.value
|
|
791
|
+
: '',
|
|
792
|
+
}), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Operator' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: operatorOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) }) })] }), needsValue ? (_jsxs("div", { className: 'grid grid-cols-[minmax(7rem,0.7fr)_minmax(0,1fr)] gap-2', children: [_jsxs(Select, { value: condition.valueSource ?? 'literal', onValueChange: (valueSource) => updateFilterCondition(index, {
|
|
793
|
+
...condition,
|
|
794
|
+
valueSource: valueSource,
|
|
795
|
+
value: '',
|
|
796
|
+
}), children: [_jsx(SelectTrigger, { className: 'w-full', "aria-label": 'Value source', children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: 'literal', children: "Fixed value" }), allowsCurrentValue ? (_jsx(SelectItem, { value: 'current', children: "Current document" })) : null] })] }), condition.valueSource === 'current' ? (_jsxs(Select, { disabled: !condition.field, value: condition.value, onValueChange: (value) => updateFilterCondition(index, {
|
|
797
|
+
...condition,
|
|
798
|
+
value,
|
|
799
|
+
}), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Current field' }) }), _jsx(SelectContent, { children: _jsx(SelectGroup, { children: currentValueOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) }) })] })) : (_jsx(Input, { type: fieldOption?.kind === 'number' &&
|
|
800
|
+
condition.operator !== 'in' &&
|
|
801
|
+
condition.operator !== 'notIn'
|
|
802
|
+
? 'number'
|
|
803
|
+
: 'text', value: condition.value, disabled: !condition.field, placeholder: condition.operator === 'in' ||
|
|
804
|
+
condition.operator === 'notIn'
|
|
805
|
+
? 'Comma-separated values'
|
|
806
|
+
: 'Value', onChange: (event) => updateFilterCondition(index, {
|
|
807
|
+
...condition,
|
|
808
|
+
value: event.target.value,
|
|
809
|
+
}) }))] })) : (_jsx("div", { className: 'h-9 rounded-md border border-dashed border-border bg-muted/30' })), _jsxs(Button, { type: 'button', size: 'icon', variant: 'ghost', className: 'size-9 text-muted-foreground hover:text-destructive', onClick: () => updateFilter({
|
|
547
810
|
...filterState,
|
|
548
|
-
|
|
549
|
-
})
|
|
550
|
-
|
|
811
|
+
conditions: filterState.conditions.filter((_, conditionIndex) => conditionIndex !== index),
|
|
812
|
+
}), children: [_jsx(Trash2, { className: 'h-4 w-4' }), _jsx("span", { className: 'sr-only', children: "Remove condition" })] })] }, `${index}-${condition.field}`));
|
|
813
|
+
}) }))] })] }), _jsx(PanelSection, { title: 'Mapping', children: _jsx("div", { className: 'grid gap-2', children: targetFields.map(([targetField, targetFieldConfig]) => {
|
|
551
814
|
const source = binding?.map?.[targetField];
|
|
552
815
|
const summary = mappingSourceSummary(source, selectedSource?.name ?? 'Source');
|
|
553
816
|
const isOpen = openMappingField === targetField;
|
|
@@ -646,7 +909,7 @@ export const DynamicDataControl = ({ contentType, documentContentType, fieldName
|
|
|
646
909
|
setOpen(false);
|
|
647
910
|
};
|
|
648
911
|
const trigger = (_jsxs("div", { className: 'flex min-w-0 items-center gap-1.5', children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: 'button', className: 'min-w-0 cursor-pointer rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', onClick: () => setOpen((value) => !value), children: bound && summary ? (_jsxs(Badge, { variant: 'secondary', className: 'max-w-80 min-w-0 gap-1.5 rounded-md px-2 py-1 hover:bg-secondary/80', children: [list ? (_jsx(ListFilter, { className: 'h-3.5 w-3.5 shrink-0' })) : (_jsx(Link2, { className: 'h-3.5 w-3.5 shrink-0' })), _jsx("span", { className: 'truncate', children: summary })] })) : (_jsxs(Badge, { variant: 'outline', className: 'gap-1.5 rounded-md px-2 py-1 text-muted-foreground', children: [_jsx(Cable, { className: 'h-3.5 w-3.5' }), "Not linked"] })) }) }), _jsx(TooltipContent, { side: 'top', sideOffset: 6, children: triggerLabel })] }), bound ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { type: 'button', size: 'icon', variant: 'ghost', className: 'size-6', onClick: clearBinding, children: [_jsx(X, { className: 'h-4 w-4' }), _jsx("span", { className: 'sr-only', children: "Clear dynamic data" })] }) }), _jsx(TooltipContent, { side: 'top', sideOffset: 6, children: "Clear dynamic data link" })] })) : null] }));
|
|
649
|
-
const editor = list ? (_jsx(ListBindingEditor, { contentTypes: sourceContentTypes, field: field, binding: listBinding, onChange: updateListBinding })) : (_jsx(FieldBindingEditor, { contentTypes: fieldSourceContentTypes, documentContentType: currentDocumentContentType, currentDocumentSourceEnabled: currentDocumentSourceEnabled, targetField: field, binding: fieldBinding, onChange: updateFieldBinding }));
|
|
912
|
+
const editor = list ? (_jsx(ListBindingEditor, { contentTypes: sourceContentTypes, documentContentType: currentDocumentContentType, field: field, binding: listBinding, onChange: updateListBinding })) : (_jsx(FieldBindingEditor, { contentTypes: fieldSourceContentTypes, documentContentType: currentDocumentContentType, currentDocumentSourceEnabled: currentDocumentSourceEnabled, targetField: field, binding: fieldBinding, onChange: updateFieldBinding }));
|
|
650
913
|
const panel = isOpen ? (_jsx("div", { className: 'rounded-md border border-border bg-background p-4 shadow-sm', children: editor })) : null;
|
|
651
914
|
const dialog = (_jsx(Dialog, { open: isOpen, onOpenChange: (value) => {
|
|
652
915
|
if (value) {
|