@firecms/collection_editor 3.0.1 → 3.1.0-canary.02232f4
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.
- package/dist/ConfigControllerProvider.d.ts +6 -0
- package/dist/api/generateCollectionApi.d.ts +71 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.es.js +15260 -8173
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +15257 -8170
- package/dist/index.umd.js.map +1 -1
- package/dist/locales/de.d.ts +120 -0
- package/dist/locales/en.d.ts +120 -0
- package/dist/locales/es.d.ts +120 -0
- package/dist/locales/fr.d.ts +120 -0
- package/dist/locales/hi.d.ts +120 -0
- package/dist/locales/it.d.ts +120 -0
- package/dist/locales/pt.d.ts +120 -0
- package/dist/types/collection_editor_controller.d.ts +14 -0
- package/dist/types/collection_inference.d.ts +8 -2
- package/dist/types/config_controller.d.ts +23 -2
- package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
- package/dist/ui/KanbanSetupAction.d.ts +10 -0
- package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +37 -0
- package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
- package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
- package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +24 -0
- package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +4 -1
- package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
- package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
- package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
- package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
- package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
- package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
- package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
- package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
- package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
- package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
- package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
- package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
- package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
- package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
- package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
- package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
- package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
- package/dist/useCollectionEditorPlugin.d.ts +7 -1
- package/dist/utils/validateCollectionJson.d.ts +22 -0
- package/package.json +15 -15
- package/src/ConfigControllerProvider.tsx +82 -47
- package/src/api/generateCollectionApi.ts +119 -0
- package/src/api/index.ts +1 -0
- package/src/index.ts +28 -1
- package/src/locales/de.ts +125 -0
- package/src/locales/en.ts +145 -0
- package/src/locales/es.ts +125 -0
- package/src/locales/fr.ts +125 -0
- package/src/locales/hi.ts +125 -0
- package/src/locales/it.ts +125 -0
- package/src/locales/pt.ts +125 -0
- package/src/types/collection_editor_controller.tsx +16 -3
- package/src/types/collection_inference.ts +15 -2
- package/src/types/config_controller.tsx +27 -2
- package/src/ui/AddKanbanColumnAction.tsx +203 -0
- package/src/ui/EditorCollectionAction.tsx +3 -3
- package/src/ui/EditorCollectionActionStart.tsx +1 -2
- package/src/ui/EditorEntityAction.tsx +3 -2
- package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
- package/src/ui/KanbanSetupAction.tsx +38 -0
- package/src/ui/MissingReferenceWidget.tsx +1 -1
- package/src/ui/NewCollectionButton.tsx +4 -2
- package/src/ui/NewCollectionCard.tsx +7 -4
- package/src/ui/PropertyAddColumnComponent.tsx +4 -3
- package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +243 -0
- package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +222 -267
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +270 -198
- package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +138 -71
- package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
- package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +202 -101
- package/src/ui/collection_editor/DisplaySettingsForm.tsx +335 -0
- package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -97
- package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +8 -10
- package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +5 -7
- package/src/ui/collection_editor/EnumForm.tsx +153 -102
- package/src/ui/collection_editor/ExtendSettingsForm.tsx +94 -0
- package/src/ui/collection_editor/GeneralSettingsForm.tsx +335 -0
- package/src/ui/collection_editor/GetCodeDialog.tsx +63 -41
- package/src/ui/collection_editor/KanbanConfigSection.tsx +209 -0
- package/src/ui/collection_editor/LayoutModeSwitch.tsx +27 -43
- package/src/ui/collection_editor/PropertyEditView.tsx +272 -199
- package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
- package/src/ui/collection_editor/PropertyTree.tsx +130 -58
- package/src/ui/collection_editor/SubcollectionsEditTab.tsx +169 -163
- package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
- package/src/ui/collection_editor/ViewModeSwitch.tsx +43 -0
- package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +6 -3
- package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +5 -2
- package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +4 -1
- package/src/ui/collection_editor/properties/CommonPropertyFields.tsx +6 -4
- package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +126 -42
- package/src/ui/collection_editor/properties/EnumPropertyField.tsx +32 -24
- package/src/ui/collection_editor/properties/MapPropertyField.tsx +8 -9
- package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +128 -53
- package/src/ui/collection_editor/properties/NumberPropertyField.tsx +3 -1
- package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +5 -4
- package/src/ui/collection_editor/properties/StoragePropertyField.tsx +47 -52
- package/src/ui/collection_editor/properties/StringPropertyField.tsx +3 -1
- package/src/ui/collection_editor/properties/UrlPropertyField.tsx +12 -10
- package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +23 -4
- package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +866 -0
- package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
- package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
- package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
- package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
- package/src/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +5 -2
- package/src/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +7 -5
- package/src/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +10 -7
- package/src/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +11 -9
- package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +5 -2
- package/src/useCollectionEditorPlugin.tsx +53 -22
- package/src/utils/validateCollectionJson.ts +380 -0
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useFormex } from "@firecms/formex";
|
|
3
|
+
import {
|
|
4
|
+
DeleteIcon,
|
|
5
|
+
IconButton,
|
|
6
|
+
Select,
|
|
7
|
+
SelectItem,
|
|
8
|
+
DebouncedTextField,
|
|
9
|
+
Typography,
|
|
10
|
+
Chip,
|
|
11
|
+
BooleanSwitchWithLabel,
|
|
12
|
+
TextField,
|
|
13
|
+
cls,
|
|
14
|
+
defaultBorderMixin
|
|
15
|
+
} from "@firecms/ui";
|
|
16
|
+
import {
|
|
17
|
+
Properties,
|
|
18
|
+
Property,
|
|
19
|
+
PropertyOrBuilder,
|
|
20
|
+
getFieldConfig,
|
|
21
|
+
DEFAULT_FIELD_CONFIGS,
|
|
22
|
+
getIconForWidget,
|
|
23
|
+
EnumValueConfig,
|
|
24
|
+
isPropertyBuilder,
|
|
25
|
+
useTranslation
|
|
26
|
+
} from "@firecms/core";
|
|
27
|
+
import { PropertyWithId } from "../../PropertyEditView";
|
|
28
|
+
import { getPropertyPaths } from "./property_paths";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Condition types that can be configured
|
|
32
|
+
*/
|
|
33
|
+
const CONDITION_TYPES = [
|
|
34
|
+
{ id: "disabled", label: "Disabled", description: "Disable this field when condition is true" },
|
|
35
|
+
{ id: "hidden", label: "Hidden", description: "Hide this field when condition is true" },
|
|
36
|
+
{ id: "required", label: "Required", description: "Make this field required when condition is true" },
|
|
37
|
+
{ id: "readOnly", label: "Read Only", description: "Make this field read-only when condition is true" }
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
type ConditionType = typeof CONDITION_TYPES[number]["id"];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Operators for building conditions with their applicable data types
|
|
44
|
+
*/
|
|
45
|
+
const OPERATORS = [
|
|
46
|
+
{ id: "==", label: "equals", valueType: "any", applicableTo: ["string", "number", "boolean", "date"] },
|
|
47
|
+
{ id: "!=", label: "not equals", valueType: "any", applicableTo: ["string", "number", "boolean", "date"] },
|
|
48
|
+
{ id: ">", label: "greater than", valueType: "number", applicableTo: ["number", "date"] },
|
|
49
|
+
{ id: "<", label: "less than", valueType: "number", applicableTo: ["number", "date"] },
|
|
50
|
+
{ id: ">=", label: "greater or equal", valueType: "number", applicableTo: ["number", "date"] },
|
|
51
|
+
{ id: "<=", label: "less or equal", valueType: "number", applicableTo: ["number", "date"] },
|
|
52
|
+
{ id: "in", label: "contains", valueType: "any", applicableTo: ["array"] },
|
|
53
|
+
{ id: "!in", label: "not contains", valueType: "any", applicableTo: ["array"] },
|
|
54
|
+
{ id: "!!", label: "has a value", valueType: "none", applicableTo: ["string", "number", "boolean", "array", "map", "date"] },
|
|
55
|
+
{ id: "!", label: "is empty", valueType: "none", applicableTo: ["string", "number", "boolean", "array", "map", "date"] },
|
|
56
|
+
{ id: "isPast", label: "is in the past", valueType: "none", applicableTo: ["date"] },
|
|
57
|
+
{ id: "isFuture", label: "is in the future", valueType: "none", applicableTo: ["date"] }
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
type OperatorId = typeof OPERATORS[number]["id"];
|
|
61
|
+
|
|
62
|
+
// Context fields with their types
|
|
63
|
+
const CONTEXT_FIELDS = [
|
|
64
|
+
{ id: "isNew", label: "Is New Entity", dataType: "boolean", color: "#9c27b0" },
|
|
65
|
+
{ id: "entityId", label: "Entity ID", dataType: "string", color: "#2196f3" },
|
|
66
|
+
{ id: "user.roles", label: "User Roles", dataType: "array", color: "#ff9800" }
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
interface SimpleRule {
|
|
70
|
+
field: string;
|
|
71
|
+
operator: OperatorId;
|
|
72
|
+
value: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type LogicOperator = "and" | "or";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A condition group can contain rules and/or nested groups
|
|
79
|
+
*/
|
|
80
|
+
interface ConditionGroup {
|
|
81
|
+
logic: LogicOperator;
|
|
82
|
+
rules: SimpleRule[];
|
|
83
|
+
groups?: ConditionGroup[]; // Nested groups for complex logic
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the property at a given path.
|
|
88
|
+
* Returns undefined if the path contains a PropertyBuilder (callback).
|
|
89
|
+
*/
|
|
90
|
+
function getPropertyAtPath(
|
|
91
|
+
fieldPath: string,
|
|
92
|
+
collectionProperties?: Properties
|
|
93
|
+
): Property | undefined {
|
|
94
|
+
if (!collectionProperties) return undefined;
|
|
95
|
+
|
|
96
|
+
const parts = fieldPath.split(".");
|
|
97
|
+
let current: Properties | undefined = collectionProperties;
|
|
98
|
+
let property: Property | undefined;
|
|
99
|
+
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
if (!current) return undefined;
|
|
102
|
+
const propertyOrBuilder = current[part] as PropertyOrBuilder | undefined;
|
|
103
|
+
if (!propertyOrBuilder) return undefined;
|
|
104
|
+
|
|
105
|
+
// Skip PropertyBuilder functions - they require runtime values
|
|
106
|
+
if (isPropertyBuilder(propertyOrBuilder)) return undefined;
|
|
107
|
+
|
|
108
|
+
property = propertyOrBuilder as Property;
|
|
109
|
+
|
|
110
|
+
if (property.dataType === "map" && property.properties) {
|
|
111
|
+
current = property.properties as Properties;
|
|
112
|
+
} else {
|
|
113
|
+
current = undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return property;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the data type for a field path
|
|
122
|
+
*/
|
|
123
|
+
function getFieldDataType(
|
|
124
|
+
fieldPath: string,
|
|
125
|
+
collectionProperties?: Properties
|
|
126
|
+
): string {
|
|
127
|
+
// Check context fields first
|
|
128
|
+
const contextField = CONTEXT_FIELDS.find(f => f.id === fieldPath);
|
|
129
|
+
if (contextField) return contextField.dataType;
|
|
130
|
+
|
|
131
|
+
const property = getPropertyAtPath(fieldPath, collectionProperties);
|
|
132
|
+
return property?.dataType ?? "string";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get enum values for a property if it has them
|
|
137
|
+
*/
|
|
138
|
+
function getEnumValues(property: Property | undefined): EnumValueConfig[] | undefined {
|
|
139
|
+
if (!property) return undefined;
|
|
140
|
+
if (property.dataType === "string" && property.enumValues) {
|
|
141
|
+
// Normalize to array format
|
|
142
|
+
if (Array.isArray(property.enumValues)) {
|
|
143
|
+
return property.enumValues;
|
|
144
|
+
}
|
|
145
|
+
// Handle object format { key: label }
|
|
146
|
+
return Object.entries(property.enumValues).map(([id, label]) => ({
|
|
147
|
+
id,
|
|
148
|
+
label: typeof label === "string" ? label : (label as EnumValueConfig).label
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get operators applicable for a given data type
|
|
156
|
+
*/
|
|
157
|
+
function getOperatorsForDataType(dataType: string) {
|
|
158
|
+
return OPERATORS.filter(op => {
|
|
159
|
+
const applicable = op.applicableTo as readonly string[];
|
|
160
|
+
return applicable.includes(dataType) ||
|
|
161
|
+
(applicable.includes("string") && !["number", "date", "array"].includes(dataType));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Props for a single rule row (used inside a group)
|
|
167
|
+
*/
|
|
168
|
+
interface RuleRowProps {
|
|
169
|
+
rule: SimpleRule;
|
|
170
|
+
onRuleChange: (rule: SimpleRule) => void;
|
|
171
|
+
onRemove: () => void;
|
|
172
|
+
disabled: boolean;
|
|
173
|
+
availableFields: string[];
|
|
174
|
+
collectionProperties?: Properties;
|
|
175
|
+
showErrors: boolean;
|
|
176
|
+
showRemoveButton: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Props for a condition group (with AND/OR logic)
|
|
181
|
+
*/
|
|
182
|
+
interface ConditionGroupRowProps {
|
|
183
|
+
conditionType: ConditionType;
|
|
184
|
+
group: ConditionGroup;
|
|
185
|
+
onGroupChange: (group: ConditionGroup) => void;
|
|
186
|
+
onRemove: () => void;
|
|
187
|
+
disabled: boolean;
|
|
188
|
+
availableFields: string[];
|
|
189
|
+
collectionProperties?: Properties;
|
|
190
|
+
showErrors: boolean;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Dynamic value input based on field data type and enum values
|
|
195
|
+
*/
|
|
196
|
+
function ConditionValueInput({
|
|
197
|
+
value,
|
|
198
|
+
onChange,
|
|
199
|
+
dataType,
|
|
200
|
+
enumValues,
|
|
201
|
+
disabled,
|
|
202
|
+
operator
|
|
203
|
+
}: {
|
|
204
|
+
value: string;
|
|
205
|
+
onChange: (value: string) => void;
|
|
206
|
+
dataType: string;
|
|
207
|
+
enumValues?: EnumValueConfig[];
|
|
208
|
+
disabled: boolean;
|
|
209
|
+
operator: OperatorId;
|
|
210
|
+
}) {
|
|
211
|
+
// For truthy/falsy operators, no value needed
|
|
212
|
+
if (operator === "!!" || operator === "!") {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Boolean - use switch
|
|
217
|
+
if (dataType === "boolean") {
|
|
218
|
+
return (
|
|
219
|
+
<BooleanSwitchWithLabel
|
|
220
|
+
value={value === "true"}
|
|
221
|
+
size="small"
|
|
222
|
+
position="start"
|
|
223
|
+
disabled={disabled}
|
|
224
|
+
onValueChange={(newValue) => onChange(newValue ? "true" : "false")}
|
|
225
|
+
/>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Enum/string with enumValues - use select
|
|
230
|
+
if (enumValues && enumValues.length > 0) {
|
|
231
|
+
return (
|
|
232
|
+
<Select
|
|
233
|
+
value={value}
|
|
234
|
+
onValueChange={onChange}
|
|
235
|
+
disabled={disabled}
|
|
236
|
+
size="small"
|
|
237
|
+
className="min-w-[120px]"
|
|
238
|
+
placeholder="Select value">
|
|
239
|
+
{enumValues.map(ev => (
|
|
240
|
+
<SelectItem key={ev.id} value={String(ev.id)}>
|
|
241
|
+
{ev.label}
|
|
242
|
+
</SelectItem>
|
|
243
|
+
))}
|
|
244
|
+
</Select>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Number - use number input
|
|
249
|
+
if (dataType === "number") {
|
|
250
|
+
return (
|
|
251
|
+
<TextField
|
|
252
|
+
value={value}
|
|
253
|
+
type="number"
|
|
254
|
+
size="small"
|
|
255
|
+
disabled={disabled}
|
|
256
|
+
placeholder="Value"
|
|
257
|
+
className="flex-1 min-w-[80px]"
|
|
258
|
+
onChange={(e) => onChange(e.target.value)}
|
|
259
|
+
/>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Array - comma separated values
|
|
264
|
+
if (dataType === "array") {
|
|
265
|
+
return (
|
|
266
|
+
<DebouncedTextField
|
|
267
|
+
value={value}
|
|
268
|
+
onChange={(e) => onChange(e.target.value)}
|
|
269
|
+
disabled={disabled}
|
|
270
|
+
size="small"
|
|
271
|
+
placeholder="value1, value2"
|
|
272
|
+
className="flex-1 min-w-[120px]"
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Default - text input
|
|
278
|
+
return (
|
|
279
|
+
<DebouncedTextField
|
|
280
|
+
value={value}
|
|
281
|
+
onChange={(e) => onChange(e.target.value)}
|
|
282
|
+
disabled={disabled}
|
|
283
|
+
size="small"
|
|
284
|
+
placeholder="Value"
|
|
285
|
+
className="flex-1 min-w-[100px]"
|
|
286
|
+
inputClassName="min-w-[100px]"
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function ConditionRow({
|
|
292
|
+
rule,
|
|
293
|
+
onRuleChange,
|
|
294
|
+
onRemove,
|
|
295
|
+
disabled,
|
|
296
|
+
availableFields,
|
|
297
|
+
collectionProperties,
|
|
298
|
+
showErrors,
|
|
299
|
+
showRemoveButton = true
|
|
300
|
+
}: RuleRowProps & { showRemoveButton?: boolean }) {
|
|
301
|
+
const fieldDataType = getFieldDataType(rule.field, collectionProperties);
|
|
302
|
+
const applicableOperators = getOperatorsForDataType(fieldDataType);
|
|
303
|
+
const property = getPropertyAtPath(rule.field, collectionProperties);
|
|
304
|
+
const enumValues = getEnumValues(property);
|
|
305
|
+
const operator = OPERATORS.find(op => op.id === rule.operator);
|
|
306
|
+
const showValueField = operator?.valueType !== "none";
|
|
307
|
+
|
|
308
|
+
// Validation: check for incomplete condition (only show after submission attempt)
|
|
309
|
+
const isFieldMissing = !rule.field;
|
|
310
|
+
const isValueMissing = showValueField && !rule.value && rule.value !== "0";
|
|
311
|
+
const hasError = showErrors && (isFieldMissing || isValueMissing);
|
|
312
|
+
|
|
313
|
+
// Get color for the field
|
|
314
|
+
const getFieldColor = (fieldPath: string): string => {
|
|
315
|
+
const contextField = CONTEXT_FIELDS.find(f => f.id === fieldPath);
|
|
316
|
+
if (contextField) return contextField.color;
|
|
317
|
+
|
|
318
|
+
if (!collectionProperties) return "#888";
|
|
319
|
+
|
|
320
|
+
const prop = getPropertyAtPath(fieldPath, collectionProperties);
|
|
321
|
+
if (!prop) return "#888";
|
|
322
|
+
const config = getFieldConfig(prop, DEFAULT_FIELD_CONFIGS);
|
|
323
|
+
return config?.color ?? "#888";
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div className={cls(
|
|
328
|
+
"flex items-center gap-2 p-2 rounded-md",
|
|
329
|
+
hasError ? "bg-red-50 dark:bg-red-900/20" : "bg-surface-100 dark:bg-surface-800"
|
|
330
|
+
)}>
|
|
331
|
+
{/* Field selector with colored badge */}
|
|
332
|
+
<Select
|
|
333
|
+
value={rule.field}
|
|
334
|
+
onValueChange={(value) => {
|
|
335
|
+
const newDataType = getFieldDataType(value, collectionProperties);
|
|
336
|
+
const newApplicableOps = getOperatorsForDataType(newDataType);
|
|
337
|
+
// Reset operator if current one isn't applicable
|
|
338
|
+
const newOperator = newApplicableOps.find(op => op.id === rule.operator)
|
|
339
|
+
? rule.operator
|
|
340
|
+
: newApplicableOps[0]?.id ?? "==";
|
|
341
|
+
// Reset value when changing field
|
|
342
|
+
onRuleChange({ ...rule, field: value, operator: newOperator as OperatorId, value: "" });
|
|
343
|
+
}}
|
|
344
|
+
disabled={disabled}
|
|
345
|
+
size="small"
|
|
346
|
+
inputClassName="min-w-[180px]"
|
|
347
|
+
renderValue={(value) => (
|
|
348
|
+
<div className="flex items-center gap-2">
|
|
349
|
+
<div
|
|
350
|
+
className="w-2 h-2 rounded-full shrink-0"
|
|
351
|
+
style={{ backgroundColor: getFieldColor(value) }}
|
|
352
|
+
/>
|
|
353
|
+
<span className="truncate">{value}</span>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
placeholder="Select field">
|
|
357
|
+
{/* Context fields */}
|
|
358
|
+
{CONTEXT_FIELDS.map(field => (
|
|
359
|
+
<SelectItem key={field.id} value={field.id}>
|
|
360
|
+
<div className="flex items-center gap-2">
|
|
361
|
+
<div
|
|
362
|
+
className="w-3 h-3 rounded-full shrink-0"
|
|
363
|
+
style={{ backgroundColor: field.color }}
|
|
364
|
+
/>
|
|
365
|
+
<span>{field.label}</span>
|
|
366
|
+
<Chip size="small" colorScheme="grayLight" className="ml-auto">
|
|
367
|
+
{field.dataType}
|
|
368
|
+
</Chip>
|
|
369
|
+
</div>
|
|
370
|
+
</SelectItem>
|
|
371
|
+
))}
|
|
372
|
+
|
|
373
|
+
{/* Separator */}
|
|
374
|
+
{availableFields.length > 0 && (
|
|
375
|
+
<SelectItem value="_divider" disabled>
|
|
376
|
+
<span className="text-xs text-surface-500">─ Entity Fields ─</span>
|
|
377
|
+
</SelectItem>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{/* Property fields */}
|
|
381
|
+
{availableFields.map(field => {
|
|
382
|
+
const color = getFieldColor(field);
|
|
383
|
+
const dataType = getFieldDataType(field, collectionProperties);
|
|
384
|
+
const prop = getPropertyAtPath(field, collectionProperties);
|
|
385
|
+
const hasEnum = !!getEnumValues(prop);
|
|
386
|
+
return (
|
|
387
|
+
<SelectItem key={field} value={field}>
|
|
388
|
+
<div className="flex items-center gap-2">
|
|
389
|
+
<div
|
|
390
|
+
className="w-3 h-3 rounded-full shrink-0"
|
|
391
|
+
style={{ backgroundColor: color }}
|
|
392
|
+
/>
|
|
393
|
+
<span>{field}</span>
|
|
394
|
+
<Chip size="small" colorScheme="grayLight" className="ml-auto">
|
|
395
|
+
{hasEnum ? "enum" : dataType}
|
|
396
|
+
</Chip>
|
|
397
|
+
</div>
|
|
398
|
+
</SelectItem>
|
|
399
|
+
);
|
|
400
|
+
})}
|
|
401
|
+
</Select>
|
|
402
|
+
|
|
403
|
+
{/* Operator - filtered by field type */}
|
|
404
|
+
<Select
|
|
405
|
+
value={rule.operator}
|
|
406
|
+
onValueChange={(value) => onRuleChange({ ...rule, operator: value as OperatorId })}
|
|
407
|
+
disabled={disabled}
|
|
408
|
+
size="small"
|
|
409
|
+
inputClassName="min-w-[120px]">
|
|
410
|
+
{applicableOperators.map(op => (
|
|
411
|
+
<SelectItem key={op.id} value={op.id}>
|
|
412
|
+
{op.label}
|
|
413
|
+
</SelectItem>
|
|
414
|
+
))}
|
|
415
|
+
</Select>
|
|
416
|
+
|
|
417
|
+
{/* Dynamic Value input */}
|
|
418
|
+
{showValueField && (
|
|
419
|
+
<ConditionValueInput
|
|
420
|
+
value={rule.value}
|
|
421
|
+
onChange={(newValue) => onRuleChange({ ...rule, value: newValue })}
|
|
422
|
+
dataType={fieldDataType}
|
|
423
|
+
enumValues={enumValues}
|
|
424
|
+
disabled={disabled}
|
|
425
|
+
operator={rule.operator}
|
|
426
|
+
/>
|
|
427
|
+
)}
|
|
428
|
+
|
|
429
|
+
{/* Remove button */}
|
|
430
|
+
{showRemoveButton && (
|
|
431
|
+
<IconButton onClick={onRemove} disabled={disabled} size="small" variant="ghost">
|
|
432
|
+
<DeleteIcon size="smallest" />
|
|
433
|
+
</IconButton>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* A condition group row with AND/OR logic selector and multiple rules
|
|
441
|
+
*/
|
|
442
|
+
function ConditionGroupRow({
|
|
443
|
+
conditionType,
|
|
444
|
+
group,
|
|
445
|
+
onGroupChange,
|
|
446
|
+
onRemove,
|
|
447
|
+
disabled,
|
|
448
|
+
availableFields,
|
|
449
|
+
collectionProperties,
|
|
450
|
+
showErrors
|
|
451
|
+
}: ConditionGroupRowProps) {
|
|
452
|
+
const { t } = useTranslation();
|
|
453
|
+
|
|
454
|
+
const handleLogicChange = (logic: LogicOperator) => {
|
|
455
|
+
onGroupChange({ ...group, logic });
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const handleRuleChange = (index: number, rule: SimpleRule) => {
|
|
459
|
+
const newRules = [...group.rules];
|
|
460
|
+
newRules[index] = rule;
|
|
461
|
+
onGroupChange({ ...group, rules: newRules });
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const handleRemoveRule = (index: number) => {
|
|
465
|
+
const newRules = group.rules.filter((_, i) => i !== index);
|
|
466
|
+
if (newRules.length === 0) {
|
|
467
|
+
onRemove();
|
|
468
|
+
} else {
|
|
469
|
+
onGroupChange({ ...group, rules: newRules });
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const handleAddRule = () => {
|
|
474
|
+
const defaultRule: SimpleRule = {
|
|
475
|
+
field: availableFields[0] ?? "isNew",
|
|
476
|
+
operator: "==",
|
|
477
|
+
value: ""
|
|
478
|
+
};
|
|
479
|
+
onGroupChange({ ...group, rules: [...group.rules, defaultRule] });
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<div className={cls("p-3 bg-surface-50 dark:bg-surface-900 rounded-lg border", defaultBorderMixin)}>
|
|
484
|
+
<div className="flex items-center justify-between mb-3">
|
|
485
|
+
<div className="flex items-center gap-2">
|
|
486
|
+
<Typography variant="label" className="font-medium text-primary">
|
|
487
|
+
{CONDITION_TYPES.find(ct => ct.id === conditionType)?.label} when
|
|
488
|
+
</Typography>
|
|
489
|
+
{group.rules.length > 1 && (
|
|
490
|
+
<Select
|
|
491
|
+
value={group.logic}
|
|
492
|
+
onValueChange={(value) => handleLogicChange(value as LogicOperator)}
|
|
493
|
+
disabled={disabled}
|
|
494
|
+
size="small"
|
|
495
|
+
inputClassName="min-w-[100px]">
|
|
496
|
+
<SelectItem value="and">{t("all_of_these")}</SelectItem>
|
|
497
|
+
<SelectItem value="or">{t("any_of_these")}</SelectItem>
|
|
498
|
+
</Select>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
<IconButton onClick={onRemove} disabled={disabled} size="small" variant="ghost">
|
|
502
|
+
<DeleteIcon size="smallest" />
|
|
503
|
+
</IconButton>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div className="flex flex-col gap-2">
|
|
507
|
+
{group.rules.map((rule, index) => (
|
|
508
|
+
<React.Fragment key={index}>
|
|
509
|
+
{index > 0 && (
|
|
510
|
+
<Typography variant="caption" className="text-center text-secondary font-medium uppercase">
|
|
511
|
+
{group.logic === "and" ? "AND" : "OR"}
|
|
512
|
+
</Typography>
|
|
513
|
+
)}
|
|
514
|
+
<ConditionRow
|
|
515
|
+
rule={rule}
|
|
516
|
+
onRuleChange={(newRule: SimpleRule) => handleRuleChange(index, newRule)}
|
|
517
|
+
onRemove={() => handleRemoveRule(index)}
|
|
518
|
+
disabled={disabled}
|
|
519
|
+
availableFields={availableFields}
|
|
520
|
+
collectionProperties={collectionProperties}
|
|
521
|
+
showErrors={showErrors}
|
|
522
|
+
showRemoveButton={group.rules.length > 1}
|
|
523
|
+
/>
|
|
524
|
+
</React.Fragment>
|
|
525
|
+
))}
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<button
|
|
529
|
+
onClick={handleAddRule}
|
|
530
|
+
disabled={disabled}
|
|
531
|
+
type="button"
|
|
532
|
+
className={cls(
|
|
533
|
+
"mt-3 w-full py-2 px-3 rounded-md text-sm",
|
|
534
|
+
"border border-dashed text-secondary",
|
|
535
|
+
"hover:bg-surface-100 dark:hover:bg-surface-800",
|
|
536
|
+
"transition-colors",
|
|
537
|
+
defaultBorderMixin
|
|
538
|
+
)}
|
|
539
|
+
>
|
|
540
|
+
+ Add condition
|
|
541
|
+
</button>
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Convert a simple rule to JSON-Logic format
|
|
548
|
+
*/
|
|
549
|
+
function simpleRuleToJsonLogic(rule: SimpleRule): Record<string, any> {
|
|
550
|
+
// Guard against empty field or operator which creates invalid Firestore keys
|
|
551
|
+
if (!rule.field || !rule.operator) {
|
|
552
|
+
// Return a valid default rule instead of creating invalid data
|
|
553
|
+
return { "!!": { var: "values._placeholder" } };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const varRef = { var: `values.${rule.field}` };
|
|
557
|
+
|
|
558
|
+
// Handle special fields that don't need the "values." prefix
|
|
559
|
+
const fieldVar = rule.field.startsWith("user.") || rule.field === "isNew" || rule.field === "entityId"
|
|
560
|
+
? { var: rule.field }
|
|
561
|
+
: varRef;
|
|
562
|
+
|
|
563
|
+
if (rule.operator === "!!") {
|
|
564
|
+
return { "!!": fieldVar };
|
|
565
|
+
}
|
|
566
|
+
if (rule.operator === "!") {
|
|
567
|
+
return { "!": fieldVar };
|
|
568
|
+
}
|
|
569
|
+
if (rule.operator === "isPast") {
|
|
570
|
+
return { "isPast": fieldVar };
|
|
571
|
+
}
|
|
572
|
+
if (rule.operator === "isFuture") {
|
|
573
|
+
return { "isFuture": fieldVar };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Parse value - try to convert to appropriate type
|
|
577
|
+
let parsedValue: any = rule.value;
|
|
578
|
+
if (rule.value === "true") parsedValue = true;
|
|
579
|
+
else if (rule.value === "false") parsedValue = false;
|
|
580
|
+
else if (rule.value === "null") parsedValue = null;
|
|
581
|
+
else if (!isNaN(Number(rule.value)) && rule.value !== "") parsedValue = Number(rule.value);
|
|
582
|
+
|
|
583
|
+
// Handle array values for "in" and "!in" operators
|
|
584
|
+
if ((rule.operator === "in" || rule.operator === "!in") && typeof parsedValue === "string") {
|
|
585
|
+
parsedValue = parsedValue.split(",").map(s => s.trim());
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Handle "!in" (not contains) - wrap "in" with negation
|
|
589
|
+
if (rule.operator === "!in") {
|
|
590
|
+
return { "!": { "in": [fieldVar, parsedValue] } };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return { [rule.operator]: [fieldVar, parsedValue] };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Try to parse JSON-Logic back to a simple rule
|
|
598
|
+
*/
|
|
599
|
+
function jsonLogicToSimpleRule(jsonLogic: Record<string, any>): SimpleRule | null {
|
|
600
|
+
try {
|
|
601
|
+
const operator = Object.keys(jsonLogic)[0] as OperatorId;
|
|
602
|
+
const args = jsonLogic[operator];
|
|
603
|
+
|
|
604
|
+
// Handle "!" operator - could be negation of a value or negation of "in" (not contains)
|
|
605
|
+
if (operator === "!") {
|
|
606
|
+
// Check if it's a negated "in" operation: {"!": {"in": [...]}}
|
|
607
|
+
if (typeof args === "object" && args !== null && "in" in args) {
|
|
608
|
+
const inArgs = args.in;
|
|
609
|
+
if (Array.isArray(inArgs) && inArgs.length === 2) {
|
|
610
|
+
const [left, right] = inArgs;
|
|
611
|
+
if (left?.var) {
|
|
612
|
+
const field = left.var.replace(/^values\./, "");
|
|
613
|
+
const value = Array.isArray(right)
|
|
614
|
+
? right.join(", ")
|
|
615
|
+
: String(right ?? "");
|
|
616
|
+
return { field, operator: "!in", value };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Otherwise it's a simple negation (is falsy)
|
|
621
|
+
if (args?.var) {
|
|
622
|
+
const field = args.var.replace(/^values\./, "");
|
|
623
|
+
return { field, operator: "!", value: "" };
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (operator === "!!") {
|
|
628
|
+
const varObj = args;
|
|
629
|
+
if (varObj?.var) {
|
|
630
|
+
const field = varObj.var.replace(/^values\./, "");
|
|
631
|
+
return { field, operator, value: "" };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Handle isPast and isFuture operators
|
|
636
|
+
if (operator === "isPast" || operator === "isFuture") {
|
|
637
|
+
const varObj = args;
|
|
638
|
+
if (varObj?.var) {
|
|
639
|
+
const field = varObj.var.replace(/^values\./, "");
|
|
640
|
+
return { field, operator, value: "" };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (Array.isArray(args) && args.length === 2) {
|
|
645
|
+
const [left, right] = args;
|
|
646
|
+
if (left?.var) {
|
|
647
|
+
const field = left.var.replace(/^values\./, "");
|
|
648
|
+
const value = Array.isArray(right)
|
|
649
|
+
? right.join(", ")
|
|
650
|
+
: String(right ?? "");
|
|
651
|
+
return { field, operator, value };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
// Fall through
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Convert a condition group to JSON-Logic format
|
|
662
|
+
*/
|
|
663
|
+
function groupToJsonLogic(group: ConditionGroup): Record<string, any> {
|
|
664
|
+
const ruleLogics = group.rules
|
|
665
|
+
.filter(rule => rule.field && rule.operator) // Filter out incomplete rules
|
|
666
|
+
.map(rule => simpleRuleToJsonLogic(rule));
|
|
667
|
+
|
|
668
|
+
const groupLogics = (group.groups || [])
|
|
669
|
+
.map(g => groupToJsonLogic(g));
|
|
670
|
+
|
|
671
|
+
const allLogics = [...ruleLogics, ...groupLogics];
|
|
672
|
+
|
|
673
|
+
// If only one condition, don't wrap in and/or
|
|
674
|
+
if (allLogics.length === 1) {
|
|
675
|
+
return allLogics[0];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// If no conditions, return a default true condition
|
|
679
|
+
if (allLogics.length === 0) {
|
|
680
|
+
return { "!!": { var: "values._placeholder" } };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return { [group.logic]: allLogics };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Try to parse JSON-Logic back to a condition group
|
|
688
|
+
*/
|
|
689
|
+
function jsonLogicToGroup(jsonLogic: Record<string, any>): ConditionGroup | null {
|
|
690
|
+
try {
|
|
691
|
+
const operator = Object.keys(jsonLogic)[0];
|
|
692
|
+
|
|
693
|
+
// Check if it's an AND or OR group
|
|
694
|
+
if (operator === "and" || operator === "or") {
|
|
695
|
+
const args = jsonLogic[operator];
|
|
696
|
+
if (Array.isArray(args)) {
|
|
697
|
+
const rules: SimpleRule[] = [];
|
|
698
|
+
const groups: ConditionGroup[] = [];
|
|
699
|
+
|
|
700
|
+
for (const arg of args) {
|
|
701
|
+
// Check if this is a nested group
|
|
702
|
+
const nestedOperator = Object.keys(arg)[0];
|
|
703
|
+
if (nestedOperator === "and" || nestedOperator === "or") {
|
|
704
|
+
const nestedGroup = jsonLogicToGroup(arg);
|
|
705
|
+
if (nestedGroup) {
|
|
706
|
+
groups.push(nestedGroup);
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
// It's a simple rule
|
|
710
|
+
const rule = jsonLogicToSimpleRule(arg);
|
|
711
|
+
if (rule) {
|
|
712
|
+
rules.push(rule);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
logic: operator,
|
|
719
|
+
rules,
|
|
720
|
+
groups: groups.length > 0 ? groups : undefined
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// It's a single rule - wrap in an AND group
|
|
726
|
+
const rule = jsonLogicToSimpleRule(jsonLogic);
|
|
727
|
+
if (rule) {
|
|
728
|
+
return {
|
|
729
|
+
logic: "and",
|
|
730
|
+
rules: [rule]
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
} catch {
|
|
734
|
+
// Fall through
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export interface ConditionsEditorProps {
|
|
740
|
+
disabled: boolean;
|
|
741
|
+
/**
|
|
742
|
+
* Optional collection properties for populating the field selector.
|
|
743
|
+
* If not provided, a basic set of common fields is used.
|
|
744
|
+
*/
|
|
745
|
+
collectionProperties?: Properties;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export function ConditionsEditor({ disabled, collectionProperties }: ConditionsEditorProps) {
|
|
749
|
+
const { t } = useTranslation();
|
|
750
|
+
|
|
751
|
+
const { values, setFieldValue, submitCount } = useFormex<PropertyWithId>();
|
|
752
|
+
|
|
753
|
+
// Get property paths from collection properties (includes nested maps)
|
|
754
|
+
const availableFields: string[] = collectionProperties
|
|
755
|
+
? getPropertyPaths(collectionProperties)
|
|
756
|
+
: [];
|
|
757
|
+
|
|
758
|
+
// Get current conditions from form values
|
|
759
|
+
const conditions = (values as any).conditions ?? {};
|
|
760
|
+
|
|
761
|
+
// DEBUG: Log conditions to see what's being loaded
|
|
762
|
+
console.log("[ConditionsEditor] Loaded conditions:", conditions);
|
|
763
|
+
|
|
764
|
+
const activeConditions: { type: ConditionType; group: ConditionGroup }[] = [];
|
|
765
|
+
|
|
766
|
+
for (const type of CONDITION_TYPES) {
|
|
767
|
+
const jsonLogic = conditions[type.id as keyof typeof conditions];
|
|
768
|
+
if (jsonLogic) {
|
|
769
|
+
console.log(`[ConditionsEditor] Parsing ${type.id}:`, jsonLogic);
|
|
770
|
+
const group = jsonLogicToGroup(jsonLogic as Record<string, any>);
|
|
771
|
+
console.log(`[ConditionsEditor] Parsed group:`, group);
|
|
772
|
+
if (group) {
|
|
773
|
+
activeConditions.push({ type: type.id, group });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const handleAddCondition = (conditionType: ConditionType) => {
|
|
779
|
+
const defaultRule: SimpleRule = {
|
|
780
|
+
field: availableFields[0] ?? "isNew",
|
|
781
|
+
operator: "==",
|
|
782
|
+
value: ""
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const defaultGroup: ConditionGroup = {
|
|
786
|
+
logic: "and",
|
|
787
|
+
rules: [defaultRule]
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const jsonLogic = groupToJsonLogic(defaultGroup);
|
|
791
|
+
setFieldValue(`conditions.${conditionType}`, jsonLogic);
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const handleGroupChange = (type: ConditionType, group: ConditionGroup) => {
|
|
795
|
+
const jsonLogic = groupToJsonLogic(group);
|
|
796
|
+
setFieldValue(`conditions.${type}`, jsonLogic);
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const handleRemoveCondition = (type: ConditionType) => {
|
|
800
|
+
const newConditions = { ...conditions };
|
|
801
|
+
delete (newConditions as any)[type];
|
|
802
|
+
if (Object.keys(newConditions).length === 0) {
|
|
803
|
+
setFieldValue("conditions", undefined);
|
|
804
|
+
} else {
|
|
805
|
+
setFieldValue("conditions", newConditions);
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// Condition types that aren't already used
|
|
810
|
+
const availableConditionTypes = CONDITION_TYPES.filter(
|
|
811
|
+
ct => !conditions[ct.id as keyof typeof conditions]
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
return (
|
|
815
|
+
<div className="flex flex-col gap-4">
|
|
816
|
+
<Typography variant="caption" color="secondary">
|
|
817
|
+
Add conditions to dynamically control this field based on other field values or user context.
|
|
818
|
+
</Typography>
|
|
819
|
+
|
|
820
|
+
{/* Active conditions */}
|
|
821
|
+
{activeConditions.length > 0 && (
|
|
822
|
+
<div className="flex flex-col gap-3">
|
|
823
|
+
{activeConditions.map(({ type, group }) => (
|
|
824
|
+
<ConditionGroupRow
|
|
825
|
+
key={type}
|
|
826
|
+
conditionType={type}
|
|
827
|
+
group={group}
|
|
828
|
+
onGroupChange={(newGroup) => handleGroupChange(type, newGroup)}
|
|
829
|
+
onRemove={() => handleRemoveCondition(type)}
|
|
830
|
+
disabled={disabled}
|
|
831
|
+
availableFields={availableFields}
|
|
832
|
+
collectionProperties={collectionProperties}
|
|
833
|
+
showErrors={submitCount > 0}
|
|
834
|
+
/>
|
|
835
|
+
))}
|
|
836
|
+
</div>
|
|
837
|
+
)}
|
|
838
|
+
|
|
839
|
+
{/* Add new condition - click to add directly */}
|
|
840
|
+
{availableConditionTypes.length > 0 && (
|
|
841
|
+
<Select
|
|
842
|
+
value=""
|
|
843
|
+
onValueChange={(value) => handleAddCondition(value as ConditionType)}
|
|
844
|
+
disabled={disabled}
|
|
845
|
+
size="small"
|
|
846
|
+
placeholder="+ Add condition..."
|
|
847
|
+
className="w-full max-w-xs">
|
|
848
|
+
{availableConditionTypes.map(ct => (
|
|
849
|
+
<SelectItem key={ct.id} value={ct.id}>
|
|
850
|
+
<div className="flex flex-col">
|
|
851
|
+
<span className="font-medium">{ct.label}</span>
|
|
852
|
+
<span className="text-xs text-surface-500">{ct.description}</span>
|
|
853
|
+
</div>
|
|
854
|
+
</SelectItem>
|
|
855
|
+
))}
|
|
856
|
+
</Select>
|
|
857
|
+
)}
|
|
858
|
+
|
|
859
|
+
{activeConditions.length === 0 && availableConditionTypes.length === 0 && (
|
|
860
|
+
<Typography variant="caption" color="disabled" className="text-center py-4">
|
|
861
|
+
All condition types are configured.
|
|
862
|
+
</Typography>
|
|
863
|
+
)}
|
|
864
|
+
</div>
|
|
865
|
+
);
|
|
866
|
+
}
|