@firecms/collection_editor 3.0.0 → 3.1.0-canary.1df3b2c

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.
Files changed (91) hide show
  1. package/dist/ConfigControllerProvider.d.ts +6 -0
  2. package/dist/api/generateCollectionApi.d.ts +71 -0
  3. package/dist/api/index.d.ts +1 -0
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.es.js +9677 -5837
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +9653 -5813
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/types/collection_editor_controller.d.ts +14 -0
  10. package/dist/types/collection_inference.d.ts +8 -2
  11. package/dist/types/config_controller.d.ts +31 -1
  12. package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
  13. package/dist/ui/KanbanSetupAction.d.ts +10 -0
  14. package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +33 -0
  15. package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
  16. package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
  17. package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +20 -0
  18. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +3 -1
  19. package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
  20. package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
  21. package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
  22. package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
  23. package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
  24. package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
  25. package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
  26. package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
  27. package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
  28. package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
  29. package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
  30. package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
  31. package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
  32. package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
  33. package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
  34. package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
  35. package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
  36. package/dist/useCollectionEditorPlugin.d.ts +7 -1
  37. package/dist/utils/validateCollectionJson.d.ts +22 -0
  38. package/package.json +11 -11
  39. package/src/ConfigControllerProvider.tsx +81 -47
  40. package/src/api/generateCollectionApi.ts +119 -0
  41. package/src/api/index.ts +1 -0
  42. package/src/index.ts +28 -1
  43. package/src/types/collection_editor_controller.tsx +16 -3
  44. package/src/types/collection_inference.ts +15 -2
  45. package/src/types/config_controller.tsx +37 -1
  46. package/src/ui/AddKanbanColumnAction.tsx +203 -0
  47. package/src/ui/EditorCollectionActionStart.tsx +1 -2
  48. package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
  49. package/src/ui/KanbanSetupAction.tsx +38 -0
  50. package/src/ui/MissingReferenceWidget.tsx +1 -1
  51. package/src/ui/NewCollectionButton.tsx +1 -1
  52. package/src/ui/PropertyAddColumnComponent.tsx +1 -1
  53. package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +225 -0
  54. package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
  55. package/src/ui/collection_editor/CollectionDetailsForm.tsx +209 -258
  56. package/src/ui/collection_editor/CollectionEditorDialog.tsx +226 -173
  57. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +130 -67
  58. package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
  59. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +190 -91
  60. package/src/ui/collection_editor/DisplaySettingsForm.tsx +333 -0
  61. package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -96
  62. package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +6 -7
  63. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +1 -3
  64. package/src/ui/collection_editor/EnumForm.tsx +147 -100
  65. package/src/ui/collection_editor/ExtendSettingsForm.tsx +93 -0
  66. package/src/ui/collection_editor/GeneralSettingsForm.tsx +335 -0
  67. package/src/ui/collection_editor/GetCodeDialog.tsx +57 -36
  68. package/src/ui/collection_editor/KanbanConfigSection.tsx +207 -0
  69. package/src/ui/collection_editor/LayoutModeSwitch.tsx +22 -41
  70. package/src/ui/collection_editor/PropertyEditView.tsx +205 -141
  71. package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
  72. package/src/ui/collection_editor/PropertyTree.tsx +130 -58
  73. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +171 -162
  74. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
  75. package/src/ui/collection_editor/ViewModeSwitch.tsx +41 -0
  76. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
  77. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +1 -0
  78. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +117 -35
  79. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +28 -21
  80. package/src/ui/collection_editor/properties/MapPropertyField.tsx +0 -2
  81. package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +115 -39
  82. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +1 -5
  83. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +23 -2
  84. package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +861 -0
  85. package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
  86. package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
  87. package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
  88. package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
  89. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -1
  90. package/src/useCollectionEditorPlugin.tsx +32 -17
  91. package/src/utils/validateCollectionJson.ts +380 -0
@@ -0,0 +1,28 @@
1
+ import { PropsWithChildren } from "react";
2
+
3
+ import { ExpandablePanel, SettingsIcon, Typography } from "@firecms/ui";
4
+
5
+ export function ConditionsPanel({
6
+ children
7
+ }: PropsWithChildren<{}>) {
8
+
9
+ return (
10
+ <ExpandablePanel
11
+ initiallyExpanded={true}
12
+ asField={true}
13
+ innerClassName="p-4"
14
+ title={
15
+ <div className="flex flex-row text-surface-500 text-text-secondary dark:text-text-secondary-dark">
16
+ <SettingsIcon />
17
+ <Typography variant={"subtitle2"}
18
+ className="ml-4">
19
+ Conditions
20
+ </Typography>
21
+ </div>
22
+ }>
23
+
24
+ {children}
25
+
26
+ </ExpandablePanel>
27
+ )
28
+ }
@@ -0,0 +1,599 @@
1
+ import React from "react";
2
+ import { useFormex } from "@firecms/formex";
3
+ import {
4
+ DeleteIcon,
5
+ IconButton,
6
+ Select,
7
+ SelectItem,
8
+ Typography,
9
+ MultiSelect,
10
+ MultiSelectItem,
11
+ cls,
12
+ defaultBorderMixin,
13
+ DebouncedTextField,
14
+ BooleanSwitchWithLabel
15
+ } from "@firecms/ui";
16
+ import {
17
+ Properties,
18
+ Property,
19
+ PropertyOrBuilder,
20
+ getFieldConfig,
21
+ DEFAULT_FIELD_CONFIGS,
22
+ EnumValueConfig,
23
+ isPropertyBuilder
24
+ } from "@firecms/core";
25
+ import { PropertyWithId } from "../../PropertyEditView";
26
+ import { getPropertyPaths } from "./property_paths";
27
+
28
+ /**
29
+ * Enum condition types - maps to PropertyConditions structure:
30
+ * - "filter" → conditions.allowedEnumValues (JSON Logic returning allowed IDs)
31
+ * - "exclude" → conditions.excludedEnumValues (JSON Logic returning excluded IDs)
32
+ */
33
+ const ENUM_CONDITION_TYPES = [
34
+ { id: "allowedEnumValues", label: "Filter Options", description: "Only show selected enum values when condition is true" },
35
+ { id: "excludedEnumValues", label: "Exclude Options", description: "Hide selected enum values when condition is true" }
36
+ ] as const;
37
+
38
+ type EnumConditionType = typeof ENUM_CONDITION_TYPES[number]["id"];
39
+
40
+ /**
41
+ * Operators for conditions
42
+ */
43
+ const OPERATORS = [
44
+ { id: "==", label: "equals", valueType: "any" },
45
+ { id: "!=", label: "not equals", valueType: "any" },
46
+ { id: "in", label: "contains", valueType: "array" },
47
+ { id: "!!", label: "has a value", valueType: "none" },
48
+ { id: "!", label: "is empty", valueType: "none" }
49
+ ] as const;
50
+
51
+ type OperatorId = typeof OPERATORS[number]["id"];
52
+
53
+ // Context fields
54
+ const CONTEXT_FIELDS = [
55
+ { id: "isNew", label: "Is New Entity", dataType: "boolean", color: "#9c27b0" },
56
+ { id: "entityId", label: "Entity ID", dataType: "string", color: "#2196f3" },
57
+ { id: "user.roles", label: "User Roles", dataType: "array", color: "#ff9800" }
58
+ ] as const;
59
+
60
+ interface EnumConditionConfig {
61
+ field: string;
62
+ operator: OperatorId;
63
+ value: string;
64
+ selectedEnumIds: string[];
65
+ }
66
+
67
+ /**
68
+ * Get the property at a given path.
69
+ * Returns undefined if the path contains a PropertyBuilder (callback).
70
+ */
71
+ function getPropertyAtPath(
72
+ fieldPath: string,
73
+ collectionProperties?: Properties
74
+ ): Property | undefined {
75
+ if (!collectionProperties) return undefined;
76
+
77
+ const parts = fieldPath.split(".");
78
+ let current: Properties | undefined = collectionProperties;
79
+ let property: Property | undefined;
80
+
81
+ for (const part of parts) {
82
+ if (!current) return undefined;
83
+ const propertyOrBuilder = current[part] as PropertyOrBuilder | undefined;
84
+ if (!propertyOrBuilder) return undefined;
85
+
86
+ if (isPropertyBuilder(propertyOrBuilder)) return undefined;
87
+
88
+ property = propertyOrBuilder as Property;
89
+
90
+ if (property.dataType === "map" && property.properties) {
91
+ current = property.properties as Properties;
92
+ } else {
93
+ current = undefined;
94
+ }
95
+ }
96
+
97
+ return property;
98
+ }
99
+
100
+ /**
101
+ * Get enum values from the current property being edited
102
+ */
103
+ function getCurrentPropertyEnumValues(values: PropertyWithId): EnumValueConfig[] {
104
+ if (values.dataType === "string" && values.enumValues) {
105
+ if (Array.isArray(values.enumValues)) {
106
+ return values.enumValues;
107
+ }
108
+ return Object.entries(values.enumValues).map(([id, label]) => ({
109
+ id,
110
+ label: typeof label === "string" ? label : (label as EnumValueConfig).label
111
+ }));
112
+ }
113
+ return [];
114
+ }
115
+
116
+ /**
117
+ * Get color for a field
118
+ */
119
+ function getFieldColor(fieldPath: string, collectionProperties?: Properties): string {
120
+ const contextField = CONTEXT_FIELDS.find(f => f.id === fieldPath);
121
+ if (contextField) return contextField.color;
122
+
123
+ if (!collectionProperties) return "#888";
124
+
125
+ const prop = getPropertyAtPath(fieldPath, collectionProperties);
126
+ if (!prop) return "#888";
127
+ const config = getFieldConfig(prop, DEFAULT_FIELD_CONFIGS);
128
+ return config?.color ?? "#888";
129
+ }
130
+
131
+ /**
132
+ * Get enum values from a property (for the selected condition field)
133
+ */
134
+ function getFieldEnumValues(property: Property | undefined): EnumValueConfig[] {
135
+ if (!property) return [];
136
+ if (property.dataType === "string" && property.enumValues) {
137
+ if (Array.isArray(property.enumValues)) {
138
+ return property.enumValues;
139
+ }
140
+ return Object.entries(property.enumValues).map(([id, label]) => ({
141
+ id,
142
+ label: typeof label === "string" ? label : (label as EnumValueConfig).label
143
+ }));
144
+ }
145
+ return [];
146
+ }
147
+
148
+ /**
149
+ * Convert an array to an object with numeric keys for Firestore storage.
150
+ * Firestore doesn't allow nested arrays, so we store as {"0": "a", "1": "b"}
151
+ * The runtime will convert back via objectToArray().
152
+ */
153
+ function arrayToObject(arr: string[]): Record<string, string> {
154
+ const result: Record<string, string> = {};
155
+ arr.forEach((v, i) => {
156
+ result[String(i)] = v;
157
+ });
158
+ return result;
159
+ }
160
+
161
+ /**
162
+ * Build JSON Logic rule: { "if": [ condition, thenValue, elseValue ] }
163
+ * When condition is true, return the selected enum IDs.
164
+ * When false, return all enum IDs or empty (depending on type).
165
+ *
166
+ * IMPORTANT: We store arrays as objects with numeric keys to avoid
167
+ * Firestore's "Nested arrays are not supported" error.
168
+ */
169
+ function buildEnumFilterRule(
170
+ config: EnumConditionConfig,
171
+ conditionType: EnumConditionType,
172
+ allEnumIds: string[]
173
+ ): Record<string, unknown> {
174
+ // Guard against empty field or operator which creates invalid Firestore keys
175
+ if (!config.field || !config.operator) {
176
+ // Return a valid default rule
177
+ return {
178
+ "if": [
179
+ { "!!": { var: "values._placeholder" } },
180
+ {},
181
+ arrayToObject(allEnumIds)
182
+ ]
183
+ };
184
+ }
185
+
186
+ const fieldVar = config.field.startsWith("user.") || config.field === "isNew" || config.field === "entityId"
187
+ ? { var: config.field }
188
+ : { var: `values.${config.field}` };
189
+
190
+ // Build the condition
191
+ let condition: Record<string, unknown>;
192
+ if (config.operator === "!!") {
193
+ condition = { "!!": fieldVar };
194
+ } else if (config.operator === "!") {
195
+ condition = { "!": fieldVar };
196
+ } else {
197
+ let parsedValue: unknown = config.value;
198
+ if (config.value === "true") parsedValue = true;
199
+ else if (config.value === "false") parsedValue = false;
200
+ else if (!isNaN(Number(config.value)) && config.value !== "") parsedValue = Number(config.value);
201
+
202
+ if (config.operator === "in" && typeof parsedValue === "string") {
203
+ parsedValue = parsedValue.split(",").map(s => s.trim());
204
+ }
205
+
206
+ condition = { [config.operator]: [fieldVar, parsedValue] };
207
+ }
208
+
209
+ // For allowedEnumValues: when condition true, show selected; when false, show all
210
+ // For excludedEnumValues: when condition true, exclude selected; when false, exclude nothing
211
+ // Convert arrays to objects to avoid Firestore nested array error
212
+ if (conditionType === "allowedEnumValues") {
213
+ return {
214
+ "if": [
215
+ condition,
216
+ arrayToObject(config.selectedEnumIds), // When true: only these
217
+ arrayToObject(allEnumIds) // When false: all
218
+ ]
219
+ };
220
+ } else {
221
+ // excludedEnumValues
222
+ return {
223
+ "if": [
224
+ condition,
225
+ arrayToObject(config.selectedEnumIds), // When true: exclude these
226
+ {} // When false: exclude nothing
227
+ ]
228
+ };
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Convert an object with numeric keys (Firestore serialization) back to an array.
234
+ * {"0": "a", "1": "b"} -> ["a", "b"]
235
+ */
236
+ function objectToArray(obj: unknown): string[] {
237
+ if (Array.isArray(obj)) return obj.map(String);
238
+ if (obj && typeof obj === "object") {
239
+ const keys = Object.keys(obj);
240
+ // Check if all keys are numeric
241
+ if (keys.length > 0 && keys.every(k => !isNaN(Number(k)))) {
242
+ return keys
243
+ .sort((a, b) => Number(a) - Number(b))
244
+ .map(k => String((obj as Record<string, unknown>)[k]));
245
+ }
246
+ }
247
+ return [];
248
+ }
249
+
250
+ /**
251
+ * Parse JSON Logic back to EnumConditionConfig
252
+ */
253
+ function parseEnumFilterRule(rule: Record<string, unknown>): EnumConditionConfig | null {
254
+ try {
255
+ // Expected: { "if": [ condition, selectedIds, fallbackIds ] }
256
+ const ifRule = rule["if"];
257
+ if (!Array.isArray(ifRule) || ifRule.length < 2) return null;
258
+
259
+ const [condition, selectedEnumIdsRaw] = ifRule;
260
+
261
+ // Handle both array format and Firestore's object-with-numeric-keys format
262
+ const selectedEnumIds = objectToArray(selectedEnumIdsRaw);
263
+ if (selectedEnumIds.length === 0 && selectedEnumIdsRaw) {
264
+ // If it was truthy but we got no values, parsing failed
265
+ console.log("[EnumConditionsEditor] Warning: Could not parse selectedEnumIds:", selectedEnumIdsRaw);
266
+ }
267
+
268
+ // Parse the condition
269
+ const conditionObj = condition as Record<string, unknown>;
270
+ const operator = Object.keys(conditionObj)[0] as OperatorId;
271
+ const args = conditionObj[operator] as unknown;
272
+
273
+ let field = "";
274
+ let value = "";
275
+
276
+ if (operator === "!!" || operator === "!") {
277
+ const varObj = args as Record<string, string> | null;
278
+ if (varObj?.var) {
279
+ field = varObj.var.replace(/^values\./, "");
280
+ }
281
+ } else if (Array.isArray(args) && args.length === 2) {
282
+ const [left, right] = args as [Record<string, string>, unknown];
283
+ if (left?.var) {
284
+ field = left.var.replace(/^values\./, "");
285
+ value = Array.isArray(right) ? right.join(", ") : String(right ?? "");
286
+ }
287
+ }
288
+
289
+ return {
290
+ field,
291
+ operator,
292
+ value,
293
+ selectedEnumIds
294
+ };
295
+ } catch (e) {
296
+ console.error("[EnumConditionsEditor] Error parsing rule:", e);
297
+ return null;
298
+ }
299
+ }
300
+
301
+ interface EnumConditionRowProps {
302
+ conditionType: EnumConditionType;
303
+ config: EnumConditionConfig;
304
+ onConfigChange: (config: EnumConditionConfig) => void;
305
+ onRemove: () => void;
306
+ disabled: boolean;
307
+ availableFields: string[];
308
+ collectionProperties?: Properties;
309
+ propertyEnumValues: EnumValueConfig[];
310
+ showErrors: boolean;
311
+ }
312
+
313
+ function EnumConditionRow({
314
+ conditionType,
315
+ config,
316
+ onConfigChange,
317
+ onRemove,
318
+ disabled,
319
+ availableFields,
320
+ collectionProperties,
321
+ propertyEnumValues,
322
+ showErrors
323
+ }: EnumConditionRowProps) {
324
+ const operator = OPERATORS.find(op => op.id === config.operator);
325
+ const showValueField = operator?.valueType !== "none";
326
+
327
+ // Get the property for the selected field
328
+ const selectedFieldProperty = getPropertyAtPath(config.field, collectionProperties);
329
+ const isBoolean = config.field === "isNew" || selectedFieldProperty?.dataType === "boolean";
330
+
331
+ // Check if the selected field has enum values
332
+ const fieldEnumValues = getFieldEnumValues(selectedFieldProperty);
333
+
334
+ // Validation: check for incomplete condition (only show after submission attempt)
335
+ const isFieldMissing = !config.field;
336
+ const isValueMissing = showValueField && !config.value && config.value !== "0";
337
+ const isEnumSelectionMissing = config.selectedEnumIds.length === 0;
338
+ const hasError = showErrors && (isFieldMissing || isValueMissing || isEnumSelectionMissing);
339
+
340
+ return (
341
+ <div className={cls(
342
+ "p-3 bg-surface-50 dark:bg-surface-900 rounded-lg border",
343
+ hasError ? "border-red-300 dark:border-red-700" : defaultBorderMixin
344
+ )}>
345
+ {/* Title line */}
346
+ <div className="flex items-center justify-between mb-2">
347
+ <Typography variant="label" className="font-medium text-primary">
348
+ {ENUM_CONDITION_TYPES.find(ct => ct.id === conditionType)?.label} when
349
+ </Typography>
350
+ <IconButton
351
+ onClick={onRemove}
352
+ disabled={disabled}
353
+ size="small"
354
+ variant="ghost">
355
+ <DeleteIcon size="smallest" />
356
+ </IconButton>
357
+ </div>
358
+
359
+ {/* Condition row: Field, Operator, Value */}
360
+ <div className="flex items-center gap-2 mb-3">
361
+ {/* Field selector */}
362
+ <Select
363
+ value={config.field}
364
+ onValueChange={(value) => onConfigChange({ ...config, field: value, value: "" })}
365
+ disabled={disabled}
366
+ size="small"
367
+ inputClassName="min-w-[140px]"
368
+ placeholder="Field">
369
+ {CONTEXT_FIELDS.map(field => (
370
+ <SelectItem key={field.id} value={field.id}>
371
+ <div className="flex items-center gap-2">
372
+ <div
373
+ className="w-2 h-2 rounded-full"
374
+ style={{ backgroundColor: field.color }}
375
+ />
376
+ <span>{field.label}</span>
377
+ </div>
378
+ </SelectItem>
379
+ ))}
380
+ {availableFields.length > 0 && (
381
+ <SelectItem value="_divider" disabled>
382
+ <span className="text-xs text-surface-500">─ Fields ─</span>
383
+ </SelectItem>
384
+ )}
385
+ {availableFields.map(field => (
386
+ <SelectItem key={field} value={field}>
387
+ <div className="flex items-center gap-2">
388
+ <div
389
+ className="w-2 h-2 rounded-full"
390
+ style={{ backgroundColor: getFieldColor(field, collectionProperties) }}
391
+ />
392
+ <span>{field}</span>
393
+ </div>
394
+ </SelectItem>
395
+ ))}
396
+ </Select>
397
+
398
+ {/* Operator */}
399
+ <Select
400
+ value={config.operator}
401
+ onValueChange={(value) => onConfigChange({ ...config, operator: value as OperatorId })}
402
+ disabled={disabled}
403
+ size="small"
404
+ inputClassName="min-w-[100px]">
405
+ {OPERATORS.map(op => (
406
+ <SelectItem key={op.id} value={op.id}>{op.label}</SelectItem>
407
+ ))}
408
+ </Select>
409
+
410
+ {/* Value - dynamic based on field type */}
411
+ {showValueField && (
412
+ isBoolean ? (
413
+ <BooleanSwitchWithLabel
414
+ value={config.value === "true"}
415
+ size="small"
416
+ position="start"
417
+ disabled={disabled}
418
+ onValueChange={(v) => onConfigChange({ ...config, value: v ? "true" : "false" })}
419
+ />
420
+ ) : fieldEnumValues.length > 0 ? (
421
+ // Show Select for enum fields
422
+ <Select
423
+ value={config.value}
424
+ onValueChange={(value) => onConfigChange({ ...config, value })}
425
+ disabled={disabled}
426
+ size="small"
427
+ placeholder="Select value..."
428
+ className="min-w-[120px]">
429
+ {fieldEnumValues.map(ev => (
430
+ <SelectItem key={String(ev.id)} value={String(ev.id)}>
431
+ {ev.label}
432
+ </SelectItem>
433
+ ))}
434
+ </Select>
435
+ ) : (
436
+ <DebouncedTextField
437
+ value={config.value}
438
+ onChange={(e) => onConfigChange({ ...config, value: e.target.value })}
439
+ disabled={disabled}
440
+ size="small"
441
+ placeholder="Value"
442
+ className="flex-1 min-w-[80px]"
443
+ />
444
+ )
445
+ )}
446
+ </div>
447
+
448
+ {/* Enum selection */}
449
+ <div>
450
+ <Typography variant="caption" color="secondary" className="block mb-2">
451
+ {conditionType === "allowedEnumValues" ? "Show only:" : "Hide:"}
452
+ </Typography>
453
+ <MultiSelect
454
+ value={config.selectedEnumIds}
455
+ onValueChange={(values) => onConfigChange({ ...config, selectedEnumIds: values })}
456
+ disabled={disabled}
457
+ size="small"
458
+ placeholder="Select values..."
459
+ useChips={true}>
460
+ {propertyEnumValues.map(ev => (
461
+ <MultiSelectItem key={String(ev.id)} value={String(ev.id)}>
462
+ {ev.label}
463
+ </MultiSelectItem>
464
+ ))}
465
+ </MultiSelect>
466
+ </div>
467
+
468
+ {/* Validation error message */}
469
+ {hasError && (
470
+ <Typography variant="caption" className="mt-2 text-red-500 dark:text-red-400">
471
+ {isFieldMissing
472
+ ? "Please select a field for this condition"
473
+ : isValueMissing
474
+ ? "Please enter a value for this condition"
475
+ : "Please select at least one value to filter"
476
+ }
477
+ </Typography>
478
+ )}
479
+ </div>
480
+ );
481
+ }
482
+
483
+ export interface EnumConditionsEditorProps {
484
+ disabled: boolean;
485
+ collectionProperties?: Properties;
486
+ }
487
+
488
+ export function EnumConditionsEditor({ disabled, collectionProperties }: EnumConditionsEditorProps) {
489
+ const { values, setFieldValue, submitCount } = useFormex<PropertyWithId>();
490
+
491
+ const availableFields: string[] = collectionProperties
492
+ ? getPropertyPaths(collectionProperties)
493
+ : [];
494
+
495
+ const propertyEnumValues = getCurrentPropertyEnumValues(values);
496
+
497
+ // Only show for enum properties
498
+ if (propertyEnumValues.length === 0) {
499
+ return null;
500
+ }
501
+
502
+ const allEnumIds = propertyEnumValues.map(ev => String(ev.id));
503
+
504
+ // Get conditions from the correct path: values.conditions
505
+ const conditions = (values as PropertyWithId & { conditions?: Record<string, unknown> }).conditions ?? {};
506
+
507
+ // Parse existing enum conditions
508
+ const activeConditions: { type: EnumConditionType; config: EnumConditionConfig }[] = [];
509
+
510
+ for (const type of ENUM_CONDITION_TYPES) {
511
+ const rule = conditions[type.id] as Record<string, unknown> | undefined;
512
+ if (rule) {
513
+ const config = parseEnumFilterRule(rule);
514
+ if (config) {
515
+ activeConditions.push({ type: type.id, config });
516
+ }
517
+ }
518
+ }
519
+
520
+ const handleAddCondition = (conditionType: EnumConditionType) => {
521
+ const defaultConfig: EnumConditionConfig = {
522
+ field: availableFields[0] ?? "isNew",
523
+ operator: "==",
524
+ value: "",
525
+ selectedEnumIds: []
526
+ };
527
+
528
+ const rule = buildEnumFilterRule(defaultConfig, conditionType, allEnumIds);
529
+ setFieldValue(`conditions.${conditionType}`, rule);
530
+ };
531
+
532
+ const handleConfigChange = (type: EnumConditionType, config: EnumConditionConfig) => {
533
+ const rule = buildEnumFilterRule(config, type, allEnumIds);
534
+ setFieldValue(`conditions.${type}`, rule);
535
+ };
536
+
537
+ const handleRemoveCondition = (type: EnumConditionType) => {
538
+ const newConditions = { ...conditions };
539
+ delete newConditions[type];
540
+ if (Object.keys(newConditions).length === 0) {
541
+ setFieldValue("conditions", undefined);
542
+ } else {
543
+ setFieldValue("conditions", newConditions);
544
+ }
545
+ };
546
+
547
+ const availableConditionTypes = ENUM_CONDITION_TYPES.filter(
548
+ ct => !conditions[ct.id]
549
+ );
550
+
551
+ return (
552
+ <div className={cls("flex flex-col gap-4 mt-4 pt-4 border-t", defaultBorderMixin)}>
553
+ <Typography variant="label" className="font-medium">
554
+ Enum Value Conditions
555
+ </Typography>
556
+ <Typography variant="caption" color="secondary">
557
+ Dynamically filter which options are available.
558
+ </Typography>
559
+
560
+ {activeConditions.length > 0 && (
561
+ <div className="flex flex-col gap-3">
562
+ {activeConditions.map(({ type, config }) => (
563
+ <EnumConditionRow
564
+ key={type}
565
+ conditionType={type}
566
+ config={config}
567
+ onConfigChange={(newConfig) => handleConfigChange(type, newConfig)}
568
+ onRemove={() => handleRemoveCondition(type)}
569
+ disabled={disabled}
570
+ availableFields={availableFields}
571
+ collectionProperties={collectionProperties}
572
+ propertyEnumValues={propertyEnumValues}
573
+ showErrors={submitCount > 0}
574
+ />
575
+ ))}
576
+ </div>
577
+ )}
578
+
579
+ {availableConditionTypes.length > 0 && (
580
+ <Select
581
+ value=""
582
+ onValueChange={(value) => handleAddCondition(value as EnumConditionType)}
583
+ disabled={disabled}
584
+ size="small"
585
+ placeholder="+ Add enum condition..."
586
+ className="w-full max-w-xs">
587
+ {availableConditionTypes.map(ct => (
588
+ <SelectItem key={ct.id} value={ct.id}>
589
+ <div className="flex flex-col">
590
+ <span className="font-medium">{ct.label}</span>
591
+ <span className="text-xs text-surface-500">{ct.description}</span>
592
+ </div>
593
+ </SelectItem>
594
+ ))}
595
+ </Select>
596
+ )}
597
+ </div>
598
+ );
599
+ }
@@ -0,0 +1,6 @@
1
+ export { ConditionsPanel } from "./ConditionsPanel";
2
+ export { ConditionsEditor } from "./ConditionsEditor";
3
+ export type { ConditionsEditorProps } from "./ConditionsEditor";
4
+ export { EnumConditionsEditor } from "./EnumConditionsEditor";
5
+ export type { EnumConditionsEditorProps } from "./EnumConditionsEditor";
6
+ export { getPropertyPaths, getGroupedPropertyPaths } from "./property_paths";