@firecms/core 3.0.1 → 3.1.0-canary.9e89e98

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 (170) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AIIcon.d.ts +16 -0
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
  5. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
  6. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
  7. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
  8. package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
  9. package/dist/components/EntityCollectionView/Board.d.ts +2 -0
  10. package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
  11. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
  12. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
  13. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
  14. package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
  15. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
  16. package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
  17. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
  18. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
  19. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
  20. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +44 -0
  21. package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
  22. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
  23. package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
  24. package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
  25. package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
  26. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +2 -0
  27. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  28. package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
  29. package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
  30. package/dist/components/VirtualTable/types.d.ts +2 -0
  31. package/dist/components/index.d.ts +3 -0
  32. package/dist/contexts/index.d.ts +10 -0
  33. package/dist/core/DrawerNavigationGroup.d.ts +45 -0
  34. package/dist/core/index.d.ts +1 -0
  35. package/dist/form/validation.d.ts +3 -2
  36. package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
  37. package/dist/hooks/useCollapsedGroups.d.ts +4 -1
  38. package/dist/index.es.js +5185 -1561
  39. package/dist/index.es.js.map +1 -1
  40. package/dist/index.umd.js +5179 -1556
  41. package/dist/index.umd.js.map +1 -1
  42. package/dist/preview/PropertyPreviewProps.d.ts +5 -0
  43. package/dist/preview/components/DatePreview.d.ts +13 -3
  44. package/dist/preview/components/ImagePreview.d.ts +5 -1
  45. package/dist/preview/components/StorageThumbnail.d.ts +2 -1
  46. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  47. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  48. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  49. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  50. package/dist/types/collections.d.ts +50 -2
  51. package/dist/types/datasource.d.ts +0 -1
  52. package/dist/types/plugins.d.ts +46 -1
  53. package/dist/types/properties.d.ts +259 -4
  54. package/dist/util/__tests__/conditions.test.d.ts +1 -0
  55. package/dist/util/__tests__/objects.test.d.ts +1 -0
  56. package/dist/util/conditions.d.ts +26 -0
  57. package/dist/util/entities.d.ts +1 -2
  58. package/dist/util/index.d.ts +2 -1
  59. package/dist/util/property_utils.d.ts +2 -1
  60. package/dist/util/resolutions.d.ts +1 -1
  61. package/package.json +10 -7
  62. package/src/app/Scaffold.tsx +14 -15
  63. package/src/components/AIIcon.tsx +39 -0
  64. package/src/components/ArrayContainer.tsx +1 -4
  65. package/src/components/ClearFilterSortButton.tsx +19 -16
  66. package/src/components/ConfirmationDialog.tsx +0 -2
  67. package/src/components/DeleteEntityDialog.tsx +2 -4
  68. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
  69. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
  70. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
  71. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
  72. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
  73. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
  74. package/src/components/EntityCollectionView/Board.tsx +324 -0
  75. package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
  76. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
  77. package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
  78. package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
  79. package/src/components/EntityCollectionView/EntityCard.tsx +231 -0
  80. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +713 -0
  81. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
  82. package/src/components/EntityCollectionView/EntityCollectionView.tsx +490 -203
  83. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
  84. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
  85. package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
  86. package/src/components/EntityCollectionView/ViewModeToggle.tsx +199 -0
  87. package/src/components/EntityCollectionView/board_types.ts +113 -0
  88. package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
  89. package/src/components/ErrorTooltip.tsx +2 -1
  90. package/src/components/HomePage/DefaultHomePage.tsx +47 -10
  91. package/src/components/HomePage/HomePageDnD.tsx +56 -41
  92. package/src/components/HomePage/NavigationCard.tsx +20 -18
  93. package/src/components/HomePage/NavigationGroup.tsx +17 -16
  94. package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
  95. package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
  96. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
  97. package/src/components/ReferenceWidget.tsx +2 -4
  98. package/src/components/SelectableTable/SelectableTable.tsx +75 -67
  99. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
  100. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
  101. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
  102. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
  103. package/src/components/UnsavedChangesDialog.tsx +0 -2
  104. package/src/components/UserDisplay.tsx +4 -4
  105. package/src/components/VirtualTable/VirtualTable.tsx +170 -19
  106. package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
  107. package/src/components/VirtualTable/VirtualTableHeader.tsx +20 -11
  108. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
  109. package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
  110. package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
  111. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
  112. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +17 -4
  113. package/src/components/VirtualTable/types.tsx +2 -0
  114. package/src/components/common/useColumnsIds.tsx +95 -3
  115. package/src/components/index.tsx +4 -0
  116. package/src/contexts/BreacrumbsContext.tsx +15 -8
  117. package/src/contexts/index.ts +10 -0
  118. package/src/core/DefaultAppBar.tsx +39 -26
  119. package/src/core/DefaultDrawer.tsx +42 -56
  120. package/src/core/DrawerNavigationGroup.tsx +118 -0
  121. package/src/core/DrawerNavigationItem.tsx +4 -3
  122. package/src/core/EntityEditView.tsx +41 -43
  123. package/src/core/SideDialogs.tsx +4 -2
  124. package/src/core/index.tsx +1 -0
  125. package/src/form/PropertyFieldBinding.tsx +58 -43
  126. package/src/form/components/StorageItemPreview.tsx +2 -1
  127. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
  128. package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
  129. package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
  130. package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
  131. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +21 -17
  132. package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
  133. package/src/form/validation.ts +245 -160
  134. package/src/hooks/useBreadcrumbsController.tsx +18 -0
  135. package/src/hooks/useBuildNavigationController.tsx +42 -19
  136. package/src/hooks/useCollapsedGroups.ts +12 -4
  137. package/src/internal/useBuildDataSource.ts +69 -34
  138. package/src/internal/useBuildSideDialogsController.tsx +11 -8
  139. package/src/internal/useBuildSideEntityController.tsx +2 -4
  140. package/src/internal/useRestoreScroll.tsx +26 -14
  141. package/src/preview/PropertyPreview.tsx +40 -32
  142. package/src/preview/PropertyPreviewProps.tsx +6 -0
  143. package/src/preview/components/DatePreview.tsx +72 -4
  144. package/src/preview/components/EmptyValue.tsx +1 -1
  145. package/src/preview/components/ImagePreview.tsx +37 -21
  146. package/src/preview/components/StorageThumbnail.tsx +16 -12
  147. package/src/preview/components/UrlComponentPreview.tsx +28 -25
  148. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
  149. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
  150. package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
  151. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
  152. package/src/routes/CustomCMSRoute.tsx +1 -0
  153. package/src/routes/FireCMSRoute.tsx +26 -13
  154. package/src/types/collections.ts +57 -3
  155. package/src/types/datasource.ts +54 -56
  156. package/src/types/plugins.tsx +51 -1
  157. package/src/types/properties.ts +347 -27
  158. package/src/util/__tests__/conditions.test.ts +506 -0
  159. package/src/util/__tests__/objects.test.ts +196 -0
  160. package/src/util/callbacks.ts +6 -3
  161. package/src/util/collections.ts +51 -6
  162. package/src/util/conditions.ts +339 -0
  163. package/src/util/entities.ts +28 -29
  164. package/src/util/entity_cache.ts +2 -1
  165. package/src/util/index.ts +2 -1
  166. package/src/util/objects.ts +31 -13
  167. package/src/util/{references.ts → previews.ts} +14 -0
  168. package/src/util/property_utils.tsx +36 -10
  169. package/src/util/resolutions.ts +57 -55
  170. /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
@@ -0,0 +1,506 @@
1
+ import {
2
+ applyPropertyConditions,
3
+ buildConditionContext,
4
+ evaluateCondition,
5
+ registerConditionOperations
6
+ } from "../conditions";
7
+ import { ConditionContext, ResolvedProperty } from "../../types";
8
+
9
+ describe("Property Conditions", () => {
10
+
11
+ beforeAll(() => {
12
+ registerConditionOperations();
13
+ });
14
+
15
+ describe("evaluateCondition", () => {
16
+
17
+ it("should evaluate simple equality", () => {
18
+ const context: ConditionContext = {
19
+ values: { status: "archived" },
20
+ previousValues: {},
21
+ propertyValue: undefined,
22
+ path: "products",
23
+ entityId: "123",
24
+ isNew: false,
25
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
26
+ now: Date.now()
27
+ };
28
+
29
+ const rule = { "==": [{ "var": "values.status" }, "archived"] };
30
+ expect(evaluateCondition(rule, context)).toBe(true);
31
+ });
32
+
33
+ it("should evaluate var access to nested values", () => {
34
+ const context: ConditionContext = {
35
+ values: { shipping: { method: "pickup" } },
36
+ previousValues: {},
37
+ propertyValue: undefined,
38
+ path: "orders",
39
+ isNew: false,
40
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
41
+ now: Date.now()
42
+ };
43
+
44
+ const rule = { "==": [{ "var": "values.shipping.method" }, "pickup"] };
45
+ expect(evaluateCondition(rule, context)).toBe(true);
46
+ });
47
+
48
+ it("should check isNew status", () => {
49
+ const contextNew: ConditionContext = {
50
+ values: {},
51
+ previousValues: {},
52
+ propertyValue: undefined,
53
+ path: "products",
54
+ isNew: true,
55
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
56
+ now: Date.now()
57
+ };
58
+
59
+ const rule = { "var": "isNew" };
60
+ expect(evaluateCondition(rule, contextNew)).toBe(true);
61
+
62
+ const contextExisting: ConditionContext = {
63
+ ...contextNew,
64
+ entityId: "123",
65
+ isNew: false
66
+ };
67
+ expect(evaluateCondition(rule, contextExisting)).toBe(false);
68
+ });
69
+
70
+ it("should handle if/then/else with object values (Firestore workaround)", () => {
71
+ const context: ConditionContext = {
72
+ values: { status: "active" },
73
+ previousValues: {},
74
+ propertyValue: undefined,
75
+ path: "products",
76
+ isNew: false,
77
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
78
+ now: Date.now()
79
+ };
80
+
81
+ // Firestore stores arrays as objects like {"0": "a", "1": "b"}
82
+ const rule = {
83
+ "if": [
84
+ { "==": [{ "var": "values.status" }, "active"] },
85
+ { "0": "electronics", "1": "clothing" },
86
+ { "0": "electronics", "1": "clothing", "2": "food" }
87
+ ]
88
+ };
89
+
90
+ const result = evaluateCondition(rule, context);
91
+ expect(result).toEqual({ "0": "electronics", "1": "clothing" });
92
+ });
93
+
94
+ it("should evaluate truthy operator (!!)", () => {
95
+ const context: ConditionContext = {
96
+ values: { name: "Product", emptyField: "" },
97
+ previousValues: {},
98
+ propertyValue: undefined,
99
+ path: "products",
100
+ isNew: false,
101
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
102
+ now: Date.now()
103
+ };
104
+
105
+ expect(evaluateCondition({ "!!": { "var": "values.name" } }, context)).toBe(true);
106
+ expect(evaluateCondition({ "!!": { "var": "values.emptyField" } }, context)).toBe(false);
107
+ });
108
+
109
+ it("should evaluate falsy operator (!)", () => {
110
+ const context: ConditionContext = {
111
+ values: { name: "Product", emptyField: "" },
112
+ previousValues: {},
113
+ propertyValue: undefined,
114
+ path: "products",
115
+ isNew: false,
116
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
117
+ now: Date.now()
118
+ };
119
+
120
+ expect(evaluateCondition({ "!": { "var": "values.name" } }, context)).toBe(false);
121
+ expect(evaluateCondition({ "!": { "var": "values.emptyField" } }, context)).toBe(true);
122
+ });
123
+
124
+ it("should evaluate greater than operator", () => {
125
+ const context: ConditionContext = {
126
+ values: { price: 100 },
127
+ previousValues: {},
128
+ propertyValue: undefined,
129
+ path: "products",
130
+ isNew: false,
131
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
132
+ now: Date.now()
133
+ };
134
+
135
+ expect(evaluateCondition({ ">": [{ "var": "values.price" }, 50] }, context)).toBe(true);
136
+ expect(evaluateCondition({ ">": [{ "var": "values.price" }, 100] }, context)).toBe(false);
137
+ });
138
+
139
+ it("should evaluate in operator with array", () => {
140
+ const context: ConditionContext = {
141
+ values: { category: "electronics" },
142
+ previousValues: {},
143
+ propertyValue: undefined,
144
+ path: "products",
145
+ isNew: false,
146
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
147
+ now: Date.now()
148
+ };
149
+
150
+ expect(evaluateCondition({ "in": [{ "var": "values.category" }, ["electronics", "clothing"]] }, context)).toBe(true);
151
+ expect(evaluateCondition({ "in": [{ "var": "values.category" }, ["food", "toys"]] }, context)).toBe(false);
152
+ });
153
+ });
154
+
155
+ describe("Custom operations", () => {
156
+
157
+ it("isPast should check if timestamp is in the past", () => {
158
+ const context: ConditionContext = {
159
+ values: {},
160
+ previousValues: {},
161
+ propertyValue: undefined,
162
+ path: "products",
163
+ isNew: false,
164
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
165
+ now: Date.now()
166
+ };
167
+
168
+ const pastTimestamp = Date.now() - 86400000;
169
+ const futureTimestamp = Date.now() + 86400000;
170
+
171
+ expect(evaluateCondition({ "isPast": pastTimestamp }, context)).toBe(true);
172
+ expect(evaluateCondition({ "isPast": futureTimestamp }, context)).toBe(false);
173
+ });
174
+
175
+ it("isFuture should check if timestamp is in the future", () => {
176
+ const context: ConditionContext = {
177
+ values: {},
178
+ previousValues: {},
179
+ propertyValue: undefined,
180
+ path: "products",
181
+ isNew: false,
182
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: [] },
183
+ now: Date.now()
184
+ };
185
+
186
+ const pastTimestamp = Date.now() - 86400000;
187
+ const futureTimestamp = Date.now() + 86400000;
188
+
189
+ expect(evaluateCondition({ "isFuture": futureTimestamp }, context)).toBe(true);
190
+ expect(evaluateCondition({ "isFuture": pastTimestamp }, context)).toBe(false);
191
+ });
192
+ });
193
+
194
+ describe("applyPropertyConditions", () => {
195
+
196
+ const baseContext: ConditionContext = {
197
+ values: { status: "archived" },
198
+ previousValues: {},
199
+ propertyValue: undefined,
200
+ path: "products",
201
+ entityId: "123",
202
+ isNew: false,
203
+ user: { uid: "user1", email: null, displayName: null, photoURL: null, roles: ["admin"] },
204
+ now: Date.now()
205
+ };
206
+
207
+ it("should apply disabled condition", () => {
208
+ const property = {
209
+ dataType: "string",
210
+ name: "Title",
211
+ resolved: true,
212
+ fromBuilder: false,
213
+ conditions: {
214
+ disabled: { "==": [{ "var": "values.status" }, "archived"] },
215
+ disabledMessage: "Cannot edit archived items"
216
+ }
217
+ } as ResolvedProperty<string>;
218
+
219
+ const result = applyPropertyConditions(property, baseContext);
220
+
221
+ expect(result.disabled).toEqual({
222
+ clearOnDisabled: false,
223
+ disabledMessage: "Cannot edit archived items",
224
+ hidden: false
225
+ });
226
+ });
227
+
228
+ it("should apply hidden condition", () => {
229
+ const property = {
230
+ dataType: "string",
231
+ name: "Internal Notes",
232
+ resolved: true,
233
+ fromBuilder: false,
234
+ conditions: {
235
+ hidden: { "==": [{ "var": "values.status" }, "archived"] }
236
+ }
237
+ } as ResolvedProperty<string>;
238
+
239
+ const result = applyPropertyConditions(property, baseContext);
240
+
241
+ expect(result.disabled).toEqual(expect.objectContaining({
242
+ hidden: true
243
+ }));
244
+ });
245
+
246
+ it("should apply required condition", () => {
247
+ const property = {
248
+ dataType: "string",
249
+ name: "Email",
250
+ resolved: true,
251
+ fromBuilder: false,
252
+ conditions: {
253
+ required: { "!!": { "var": "values.status" } }
254
+ }
255
+ } as ResolvedProperty<string>;
256
+
257
+ const result = applyPropertyConditions(property, baseContext);
258
+
259
+ expect(result.validation?.required).toBe(true);
260
+ });
261
+
262
+ it("should not apply disabled when condition is false", () => {
263
+ const property = {
264
+ dataType: "string",
265
+ name: "Title",
266
+ resolved: true,
267
+ fromBuilder: false,
268
+ conditions: {
269
+ disabled: { "==": [{ "var": "values.status" }, "draft"] }
270
+ }
271
+ } as ResolvedProperty<string>;
272
+
273
+ const result = applyPropertyConditions(property, baseContext);
274
+
275
+ expect(result.disabled).toBeUndefined();
276
+ });
277
+
278
+ it("should apply enum conditions to filter values", () => {
279
+ const property = {
280
+ dataType: "string",
281
+ name: "Category",
282
+ resolved: true,
283
+ fromBuilder: false,
284
+ enumValues: [
285
+ { id: "electronics", label: "Electronics" },
286
+ { id: "clothing", label: "Clothing" },
287
+ { id: "food", label: "Food" }
288
+ ],
289
+ conditions: {
290
+ allowedEnumValues: ["electronics", "clothing"]
291
+ }
292
+ } as any;
293
+
294
+ const result = applyPropertyConditions(property, baseContext) as any;
295
+
296
+ expect(result.enumValues).toHaveLength(2);
297
+ expect(result.enumValues.map((e: any) => e.id)).toEqual(["electronics", "clothing"]);
298
+ });
299
+
300
+ it("should apply enum conditions with object format (Firestore workaround)", () => {
301
+ const property = {
302
+ dataType: "string",
303
+ name: "Category",
304
+ resolved: true,
305
+ fromBuilder: false,
306
+ enumValues: [
307
+ { id: "electronics", label: "Electronics" },
308
+ { id: "clothing", label: "Clothing" },
309
+ { id: "food", label: "Food" }
310
+ ],
311
+ conditions: {
312
+ allowedEnumValues: {
313
+ "if": [
314
+ { "!!": { "var": "values.status" } },
315
+ { "0": "electronics", "1": "clothing" },
316
+ { "0": "electronics", "1": "clothing", "2": "food" }
317
+ ]
318
+ }
319
+ }
320
+ } as any;
321
+
322
+ const result = applyPropertyConditions(property, baseContext) as any;
323
+
324
+ expect(result.enumValues).toHaveLength(2);
325
+ expect(result.enumValues.map((e: any) => e.id)).toEqual(["electronics", "clothing"]);
326
+ });
327
+
328
+ it("should apply excludedEnumValues to remove specific values", () => {
329
+ const property = {
330
+ dataType: "string",
331
+ name: "Status",
332
+ resolved: true,
333
+ fromBuilder: false,
334
+ enumValues: [
335
+ { id: "draft", label: "Draft" },
336
+ { id: "published", label: "Published" },
337
+ { id: "archived", label: "Archived" }
338
+ ],
339
+ conditions: {
340
+ // Simple array of excluded values
341
+ excludedEnumValues: ["published"]
342
+ }
343
+ } as any;
344
+
345
+ const result = applyPropertyConditions(property, baseContext) as any;
346
+
347
+ expect(result.enumValues).toHaveLength(2);
348
+ expect(result.enumValues.map((e: any) => e.id)).toEqual(["draft", "archived"]);
349
+ });
350
+
351
+ it("should apply enum conditions to disable specific values", () => {
352
+ const property = {
353
+ dataType: "string",
354
+ name: "Status",
355
+ resolved: true,
356
+ fromBuilder: false,
357
+ enumValues: [
358
+ { id: "draft", label: "Draft" },
359
+ { id: "published", label: "Published" },
360
+ { id: "archived", label: "Archived" }
361
+ ],
362
+ conditions: {
363
+ enumConditions: {
364
+ archived: {
365
+ disabled: { "!=": [{ "var": "values.status" }, "archived"] }
366
+ }
367
+ }
368
+ }
369
+ } as any;
370
+
371
+ const result = applyPropertyConditions(property, baseContext) as any;
372
+ const archivedOption = result.enumValues.find((e: any) => e.id === "archived");
373
+ expect(archivedOption.disabled).toBeFalsy();
374
+
375
+ const contextDraft = { ...baseContext, values: { status: "draft" } };
376
+ const resultDraft = applyPropertyConditions(property, contextDraft) as any;
377
+ const archivedOptionDraft = resultDraft.enumValues.find((e: any) => e.id === "archived");
378
+ expect(archivedOptionDraft.disabled).toBe(true);
379
+ });
380
+
381
+ it("should handle multiple conditions together", () => {
382
+ const property = {
383
+ dataType: "string",
384
+ name: "Notes",
385
+ resolved: true,
386
+ fromBuilder: false,
387
+ conditions: {
388
+ disabled: { "==": [{ "var": "values.status" }, "archived"] },
389
+ required: { "==": [{ "var": "values.status" }, "published"] },
390
+ disabledMessage: "Cannot edit notes on archived items"
391
+ }
392
+ } as ResolvedProperty<string>;
393
+
394
+ const resultArchived = applyPropertyConditions(property, baseContext);
395
+ expect(resultArchived.disabled).toBeDefined();
396
+ expect(resultArchived.validation?.required).toBeFalsy();
397
+
398
+ const contextPublished = { ...baseContext, values: { status: "published" } };
399
+ const resultPublished = applyPropertyConditions(property, contextPublished);
400
+ expect(resultPublished.disabled).toBeUndefined();
401
+ expect(resultPublished.validation?.required).toBe(true);
402
+ });
403
+
404
+ it("should handle clearOnDisabled option", () => {
405
+ const property = {
406
+ dataType: "string",
407
+ name: "Title",
408
+ resolved: true,
409
+ fromBuilder: false,
410
+ conditions: {
411
+ disabled: { "==": [{ "var": "values.status" }, "archived"] },
412
+ clearOnDisabled: true
413
+ }
414
+ } as ResolvedProperty<string>;
415
+
416
+ const result = applyPropertyConditions(property, baseContext);
417
+
418
+ expect(result.disabled).toEqual(expect.objectContaining({
419
+ clearOnDisabled: true
420
+ }));
421
+ });
422
+ });
423
+
424
+ describe("buildConditionContext", () => {
425
+
426
+ it("should build context with serialized dates", () => {
427
+ const mockAuthController = {
428
+ user: {
429
+ uid: "user123",
430
+ email: "test@example.com",
431
+ displayName: "Test User",
432
+ photoURL: null,
433
+ providerId: "google.com",
434
+ isAnonymous: false,
435
+ roles: [{ id: "admin", name: "Admin" }, { id: "editor", name: "Editor" }]
436
+ }
437
+ };
438
+
439
+ const now = new Date();
440
+ const context = buildConditionContext({
441
+ propertyKey: "title",
442
+ values: { title: "Hello", createdAt: now },
443
+ path: "products",
444
+ entityId: "123",
445
+ authController: mockAuthController as any
446
+ });
447
+
448
+ expect(context.values.createdAt).toBe(now.getTime());
449
+ expect(context.user.uid).toBe("user123");
450
+ expect(context.user.roles).toEqual(["admin", "editor"]);
451
+ expect(context.isNew).toBe(false);
452
+ expect(context.entityId).toBe("123");
453
+ });
454
+
455
+ it("should mark isNew as true when no entityId", () => {
456
+ const mockAuthController = {
457
+ user: null
458
+ };
459
+
460
+ const context = buildConditionContext({
461
+ path: "products",
462
+ authController: mockAuthController as any
463
+ });
464
+
465
+ expect(context.isNew).toBe(true);
466
+ expect(context.entityId).toBeUndefined();
467
+ });
468
+
469
+ it("should handle user with Role object roles", () => {
470
+ const mockAuthController = {
471
+ user: {
472
+ uid: "user123",
473
+ email: "test@example.com",
474
+ displayName: "Test User",
475
+ photoURL: null,
476
+ roles: [{ id: "admin", name: "Admin" }, { id: "editor", name: "Editor" }]
477
+ }
478
+ };
479
+
480
+ const context = buildConditionContext({
481
+ path: "products",
482
+ entityId: "123",
483
+ authController: mockAuthController as any
484
+ });
485
+
486
+ // Roles are mapped from Role.id
487
+ expect(context.user.roles).toEqual(["admin", "editor"]);
488
+ });
489
+
490
+ it("should handle null user", () => {
491
+ const mockAuthController = {
492
+ user: null
493
+ };
494
+
495
+ const context = buildConditionContext({
496
+ path: "products",
497
+ authController: mockAuthController as any
498
+ });
499
+
500
+ expect(context.user).toBeDefined();
501
+ // uid defaults to empty string when user is null
502
+ expect(context.user.uid).toBe("");
503
+ expect(context.user.roles).toEqual([]);
504
+ });
505
+ });
506
+ });
@@ -0,0 +1,196 @@
1
+ import { removeFunctions, removeUndefined, isPlainObject } from "../objects";
2
+
3
+ describe("objects utilities", () => {
4
+
5
+ describe("removeFunctions", () => {
6
+
7
+ it("should remove functions from objects", () => {
8
+ const obj = {
9
+ name: "test",
10
+ value: 42,
11
+ callback: () => console.log("hi")
12
+ };
13
+
14
+ const result = removeFunctions(obj);
15
+
16
+ expect(result.name).toBe("test");
17
+ expect(result.value).toBe(42);
18
+ expect(result.callback).toBeUndefined();
19
+ });
20
+
21
+ it("should preserve arrays at top level", () => {
22
+ const arr = ["a", "b", "c"];
23
+ const result = removeFunctions(arr);
24
+
25
+ expect(Array.isArray(result)).toBe(true);
26
+ expect(result).toEqual(["a", "b", "c"]);
27
+ });
28
+
29
+ it("should preserve nested arrays", () => {
30
+ const obj = {
31
+ items: ["a", "b", "c"],
32
+ numbers: [1, 2, 3]
33
+ };
34
+
35
+ const result = removeFunctions(obj);
36
+
37
+ expect(Array.isArray(result.items)).toBe(true);
38
+ expect(result.items).toEqual(["a", "b", "c"]);
39
+ expect(Array.isArray(result.numbers)).toBe(true);
40
+ expect(result.numbers).toEqual([1, 2, 3]);
41
+ });
42
+
43
+ it("should NOT convert arrays to objects with numeric keys", () => {
44
+ const arr = ["first", "second", "third"];
45
+ const result = removeFunctions(arr);
46
+
47
+ // Key check: the result should be an Array, not a plain object
48
+ // If it were converted to an object, Array.isArray would return false
49
+ expect(Array.isArray(result)).toBe(true);
50
+ expect(result.length).toBe(3);
51
+ expect(result[0]).toBe("first");
52
+ expect(result[1]).toBe("second");
53
+ expect(result[2]).toBe("third");
54
+ });
55
+
56
+ it("should handle undefined", () => {
57
+ expect(removeFunctions(undefined)).toBeUndefined();
58
+ });
59
+
60
+ it("should handle null", () => {
61
+ expect(removeFunctions(null as any)).toBeNull();
62
+ });
63
+
64
+ it("should preserve class instances (not recurse into them)", () => {
65
+ class CustomClass {
66
+ value = 42;
67
+ getDouble() { return this.value * 2; }
68
+ }
69
+
70
+ const instance = new CustomClass();
71
+ const obj = { custom: instance };
72
+
73
+ const result = removeFunctions(obj);
74
+
75
+ // Class instances should be preserved as-is (not have functions stripped)
76
+ expect(result.custom).toBe(instance);
77
+ expect(result.custom.getDouble()).toBe(84);
78
+ });
79
+
80
+ it("should recursively remove functions from nested objects", () => {
81
+ const obj = {
82
+ level1: {
83
+ name: "nested",
84
+ fn: () => "should be removed",
85
+ level2: {
86
+ value: 100,
87
+ anotherFn: () => "also removed"
88
+ }
89
+ }
90
+ };
91
+
92
+ const result = removeFunctions(obj);
93
+
94
+ expect(result.level1.name).toBe("nested");
95
+ expect(result.level1.fn).toBeUndefined();
96
+ expect(result.level1.level2.value).toBe(100);
97
+ expect(result.level1.level2.anotherFn).toBeUndefined();
98
+ });
99
+
100
+ it("should handle arrays containing objects with functions", () => {
101
+ const arr = [
102
+ { name: "item1", fn: () => { } },
103
+ { name: "item2", fn: () => { } }
104
+ ];
105
+
106
+ const result = removeFunctions(arr);
107
+
108
+ expect(Array.isArray(result)).toBe(true);
109
+ expect(result[0].name).toBe("item1");
110
+ expect(result[0].fn).toBeUndefined();
111
+ expect(result[1].name).toBe("item2");
112
+ expect(result[1].fn).toBeUndefined();
113
+ });
114
+
115
+ it("should handle deeply nested arrays", () => {
116
+ const obj = {
117
+ conditions: {
118
+ allowedEnumValues: {
119
+ if: [
120
+ { "==": [{ var: "values.status" }, "active"] },
121
+ ["a", "b"],
122
+ ["a", "b", "c"]
123
+ ]
124
+ }
125
+ }
126
+ };
127
+
128
+ const result = removeFunctions(obj);
129
+
130
+ // Ensure arrays inside the if array are preserved as arrays
131
+ expect(Array.isArray(result.conditions.allowedEnumValues.if)).toBe(true);
132
+ expect(Array.isArray(result.conditions.allowedEnumValues.if[1])).toBe(true);
133
+ expect(result.conditions.allowedEnumValues.if[1]).toEqual(["a", "b"]);
134
+ });
135
+ });
136
+
137
+ describe("removeUndefined", () => {
138
+
139
+ it("should remove undefined values from objects", () => {
140
+ const obj = {
141
+ name: "test",
142
+ value: undefined,
143
+ count: 0
144
+ };
145
+
146
+ const result = removeUndefined(obj);
147
+
148
+ expect(result.name).toBe("test");
149
+ expect(result.count).toBe(0);
150
+ expect("value" in result).toBe(false);
151
+ });
152
+
153
+ it("should preserve arrays", () => {
154
+ const arr = ["a", "b", "c"];
155
+ const result = removeUndefined(arr);
156
+
157
+ expect(Array.isArray(result)).toBe(true);
158
+ expect(result).toEqual(["a", "b", "c"]);
159
+ });
160
+
161
+ it("should handle null values", () => {
162
+ const obj = { value: null };
163
+ const result = removeUndefined(obj);
164
+
165
+ expect(result.value).toBeNull();
166
+ });
167
+ });
168
+
169
+ describe("isPlainObject", () => {
170
+
171
+ it("should return true for plain objects", () => {
172
+ expect(isPlainObject({})).toBe(true);
173
+ expect(isPlainObject({ a: 1 })).toBe(true);
174
+ });
175
+
176
+ it("should return false for arrays", () => {
177
+ expect(isPlainObject([])).toBe(false);
178
+ expect(isPlainObject([1, 2, 3])).toBe(false);
179
+ });
180
+
181
+ it("should return false for null", () => {
182
+ expect(isPlainObject(null)).toBe(false);
183
+ });
184
+
185
+ it("should return false for primitives", () => {
186
+ expect(isPlainObject("string")).toBe(false);
187
+ expect(isPlainObject(42)).toBe(false);
188
+ expect(isPlainObject(true)).toBe(false);
189
+ });
190
+
191
+ it("should return false for class instances", () => {
192
+ class MyClass { }
193
+ expect(isPlainObject(new MyClass())).toBe(false);
194
+ });
195
+ });
196
+ });