@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.
- 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 +9677 -5837
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9653 -5813
- package/dist/index.umd.js.map +1 -1
- 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 +31 -1
- package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
- package/dist/ui/KanbanSetupAction.d.ts +10 -0
- package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +33 -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 +20 -0
- package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +3 -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 +11 -11
- package/src/ConfigControllerProvider.tsx +81 -47
- package/src/api/generateCollectionApi.ts +119 -0
- package/src/api/index.ts +1 -0
- package/src/index.ts +28 -1
- package/src/types/collection_editor_controller.tsx +16 -3
- package/src/types/collection_inference.ts +15 -2
- package/src/types/config_controller.tsx +37 -1
- package/src/ui/AddKanbanColumnAction.tsx +203 -0
- package/src/ui/EditorCollectionActionStart.tsx +1 -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 +1 -1
- package/src/ui/PropertyAddColumnComponent.tsx +1 -1
- package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +225 -0
- package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +209 -258
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +226 -173
- package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +130 -67
- package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
- package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +190 -91
- package/src/ui/collection_editor/DisplaySettingsForm.tsx +333 -0
- package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -96
- package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +6 -7
- package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +1 -3
- package/src/ui/collection_editor/EnumForm.tsx +147 -100
- package/src/ui/collection_editor/ExtendSettingsForm.tsx +93 -0
- package/src/ui/collection_editor/GeneralSettingsForm.tsx +335 -0
- package/src/ui/collection_editor/GetCodeDialog.tsx +57 -36
- package/src/ui/collection_editor/KanbanConfigSection.tsx +207 -0
- package/src/ui/collection_editor/LayoutModeSwitch.tsx +22 -41
- package/src/ui/collection_editor/PropertyEditView.tsx +205 -141
- 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 +171 -162
- package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
- package/src/ui/collection_editor/ViewModeSwitch.tsx +41 -0
- package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +1 -0
- package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +117 -35
- package/src/ui/collection_editor/properties/EnumPropertyField.tsx +28 -21
- package/src/ui/collection_editor/properties/MapPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +115 -39
- package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +1 -5
- package/src/ui/collection_editor/properties/StoragePropertyField.tsx +23 -2
- package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +861 -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/ValidationPanel.tsx +1 -1
- package/src/useCollectionEditorPlugin.tsx +32 -17
- 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";
|