@rakun-kit/manager-react 1.4.5 → 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';
@@ -130,12 +130,24 @@ const nestedSourceFieldOptions = ({ contentType, prefix = '', targetField, depth
130
130
  return [];
131
131
  if (field.config.type === 'Relation' && depth < 3) {
132
132
  const relationContentType = field.contentType;
133
- return nestedSourceFieldOptions({
134
- contentType: relationContentType,
135
- prefix: path,
136
- targetField,
137
- depth: depth + 1,
138
- });
133
+ const idOption = isCompatibleSourceKind('string', targetField)
134
+ ? [
135
+ {
136
+ label: fieldLabel(`${path}._id`),
137
+ value: `${path}._id`,
138
+ kind: 'string',
139
+ },
140
+ ]
141
+ : [];
142
+ return [
143
+ ...idOption,
144
+ ...nestedSourceFieldOptions({
145
+ contentType: relationContentType,
146
+ prefix: path,
147
+ targetField,
148
+ depth: depth + 1,
149
+ }),
150
+ ];
139
151
  }
140
152
  if (field.config.type === 'File') {
141
153
  const fieldOption = areFieldKindsCompatible(field, targetField)
@@ -200,17 +212,55 @@ const sourceContentTypeLabel = (source, documentContentTypeName) => source.conte
200
212
  ? 'Current document'
201
213
  : source.contentType;
202
214
  const getSourceContentTypes = (contentTypes) => contentTypes.filter((sourceContentType) => isDynamicDataSourceContentTypeAllowed(sourceContentType));
203
- const readFilterState = (filter) => {
204
- const entry = filter ? Object.entries(filter)[0] : undefined;
205
- if (!entry)
206
- return undefined;
207
- 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) => {
208
239
  if (value === true)
209
240
  return { field, operator: 'true', value: '' };
210
241
  if (value === false)
211
242
  return { field, operator: 'false', value: '' };
212
- if (isRecord(value) && typeof value.$contains === 'string') {
213
- 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
+ }
214
264
  }
215
265
  return {
216
266
  field,
@@ -218,31 +268,122 @@ const readFilterState = (filter) => {
218
268
  value: typeof value === 'string' ? value : String(value ?? ''),
219
269
  };
220
270
  };
221
- const buildFilter = (state) => {
222
- if (!state?.field)
223
- return undefined;
224
- if (state.operator === 'true')
225
- return { [state.field]: true };
226
- if (state.operator === 'false')
227
- return { [state.field]: false };
228
- 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)
229
310
  return undefined;
230
- if (state.operator === 'contains') {
231
- 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 } };
317
+ }
318
+ if (condition.operator === 'notExists') {
319
+ return { [condition.field]: { $exists: false } };
232
320
  }
233
- return { [state.field]: state.value };
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 };
234
351
  };
235
352
  const filterSummary = (filter) => {
236
353
  const state = readFilterState(filter);
237
- if (!state)
354
+ if (state.conditions.length === 0)
238
355
  return '';
239
- if (state.operator === 'true')
240
- return `${state.field} = true`;
241
- if (state.operator === 'false')
242
- return `${state.field} = false`;
243
- if (state.operator === 'contains')
244
- return `${state.field} contains ${state.value}`;
245
- 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;
246
387
  };
247
388
  const bindingSummary = ({ list, fieldBinding, listBinding, documentContentTypeName, }) => {
248
389
  if (list) {
@@ -377,6 +518,57 @@ const getSortFieldOptions = (contentType) => Object.entries(contentType.fields).
377
518
  isSingleSelect;
378
519
  return isSortable ? [{ label: fieldLabel(name), value: name, kind }] : [];
379
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');
380
572
  const createRelatedCollectionSource = ({ contentType, currentSource, targetField, }) => {
381
573
  const relation = getRelatedRelationOptions(contentType, currentSource)[0];
382
574
  const path = getRelatedPathOptions(contentType, targetField)[0]?.value;
@@ -457,7 +649,7 @@ const MappingSourceEditor = ({ contentTypes, currentSource, targetField, source,
457
649
  })
458
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" })] })] })] })] })] }));
459
651
  };
460
- const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
652
+ const ListBindingEditor = ({ contentTypes, documentContentType, field, binding, onChange, }) => {
461
653
  const [sourceType, setSourceType] = useState(binding?.contentType || '');
462
654
  const [itemName, setItemName] = useState(binding?.itemName || field.fields[0]?.name || '');
463
655
  const [filterState, setFilterState] = useState(readFilterState(binding?.query?.filter));
@@ -473,6 +665,15 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
473
665
  const filterFieldOptions = selectedSource
474
666
  ? sourceFieldOptions(selectedSource).filter((item) => item.value !== '$href')
475
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';
476
677
  const emit = (patch) => {
477
678
  const next = {
478
679
  contentType: sourceType,
@@ -492,57 +693,124 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
492
693
  emit({
493
694
  query: {
494
695
  ...binding?.query,
495
- filter: buildFilter(nextFilterState),
696
+ filter: buildFilter(nextFilterState, filterFieldOptions),
496
697
  },
497
698
  });
498
699
  };
700
+ const updateFilterCondition = (index, nextCondition) => {
701
+ const conditions = [...filterState.conditions];
702
+ conditions[index] = nextCondition;
703
+ updateFilter({ ...filterState, conditions });
704
+ };
499
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) => {
500
706
  setSourceType(value);
501
- 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
+ });
502
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) => {
503
722
  setItemName(value);
504
723
  emit({ itemName: value, map: {} });
505
- }, 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({
506
- query: {
507
- ...binding?.query,
508
- options: {
509
- ...binding?.query?.options,
510
- 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
+ },
511
731
  },
512
- },
513
- }) })] }), _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({
514
- query: {
515
- ...binding?.query,
516
- options: {
517
- ...binding?.query?.options,
518
- 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
+ },
519
741
  },
520
- },
521
- }), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: sortFieldOptions.length > 0
522
- ? 'Field'
523
- : '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
524
- ? {
525
- field: value,
526
- operator: filterState?.operator ?? 'equals',
527
- value: filterState?.value ?? '',
528
- }
529
- : undefined), children: [_jsx(SelectTrigger, { className: 'w-full', children: _jsx(SelectValue, { placeholder: filterFieldOptions.length > 0
530
- ? 'Field'
531
- : '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
532
- ? 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({
533
761
  ...filterState,
534
- operator: value,
535
- value: value === 'true' || value === 'false'
536
- ? ''
537
- : filterState.value,
538
- })
539
- : 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' ||
540
- 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
541
- ? 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({
542
810
  ...filterState,
543
- value: event.target.value,
544
- })
545
- : 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]) => {
546
814
  const source = binding?.map?.[targetField];
547
815
  const summary = mappingSourceSummary(source, selectedSource?.name ?? 'Source');
548
816
  const isOpen = openMappingField === targetField;
@@ -641,7 +909,7 @@ export const DynamicDataControl = ({ contentType, documentContentType, fieldName
641
909
  setOpen(false);
642
910
  };
643
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] }));
644
- 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 }));
645
913
  const panel = isOpen ? (_jsx("div", { className: 'rounded-md border border-border bg-background p-4 shadow-sm', children: editor })) : null;
646
914
  const dialog = (_jsx(Dialog, { open: isOpen, onOpenChange: (value) => {
647
915
  if (value) {