@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 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 getMultipleFileField = (field) => field.config.type === 'File' &&
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
- if (sourceKind !== 'array')
84
- return true;
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.mediaType === 'Any' ||
92
- targetFile.mediaType === 'Any' ||
93
- sourceFile.mediaType === targetFile.mediaType);
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
- return nestedSourceFieldOptions({
138
- contentType: relationContentType,
139
- prefix: path,
140
- targetField,
141
- depth: depth + 1,
142
- });
143
- }
144
- if (field.config.type === 'File' &&
145
- field.isMultiple) {
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
- label: fieldLabel(path),
151
- value: path,
152
- kind,
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
- return fileFieldOptions(path, targetField);
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 readFilterState = (filter) => {
209
- const entry = filter ? Object.entries(filter)[0] : undefined;
210
- if (!entry)
211
- return undefined;
212
- const [field, value] = entry;
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
- if (isRecord(value) && typeof value.$contains === 'string') {
218
- return { field, operator: 'contains', value: value.$contains };
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 buildFilter = (state) => {
227
- if (!state?.field)
228
- return undefined;
229
- if (state.operator === 'true')
230
- return { [state.field]: true };
231
- if (state.operator === 'false')
232
- return { [state.field]: false };
233
- if (!state.value.trim())
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 (state.operator === 'contains') {
236
- return { [state.field]: { $contains: state.value } };
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
- return { [state.field]: state.value };
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 (!state)
354
+ if (state.conditions.length === 0)
243
355
  return '';
244
- if (state.operator === 'true')
245
- return `${state.field} = true`;
246
- if (state.operator === 'false')
247
- return `${state.field} = false`;
248
- if (state.operator === 'contains')
249
- return `${state.field} contains ${state.value}`;
250
- return `${state.field} = ${state.value}`;
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
- emit({ contentType: value, map: {} });
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))) }) })] })] })] }) }), _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({
511
- query: {
512
- ...binding?.query,
513
- options: {
514
- ...binding?.query?.options,
515
- limit: Number(event.target.value || 10),
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
- }) })] }), _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({
519
- query: {
520
- ...binding?.query,
521
- options: {
522
- ...binding?.query?.options,
523
- sort: value ? { [value]: 'desc' } : undefined,
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
- }), 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
529
- ? {
530
- field: value,
531
- operator: filterState?.operator ?? 'equals',
532
- value: filterState?.value ?? '',
533
- }
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
537
- ? updateFilter({
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
- operator: value,
540
- value: value === 'true' || value === 'false'
541
- ? ''
542
- : filterState.value,
543
- })
544
- : undefined, children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: 'Operator' }) }), _jsx(SelectContent, { children: _jsxs(SelectGroup, { children: [_jsx(SelectItem, { value: 'equals', children: "equals" }), _jsx(SelectItem, { value: 'contains', children: "contains" }), _jsx(SelectItem, { value: 'true', children: "true" }), _jsx(SelectItem, { value: 'false', children: "false" })] }) })] }), filterState?.operator === 'true' ||
545
- filterState?.operator === 'false' ? (_jsx("div", { className: 'h-9 rounded-md border border-dashed border-border bg-background/60' })) : (_jsx(Input, { value: filterState?.value ?? '', disabled: !filterState?.field, onChange: (event) => filterState
546
- ? updateFilter({
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
- value: event.target.value,
549
- })
550
- : undefined }))] })] })] }) }), _jsx(PanelSection, { title: 'Mapping', children: _jsx("div", { className: 'grid gap-2', children: targetFields.map(([targetField, targetFieldConfig]) => {
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) {