@firecms/core 3.0.1 → 3.1.0-canary.7d91b7c
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/README.md +1 -1
- package/dist/components/AIIcon.d.ts +16 -0
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
- package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
- package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
- package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
- package/dist/components/EntityCollectionView/Board.d.ts +2 -0
- package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
- package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
- package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
- package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
- package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
- package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
- package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +44 -0
- package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
- package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
- package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
- package/dist/components/VirtualTable/VirtualTableHeader.d.ts +3 -1
- package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
- package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
- package/dist/components/VirtualTable/types.d.ts +2 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/contexts/index.d.ts +10 -0
- package/dist/core/DrawerNavigationGroup.d.ts +45 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/form/components/ErrorFocus.d.ts +1 -1
- package/dist/form/validation.d.ts +3 -2
- package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
- package/dist/hooks/useCollapsedGroups.d.ts +4 -1
- package/dist/index.es.js +5266 -1578
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5260 -1573
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useRestoreScroll.d.ts +1 -1
- package/dist/preview/PropertyPreviewProps.d.ts +5 -0
- package/dist/preview/components/DatePreview.d.ts +13 -3
- package/dist/preview/components/ImagePreview.d.ts +5 -1
- package/dist/preview/components/StorageThumbnail.d.ts +2 -1
- package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
- package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
- package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +50 -2
- package/dist/types/datasource.d.ts +0 -1
- package/dist/types/plugins.d.ts +62 -1
- package/dist/types/properties.d.ts +259 -4
- package/dist/util/__tests__/conditions.test.d.ts +1 -0
- package/dist/util/__tests__/objects.test.d.ts +1 -0
- package/dist/util/conditions.d.ts +26 -0
- package/dist/util/entities.d.ts +2 -3
- package/dist/util/index.d.ts +2 -1
- package/dist/util/property_utils.d.ts +2 -1
- package/dist/util/resolutions.d.ts +3 -3
- package/package.json +14 -11
- package/src/app/Scaffold.tsx +14 -15
- package/src/components/AIIcon.tsx +39 -0
- package/src/components/ArrayContainer.tsx +1 -4
- package/src/components/ClearFilterSortButton.tsx +19 -16
- package/src/components/ConfirmationDialog.tsx +0 -2
- package/src/components/DeleteEntityDialog.tsx +2 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
- package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
- package/src/components/EntityCollectionView/Board.tsx +324 -0
- package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
- package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
- package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
- package/src/components/EntityCollectionView/EntityCard.tsx +235 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +733 -0
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +519 -203
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
- package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +199 -0
- package/src/components/EntityCollectionView/board_types.ts +113 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
- package/src/components/ErrorTooltip.tsx +2 -1
- package/src/components/HomePage/DefaultHomePage.tsx +47 -10
- package/src/components/HomePage/HomePageDnD.tsx +56 -41
- package/src/components/HomePage/NavigationCard.tsx +20 -18
- package/src/components/HomePage/NavigationGroup.tsx +17 -16
- package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
- package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
- package/src/components/ReferenceWidget.tsx +2 -4
- package/src/components/SelectableTable/SelectableTable.tsx +75 -67
- package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
- package/src/components/UnsavedChangesDialog.tsx +0 -2
- package/src/components/UserDisplay.tsx +4 -4
- package/src/components/VirtualTable/VirtualTable.tsx +272 -118
- package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
- package/src/components/VirtualTable/VirtualTableHeader.tsx +59 -50
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
- package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
- package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +19 -6
- package/src/components/VirtualTable/types.tsx +2 -0
- package/src/components/common/useColumnsIds.tsx +95 -3
- package/src/components/index.tsx +4 -0
- package/src/contexts/BreacrumbsContext.tsx +15 -8
- package/src/contexts/index.ts +10 -0
- package/src/core/DefaultAppBar.tsx +40 -27
- package/src/core/DefaultDrawer.tsx +42 -56
- package/src/core/DrawerNavigationGroup.tsx +118 -0
- package/src/core/DrawerNavigationItem.tsx +4 -3
- package/src/core/EntityEditView.tsx +41 -43
- package/src/core/EntitySidePanel.tsx +28 -26
- package/src/core/SideDialogs.tsx +4 -2
- package/src/core/field_configs.tsx +14 -9
- package/src/core/index.tsx +1 -0
- package/src/form/EntityForm.tsx +69 -60
- package/src/form/PropertyFieldBinding.tsx +61 -46
- package/src/form/components/ErrorFocus.tsx +3 -3
- package/src/form/components/StorageItemPreview.tsx +2 -1
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +22 -18
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
- package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
- package/src/form/validation.ts +245 -160
- package/src/hooks/useBreadcrumbsController.tsx +18 -0
- package/src/hooks/useBuildNavigationController.tsx +46 -23
- package/src/hooks/useCollapsedGroups.ts +12 -4
- package/src/hooks/useValidateAuthenticator.tsx +1 -1
- package/src/internal/useBuildDataSource.ts +68 -34
- package/src/internal/useBuildSideDialogsController.tsx +11 -8
- package/src/internal/useBuildSideEntityController.tsx +2 -4
- package/src/internal/useRestoreScroll.tsx +26 -14
- package/src/preview/PropertyPreview.tsx +41 -32
- package/src/preview/PropertyPreviewProps.tsx +6 -0
- package/src/preview/components/DatePreview.tsx +72 -4
- package/src/preview/components/EmptyValue.tsx +1 -1
- package/src/preview/components/ImagePreview.tsx +37 -21
- package/src/preview/components/StorageThumbnail.tsx +16 -12
- package/src/preview/components/UrlComponentPreview.tsx +28 -25
- package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
- package/src/routes/CustomCMSRoute.tsx +1 -0
- package/src/routes/FireCMSRoute.tsx +26 -13
- package/src/types/analytics.ts +10 -0
- package/src/types/collections.ts +57 -3
- package/src/types/datasource.ts +54 -56
- package/src/types/plugins.tsx +69 -1
- package/src/types/properties.ts +347 -27
- package/src/util/__tests__/conditions.test.ts +506 -0
- package/src/util/__tests__/objects.test.ts +196 -0
- package/src/util/callbacks.ts +6 -3
- package/src/util/collections.ts +51 -6
- package/src/util/conditions.ts +339 -0
- package/src/util/entities.ts +29 -30
- package/src/util/entity_cache.ts +2 -1
- package/src/util/index.ts +2 -1
- package/src/util/join_collections.ts +10 -8
- package/src/util/objects.ts +31 -13
- package/src/util/{references.ts → previews.ts} +16 -2
- package/src/util/property_utils.tsx +37 -11
- package/src/util/resolutions.ts +62 -58
- /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
package/src/util/callbacks.ts
CHANGED
|
@@ -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 =
|
|
59
|
+
let values = props.values;
|
|
59
60
|
if (baseCallbacks.onPreSave) {
|
|
60
61
|
const baseValues = await Promise.resolve(baseCallbacks.onPreSave(props));
|
|
61
|
-
|
|
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
|
-
|
|
70
|
+
// Use mergeDeep to preserve class instances like EntityReference, GeoPoint
|
|
71
|
+
values = mergeDeep(values, pluginValues);
|
|
69
72
|
}
|
|
70
73
|
return values;
|
|
71
74
|
};
|
package/src/util/collections.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
+
}
|
package/src/util/entities.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
105
|
+
return timestampNowValue;
|
|
108
106
|
} else if ((status === "new" || status === "copy") &&
|
|
109
107
|
(property.autoValue === "on_update" || property.autoValue === "on_create")) {
|
|
110
|
-
|
|
108
|
+
return timestampNowValue;
|
|
111
109
|
} else {
|
|
112
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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 =
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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) {
|
package/src/util/entity_cache.ts
CHANGED
|
@@ -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.
|
|
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 "./
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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)) {
|