@firecms/core 3.0.1 → 3.1.0-canary.768c91f

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 (185) 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/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  10. package/dist/components/EntityCollectionView/Board.d.ts +2 -0
  11. package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
  12. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
  13. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
  14. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
  15. package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
  16. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
  17. package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
  18. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
  19. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
  20. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
  21. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +44 -0
  22. package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
  23. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
  24. package/dist/components/ErrorBoundary.d.ts +1 -1
  25. package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
  26. package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
  27. package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
  28. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +3 -1
  29. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  30. package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
  31. package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
  32. package/dist/components/VirtualTable/types.d.ts +2 -0
  33. package/dist/components/index.d.ts +3 -0
  34. package/dist/contexts/index.d.ts +10 -0
  35. package/dist/core/DrawerNavigationGroup.d.ts +45 -0
  36. package/dist/core/index.d.ts +1 -0
  37. package/dist/form/components/ErrorFocus.d.ts +1 -1
  38. package/dist/form/validation.d.ts +3 -2
  39. package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
  40. package/dist/hooks/useCollapsedGroups.d.ts +4 -1
  41. package/dist/index.es.js +5266 -1578
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/index.umd.js +5260 -1573
  44. package/dist/index.umd.js.map +1 -1
  45. package/dist/internal/useRestoreScroll.d.ts +1 -1
  46. package/dist/preview/PropertyPreviewProps.d.ts +5 -0
  47. package/dist/preview/components/DatePreview.d.ts +13 -3
  48. package/dist/preview/components/ImagePreview.d.ts +5 -1
  49. package/dist/preview/components/StorageThumbnail.d.ts +2 -1
  50. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  51. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  52. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  53. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  54. package/dist/types/analytics.d.ts +1 -1
  55. package/dist/types/collections.d.ts +50 -2
  56. package/dist/types/datasource.d.ts +0 -1
  57. package/dist/types/plugins.d.ts +62 -1
  58. package/dist/types/properties.d.ts +259 -4
  59. package/dist/util/__tests__/conditions.test.d.ts +1 -0
  60. package/dist/util/__tests__/objects.test.d.ts +1 -0
  61. package/dist/util/conditions.d.ts +26 -0
  62. package/dist/util/entities.d.ts +2 -3
  63. package/dist/util/index.d.ts +2 -1
  64. package/dist/util/property_utils.d.ts +2 -1
  65. package/dist/util/resolutions.d.ts +3 -3
  66. package/package.json +14 -11
  67. package/src/app/Scaffold.tsx +14 -15
  68. package/src/components/AIIcon.tsx +39 -0
  69. package/src/components/ArrayContainer.tsx +1 -4
  70. package/src/components/ClearFilterSortButton.tsx +19 -16
  71. package/src/components/ConfirmationDialog.tsx +0 -2
  72. package/src/components/DeleteEntityDialog.tsx +2 -4
  73. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
  74. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
  75. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
  76. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
  77. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
  78. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
  79. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  80. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  81. package/src/components/EntityCollectionView/Board.tsx +324 -0
  82. package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
  83. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
  84. package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
  85. package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
  86. package/src/components/EntityCollectionView/EntityCard.tsx +235 -0
  87. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +733 -0
  88. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
  89. package/src/components/EntityCollectionView/EntityCollectionView.tsx +519 -203
  90. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
  91. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
  92. package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
  93. package/src/components/EntityCollectionView/ViewModeToggle.tsx +199 -0
  94. package/src/components/EntityCollectionView/board_types.ts +113 -0
  95. package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
  96. package/src/components/ErrorTooltip.tsx +2 -1
  97. package/src/components/HomePage/DefaultHomePage.tsx +47 -10
  98. package/src/components/HomePage/HomePageDnD.tsx +56 -41
  99. package/src/components/HomePage/NavigationCard.tsx +20 -18
  100. package/src/components/HomePage/NavigationGroup.tsx +17 -16
  101. package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
  102. package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
  103. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
  104. package/src/components/ReferenceWidget.tsx +2 -4
  105. package/src/components/SelectableTable/SelectableTable.tsx +75 -67
  106. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
  107. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
  108. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
  109. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
  110. package/src/components/UnsavedChangesDialog.tsx +0 -2
  111. package/src/components/UserDisplay.tsx +4 -4
  112. package/src/components/VirtualTable/VirtualTable.tsx +272 -118
  113. package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
  114. package/src/components/VirtualTable/VirtualTableHeader.tsx +59 -50
  115. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
  116. package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
  117. package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
  118. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
  119. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +19 -6
  120. package/src/components/VirtualTable/types.tsx +2 -0
  121. package/src/components/common/useColumnsIds.tsx +95 -3
  122. package/src/components/index.tsx +4 -0
  123. package/src/contexts/BreacrumbsContext.tsx +15 -8
  124. package/src/contexts/index.ts +10 -0
  125. package/src/core/DefaultAppBar.tsx +40 -27
  126. package/src/core/DefaultDrawer.tsx +42 -56
  127. package/src/core/DrawerNavigationGroup.tsx +118 -0
  128. package/src/core/DrawerNavigationItem.tsx +4 -3
  129. package/src/core/EntityEditView.tsx +41 -43
  130. package/src/core/EntitySidePanel.tsx +28 -26
  131. package/src/core/SideDialogs.tsx +4 -2
  132. package/src/core/field_configs.tsx +14 -9
  133. package/src/core/index.tsx +1 -0
  134. package/src/form/EntityForm.tsx +69 -60
  135. package/src/form/PropertyFieldBinding.tsx +61 -46
  136. package/src/form/components/ErrorFocus.tsx +3 -3
  137. package/src/form/components/StorageItemPreview.tsx +2 -1
  138. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
  139. package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
  140. package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
  141. package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
  142. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +22 -18
  143. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
  144. package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
  145. package/src/form/validation.ts +245 -160
  146. package/src/hooks/useBreadcrumbsController.tsx +18 -0
  147. package/src/hooks/useBuildNavigationController.tsx +46 -23
  148. package/src/hooks/useCollapsedGroups.ts +12 -4
  149. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  150. package/src/internal/useBuildDataSource.ts +68 -34
  151. package/src/internal/useBuildSideDialogsController.tsx +11 -8
  152. package/src/internal/useBuildSideEntityController.tsx +2 -4
  153. package/src/internal/useRestoreScroll.tsx +26 -14
  154. package/src/preview/PropertyPreview.tsx +41 -32
  155. package/src/preview/PropertyPreviewProps.tsx +6 -0
  156. package/src/preview/components/DatePreview.tsx +72 -4
  157. package/src/preview/components/EmptyValue.tsx +1 -1
  158. package/src/preview/components/ImagePreview.tsx +37 -21
  159. package/src/preview/components/StorageThumbnail.tsx +16 -12
  160. package/src/preview/components/UrlComponentPreview.tsx +28 -25
  161. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
  162. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
  163. package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
  164. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
  165. package/src/routes/CustomCMSRoute.tsx +1 -0
  166. package/src/routes/FireCMSRoute.tsx +26 -13
  167. package/src/types/analytics.ts +10 -0
  168. package/src/types/collections.ts +57 -3
  169. package/src/types/datasource.ts +54 -56
  170. package/src/types/plugins.tsx +69 -1
  171. package/src/types/properties.ts +347 -27
  172. package/src/util/__tests__/conditions.test.ts +506 -0
  173. package/src/util/__tests__/objects.test.ts +196 -0
  174. package/src/util/callbacks.ts +6 -3
  175. package/src/util/collections.ts +51 -6
  176. package/src/util/conditions.ts +339 -0
  177. package/src/util/entities.ts +29 -30
  178. package/src/util/entity_cache.ts +2 -1
  179. package/src/util/index.ts +2 -1
  180. package/src/util/join_collections.ts +10 -8
  181. package/src/util/objects.ts +31 -13
  182. package/src/util/{references.ts → previews.ts} +16 -2
  183. package/src/util/property_utils.tsx +37 -11
  184. package/src/util/resolutions.ts +62 -58
  185. /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { EntityCallbacks } from "../types";
2
+ import { mergeDeep } from "./objects";
2
3
 
3
4
  export const mergeCallbacks = (
4
5
  baseCallbacks: EntityCallbacks = {},
@@ -55,17 +56,19 @@ export const mergeCallbacks = (
55
56
  // Handle onPreSave - returns Partial<EntityValues<M>> or Promise<Partial<EntityValues<M>>>
56
57
  if (baseCallbacks.onPreSave || pluginCallbacks.onPreSave) {
57
58
  mergedCallbacks.onPreSave = async (props) => {
58
- let values = { ...props.values };
59
+ let values = props.values;
59
60
  if (baseCallbacks.onPreSave) {
60
61
  const baseValues = await Promise.resolve(baseCallbacks.onPreSave(props));
61
- values = { ...values, ...baseValues };
62
+ // Use mergeDeep to preserve class instances like EntityReference, GeoPoint
63
+ values = mergeDeep(values, baseValues);
62
64
  }
63
65
  if (pluginCallbacks.onPreSave) {
64
66
  const pluginValues = await Promise.resolve(pluginCallbacks.onPreSave({
65
67
  ...props,
66
68
  values
67
69
  }));
68
- values = { ...values, ...pluginValues };
70
+ // Use mergeDeep to preserve class instances like EntityReference, GeoPoint
71
+ values = mergeDeep(values, pluginValues);
69
72
  }
70
73
  return values;
71
74
  };
@@ -11,10 +11,11 @@ import { isPropertyBuilder } from "./entities";
11
11
  export function sortProperties<M extends Record<string, any>>(properties: PropertiesOrBuilders<M>, propertiesOrder?: (keyof M)[]): PropertiesOrBuilders<M> {
12
12
  try {
13
13
  const propertiesKeys = Object.keys(properties);
14
- const allPropertiesOrder = propertiesOrder ?? propertiesKeys;
15
- return allPropertiesOrder
16
- .map((key) => {
17
- if (properties[key as keyof M]) {
14
+
15
+ // If no propertiesOrder, just use the original keys order
16
+ if (!propertiesOrder || propertiesOrder.length === 0) {
17
+ return propertiesKeys
18
+ .map((key) => {
18
19
  const property = properties[key] as PropertyOrBuilder;
19
20
  if (!isPropertyBuilder(property) && property?.dataType === "map" && property.properties) {
20
21
  return ({
@@ -26,12 +27,56 @@ export function sortProperties<M extends Record<string, any>>(properties: Proper
26
27
  } else {
27
28
  return ({ [key]: property });
28
29
  }
30
+ })
31
+ .reduce((a: any, b: any) => ({ ...a, ...b }), {}) as PropertiesOrBuilders<M>;
32
+ }
33
+
34
+ // Filter propertiesOrder to only include TOP-LEVEL property keys that exist
35
+ // (ignore nested keys like "data.mode" - they are for column ordering, not property filtering)
36
+ const validOrderKeys = (propertiesOrder as string[]).filter(key => {
37
+ // Only include top-level keys (no dots) that exist in properties
38
+ return !key.includes(".") && properties[key as keyof M];
39
+ });
40
+
41
+ // Track which properties we've processed
42
+ const processedKeys = new Set<string>(validOrderKeys);
43
+
44
+ // Build result starting with ordered properties
45
+ const orderedResult = validOrderKeys
46
+ .map((key) => {
47
+ const property = properties[key] as PropertyOrBuilder;
48
+ if (!isPropertyBuilder(property) && property?.dataType === "map" && property.properties) {
49
+ return ({
50
+ [key]: {
51
+ ...property,
52
+ properties: sortProperties(property.properties, property.propertiesOrder)
53
+ }
54
+ });
29
55
  } else {
30
- return undefined;
56
+ return ({ [key]: property });
31
57
  }
32
58
  })
33
- .filter((a) => a !== undefined)
34
59
  .reduce((a: any, b: any) => ({ ...a, ...b }), {}) as PropertiesOrBuilders<M>;
60
+
61
+ // Append any properties that were NOT in propertiesOrder (so they don't disappear!)
62
+ const missingProperties = propertiesKeys
63
+ .filter(key => !processedKeys.has(key))
64
+ .map((key) => {
65
+ const property = properties[key] as PropertyOrBuilder;
66
+ if (!isPropertyBuilder(property) && property?.dataType === "map" && property.properties) {
67
+ return ({
68
+ [key]: {
69
+ ...property,
70
+ properties: sortProperties(property.properties, property.propertiesOrder)
71
+ }
72
+ });
73
+ } else {
74
+ return ({ [key]: property });
75
+ }
76
+ })
77
+ .reduce((a: any, b: any) => ({ ...a, ...b }), {}) as PropertiesOrBuilders<M>;
78
+
79
+ return { ...orderedResult, ...missingProperties };
35
80
  } catch (e) {
36
81
  console.error("Error sorting properties", e);
37
82
  return properties;
@@ -0,0 +1,339 @@
1
+ import jsonLogic from "json-logic-js";
2
+ import {
3
+ AuthController,
4
+ ConditionContext,
5
+ EnumValueConfig,
6
+ JsonLogicRule,
7
+ PropertyConditions,
8
+ ResolvedProperty,
9
+ CMSType,
10
+ Role
11
+ } from "../types";
12
+ import { getIn } from "@firecms/formex";
13
+
14
+ let operationsRegistered = false;
15
+
16
+ /**
17
+ * Register custom JSON Logic operations for FireCMS.
18
+ * Call this once at app initialization.
19
+ */
20
+ export function registerConditionOperations(): void {
21
+ if (operationsRegistered) return;
22
+
23
+ // Check if user has a specific role by ID
24
+ jsonLogic.add_operation("hasRole", function (this: ConditionContext, roleId: string) {
25
+ return this?.user?.roles?.includes(roleId) ?? false;
26
+ });
27
+
28
+ // Check if user has any of the specified roles
29
+ jsonLogic.add_operation("hasAnyRole", function (this: ConditionContext, roleIds: string[]) {
30
+ if (!this?.user?.roles || !Array.isArray(roleIds)) return false;
31
+ return roleIds.some(role => this.user.roles.includes(role));
32
+ });
33
+
34
+ // Check if a timestamp is today
35
+ jsonLogic.add_operation("isToday", (timestamp: number) => {
36
+ if (!timestamp) return false;
37
+ const date = new Date(timestamp);
38
+ const today = new Date();
39
+ return date.getFullYear() === today.getFullYear() &&
40
+ date.getMonth() === today.getMonth() &&
41
+ date.getDate() === today.getDate();
42
+ });
43
+
44
+ // Check if a timestamp is in the past
45
+ jsonLogic.add_operation("isPast", (timestamp: number) => {
46
+ if (!timestamp) return false;
47
+ return timestamp < Date.now();
48
+ });
49
+
50
+ // Check if a timestamp is in the future
51
+ jsonLogic.add_operation("isFuture", (timestamp: number) => {
52
+ if (!timestamp) return false;
53
+ return timestamp > Date.now();
54
+ });
55
+
56
+ operationsRegistered = true;
57
+ }
58
+
59
+ /**
60
+ * Evaluate a JSON Logic rule against the given context.
61
+ */
62
+ export function evaluateCondition(rule: JsonLogicRule, context: ConditionContext): any {
63
+ // Ensure operations are registered
64
+ registerConditionOperations();
65
+ return jsonLogic.apply(rule, context);
66
+ }
67
+
68
+ /**
69
+ * Convert a value to a format suitable for JSON Logic evaluation.
70
+ * Specifically handles Date objects by converting them to Unix timestamps.
71
+ */
72
+ function serializeValueForConditions(value: any): any {
73
+ if (value === null || value === undefined) {
74
+ return value;
75
+ }
76
+
77
+ // Handle Date objects
78
+ if (value instanceof Date) {
79
+ return value.getTime();
80
+ }
81
+
82
+ // Handle Firestore Timestamp-like objects (have toDate or toMillis)
83
+ if (typeof value?.toMillis === "function") {
84
+ return value.toMillis();
85
+ }
86
+ if (typeof value?.toDate === "function") {
87
+ return value.toDate().getTime();
88
+ }
89
+
90
+ // Handle arrays recursively
91
+ if (Array.isArray(value)) {
92
+ return value.map(serializeValueForConditions);
93
+ }
94
+
95
+ // Handle plain objects recursively
96
+ if (typeof value === "object") {
97
+ const result: Record<string, any> = {};
98
+ for (const key of Object.keys(value)) {
99
+ result[key] = serializeValueForConditions(value[key]);
100
+ }
101
+ return result;
102
+ }
103
+
104
+ return value;
105
+ }
106
+
107
+ /**
108
+ * Build a ConditionContext from the current property resolution context.
109
+ */
110
+ export function buildConditionContext(params: {
111
+ propertyKey?: string;
112
+ values?: Record<string, any>;
113
+ previousValues?: Record<string, any>;
114
+ path: string;
115
+ entityId?: string;
116
+ index?: number;
117
+ authController: AuthController;
118
+ }): ConditionContext {
119
+ const {
120
+ propertyKey,
121
+ values,
122
+ previousValues,
123
+ path,
124
+ entityId,
125
+ index,
126
+ authController
127
+ } = params;
128
+
129
+ const user = authController.user;
130
+ const serializedValues = serializeValueForConditions(values ?? {});
131
+ const serializedPreviousValues = serializeValueForConditions(previousValues ?? values ?? {});
132
+
133
+ return {
134
+ values: serializedValues,
135
+ previousValues: serializedPreviousValues,
136
+ propertyValue: propertyKey ? getIn(serializedValues, propertyKey) : undefined,
137
+ path,
138
+ entityId,
139
+ isNew: !entityId,
140
+ index,
141
+ user: {
142
+ uid: user?.uid ?? "",
143
+ email: user?.email ?? null,
144
+ displayName: user?.displayName ?? null,
145
+ photoURL: user?.photoURL ?? null,
146
+ roles: user?.roles?.map((r: Role) => r.id) ?? []
147
+ },
148
+ now: Date.now()
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
154
+ */
155
+ export function applyPropertyConditions<T extends CMSType>(
156
+ property: ResolvedProperty<T>,
157
+ context: ConditionContext
158
+ ): ResolvedProperty<T> {
159
+ const { conditions } = property;
160
+ if (!conditions) return property;
161
+
162
+ let result = { ...property };
163
+
164
+ // ═══════════════════════════════════════════════════════════════════════
165
+ // FIELD STATE CONDITIONS
166
+ // ═══════════════════════════════════════════════════════════════════════
167
+
168
+ // Evaluate disabled condition
169
+ if (conditions.disabled) {
170
+ const isDisabled = evaluateCondition(conditions.disabled, context);
171
+ if (isDisabled) {
172
+ result.disabled = {
173
+ clearOnDisabled: conditions.clearOnDisabled ?? false,
174
+ disabledMessage: conditions.disabledMessage,
175
+ hidden: false
176
+ };
177
+ }
178
+ }
179
+
180
+ // Evaluate hidden condition
181
+ if (conditions.hidden) {
182
+ const isHidden = evaluateCondition(conditions.hidden, context);
183
+ if (isHidden) {
184
+ result.disabled = {
185
+ ...(typeof result.disabled === "object" ? result.disabled : {}),
186
+ hidden: true,
187
+ clearOnDisabled: conditions.clearOnDisabled ?? false
188
+ };
189
+ }
190
+ }
191
+
192
+ // Evaluate readOnly condition
193
+ if (conditions.readOnly) {
194
+ const isReadOnly = evaluateCondition(conditions.readOnly, context);
195
+ if (isReadOnly) {
196
+ result.readOnly = true;
197
+ }
198
+ }
199
+
200
+ // ═══════════════════════════════════════════════════════════════════════
201
+ // VALIDATION CONDITIONS
202
+ // ═══════════════════════════════════════════════════════════════════════
203
+
204
+ // Evaluate required condition
205
+ if (conditions.required !== undefined) {
206
+ const isRequired = evaluateCondition(conditions.required, context);
207
+ result.validation = {
208
+ ...result.validation,
209
+ required: isRequired,
210
+ requiredMessage: conditions.requiredMessage
211
+ };
212
+ }
213
+
214
+ // ═══════════════════════════════════════════════════════════════════════
215
+ // VALUE CONDITIONS
216
+ // ═══════════════════════════════════════════════════════════════════════
217
+
218
+ // Apply default value for new entities
219
+ if (context.isNew && conditions.defaultValue !== undefined) {
220
+ result.defaultValue = evaluateCondition(conditions.defaultValue, context);
221
+ }
222
+
223
+ // ═══════════════════════════════════════════════════════════════════════
224
+ // ENUM CONDITIONS
225
+ // ═══════════════════════════════════════════════════════════════════════
226
+
227
+ if ("enumValues" in result && result.enumValues && (conditions.enumConditions || conditions.allowedEnumValues || conditions.excludedEnumValues)) {
228
+ (result as any).enumValues = applyEnumConditions(
229
+ result.enumValues as EnumValueConfig[],
230
+ conditions,
231
+ context
232
+ );
233
+ }
234
+
235
+ // ═══════════════════════════════════════════════════════════════════════
236
+ // REFERENCE CONDITIONS
237
+ // ═══════════════════════════════════════════════════════════════════════
238
+
239
+ if (result.dataType === "reference") {
240
+ if (conditions.referencePath) {
241
+ (result as any).path = evaluateCondition(conditions.referencePath, context);
242
+ }
243
+ if (conditions.referenceFilter) {
244
+ (result as any).forceFilter = evaluateCondition(conditions.referenceFilter, context);
245
+ }
246
+ }
247
+
248
+ // ═══════════════════════════════════════════════════════════════════════
249
+ // ARRAY CONDITIONS
250
+ // ═══════════════════════════════════════════════════════════════════════
251
+
252
+ if (result.dataType === "array") {
253
+ if (conditions.canAddElements !== undefined) {
254
+ (result as any).canAddElements = evaluateCondition(conditions.canAddElements, context);
255
+ }
256
+ if (conditions.sortable !== undefined) {
257
+ (result as any).sortable = evaluateCondition(conditions.sortable, context);
258
+ }
259
+ }
260
+
261
+ return result;
262
+ }
263
+
264
+ /**
265
+ * Convert an object with numeric keys back to an array.
266
+ * Firestore stores arrays as {"0": "a", "1": "b"} to avoid nested arrays.
267
+ */
268
+ function objectToArray(obj: unknown): string[] {
269
+ if (Array.isArray(obj)) return obj.map(String);
270
+ if (obj && typeof obj === "object") {
271
+ const keys = Object.keys(obj);
272
+ if (keys.length > 0 && keys.every(k => !isNaN(Number(k)))) {
273
+ return keys
274
+ .sort((a, b) => Number(a) - Number(b))
275
+ .map(k => (obj as Record<string, unknown>)[k])
276
+ .filter((v): v is string => typeof v === "string" || typeof v === "number")
277
+ .map(String);
278
+ }
279
+ }
280
+ return [];
281
+ }
282
+
283
+ /**
284
+ * Apply enum-specific conditions to filter and modify enum values.
285
+ */
286
+ function applyEnumConditions(
287
+ enumValues: EnumValueConfig[],
288
+ conditions: PropertyConditions,
289
+ context: ConditionContext
290
+ ): EnumValueConfig[] {
291
+ let result = [...enumValues];
292
+
293
+ // Apply allowedEnumValues filter
294
+ if (conditions.allowedEnumValues) {
295
+ const allowed = evaluateCondition(conditions.allowedEnumValues, context);
296
+ // Handle both array format and object-with-numeric-keys format (Firestore workaround)
297
+ const allowedArray = objectToArray(allowed);
298
+ if (allowedArray.length > 0) {
299
+ result = result.filter(ev => allowedArray.includes(String(ev.id)));
300
+ }
301
+ }
302
+
303
+ // Apply excludedEnumValues filter
304
+ if (conditions.excludedEnumValues) {
305
+ const excluded = evaluateCondition(conditions.excludedEnumValues, context);
306
+ // Handle both array format and object-with-numeric-keys format
307
+ const excludedArray = objectToArray(excluded);
308
+ if (excludedArray.length > 0) {
309
+ result = result.filter(ev => !excludedArray.includes(String(ev.id)));
310
+ }
311
+ }
312
+
313
+ // Apply individual enum conditions
314
+ if (conditions.enumConditions) {
315
+ result = result
316
+ .map(ev => {
317
+ const evConditions = conditions.enumConditions?.[ev.id];
318
+ if (!evConditions) return ev;
319
+
320
+ // Check hidden condition first
321
+ if (evConditions.hidden && evaluateCondition(evConditions.hidden, context)) {
322
+ return null; // Will be filtered out
323
+ }
324
+
325
+ // Check disabled condition
326
+ if (evConditions.disabled && evaluateCondition(evConditions.disabled, context)) {
327
+ return {
328
+ ...ev,
329
+ disabled: true
330
+ };
331
+ }
332
+
333
+ return ev;
334
+ })
335
+ .filter((ev): ev is EnumValueConfig => ev !== null);
336
+ }
337
+
338
+ return result;
339
+ }
@@ -14,6 +14,7 @@ import {
14
14
  ResolvedProperty
15
15
  } from "../types";
16
16
  import { DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE } from "./common";
17
+ import { mergeDeep } from "./objects";
17
18
 
18
19
  export function isReadOnly(property: Property<any> | ResolvedProperty<any>): boolean {
19
20
  if (property.readOnly)
@@ -32,7 +33,7 @@ export function isHidden(property: Property | ResolvedProperty): boolean {
32
33
  return typeof property.disabled === "object" && Boolean(property.disabled.hidden);
33
34
  }
34
35
 
35
- export function isPropertyBuilder<T extends CMSType, M extends Record<string, any>>(propertyOrBuilder?: PropertyOrBuilder<T, M> | Property<T> | ResolvedProperty<T>): propertyOrBuilder is PropertyBuilder<T, M> {
36
+ export function isPropertyBuilder<T extends CMSType = CMSType, M extends Record<string, any> = any>(propertyOrBuilder?: PropertyOrBuilder<T, M> | Property | ResolvedProperty): propertyOrBuilder is PropertyBuilder<T, M> {
36
37
  return typeof propertyOrBuilder === "function";
37
38
  }
38
39
 
@@ -84,36 +85,30 @@ export function getDefaultValueForDataType(dataType: DataType) {
84
85
  * @group Datasource
85
86
  */
86
87
  export function updateDateAutoValues<M extends Record<string, any>>({
87
- inputValues,
88
- properties,
89
- status,
90
- timestampNowValue,
91
- setDateToMidnight
92
- }:
93
- {
94
- inputValues: Partial<EntityValues<M>>,
95
- properties: ResolvedProperties<M>,
96
- status: EntityStatus,
97
- timestampNowValue: any,
98
- setDateToMidnight: (input?: any) => any | undefined
99
- }): EntityValues<M> {
88
+ inputValues,
89
+ properties,
90
+ status,
91
+ timestampNowValue
92
+ }:
93
+ {
94
+ inputValues: Partial<EntityValues<M>>,
95
+ properties: ResolvedProperties<M>,
96
+ status: EntityStatus,
97
+ timestampNowValue: any
98
+ }): EntityValues<M> {
100
99
  return traverseValuesProperties(
101
100
  inputValues,
102
101
  properties,
103
102
  (inputValue, property) => {
104
103
  if (property.dataType === "date") {
105
- let resultDate;
106
104
  if (status === "existing" && property.autoValue === "on_update") {
107
- resultDate = timestampNowValue;
105
+ return timestampNowValue;
108
106
  } else if ((status === "new" || status === "copy") &&
109
107
  (property.autoValue === "on_update" || property.autoValue === "on_create")) {
110
- resultDate = timestampNowValue;
108
+ return timestampNowValue;
111
109
  } else {
112
- resultDate = inputValue;
110
+ return inputValue;
113
111
  }
114
- if (property.mode === "date")
115
- resultDate = setDateToMidnight(resultDate);
116
- return resultDate;
117
112
  } else {
118
113
  return inputValue;
119
114
  }
@@ -128,10 +123,10 @@ export function updateDateAutoValues<M extends Record<string, any>>({
128
123
  * @group Datasource
129
124
  */
130
125
  export function sanitizeData<M extends Record<string, any>>
131
- (
132
- values: EntityValues<M>,
133
- properties: ResolvedProperties<M>
134
- ) {
126
+ (
127
+ values: EntityValues<M>,
128
+ properties: ResolvedProperties<M>
129
+ ) {
135
130
  const result: any = values;
136
131
  Object.entries(properties)
137
132
  .forEach(([key, property]) => {
@@ -150,23 +145,27 @@ export function traverseValuesProperties<M extends Record<string, any>>(
150
145
  properties: ResolvedProperties<M>,
151
146
  operation: (value: any, property: Property) => any
152
147
  ): EntityValues<M> | undefined {
148
+ // Handle null/undefined inputValues - use empty object as base for mergeDeep
149
+ const safeInputValues = inputValues ?? {};
150
+
153
151
  const updatedValues = Object.entries(properties)
154
152
  .map(([key, property]) => {
155
- const inputValue = inputValues && (inputValues)[key];
153
+ const inputValue = safeInputValues && (safeInputValues)[key];
156
154
  const updatedValue = traverseValueProperty(inputValue, property as Property, operation);
157
155
  if (updatedValue === null) return null;
158
156
  if (updatedValue === undefined) return undefined;
159
157
  return ({ [key]: updatedValue });
160
158
  })
161
159
  .reduce((a, b) => ({ ...a, ...b }), {}) as EntityValues<M>;
162
- const result = { ...inputValues, ...updatedValues };
163
- if (Object.keys(result).length === 0) return undefined;
160
+ // Use mergeDeep to preserve class instances like EntityReference, GeoPoint
161
+ const result = mergeDeep(safeInputValues, updatedValues);
162
+ if (!result || Object.keys(result).length === 0) return undefined;
164
163
  return result;
165
164
  }
166
165
 
167
166
  export function traverseValueProperty(inputValue: any,
168
- property: Property,
169
- operation: (value: any, property: Property) => any): any {
167
+ property: Property,
168
+ operation: (value: any, property: Property) => any): any {
170
169
 
171
170
  let value;
172
171
  if (property.dataType === "map" && property.properties) {
@@ -87,8 +87,9 @@ export function saveEntityToCache(path: string, data: object): void {
87
87
  if (isLocalStorageAvailable) {
88
88
  try {
89
89
  const key = LOCAL_STORAGE_PREFIX + path;
90
+
90
91
  const entityString = JSON.stringify(data, customReplacer);
91
- console.log("Saving entity to localStorage:", {
92
+ console.debug("Saving entity to localStorage:", {
92
93
  key,
93
94
  entityString
94
95
  });
package/src/util/index.ts CHANGED
@@ -16,7 +16,7 @@ export * from "./icon_list";
16
16
  export * from "./icon_synonyms";
17
17
  export * from "./icons";
18
18
  export * from "./plurals";
19
- export * from "./references";
19
+ export * from "./previews";
20
20
  export * from "./flatten_object";
21
21
  export * from "./make_properties_editable";
22
22
  export * from "./join_collections";
@@ -24,3 +24,4 @@ export * from "./builders";
24
24
  export * from "./useTraceUpdate";
25
25
  export * from "./storage";
26
26
  export * from "./callbacks";
27
+ export * from "./conditions";
@@ -11,8 +11,8 @@ import { sortProperties } from "./collections";
11
11
  import { isPropertyBuilder } from "./entities";
12
12
 
13
13
  function applyModifyFunction(modifyCollection: ((props: ModifyCollectionProps) => (EntityCollection | void)) | undefined,
14
- collection: EntityCollection,
15
- parentPaths: string[]) {
14
+ collection: EntityCollection,
15
+ parentPaths: string[]) {
16
16
  if (modifyCollection) {
17
17
  const modified = modifyCollection({
18
18
  collection,
@@ -34,9 +34,9 @@ function applyModifyFunction(modifyCollection: ((props: ModifyCollectionProps) =
34
34
  *
35
35
  */
36
36
  export function joinCollectionLists(targetCollections: EntityCollection[],
37
- sourceCollections: EntityCollection[] | undefined,
38
- parentPaths: string[] = [],
39
- modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void): EntityCollection[] {
37
+ sourceCollections: EntityCollection[] | undefined,
38
+ parentPaths: string[] = [],
39
+ modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void): EntityCollection[] {
40
40
 
41
41
  // merge collections that are in both lists
42
42
  const updatedCollections = (sourceCollections ?? [])
@@ -73,9 +73,9 @@ export function joinCollectionLists(targetCollections: EntityCollection[],
73
73
  *
74
74
  */
75
75
  export function mergeCollection(target: EntityCollection,
76
- source: EntityCollection,
77
- parentPaths: string[] = [],
78
- modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void
76
+ source: EntityCollection,
77
+ parentPaths: string[] = [],
78
+ modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void
79
79
  ): EntityCollection {
80
80
 
81
81
  const subcollectionsMerged = joinCollectionLists(
@@ -125,6 +125,8 @@ export function mergeCollection(target: EntityCollection,
125
125
  }
126
126
 
127
127
  function mergePropertyOrBuilder(target: PropertyOrBuilder, source: PropertyOrBuilder): PropertyOrBuilder {
128
+ if (!source) return target;
129
+ if (!target) return source;
128
130
  if (isPropertyBuilder(source)) {
129
131
  return source;
130
132
  } else if (isPropertyBuilder(target)) {