@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 strict";
2
2
  'use client';
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.DynamicDataControl = exports.sourceFieldOptions = 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");
@@ -134,12 +134,24 @@ const nestedSourceFieldOptions = ({ contentType, prefix = '', targetField, depth
134
134
  return [];
135
135
  if (field.config.type === 'Relation' && depth < 3) {
136
136
  const relationContentType = field.contentType;
137
- return nestedSourceFieldOptions({
138
- contentType: relationContentType,
139
- prefix: path,
140
- targetField,
141
- depth: depth + 1,
142
- });
137
+ const idOption = isCompatibleSourceKind('string', targetField)
138
+ ? [
139
+ {
140
+ label: fieldLabel(`${path}._id`),
141
+ value: `${path}._id`,
142
+ kind: 'string',
143
+ },
144
+ ]
145
+ : [];
146
+ return [
147
+ ...idOption,
148
+ ...nestedSourceFieldOptions({
149
+ contentType: relationContentType,
150
+ prefix: path,
151
+ targetField,
152
+ depth: depth + 1,
153
+ }),
154
+ ];
143
155
  }
144
156
  if (field.config.type === 'File') {
145
157
  const fieldOption = areFieldKindsCompatible(field, targetField)
@@ -205,17 +217,55 @@ const sourceContentTypeLabel = (source, documentContentTypeName) => source.conte
205
217
  ? 'Current document'
206
218
  : source.contentType;
207
219
  const getSourceContentTypes = (contentTypes) => contentTypes.filter((sourceContentType) => (0, client_1.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;
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) => {
213
244
  if (value === true)
214
245
  return { field, operator: 'true', value: '' };
215
246
  if (value === false)
216
247
  return { field, operator: 'false', value: '' };
217
- if (isRecord(value) && typeof value.$contains === 'string') {
218
- 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
+ }
219
269
  }
220
270
  return {
221
271
  field,
@@ -223,31 +273,124 @@ const readFilterState = (filter) => {
223
273
  value: typeof value === 'string' ? value : String(value ?? ''),
224
274
  };
225
275
  };
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())
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)
234
316
  return undefined;
235
- if (state.operator === 'contains') {
236
- 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 } };
237
323
  }
238
- return { [state.field]: state.value };
324
+ if (condition.operator === 'notExists') {
325
+ return { [condition.field]: { $exists: false } };
326
+ }
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 };
239
357
  };
358
+ exports.buildFilter = buildFilter;
240
359
  const filterSummary = (filter) => {
241
- const state = readFilterState(filter);
242
- if (!state)
360
+ const state = (0, exports.readFilterState)(filter);
361
+ if (state.conditions.length === 0)
243
362
  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}`;
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;
251
394
  };
252
395
  const bindingSummary = ({ list, fieldBinding, listBinding, documentContentTypeName, }) => {
253
396
  if (list) {
@@ -382,6 +525,57 @@ const getSortFieldOptions = (contentType) => Object.entries(contentType.fields).
382
525
  isSingleSelect;
383
526
  return isSortable ? [{ label: fieldLabel(name), value: name, kind }] : [];
384
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');
385
579
  const createRelatedCollectionSource = ({ contentType, currentSource, targetField, }) => {
386
580
  const relation = getRelatedRelationOptions(contentType, currentSource)[0];
387
581
  const path = getRelatedPathOptions(contentType, targetField)[0]?.value;
@@ -462,10 +656,10 @@ const MappingSourceEditor = ({ contentTypes, currentSource, targetField, source,
462
656
  })
463
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" })] })] })] })] })] }));
464
658
  };
465
- const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
659
+ const ListBindingEditor = ({ contentTypes, documentContentType, field, binding, onChange, }) => {
466
660
  const [sourceType, setSourceType] = (0, react_1.useState)(binding?.contentType || '');
467
661
  const [itemName, setItemName] = (0, react_1.useState)(binding?.itemName || field.fields[0]?.name || '');
468
- 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));
469
663
  const [openMappingField, setOpenMappingField] = (0, react_1.useState)(null);
470
664
  const selectedSource = contentTypes.find((ct) => ct.name === sourceType);
471
665
  const targetContentType = getListTargetContentType(field, itemName);
@@ -478,6 +672,15 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
478
672
  const filterFieldOptions = selectedSource
479
673
  ? (0, exports.sourceFieldOptions)(selectedSource).filter((item) => item.value !== '$href')
480
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';
481
684
  const emit = (patch) => {
482
685
  const next = {
483
686
  contentType: sourceType,
@@ -497,57 +700,124 @@ const ListBindingEditor = ({ contentTypes, field, binding, onChange, }) => {
497
700
  emit({
498
701
  query: {
499
702
  ...binding?.query,
500
- filter: buildFilter(nextFilterState),
703
+ filter: (0, exports.buildFilter)(nextFilterState, filterFieldOptions),
501
704
  },
502
705
  });
503
706
  };
707
+ const updateFilterCondition = (index, nextCondition) => {
708
+ const conditions = [...filterState.conditions];
709
+ conditions[index] = nextCondition;
710
+ updateFilter({ ...filterState, conditions });
711
+ };
504
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) => {
505
713
  setSourceType(value);
506
- 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
+ });
507
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) => {
508
729
  setItemName(value);
509
730
  emit({ itemName: value, map: {} });
510
- }, 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({
511
- query: {
512
- ...binding?.query,
513
- options: {
514
- ...binding?.query?.options,
515
- 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
+ },
516
738
  },
517
- },
518
- }) })] }), (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({
519
- query: {
520
- ...binding?.query,
521
- options: {
522
- ...binding?.query?.options,
523
- 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
+ },
524
748
  },
525
- },
526
- }), 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
527
- ? 'Field'
528
- : '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
529
- ? {
530
- field: value,
531
- operator: filterState?.operator ?? 'equals',
532
- value: filterState?.value ?? '',
533
- }
534
- : 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
535
- ? 'Field'
536
- : '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
537
- ? 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({
538
768
  ...filterState,
539
- operator: value,
540
- value: value === 'true' || value === 'false'
541
- ? ''
542
- : filterState.value,
543
- })
544
- : 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' ||
545
- 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
546
- ? 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({
547
817
  ...filterState,
548
- value: event.target.value,
549
- })
550
- : 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]) => {
551
821
  const source = binding?.map?.[targetField];
552
822
  const summary = mappingSourceSummary(source, selectedSource?.name ?? 'Source');
553
823
  const isOpen = openMappingField === targetField;
@@ -646,7 +916,7 @@ const DynamicDataControl = ({ contentType, documentContentType, fieldName, field
646
916
  setOpen(false);
647
917
  };
648
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] }));
649
- 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 }));
650
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;
651
921
  const dialog = ((0, jsx_runtime_1.jsx)(dialog_1.Dialog, { open: isOpen, onOpenChange: (value) => {
652
922
  if (value) {