@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.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  'use client';
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.DynamicDataControl = exports.isDynamicFieldEnabled = void 0;
4
+ exports.DynamicDataControl = exports.buildFilter = exports.readFilterState = exports.sourceFieldOptions = exports.isDynamicFieldEnabled = void 0;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const client_1 = require("@rakun-kit/core/client");
7
7
  const lucide_react_1 = require("lucide-react");
@@ -73,10 +73,7 @@ const isCompatibleSourceKind = (sourceKind, targetField) => {
73
73
  return sourceKind !== 'object' && sourceKind !== 'array';
74
74
  return sourceKind === getFieldKind(targetField);
75
75
  };
76
- const getMultipleFileField = (field) => field.config.type === 'File' &&
77
- field.isMultiple
78
- ? field
79
- : undefined;
76
+ const getFileField = (field) => field.config.type === 'File' ? field : undefined;
80
77
  const areFieldKindsCompatible = (sourceField, targetField) => {
81
78
  const sourceKind = getFieldKind(sourceField);
82
79
  if (!targetField)
@@ -84,23 +81,19 @@ const areFieldKindsCompatible = (sourceField, targetField) => {
84
81
  const targetKind = getFieldKind(targetField);
85
82
  if (sourceKind !== targetKind)
86
83
  return false;
87
- if (sourceKind !== 'array')
88
- return true;
89
- const sourceFile = getMultipleFileField(sourceField);
90
- const targetFile = getMultipleFileField(targetField);
84
+ const sourceFile = getFileField(sourceField);
85
+ const targetFile = getFileField(targetField);
91
86
  if (!sourceFile && !targetFile)
92
87
  return true;
93
88
  if (!sourceFile || !targetFile)
94
89
  return false;
95
- return (sourceFile.mediaType === 'Any' ||
96
- targetFile.mediaType === 'Any' ||
97
- sourceFile.mediaType === targetFile.mediaType);
90
+ return (sourceFile.isMultiple === targetFile.isMultiple &&
91
+ (sourceFile.mediaType === 'Any' ||
92
+ targetFile.mediaType === 'Any' ||
93
+ sourceFile.mediaType === targetFile.mediaType));
98
94
  };
99
95
  const fieldLabel = (path) => path.startsWith('_seo.') ? `seo.${path.slice('_seo.'.length)}` : path;
100
- const isSeoPath = (path) => path === '_seo' ||
101
- path === 'seo' ||
102
- path.startsWith('_seo.') ||
103
- path.startsWith('seo.');
96
+ const isSeoPath = (path) => path.split('.').some((segment) => segment === '_seo' || segment === 'seo');
104
97
  const fileFieldOptions = (path, targetField) => {
105
98
  const options = [
106
99
  { label: fieldLabel(`${path}.url`), value: `${path}.url`, kind: 'string' },
@@ -141,27 +134,38 @@ const nestedSourceFieldOptions = ({ contentType, prefix = '', targetField, depth
141
134
  return [];
142
135
  if (field.config.type === 'Relation' && depth < 3) {
143
136
  const relationContentType = field.contentType;
144
- return nestedSourceFieldOptions({
145
- contentType: relationContentType,
146
- prefix: path,
147
- targetField,
148
- depth: depth + 1,
149
- });
150
- }
151
- if (field.config.type === 'File' &&
152
- field.isMultiple) {
153
- if (!areFieldKindsCompatible(field, targetField))
154
- return [];
137
+ const idOption = isCompatibleSourceKind('string', targetField)
138
+ ? [
139
+ {
140
+ label: fieldLabel(`${path}._id`),
141
+ value: `${path}._id`,
142
+ kind: 'string',
143
+ },
144
+ ]
145
+ : [];
155
146
  return [
156
- {
157
- label: fieldLabel(path),
158
- value: path,
159
- kind,
160
- },
147
+ ...idOption,
148
+ ...nestedSourceFieldOptions({
149
+ contentType: relationContentType,
150
+ prefix: path,
151
+ targetField,
152
+ depth: depth + 1,
153
+ }),
161
154
  ];
162
155
  }
163
156
  if (field.config.type === 'File') {
164
- return fileFieldOptions(path, targetField);
157
+ const fieldOption = areFieldKindsCompatible(field, targetField)
158
+ ? [
159
+ {
160
+ label: fieldLabel(path),
161
+ value: path,
162
+ kind,
163
+ },
164
+ ]
165
+ : [];
166
+ if (field.isMultiple)
167
+ return fieldOption;
168
+ return [...fieldOption, ...fileFieldOptions(path, targetField)];
165
169
  }
166
170
  if (!areFieldKindsCompatible(field, targetField))
167
171
  return [];
@@ -183,6 +187,7 @@ const sourceFieldOptions = (contentType, targetField) => {
183
187
  ? [{ label: 'href', value: '$href', kind: 'string' }, ...fields]
184
188
  : fields;
185
189
  };
190
+ exports.sourceFieldOptions = sourceFieldOptions;
186
191
  const createSource = (contentType, value, id) => value === '$href'
187
192
  ? { contentType, id, virtual: 'href' }
188
193
  : { contentType, id, path: value };
@@ -212,17 +217,55 @@ const sourceContentTypeLabel = (source, documentContentTypeName) => source.conte
212
217
  ? 'Current document'
213
218
  : source.contentType;
214
219
  const getSourceContentTypes = (contentTypes) => contentTypes.filter((sourceContentType) => (0, client_1.isDynamicDataSourceContentTypeAllowed)(sourceContentType));
215
- const readFilterState = (filter) => {
216
- const entry = filter ? Object.entries(filter)[0] : undefined;
217
- if (!entry)
218
- return undefined;
219
- const [field, value] = entry;
220
+ const operatorByMongoOperator = {
221
+ $eq: 'equals',
222
+ $ne: 'notEquals',
223
+ $contains: 'contains',
224
+ $in: 'in',
225
+ $nin: 'notIn',
226
+ $gt: 'greaterThan',
227
+ $gte: 'greaterThanOrEqual',
228
+ $lt: 'lessThan',
229
+ $lte: 'lessThanOrEqual',
230
+ };
231
+ const readFilterOperand = (value) => {
232
+ if (isRecord(value) &&
233
+ typeof value[client_1.DYNAMIC_QUERY_CURRENT_VALUE_KEY] === 'string') {
234
+ return {
235
+ value: value[client_1.DYNAMIC_QUERY_CURRENT_VALUE_KEY],
236
+ valueSource: 'current',
237
+ };
238
+ }
239
+ return {
240
+ value: Array.isArray(value) ? value.join(', ') : String(value ?? ''),
241
+ };
242
+ };
243
+ const filterConditionFromEntry = (field, value) => {
220
244
  if (value === true)
221
245
  return { field, operator: 'true', value: '' };
222
246
  if (value === false)
223
247
  return { field, operator: 'false', value: '' };
224
- if (isRecord(value) && typeof value.$contains === 'string') {
225
- return { field, operator: 'contains', value: value.$contains };
248
+ const directOperand = readFilterOperand(value);
249
+ if (directOperand.valueSource === 'current') {
250
+ return { field, operator: 'equals', ...directOperand };
251
+ }
252
+ if (isRecord(value)) {
253
+ if (typeof value.$exists === 'boolean') {
254
+ return {
255
+ field,
256
+ operator: value.$exists ? 'exists' : 'notExists',
257
+ value: '',
258
+ };
259
+ }
260
+ const operatorEntry = Object.entries(value).find(([operator]) => operator in operatorByMongoOperator);
261
+ if (operatorEntry) {
262
+ const [operator, operatorValue] = operatorEntry;
263
+ return {
264
+ field,
265
+ operator: operatorByMongoOperator[operator],
266
+ ...readFilterOperand(operatorValue),
267
+ };
268
+ }
226
269
  }
227
270
  return {
228
271
  field,
@@ -230,31 +273,124 @@ const readFilterState = (filter) => {
230
273
  value: typeof value === 'string' ? value : String(value ?? ''),
231
274
  };
232
275
  };
233
- const buildFilter = (state) => {
234
- if (!state?.field)
235
- return undefined;
236
- if (state.operator === 'true')
237
- return { [state.field]: true };
238
- if (state.operator === 'false')
239
- return { [state.field]: false };
240
- if (!state.value.trim())
276
+ const readFilterState = (filter) => {
277
+ if (!filter)
278
+ return { combinator: 'and', conditions: [] };
279
+ const logicalEntry = ['$and', '$or'].find((key) => Array.isArray(filter[key]));
280
+ if (logicalEntry) {
281
+ const conditions = filter[logicalEntry].flatMap((item) => isRecord(item)
282
+ ? Object.entries(item)
283
+ .filter(([field]) => !field.startsWith('$'))
284
+ .map(([field, value]) => filterConditionFromEntry(field, value))
285
+ : []);
286
+ return {
287
+ combinator: logicalEntry === '$or' ? 'or' : 'and',
288
+ conditions,
289
+ };
290
+ }
291
+ return {
292
+ combinator: 'and',
293
+ conditions: Object.entries(filter)
294
+ .filter(([field]) => !field.startsWith('$'))
295
+ .map(([field, value]) => filterConditionFromEntry(field, value)),
296
+ };
297
+ };
298
+ exports.readFilterState = readFilterState;
299
+ const operatorNeedsValue = (operator) => !['true', 'false', 'exists', 'notExists'].includes(operator);
300
+ const parseFilterValue = (condition, fieldOptions) => {
301
+ const kind = fieldOptions.find((option) => option.value === condition.field)?.kind;
302
+ const parseValue = (value) => kind === 'number' && value.trim() !== '' && Number.isFinite(Number(value))
303
+ ? Number(value)
304
+ : value.trim();
305
+ if (condition.operator === 'in' || condition.operator === 'notIn') {
306
+ return condition.value
307
+ .split(',')
308
+ .map((value) => value.trim())
309
+ .filter(Boolean)
310
+ .map(parseValue);
311
+ }
312
+ return parseValue(condition.value);
313
+ };
314
+ const buildFilterCondition = (condition, fieldOptions) => {
315
+ if (!condition.field)
241
316
  return undefined;
242
- if (state.operator === 'contains') {
243
- return { [state.field]: { $contains: state.value } };
317
+ if (condition.operator === 'true')
318
+ return { [condition.field]: true };
319
+ if (condition.operator === 'false')
320
+ return { [condition.field]: false };
321
+ if (condition.operator === 'exists') {
322
+ return { [condition.field]: { $exists: true } };
323
+ }
324
+ if (condition.operator === 'notExists') {
325
+ return { [condition.field]: { $exists: false } };
244
326
  }
245
- return { [state.field]: state.value };
327
+ if (!condition.value.trim())
328
+ return undefined;
329
+ const value = condition.valueSource === 'current'
330
+ ? { [client_1.DYNAMIC_QUERY_CURRENT_VALUE_KEY]: condition.value }
331
+ : parseFilterValue(condition, fieldOptions);
332
+ const mongoOperator = {
333
+ notEquals: '$ne',
334
+ contains: '$contains',
335
+ in: '$in',
336
+ notIn: '$nin',
337
+ greaterThan: '$gt',
338
+ greaterThanOrEqual: '$gte',
339
+ lessThan: '$lt',
340
+ lessThanOrEqual: '$lte',
341
+ };
342
+ const operator = mongoOperator[condition.operator];
343
+ return operator
344
+ ? { [condition.field]: { [operator]: value } }
345
+ : { [condition.field]: value };
346
+ };
347
+ const buildFilter = (state, fieldOptions = []) => {
348
+ const conditions = state.conditions.flatMap((condition) => {
349
+ const builtCondition = buildFilterCondition(condition, fieldOptions);
350
+ return builtCondition ? [builtCondition] : [];
351
+ });
352
+ if (conditions.length === 0)
353
+ return undefined;
354
+ if (conditions.length === 1)
355
+ return conditions[0];
356
+ return { [state.combinator === 'or' ? '$or' : '$and']: conditions };
246
357
  };
358
+ exports.buildFilter = buildFilter;
247
359
  const filterSummary = (filter) => {
248
- const state = readFilterState(filter);
249
- if (!state)
360
+ const state = (0, exports.readFilterState)(filter);
361
+ if (state.conditions.length === 0)
250
362
  return '';
251
- if (state.operator === 'true')
252
- return `${state.field} = true`;
253
- if (state.operator === 'false')
254
- return `${state.field} = false`;
255
- if (state.operator === 'contains')
256
- return `${state.field} contains ${state.value}`;
257
- return `${state.field} = ${state.value}`;
363
+ const first = state.conditions[0];
364
+ const operatorLabels = {
365
+ equals: '=',
366
+ notEquals: '!=',
367
+ contains: 'contains',
368
+ in: 'in',
369
+ notIn: 'not in',
370
+ greaterThan: '>',
371
+ greaterThanOrEqual: '>=',
372
+ lessThan: '<',
373
+ lessThanOrEqual: '<=',
374
+ true: '= true',
375
+ false: '= false',
376
+ exists: 'is set',
377
+ notExists: 'is not set',
378
+ };
379
+ const firstSummary = [
380
+ first.field,
381
+ operatorLabels[first.operator],
382
+ operatorNeedsValue(first.operator)
383
+ ? first.valueSource === 'current'
384
+ ? `Current document.${first.value}`
385
+ : first.value
386
+ : '',
387
+ ]
388
+ .filter(Boolean)
389
+ .join(' ');
390
+ const remaining = state.conditions.length - 1;
391
+ return remaining > 0
392
+ ? `${firstSummary} + ${remaining} ${remaining === 1 ? 'condition' : 'conditions'}`
393
+ : firstSummary;
258
394
  };
259
395
  const bindingSummary = ({ list, fieldBinding, listBinding, documentContentTypeName, }) => {
260
396
  if (list) {
@@ -307,7 +443,7 @@ const FieldBindingEditor = ({ contentTypes, documentContentType, currentDocument
307
443
  const items = (sourceItemsQuery.data?.items ?? []);
308
444
  const hasCurrentDocumentItem = currentDocumentSourceEnabled && sourceType === documentContentType.name;
309
445
  const hasItemOptions = hasCurrentDocumentItem || items.length > 0;
310
- const fieldOptions = sourceFieldOptions(selectedContentType, targetField);
446
+ const fieldOptions = (0, exports.sourceFieldOptions)(selectedContentType, targetField);
311
447
  const emit = (nextType = sourceType, nextId = sourceId, nextPath = fieldPath) => {
312
448
  const usesCurrentDocument = currentDocumentSourceEnabled &&
313
449
  nextType === documentContentType.name &&
@@ -371,7 +507,7 @@ const getRelatedRelationOptions = (contentType, currentSource) => Object.entries
371
507
  const relationContentType = getRelationContentType(field);
372
508
  return relationContentType?.name === currentSource.name ? [name] : [];
373
509
  });
374
- const getRelatedPathOptions = (contentType, targetField) => sourceFieldOptions(contentType, targetField).filter((option) => option.kind === 'array' && option.value !== '$href');
510
+ const getRelatedPathOptions = (contentType, targetField) => (0, exports.sourceFieldOptions)(contentType, targetField).filter((option) => option.kind === 'array' && option.value !== '$href');
375
511
  const getSortFieldOptions = (contentType) => Object.entries(contentType.fields).flatMap(([name, field]) => {
376
512
  if (!isDynamicVisibleField(field) ||
377
513
  field.isTranslatable ||
@@ -389,6 +525,57 @@ const getSortFieldOptions = (contentType) => Object.entries(contentType.fields).
389
525
  isSingleSelect;
390
526
  return isSortable ? [{ label: fieldLabel(name), value: name, kind }] : [];
391
527
  });
528
+ const filterOperatorOptions = {
529
+ equals: { label: 'Equals', value: 'equals' },
530
+ notEquals: { label: 'Does not equal', value: 'notEquals' },
531
+ contains: { label: 'Contains', value: 'contains' },
532
+ in: { label: 'Is one of', value: 'in' },
533
+ notIn: { label: 'Is not one of', value: 'notIn' },
534
+ greaterThan: { label: 'Greater than', value: 'greaterThan' },
535
+ greaterThanOrEqual: {
536
+ label: 'Greater than or equal',
537
+ value: 'greaterThanOrEqual',
538
+ },
539
+ lessThan: { label: 'Less than', value: 'lessThan' },
540
+ lessThanOrEqual: {
541
+ label: 'Less than or equal',
542
+ value: 'lessThanOrEqual',
543
+ },
544
+ true: { label: 'Is true', value: 'true' },
545
+ false: { label: 'Is false', value: 'false' },
546
+ exists: { label: 'Is set', value: 'exists' },
547
+ notExists: { label: 'Is not set', value: 'notExists' },
548
+ };
549
+ const getFilterOperatorOptions = (kind) => {
550
+ const operators = kind === 'boolean'
551
+ ? ['true', 'false', 'exists', 'notExists']
552
+ : kind === 'number' || kind === 'date'
553
+ ? [
554
+ 'equals',
555
+ 'notEquals',
556
+ 'greaterThan',
557
+ 'greaterThanOrEqual',
558
+ 'lessThan',
559
+ 'lessThanOrEqual',
560
+ 'in',
561
+ 'notIn',
562
+ 'exists',
563
+ 'notExists',
564
+ ]
565
+ : kind === 'string' || kind === 'richText'
566
+ ? [
567
+ 'equals',
568
+ 'notEquals',
569
+ 'contains',
570
+ 'in',
571
+ 'notIn',
572
+ 'exists',
573
+ 'notExists',
574
+ ]
575
+ : ['equals', 'notEquals', 'exists', 'notExists'];
576
+ return operators.map((operator) => filterOperatorOptions[operator]);
577
+ };
578
+ const defaultFilterOperator = (kind) => (kind === 'boolean' ? 'true' : 'equals');
392
579
  const createRelatedCollectionSource = ({ contentType, currentSource, targetField, }) => {
393
580
  const relation = getRelatedRelationOptions(contentType, currentSource)[0];
394
581
  const path = getRelatedPathOptions(contentType, targetField)[0]?.value;
@@ -405,7 +592,7 @@ const createRelatedCollectionSource = ({ contentType, currentSource, targetField
405
592
  const MappingSourceEditor = ({ contentTypes, currentSource, targetField, source, onChange, }) => {
406
593
  const relatedSource = isRelatedCollectionSource(source) ? source : undefined;
407
594
  const directSource = source && !isRelatedCollectionSource(source) ? source : undefined;
408
- const directFieldOptions = sourceFieldOptions(currentSource, targetField);
595
+ const directFieldOptions = (0, exports.sourceFieldOptions)(currentSource, targetField);
409
596
  const relatedContentTypes = contentTypes.filter((contentType) => getRelatedRelationOptions(contentType, currentSource).length > 0 &&
410
597
  getRelatedPathOptions(contentType, targetField).length > 0);
411
598
  const selectedRelatedContentType = relatedSource
@@ -469,10 +656,10 @@ const MappingSourceEditor = ({ contentTypes, currentSource, targetField, source,
469
656
  })
470
657
  : undefined, children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, {}) }), (0, jsx_runtime_1.jsxs)(select_1.SelectContent, { children: [(0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'asc', children: "Ascending" }), (0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'desc', children: "Descending" })] })] })] })] })] }));
471
658
  };
472
- const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
659
+ const ListBindingEditor = ({ contentTypes, documentContentType, field, binding, onChange, }) => {
473
660
  const [sourceType, setSourceType] = (0, react_1.useState)(binding?.contentType || '');
474
661
  const [itemName, setItemName] = (0, react_1.useState)(binding?.itemName || field.fields[0]?.name || '');
475
- const [filterState, setFilterState] = (0, react_1.useState)(readFilterState(binding?.query?.filter));
662
+ const [filterState, setFilterState] = (0, react_1.useState)((0, exports.readFilterState)(binding?.query?.filter));
476
663
  const [openMappingField, setOpenMappingField] = (0, react_1.useState)(null);
477
664
  const selectedSource = contentTypes.find((ct) => ct.name === sourceType);
478
665
  const targetContentType = getListTargetContentType(field, itemName);
@@ -483,8 +670,17 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
483
670
  ? getSortFieldOptions(selectedSource)
484
671
  : [];
485
672
  const filterFieldOptions = selectedSource
486
- ? sourceFieldOptions(selectedSource).filter((item) => item.value !== '$href')
673
+ ? (0, exports.sourceFieldOptions)(selectedSource).filter((item) => item.value !== '$href')
487
674
  : [];
675
+ const currentDocumentFieldOptions = [
676
+ { label: '_id', value: '_id', kind: 'string' },
677
+ ...(0, exports.sourceFieldOptions)(documentContentType).filter((item) => item.value !== '$href' &&
678
+ item.kind !== 'object' &&
679
+ item.kind !== 'array'),
680
+ ];
681
+ const sortEntry = Object.entries(binding?.query?.options?.sort ?? {})[0];
682
+ const sortField = sortEntry?.[0] ?? '';
683
+ const sortDirection = sortEntry?.[1] ?? 'desc';
488
684
  const emit = (patch) => {
489
685
  const next = {
490
686
  contentType: sourceType,
@@ -504,57 +700,124 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
504
700
  emit({
505
701
  query: {
506
702
  ...binding?.query,
507
- filter: buildFilter(nextFilterState),
703
+ filter: (0, exports.buildFilter)(nextFilterState, filterFieldOptions),
508
704
  },
509
705
  });
510
706
  };
707
+ const updateFilterCondition = (index, nextCondition) => {
708
+ const conditions = [...filterState.conditions];
709
+ conditions[index] = nextCondition;
710
+ updateFilter({ ...filterState, conditions });
711
+ };
511
712
  return ((0, jsx_runtime_1.jsxs)("div", { className: 'grid gap-3', children: [(0, jsx_runtime_1.jsx)(PanelSection, { title: 'Collection', children: (0, jsx_runtime_1.jsxs)("div", { className: 'grid gap-3 md:grid-cols-2', children: [(0, jsx_runtime_1.jsxs)(label_1.Label, { className: 'grid gap-1.5', children: ["Source", (0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: contentTypes.length === 0, value: sourceType, onValueChange: (value) => {
512
713
  setSourceType(value);
513
- emit({ contentType: value, map: {} });
714
+ const nextFilterState = {
715
+ combinator: 'and',
716
+ conditions: [],
717
+ };
718
+ setFilterState(nextFilterState);
719
+ emit({
720
+ contentType: value,
721
+ map: {},
722
+ query: {
723
+ options: {
724
+ limit: binding?.query?.options?.limit ?? 10,
725
+ },
726
+ },
727
+ });
514
728
  }, children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: contentTypes.length > 0 ? 'Content type' : 'No content types' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsx)(select_1.SelectGroup, { children: contentTypes.map((ct) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: ct.name, children: ct.name }, ct.name))) }) })] })] }), (0, jsx_runtime_1.jsxs)(label_1.Label, { className: 'grid gap-1.5', children: ["Item", (0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: !sourceType || field.fields.length === 0, value: itemName, onValueChange: (value) => {
515
729
  setItemName(value);
516
730
  emit({ itemName: value, map: {} });
517
- }, children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: field.fields.length > 0 ? 'Item type' : 'No item types' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsx)(select_1.SelectGroup, { children: field.fields.map((item) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: item.name, children: item.name }, item.name))) }) })] })] })] }) }), (0, jsx_runtime_1.jsx)(PanelSection, { title: 'Query', children: (0, jsx_runtime_1.jsxs)("div", { className: 'grid gap-3 md:grid-cols-[0.7fr_1fr_1.1fr]', children: [(0, jsx_runtime_1.jsxs)(label_1.Label, { className: 'grid gap-1.5', children: ["Limit", (0, jsx_runtime_1.jsx)(input_1.Input, { type: 'number', min: 1, value: String(binding?.query?.options?.limit ?? 10), onChange: (event) => emit({
518
- query: {
519
- ...binding?.query,
520
- options: {
521
- ...binding?.query?.options,
522
- limit: Number(event.target.value || 10),
731
+ }, children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: field.fields.length > 0 ? 'Item type' : 'No item types' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsx)(select_1.SelectGroup, { children: field.fields.map((item) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: item.name, children: item.name }, item.name))) }) })] })] })] }) }), (0, jsx_runtime_1.jsxs)(PanelSection, { title: 'Query', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'grid grid-cols-[minmax(5rem,0.4fr)_minmax(0,1.2fr)_minmax(8rem,0.6fr)] items-end gap-3', children: [(0, jsx_runtime_1.jsxs)(label_1.Label, { className: 'grid gap-1.5', children: ["Limit", (0, jsx_runtime_1.jsx)(input_1.Input, { type: 'number', min: 1, max: 100, value: String(binding?.query?.options?.limit ?? 10), onChange: (event) => emit({
732
+ query: {
733
+ ...binding?.query,
734
+ options: {
735
+ ...binding?.query?.options,
736
+ limit: Math.min(100, Math.max(1, Number(event.target.value || 10))),
737
+ },
523
738
  },
524
- },
525
- }) })] }), (0, jsx_runtime_1.jsxs)(label_1.Label, { className: 'grid gap-1.5', children: ["Sort", (0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: sortFieldOptions.length === 0, value: Object.keys(binding?.query?.options?.sort ?? {})[0] ?? '', onValueChange: (value) => emit({
526
- query: {
527
- ...binding?.query,
528
- options: {
529
- ...binding?.query?.options,
530
- sort: value ? { [value]: 'desc' } : undefined,
739
+ }) })] }), (0, jsx_runtime_1.jsxs)(label_1.Label, { className: 'grid min-w-0 gap-1.5', children: ["Sort by", (0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: sortFieldOptions.length === 0, value: sortField || '__none__', onValueChange: (value) => emit({
740
+ query: {
741
+ ...binding?.query,
742
+ options: {
743
+ ...binding?.query?.options,
744
+ sort: value === '__none__'
745
+ ? undefined
746
+ : { [value]: sortDirection },
747
+ },
531
748
  },
532
- },
533
- }), children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: sortFieldOptions.length > 0
534
- ? 'Field'
535
- : 'No sortable fields' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsx)(select_1.SelectGroup, { children: sortFieldOptions.map((item) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: item.value, children: item.label }, item.value))) }) })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: 'grid gap-1.5', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center justify-between gap-2', children: [(0, jsx_runtime_1.jsx)("span", { className: 'text-sm font-medium', children: "Filter" }), (0, jsx_runtime_1.jsxs)(button_1.Button, { type: 'button', size: 'icon', variant: 'ghost', className: 'size-7', onClick: () => updateFilter(undefined), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: 'h-4 w-4' }), (0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: "Clear filter" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: 'grid gap-2 sm:grid-cols-[1fr_0.9fr_1fr]', children: [(0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: filterFieldOptions.length === 0, value: filterState?.field || '', onValueChange: (value) => updateFilter(value
536
- ? {
537
- field: value,
538
- operator: filterState?.operator ?? 'equals',
539
- value: filterState?.value ?? '',
540
- }
541
- : undefined), children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: filterFieldOptions.length > 0
542
- ? 'Field'
543
- : 'No filterable fields' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsx)(select_1.SelectGroup, { children: filterFieldOptions.map((item) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: item.value, children: item.label }, item.value))) }) })] }), (0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: !filterState?.field, value: filterState?.operator || 'equals', onValueChange: (value) => filterState
544
- ? updateFilter({
749
+ }), children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: sortFieldOptions.length > 0
750
+ ? 'Sort by field'
751
+ : 'No sortable fields' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsxs)(select_1.SelectGroup, { children: [(0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: '__none__', children: "No sort" }), sortFieldOptions.map((item) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: item.value, children: item.label }, item.value)))] }) })] })] }), (0, jsx_runtime_1.jsxs)(label_1.Label, { className: 'grid gap-1.5', children: ["Direction", (0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: !sortField, value: sortDirection, onValueChange: (direction) => sortField
752
+ ? emit({
753
+ query: {
754
+ ...binding?.query,
755
+ options: {
756
+ ...binding?.query?.options,
757
+ sort: {
758
+ [sortField]: direction,
759
+ },
760
+ },
761
+ },
762
+ })
763
+ : undefined, children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, {}) }), (0, jsx_runtime_1.jsxs)(select_1.SelectContent, { children: [(0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'asc', children: "Ascending" }), (0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'desc', children: "Descending" })] })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: 'grid gap-3 rounded-md border border-border bg-background/60 p-3', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-wrap items-center justify-between gap-2', children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: 'text-sm font-medium', children: "Conditions" }), (0, jsx_runtime_1.jsx)("div", { className: 'text-xs text-muted-foreground', children: "Filter the records used to build this list." })] }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-2', children: [filterState.conditions.length > 1 ? ((0, jsx_runtime_1.jsxs)(select_1.Select, { value: filterState.combinator, onValueChange: (combinator) => updateFilter({
764
+ ...filterState,
765
+ combinator: combinator,
766
+ }), children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'h-8 w-36', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, {}) }), (0, jsx_runtime_1.jsxs)(select_1.SelectContent, { children: [(0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'and', children: "Match all" }), (0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'or', children: "Match any" })] })] })) : null, (0, jsx_runtime_1.jsxs)(button_1.Button, { type: 'button', size: 'sm', variant: 'outline', disabled: filterFieldOptions.length === 0 ||
767
+ filterState.conditions.length >= 25, onClick: () => updateFilter({
545
768
  ...filterState,
546
- operator: value,
547
- value: value === 'true' || value === 'false'
548
- ? ''
549
- : filterState.value,
550
- })
551
- : undefined, children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: 'Operator' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsxs)(select_1.SelectGroup, { children: [(0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'equals', children: "equals" }), (0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'contains', children: "contains" }), (0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'true', children: "true" }), (0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'false', children: "false" })] }) })] }), filterState?.operator === 'true' ||
552
- filterState?.operator === 'false' ? ((0, jsx_runtime_1.jsx)("div", { className: 'h-9 rounded-md border border-dashed border-border bg-background/60' })) : ((0, jsx_runtime_1.jsx)(input_1.Input, { value: filterState?.value ?? '', disabled: !filterState?.field, onChange: (event) => filterState
553
- ? updateFilter({
769
+ conditions: [
770
+ ...filterState.conditions,
771
+ { field: '', operator: 'equals', value: '' },
772
+ ],
773
+ }), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { className: 'h-4 w-4' }), "Add condition"] })] })] }), filterState.conditions.length === 0 ? ((0, jsx_runtime_1.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." })) : ((0, jsx_runtime_1.jsx)("div", { className: 'grid gap-2', children: filterState.conditions.map((condition, index) => {
774
+ const fieldOption = filterFieldOptions.find((option) => option.value === condition.field);
775
+ const operatorOptions = getFilterOperatorOptions(fieldOption?.kind);
776
+ const needsValue = operatorNeedsValue(condition.operator);
777
+ const currentValueOptions = currentDocumentFieldOptions.filter((option) => !fieldOption || option.kind === fieldOption.kind);
778
+ const allowsCurrentValue = condition.operator !== 'in' &&
779
+ condition.operator !== 'notIn' &&
780
+ currentValueOptions.length > 0;
781
+ return ((0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: filterFieldOptions.length === 0, value: condition.field, onValueChange: (value) => {
782
+ const kind = filterFieldOptions.find((option) => option.value === value)?.kind;
783
+ updateFilterCondition(index, {
784
+ field: value,
785
+ operator: defaultFilterOperator(kind),
786
+ value: '',
787
+ });
788
+ }, children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: 'Field' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsx)(select_1.SelectGroup, { children: filterFieldOptions.map((item) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: item.value, children: item.label }, item.value))) }) })] }), (0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: !condition.field, value: condition.operator, onValueChange: (operator) => updateFilterCondition(index, {
789
+ ...condition,
790
+ operator: operator,
791
+ valueSource: operator === 'in' || operator === 'notIn'
792
+ ? 'literal'
793
+ : condition.valueSource,
794
+ value: operatorNeedsValue(operator)
795
+ ? operator === 'in' || operator === 'notIn'
796
+ ? ''
797
+ : condition.value
798
+ : '',
799
+ }), children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: 'Operator' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsx)(select_1.SelectGroup, { children: operatorOptions.map((option) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: option.value, children: option.label }, option.value))) }) })] }), needsValue ? ((0, jsx_runtime_1.jsxs)("div", { className: 'grid grid-cols-[minmax(7rem,0.7fr)_minmax(0,1fr)] gap-2', children: [(0, jsx_runtime_1.jsxs)(select_1.Select, { value: condition.valueSource ?? 'literal', onValueChange: (valueSource) => updateFilterCondition(index, {
800
+ ...condition,
801
+ valueSource: valueSource,
802
+ value: '',
803
+ }), children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', "aria-label": 'Value source', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, {}) }), (0, jsx_runtime_1.jsxs)(select_1.SelectContent, { children: [(0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'literal', children: "Fixed value" }), allowsCurrentValue ? ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: 'current', children: "Current document" })) : null] })] }), condition.valueSource === 'current' ? ((0, jsx_runtime_1.jsxs)(select_1.Select, { disabled: !condition.field, value: condition.value, onValueChange: (value) => updateFilterCondition(index, {
804
+ ...condition,
805
+ value,
806
+ }), children: [(0, jsx_runtime_1.jsx)(select_1.SelectTrigger, { className: 'w-full', children: (0, jsx_runtime_1.jsx)(select_1.SelectValue, { placeholder: 'Current field' }) }), (0, jsx_runtime_1.jsx)(select_1.SelectContent, { children: (0, jsx_runtime_1.jsx)(select_1.SelectGroup, { children: currentValueOptions.map((option) => ((0, jsx_runtime_1.jsx)(select_1.SelectItem, { value: option.value, children: option.label }, option.value))) }) })] })) : ((0, jsx_runtime_1.jsx)(input_1.Input, { type: fieldOption?.kind === 'number' &&
807
+ condition.operator !== 'in' &&
808
+ condition.operator !== 'notIn'
809
+ ? 'number'
810
+ : 'text', value: condition.value, disabled: !condition.field, placeholder: condition.operator === 'in' ||
811
+ condition.operator === 'notIn'
812
+ ? 'Comma-separated values'
813
+ : 'Value', onChange: (event) => updateFilterCondition(index, {
814
+ ...condition,
815
+ value: event.target.value,
816
+ }) }))] })) : ((0, jsx_runtime_1.jsx)("div", { className: 'h-9 rounded-md border border-dashed border-border bg-muted/30' })), (0, jsx_runtime_1.jsxs)(button_1.Button, { type: 'button', size: 'icon', variant: 'ghost', className: 'size-9 text-muted-foreground hover:text-destructive', onClick: () => updateFilter({
554
817
  ...filterState,
555
- value: event.target.value,
556
- })
557
- : undefined }))] })] })] }) }), (0, jsx_runtime_1.jsx)(PanelSection, { title: 'Mapping', children: (0, jsx_runtime_1.jsx)("div", { className: 'grid gap-2', children: targetFields.map(([targetField, targetFieldConfig]) => {
818
+ conditions: filterState.conditions.filter((_, conditionIndex) => conditionIndex !== index),
819
+ }), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { className: 'h-4 w-4' }), (0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: "Remove condition" })] })] }, `${index}-${condition.field}`));
820
+ }) }))] })] }), (0, jsx_runtime_1.jsx)(PanelSection, { title: 'Mapping', children: (0, jsx_runtime_1.jsx)("div", { className: 'grid gap-2', children: targetFields.map(([targetField, targetFieldConfig]) => {
558
821
  const source = binding?.map?.[targetField];
559
822
  const summary = mappingSourceSummary(source, selectedSource?.name ?? 'Source');
560
823
  const isOpen = openMappingField === targetField;
@@ -653,7 +916,7 @@ const DynamicDataControl = ({ contentType, documentContentType, fieldName, field
653
916
  setOpen(false);
654
917
  };
655
918
  const trigger = ((0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 items-center gap-1.5', children: [(0, jsx_runtime_1.jsxs)(tooltip_1.Tooltip, { children: [(0, jsx_runtime_1.jsx)(tooltip_1.TooltipTrigger, { asChild: true, children: (0, jsx_runtime_1.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 ? ((0, jsx_runtime_1.jsxs)(badge_1.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 ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { className: 'h-3.5 w-3.5 shrink-0' })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Link2, { className: 'h-3.5 w-3.5 shrink-0' })), (0, jsx_runtime_1.jsx)("span", { className: 'truncate', children: summary })] })) : ((0, jsx_runtime_1.jsxs)(badge_1.Badge, { variant: 'outline', className: 'gap-1.5 rounded-md px-2 py-1 text-muted-foreground', children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Cable, { className: 'h-3.5 w-3.5' }), "Not linked"] })) }) }), (0, jsx_runtime_1.jsx)(tooltip_1.TooltipContent, { side: 'top', sideOffset: 6, children: triggerLabel })] }), bound ? ((0, jsx_runtime_1.jsxs)(tooltip_1.Tooltip, { children: [(0, jsx_runtime_1.jsx)(tooltip_1.TooltipTrigger, { asChild: true, children: (0, jsx_runtime_1.jsxs)(button_1.Button, { type: 'button', size: 'icon', variant: 'ghost', className: 'size-6', onClick: clearBinding, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: 'h-4 w-4' }), (0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: "Clear dynamic data" })] }) }), (0, jsx_runtime_1.jsx)(tooltip_1.TooltipContent, { side: 'top', sideOffset: 6, children: "Clear dynamic data link" })] })) : null] }));
656
- const editor = list ? ((0, jsx_runtime_1.jsx)(ListBindingEditor, { contentTypes: sourceContentTypes, field: field, binding: listBinding, onChange: updateListBinding })) : ((0, jsx_runtime_1.jsx)(FieldBindingEditor, { contentTypes: fieldSourceContentTypes, documentContentType: currentDocumentContentType, currentDocumentSourceEnabled: currentDocumentSourceEnabled, targetField: field, binding: fieldBinding, onChange: updateFieldBinding }));
919
+ const editor = list ? ((0, jsx_runtime_1.jsx)(ListBindingEditor, { contentTypes: sourceContentTypes, documentContentType: currentDocumentContentType, field: field, binding: listBinding, onChange: updateListBinding })) : ((0, jsx_runtime_1.jsx)(FieldBindingEditor, { contentTypes: fieldSourceContentTypes, documentContentType: currentDocumentContentType, currentDocumentSourceEnabled: currentDocumentSourceEnabled, targetField: field, binding: fieldBinding, onChange: updateFieldBinding }));
657
920
  const panel = isOpen ? ((0, jsx_runtime_1.jsx)("div", { className: 'rounded-md border border-border bg-background p-4 shadow-sm', children: editor })) : null;
658
921
  const dialog = ((0, jsx_runtime_1.jsx)(dialog_1.Dialog, { open: isOpen, onOpenChange: (value) => {
659
922
  if (value) {