@firecms/collection_editor 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.
- package/dist/ConfigControllerProvider.d.ts +6 -0
- package/dist/api/generateCollectionApi.d.ts +71 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.es.js +9466 -5588
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9461 -5583
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collection_editor_controller.d.ts +14 -0
- package/dist/types/collection_inference.d.ts +8 -2
- package/dist/types/config_controller.d.ts +23 -2
- package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
- package/dist/ui/KanbanSetupAction.d.ts +10 -0
- package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +37 -0
- package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
- package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
- package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +24 -0
- package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +4 -1
- package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
- package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
- package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
- package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
- package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
- package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
- package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
- package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
- package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
- package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
- package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
- package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
- package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
- package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
- package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
- package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
- package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
- package/dist/useCollectionEditorPlugin.d.ts +7 -1
- package/dist/utils/validateCollectionJson.d.ts +22 -0
- package/package.json +15 -15
- package/src/ConfigControllerProvider.tsx +82 -47
- package/src/api/generateCollectionApi.ts +119 -0
- package/src/api/index.ts +1 -0
- package/src/index.ts +28 -1
- package/src/types/collection_editor_controller.tsx +16 -3
- package/src/types/collection_inference.ts +15 -2
- package/src/types/config_controller.tsx +27 -2
- package/src/ui/AddKanbanColumnAction.tsx +203 -0
- package/src/ui/EditorCollectionActionStart.tsx +1 -2
- package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
- package/src/ui/KanbanSetupAction.tsx +38 -0
- package/src/ui/MissingReferenceWidget.tsx +1 -1
- package/src/ui/NewCollectionButton.tsx +1 -1
- package/src/ui/PropertyAddColumnComponent.tsx +1 -1
- package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +242 -0
- package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +212 -259
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +237 -169
- package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +133 -67
- package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
- package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +190 -91
- package/src/ui/collection_editor/DisplaySettingsForm.tsx +333 -0
- package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -96
- package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +6 -7
- package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +1 -3
- package/src/ui/collection_editor/EnumForm.tsx +147 -100
- package/src/ui/collection_editor/ExtendSettingsForm.tsx +93 -0
- package/src/ui/collection_editor/GeneralSettingsForm.tsx +337 -0
- package/src/ui/collection_editor/GetCodeDialog.tsx +57 -36
- package/src/ui/collection_editor/KanbanConfigSection.tsx +207 -0
- package/src/ui/collection_editor/LayoutModeSwitch.tsx +22 -41
- package/src/ui/collection_editor/PropertyEditView.tsx +206 -142
- package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
- package/src/ui/collection_editor/PropertyTree.tsx +130 -58
- package/src/ui/collection_editor/SubcollectionsEditTab.tsx +171 -162
- package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
- package/src/ui/collection_editor/ViewModeSwitch.tsx +41 -0
- package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +1 -0
- package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +117 -35
- package/src/ui/collection_editor/properties/EnumPropertyField.tsx +28 -21
- package/src/ui/collection_editor/properties/MapPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +115 -39
- package/src/ui/collection_editor/properties/StoragePropertyField.tsx +1 -1
- package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +861 -0
- package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
- package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
- package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
- package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
- package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -1
- package/src/useCollectionEditorPlugin.tsx +32 -17
- package/src/utils/validateCollectionJson.ts +380 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Properties, Property, PropertyOrBuilder, isPropertyBuilder } from "@firecms/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursively extract all property paths from a Properties object.
|
|
5
|
+
* For nested map properties, creates dot-notation paths like "address.city".
|
|
6
|
+
* Skips PropertyBuilder functions (callbacks) as they cannot be statically analyzed.
|
|
7
|
+
*
|
|
8
|
+
* @param properties - The properties object to extract paths from
|
|
9
|
+
* @param prefix - Optional prefix for nested paths (used in recursion)
|
|
10
|
+
* @returns Array of property path strings
|
|
11
|
+
*/
|
|
12
|
+
export function getPropertyPaths(
|
|
13
|
+
properties: Properties | undefined,
|
|
14
|
+
prefix: string = ""
|
|
15
|
+
): string[] {
|
|
16
|
+
if (!properties) return [];
|
|
17
|
+
|
|
18
|
+
const paths: string[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [key, propertyOrBuilder] of Object.entries(properties)) {
|
|
21
|
+
if (!propertyOrBuilder) continue;
|
|
22
|
+
|
|
23
|
+
// Skip PropertyBuilder functions - they require runtime values to resolve
|
|
24
|
+
if (isPropertyBuilder(propertyOrBuilder)) continue;
|
|
25
|
+
|
|
26
|
+
const property = propertyOrBuilder as Property;
|
|
27
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
28
|
+
paths.push(fullPath);
|
|
29
|
+
|
|
30
|
+
// Recursively add nested map properties
|
|
31
|
+
if (property.dataType === "map" && property.properties) {
|
|
32
|
+
const nestedPaths = getPropertyPaths(
|
|
33
|
+
property.properties as Properties,
|
|
34
|
+
fullPath
|
|
35
|
+
);
|
|
36
|
+
paths.push(...nestedPaths);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// For arrays with object items, add the nested paths too
|
|
40
|
+
if (property.dataType === "array" && property.of) {
|
|
41
|
+
const ofPropertyOrBuilder = property.of as PropertyOrBuilder;
|
|
42
|
+
// Skip if the array's 'of' is a PropertyBuilder
|
|
43
|
+
if (!isPropertyBuilder(ofPropertyOrBuilder)) {
|
|
44
|
+
const ofProperty = ofPropertyOrBuilder as Property;
|
|
45
|
+
if (ofProperty.dataType === "map" && ofProperty.properties) {
|
|
46
|
+
const nestedPaths = getPropertyPaths(
|
|
47
|
+
ofProperty.properties as Properties,
|
|
48
|
+
`${fullPath}[]`
|
|
49
|
+
);
|
|
50
|
+
paths.push(...nestedPaths);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return paths;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get property paths grouped by top-level property for UI display.
|
|
61
|
+
* Skips PropertyBuilder functions.
|
|
62
|
+
*
|
|
63
|
+
* @param properties - The properties object
|
|
64
|
+
* @returns Object with top-level keys mapping to their nested paths
|
|
65
|
+
*/
|
|
66
|
+
export function getGroupedPropertyPaths(
|
|
67
|
+
properties: Properties | undefined
|
|
68
|
+
): Record<string, string[]> {
|
|
69
|
+
if (!properties) return {};
|
|
70
|
+
|
|
71
|
+
const grouped: Record<string, string[]> = {};
|
|
72
|
+
|
|
73
|
+
for (const [key, propertyOrBuilder] of Object.entries(properties)) {
|
|
74
|
+
if (!propertyOrBuilder) continue;
|
|
75
|
+
|
|
76
|
+
// Skip PropertyBuilder functions
|
|
77
|
+
if (isPropertyBuilder(propertyOrBuilder)) continue;
|
|
78
|
+
|
|
79
|
+
const property = propertyOrBuilder as Property;
|
|
80
|
+
grouped[key] = [key];
|
|
81
|
+
|
|
82
|
+
if (property.dataType === "map" && property.properties) {
|
|
83
|
+
const nestedPaths = getPropertyPaths(
|
|
84
|
+
property.properties as Properties,
|
|
85
|
+
key
|
|
86
|
+
);
|
|
87
|
+
grouped[key].push(...nestedPaths);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return grouped;
|
|
92
|
+
}
|
|
@@ -12,7 +12,7 @@ export function ValidationPanel({
|
|
|
12
12
|
asField={true}
|
|
13
13
|
innerClassName="p-4"
|
|
14
14
|
title={
|
|
15
|
-
<div className="flex flex-row text-surface-500">
|
|
15
|
+
<div className="flex flex-row text-surface-500 text-text-secondary dark:text-text-secondary-dark">
|
|
16
16
|
<RuleIcon/>
|
|
17
17
|
<Typography variant={"subtitle2"}
|
|
18
18
|
className="ml-4">
|
|
@@ -7,6 +7,7 @@ import { HomePageEditorCollectionAction } from "./ui/HomePageEditorCollectionAct
|
|
|
7
7
|
import { PersistedCollection } from "./types/persisted_collection";
|
|
8
8
|
import { CollectionInference } from "./types/collection_inference";
|
|
9
9
|
import { CollectionsConfigController } from "./types/config_controller";
|
|
10
|
+
import { CollectionGenerationCallback } from "./api/generateCollectionApi";
|
|
10
11
|
import { CollectionViewHeaderAction } from "./ui/CollectionViewHeaderAction";
|
|
11
12
|
import { PropertyAddColumnComponent } from "./ui/PropertyAddColumnComponent";
|
|
12
13
|
import { NewCollectionButton } from "./ui/NewCollectionButton";
|
|
@@ -15,6 +16,8 @@ import { useCollectionEditorController } from "./useCollectionEditorController";
|
|
|
15
16
|
import { EditorCollectionActionStart } from "./ui/EditorCollectionActionStart";
|
|
16
17
|
import { NewCollectionCard } from "./ui/NewCollectionCard";
|
|
17
18
|
import { EditorEntityAction } from "./ui/EditorEntityAction";
|
|
19
|
+
import { KanbanSetupAction } from "./ui/KanbanSetupAction";
|
|
20
|
+
import { AddKanbanColumnAction } from "./ui/AddKanbanColumnAction";
|
|
18
21
|
|
|
19
22
|
export interface CollectionConfigControllerProps<EC extends PersistedCollection = PersistedCollection, USER extends User = User> {
|
|
20
23
|
|
|
@@ -54,6 +57,12 @@ export interface CollectionConfigControllerProps<EC extends PersistedCollection
|
|
|
54
57
|
|
|
55
58
|
includeIntroView?: boolean;
|
|
56
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Callback function for generating/modifying collections.
|
|
62
|
+
* The plugin is API-agnostic - the consumer provides the implementation.
|
|
63
|
+
*/
|
|
64
|
+
generateCollection?: CollectionGenerationCallback;
|
|
65
|
+
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -68,18 +77,19 @@ export interface CollectionConfigControllerProps<EC extends PersistedCollection
|
|
|
68
77
|
* @param collectionInference
|
|
69
78
|
*/
|
|
70
79
|
export function useCollectionEditorPlugin<EC extends PersistedCollection = PersistedCollection, USER extends User = User>
|
|
71
|
-
({
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
({
|
|
81
|
+
collectionConfigController,
|
|
82
|
+
configPermissions,
|
|
83
|
+
reservedGroups,
|
|
84
|
+
extraView,
|
|
85
|
+
getUser,
|
|
86
|
+
collectionInference,
|
|
87
|
+
getData,
|
|
88
|
+
onAnalyticsEvent,
|
|
89
|
+
includeIntroView = true,
|
|
90
|
+
pathSuggestions,
|
|
91
|
+
generateCollection
|
|
92
|
+
}: CollectionConfigControllerProps<EC, USER>): FireCMSPlugin<any, any, PersistedCollection> {
|
|
83
93
|
|
|
84
94
|
return {
|
|
85
95
|
key: "collection_editor",
|
|
@@ -95,12 +105,13 @@ export function useCollectionEditorPlugin<EC extends PersistedCollection = Persi
|
|
|
95
105
|
getUser,
|
|
96
106
|
getData,
|
|
97
107
|
onAnalyticsEvent,
|
|
98
|
-
pathSuggestions
|
|
108
|
+
pathSuggestions,
|
|
109
|
+
generateCollection
|
|
99
110
|
}
|
|
100
111
|
},
|
|
101
112
|
homePage: {
|
|
102
|
-
additionalActions: <NewCollectionButton/>,
|
|
103
|
-
additionalChildrenStart: includeIntroView ? <IntroWidget/> : undefined,
|
|
113
|
+
additionalActions: <NewCollectionButton />,
|
|
114
|
+
additionalChildrenStart: includeIntroView ? <IntroWidget /> : undefined,
|
|
104
115
|
CollectionActions: HomePageEditorCollectionAction,
|
|
105
116
|
AdditionalCards: NewCollectionCard,
|
|
106
117
|
allowDragAndDrop: true,
|
|
@@ -111,7 +122,11 @@ export function useCollectionEditorPlugin<EC extends PersistedCollection = Persi
|
|
|
111
122
|
CollectionActionsStart: EditorCollectionActionStart,
|
|
112
123
|
CollectionActions: EditorCollectionAction,
|
|
113
124
|
HeaderAction: CollectionViewHeaderAction,
|
|
114
|
-
AddColumnComponent: PropertyAddColumnComponent
|
|
125
|
+
AddColumnComponent: PropertyAddColumnComponent,
|
|
126
|
+
onColumnsReorder: collectionConfigController.updatePropertiesOrder,
|
|
127
|
+
onKanbanColumnsReorder: collectionConfigController.updateKanbanColumnsOrder,
|
|
128
|
+
KanbanSetupComponent: KanbanSetupAction,
|
|
129
|
+
AddKanbanColumnComponent: AddKanbanColumnAction
|
|
115
130
|
},
|
|
116
131
|
form: {
|
|
117
132
|
ActionsTop: EditorEntityAction,
|
|
@@ -154,7 +169,7 @@ export function IntroWidget() {
|
|
|
154
169
|
sourceClick: "new_collection_card"
|
|
155
170
|
})
|
|
156
171
|
: undefined}>
|
|
157
|
-
<AddIcon/>Create your first collection
|
|
172
|
+
<AddIcon />Create your first collection
|
|
158
173
|
</Button>}
|
|
159
174
|
<Typography color={"secondary"}>
|
|
160
175
|
You can also define collections programmatically.
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { EntityCollection } from "@firecms/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Valid dataType values for properties
|
|
5
|
+
*/
|
|
6
|
+
const VALID_DATA_TYPES = [
|
|
7
|
+
"string",
|
|
8
|
+
"number",
|
|
9
|
+
"boolean",
|
|
10
|
+
"date",
|
|
11
|
+
"geopoint",
|
|
12
|
+
"reference",
|
|
13
|
+
"array",
|
|
14
|
+
"map"
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
type DataType = typeof VALID_DATA_TYPES[number];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validation error with path and message
|
|
21
|
+
*/
|
|
22
|
+
export interface CollectionValidationError {
|
|
23
|
+
path: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Result of collection JSON validation
|
|
29
|
+
*/
|
|
30
|
+
export interface CollectionValidationResult {
|
|
31
|
+
valid: boolean;
|
|
32
|
+
errors: CollectionValidationError[];
|
|
33
|
+
collection?: EntityCollection;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates a property object recursively
|
|
38
|
+
*/
|
|
39
|
+
function validateProperty(
|
|
40
|
+
property: any,
|
|
41
|
+
path: string,
|
|
42
|
+
errors: CollectionValidationError[]
|
|
43
|
+
): void {
|
|
44
|
+
if (typeof property !== "object" || property === null) {
|
|
45
|
+
errors.push({
|
|
46
|
+
path,
|
|
47
|
+
message: "Property must be an object"
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check dataType
|
|
53
|
+
if (!property.dataType) {
|
|
54
|
+
errors.push({
|
|
55
|
+
path: `${path}.dataType`,
|
|
56
|
+
message: "Required field is missing"
|
|
57
|
+
});
|
|
58
|
+
} else if (!VALID_DATA_TYPES.includes(property.dataType)) {
|
|
59
|
+
errors.push({
|
|
60
|
+
path: `${path}.dataType`,
|
|
61
|
+
message: `Invalid value "${property.dataType}", expected one of: ${VALID_DATA_TYPES.join(", ")}`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate name if present
|
|
66
|
+
if (property.name !== undefined && typeof property.name !== "string") {
|
|
67
|
+
errors.push({
|
|
68
|
+
path: `${path}.name`,
|
|
69
|
+
message: "Must be a string"
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validate array "of" property
|
|
74
|
+
if (property.dataType === "array") {
|
|
75
|
+
if (property.of) {
|
|
76
|
+
if (Array.isArray(property.of)) {
|
|
77
|
+
property.of.forEach((ofProp: any, index: number) => {
|
|
78
|
+
validateProperty(ofProp, `${path}.of[${index}]`, errors);
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
validateProperty(property.of, `${path}.of`, errors);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// oneOf validation
|
|
85
|
+
if (property.oneOf) {
|
|
86
|
+
if (typeof property.oneOf !== "object") {
|
|
87
|
+
errors.push({
|
|
88
|
+
path: `${path}.oneOf`,
|
|
89
|
+
message: "Must be an object"
|
|
90
|
+
});
|
|
91
|
+
} else if (property.oneOf.properties) {
|
|
92
|
+
validateProperties(property.oneOf.properties, `${path}.oneOf.properties`, errors);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate map properties
|
|
98
|
+
if (property.dataType === "map" && property.properties) {
|
|
99
|
+
validateProperties(property.properties, `${path}.properties`, errors);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate reference path
|
|
103
|
+
if (property.dataType === "reference") {
|
|
104
|
+
if (property.path !== undefined && typeof property.path !== "string") {
|
|
105
|
+
errors.push({
|
|
106
|
+
path: `${path}.path`,
|
|
107
|
+
message: "Must be a string"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate storage config for string
|
|
113
|
+
if (property.dataType === "string" && property.storage) {
|
|
114
|
+
if (typeof property.storage !== "object") {
|
|
115
|
+
errors.push({
|
|
116
|
+
path: `${path}.storage`,
|
|
117
|
+
message: "Must be an object"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Validate enumValues if present
|
|
123
|
+
if (property.enumValues !== undefined) {
|
|
124
|
+
if (!Array.isArray(property.enumValues) && typeof property.enumValues !== "object") {
|
|
125
|
+
errors.push({
|
|
126
|
+
path: `${path}.enumValues`,
|
|
127
|
+
message: "Must be an array or object"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validates a properties object (collection of property definitions)
|
|
135
|
+
*/
|
|
136
|
+
function validateProperties(
|
|
137
|
+
properties: any,
|
|
138
|
+
path: string,
|
|
139
|
+
errors: CollectionValidationError[]
|
|
140
|
+
): void {
|
|
141
|
+
if (typeof properties !== "object" || properties === null) {
|
|
142
|
+
errors.push({
|
|
143
|
+
path,
|
|
144
|
+
message: "Must be an object"
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
150
|
+
validateProperty(property, `${path}.${key}`, errors);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validates optional collection fields
|
|
156
|
+
*/
|
|
157
|
+
function validateOptionalFields(
|
|
158
|
+
collection: any,
|
|
159
|
+
errors: CollectionValidationError[]
|
|
160
|
+
): void {
|
|
161
|
+
// String fields
|
|
162
|
+
const stringFields = [
|
|
163
|
+
"singularName",
|
|
164
|
+
"description",
|
|
165
|
+
"group",
|
|
166
|
+
"databaseId"
|
|
167
|
+
];
|
|
168
|
+
for (const field of stringFields) {
|
|
169
|
+
if (collection[field] !== undefined && typeof collection[field] !== "string") {
|
|
170
|
+
errors.push({
|
|
171
|
+
path: field,
|
|
172
|
+
message: "Must be a string"
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Boolean fields
|
|
178
|
+
const booleanFields = [
|
|
179
|
+
"collectionGroup",
|
|
180
|
+
"textSearchEnabled",
|
|
181
|
+
"selectionEnabled",
|
|
182
|
+
"inlineEditing",
|
|
183
|
+
"hideFromNavigation",
|
|
184
|
+
"hideIdFromForm",
|
|
185
|
+
"hideIdFromCollection",
|
|
186
|
+
"formAutoSave",
|
|
187
|
+
"editable",
|
|
188
|
+
"alwaysApplyDefaultValues",
|
|
189
|
+
"includeJsonView",
|
|
190
|
+
"history"
|
|
191
|
+
];
|
|
192
|
+
for (const field of booleanFields) {
|
|
193
|
+
if (collection[field] !== undefined && typeof collection[field] !== "boolean") {
|
|
194
|
+
errors.push({
|
|
195
|
+
path: field,
|
|
196
|
+
message: "Must be a boolean"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Icon can be string or object (React node)
|
|
202
|
+
if (collection.icon !== undefined &&
|
|
203
|
+
typeof collection.icon !== "string" &&
|
|
204
|
+
typeof collection.icon !== "object") {
|
|
205
|
+
errors.push({
|
|
206
|
+
path: "icon",
|
|
207
|
+
message: "Must be a string (icon key) or object"
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// propertiesOrder must be array of strings
|
|
212
|
+
if (collection.propertiesOrder !== undefined) {
|
|
213
|
+
if (!Array.isArray(collection.propertiesOrder)) {
|
|
214
|
+
errors.push({
|
|
215
|
+
path: "propertiesOrder",
|
|
216
|
+
message: "Must be an array of strings"
|
|
217
|
+
});
|
|
218
|
+
} else if (!collection.propertiesOrder.every((item: any) => typeof item === "string")) {
|
|
219
|
+
errors.push({
|
|
220
|
+
path: "propertiesOrder",
|
|
221
|
+
message: "All items must be strings"
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// subcollections must be array
|
|
227
|
+
if (collection.subcollections !== undefined) {
|
|
228
|
+
if (!Array.isArray(collection.subcollections)) {
|
|
229
|
+
errors.push({
|
|
230
|
+
path: "subcollections",
|
|
231
|
+
message: "Must be an array"
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
collection.subcollections.forEach((sub: any, index: number) => {
|
|
235
|
+
const subErrors: CollectionValidationError[] = [];
|
|
236
|
+
validateCollectionObject(sub, subErrors);
|
|
237
|
+
subErrors.forEach(err => {
|
|
238
|
+
errors.push({
|
|
239
|
+
path: `subcollections[${index}].${err.path}`,
|
|
240
|
+
message: err.message
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// defaultViewMode validation
|
|
248
|
+
const validViewModes = ["table", "cards", "kanban"];
|
|
249
|
+
if (collection.defaultViewMode !== undefined) {
|
|
250
|
+
if (!validViewModes.includes(collection.defaultViewMode)) {
|
|
251
|
+
errors.push({
|
|
252
|
+
path: "defaultViewMode",
|
|
253
|
+
message: `Invalid value, expected one of: ${validViewModes.join(", ")}`
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// kanban config validation
|
|
259
|
+
if (collection.kanban !== undefined) {
|
|
260
|
+
if (typeof collection.kanban !== "object" || collection.kanban === null) {
|
|
261
|
+
errors.push({
|
|
262
|
+
path: "kanban",
|
|
263
|
+
message: "Must be an object"
|
|
264
|
+
});
|
|
265
|
+
} else if (collection.kanban.columnProperty !== undefined &&
|
|
266
|
+
typeof collection.kanban.columnProperty !== "string") {
|
|
267
|
+
errors.push({
|
|
268
|
+
path: "kanban.columnProperty",
|
|
269
|
+
message: "Must be a string"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Validates a collection object
|
|
277
|
+
*/
|
|
278
|
+
function validateCollectionObject(
|
|
279
|
+
collection: any,
|
|
280
|
+
errors: CollectionValidationError[]
|
|
281
|
+
): void {
|
|
282
|
+
// Required fields
|
|
283
|
+
if (!collection.id) {
|
|
284
|
+
errors.push({
|
|
285
|
+
path: "id",
|
|
286
|
+
message: "Required field is missing"
|
|
287
|
+
});
|
|
288
|
+
} else if (typeof collection.id !== "string") {
|
|
289
|
+
errors.push({
|
|
290
|
+
path: "id",
|
|
291
|
+
message: "Must be a string"
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!collection.name) {
|
|
296
|
+
errors.push({
|
|
297
|
+
path: "name",
|
|
298
|
+
message: "Required field is missing"
|
|
299
|
+
});
|
|
300
|
+
} else if (typeof collection.name !== "string") {
|
|
301
|
+
errors.push({
|
|
302
|
+
path: "name",
|
|
303
|
+
message: "Must be a string"
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!collection.path) {
|
|
308
|
+
errors.push({
|
|
309
|
+
path: "path",
|
|
310
|
+
message: "Required field is missing"
|
|
311
|
+
});
|
|
312
|
+
} else if (typeof collection.path !== "string") {
|
|
313
|
+
errors.push({
|
|
314
|
+
path: "path",
|
|
315
|
+
message: "Must be a string"
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Properties validation
|
|
320
|
+
if (collection.properties !== undefined) {
|
|
321
|
+
validateProperties(collection.properties, "properties", errors);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Optional fields
|
|
325
|
+
validateOptionalFields(collection, errors);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Validates a JSON string representing a collection configuration.
|
|
330
|
+
* Returns detailed validation errors if the JSON is invalid or doesn't match
|
|
331
|
+
* the expected collection schema.
|
|
332
|
+
*/
|
|
333
|
+
export function validateCollectionJson(jsonString: string): CollectionValidationResult {
|
|
334
|
+
const errors: CollectionValidationError[] = [];
|
|
335
|
+
|
|
336
|
+
// Try to parse JSON
|
|
337
|
+
let parsed: any;
|
|
338
|
+
try {
|
|
339
|
+
parsed = JSON.parse(jsonString);
|
|
340
|
+
} catch (e: any) {
|
|
341
|
+
// Try to extract line/column info from the error
|
|
342
|
+
const match = e.message.match(/position (\d+)/);
|
|
343
|
+
let message = "Invalid JSON syntax";
|
|
344
|
+
if (match) {
|
|
345
|
+
const position = parseInt(match[1], 10);
|
|
346
|
+
const lines = jsonString.substring(0, position).split("\n");
|
|
347
|
+
const line = lines.length;
|
|
348
|
+
const column = lines[lines.length - 1].length + 1;
|
|
349
|
+
message = `Invalid JSON syntax at line ${line}, column ${column}: ${e.message}`;
|
|
350
|
+
} else {
|
|
351
|
+
message = `Invalid JSON syntax: ${e.message}`;
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
valid: false,
|
|
355
|
+
errors: [{
|
|
356
|
+
path: "",
|
|
357
|
+
message
|
|
358
|
+
}]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Validate collection structure
|
|
363
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
364
|
+
return {
|
|
365
|
+
valid: false,
|
|
366
|
+
errors: [{
|
|
367
|
+
path: "",
|
|
368
|
+
message: "Collection must be an object"
|
|
369
|
+
}]
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
validateCollectionObject(parsed, errors);
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
valid: errors.length === 0,
|
|
377
|
+
errors,
|
|
378
|
+
collection: errors.length === 0 ? parsed as EntityCollection : undefined
|
|
379
|
+
};
|
|
380
|
+
}
|