@firecms/collection_editor 3.0.0 → 3.1.0-canary.1df3b2c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ConfigControllerProvider.d.ts +6 -0
- package/dist/api/generateCollectionApi.d.ts +71 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.es.js +9677 -5837
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9653 -5813
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collection_editor_controller.d.ts +14 -0
- package/dist/types/collection_inference.d.ts +8 -2
- package/dist/types/config_controller.d.ts +31 -1
- package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
- package/dist/ui/KanbanSetupAction.d.ts +10 -0
- package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +33 -0
- package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
- package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
- package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +20 -0
- package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +3 -1
- package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
- package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
- package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
- package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
- package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
- package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
- package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
- package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
- package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
- package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
- package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
- package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
- package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
- package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
- package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
- package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
- package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
- package/dist/useCollectionEditorPlugin.d.ts +7 -1
- package/dist/utils/validateCollectionJson.d.ts +22 -0
- package/package.json +11 -11
- package/src/ConfigControllerProvider.tsx +81 -47
- package/src/api/generateCollectionApi.ts +119 -0
- package/src/api/index.ts +1 -0
- package/src/index.ts +28 -1
- package/src/types/collection_editor_controller.tsx +16 -3
- package/src/types/collection_inference.ts +15 -2
- package/src/types/config_controller.tsx +37 -1
- package/src/ui/AddKanbanColumnAction.tsx +203 -0
- package/src/ui/EditorCollectionActionStart.tsx +1 -2
- package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
- package/src/ui/KanbanSetupAction.tsx +38 -0
- package/src/ui/MissingReferenceWidget.tsx +1 -1
- package/src/ui/NewCollectionButton.tsx +1 -1
- package/src/ui/PropertyAddColumnComponent.tsx +1 -1
- package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +225 -0
- package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +209 -258
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +226 -173
- package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +130 -67
- package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
- package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +190 -91
- package/src/ui/collection_editor/DisplaySettingsForm.tsx +333 -0
- package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -96
- package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +6 -7
- package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +1 -3
- package/src/ui/collection_editor/EnumForm.tsx +147 -100
- package/src/ui/collection_editor/ExtendSettingsForm.tsx +93 -0
- package/src/ui/collection_editor/GeneralSettingsForm.tsx +335 -0
- package/src/ui/collection_editor/GetCodeDialog.tsx +57 -36
- package/src/ui/collection_editor/KanbanConfigSection.tsx +207 -0
- package/src/ui/collection_editor/LayoutModeSwitch.tsx +22 -41
- package/src/ui/collection_editor/PropertyEditView.tsx +205 -141
- package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
- package/src/ui/collection_editor/PropertyTree.tsx +130 -58
- package/src/ui/collection_editor/SubcollectionsEditTab.tsx +171 -162
- package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
- package/src/ui/collection_editor/ViewModeSwitch.tsx +41 -0
- package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +1 -0
- package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +117 -35
- package/src/ui/collection_editor/properties/EnumPropertyField.tsx +28 -21
- package/src/ui/collection_editor/properties/MapPropertyField.tsx +0 -2
- package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +115 -39
- package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +1 -5
- package/src/ui/collection_editor/properties/StoragePropertyField.tsx +23 -2
- package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +861 -0
- package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
- package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
- package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
- package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
- package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -1
- package/src/useCollectionEditorPlugin.tsx +32 -17
- package/src/utils/validateCollectionJson.ts +380 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useState } from "react";
|
|
2
|
+
import { CollectionOperation } from "../../api/generateCollectionApi";
|
|
3
|
+
|
|
4
|
+
export interface AIModifiedPathsContextType {
|
|
5
|
+
/** Set of paths that were modified by AI */
|
|
6
|
+
modifiedPaths: Set<string>;
|
|
7
|
+
/** Counter that increments each time AI modifies the collection - use in keys to force remount */
|
|
8
|
+
generationCounter: number;
|
|
9
|
+
/** Add paths from operations */
|
|
10
|
+
addModifiedPaths: (operations: CollectionOperation[]) => void;
|
|
11
|
+
/** Clear a specific path (when user edits that field) */
|
|
12
|
+
clearPath: (path: string) => void;
|
|
13
|
+
/** Clear all paths (on save or cancel) */
|
|
14
|
+
clearAllPaths: () => void;
|
|
15
|
+
/** Check if a path is modified */
|
|
16
|
+
isPathModified: (path: string) => boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const AIModifiedPathsContext = createContext<AIModifiedPathsContextType | null>(null);
|
|
20
|
+
|
|
21
|
+
export function AIModifiedPathsProvider({ children }: { children: React.ReactNode }) {
|
|
22
|
+
const [modifiedPaths, setModifiedPaths] = useState<Set<string>>(new Set());
|
|
23
|
+
const [generationCounter, setGenerationCounter] = useState(0);
|
|
24
|
+
|
|
25
|
+
const addModifiedPaths = useCallback((operations: CollectionOperation[]) => {
|
|
26
|
+
setModifiedPaths(prev => {
|
|
27
|
+
const newSet = new Set(prev);
|
|
28
|
+
operations.forEach(op => {
|
|
29
|
+
// Add the path and all parent paths for nested modifications
|
|
30
|
+
newSet.add(op.path);
|
|
31
|
+
// For properties modifications, also mark the property itself
|
|
32
|
+
// e.g., "properties.email.description" -> also adds "properties.email"
|
|
33
|
+
const parts = op.path.split(".");
|
|
34
|
+
if (parts[0] === "properties" && parts.length >= 2) {
|
|
35
|
+
newSet.add(`properties.${parts[1]}`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return newSet;
|
|
39
|
+
});
|
|
40
|
+
// Increment counter to force property form remount
|
|
41
|
+
setGenerationCounter(prev => prev + 1);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const clearPath = useCallback((path: string) => {
|
|
45
|
+
setModifiedPaths(prev => {
|
|
46
|
+
const newSet = new Set(prev);
|
|
47
|
+
// Remove exact path and any child paths
|
|
48
|
+
for (const p of newSet) {
|
|
49
|
+
if (p === path || p.startsWith(path + ".")) {
|
|
50
|
+
newSet.delete(p);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return newSet;
|
|
54
|
+
});
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const clearAllPaths = useCallback(() => {
|
|
58
|
+
setModifiedPaths(new Set());
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const isPathModified = useCallback((path: string): boolean => {
|
|
62
|
+
// Check if this exact path or any parent path is modified
|
|
63
|
+
if (modifiedPaths.has(path)) return true;
|
|
64
|
+
// Check if any child paths are modified
|
|
65
|
+
for (const p of modifiedPaths) {
|
|
66
|
+
if (p.startsWith(path + ".")) return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}, [modifiedPaths]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<AIModifiedPathsContext.Provider value={{
|
|
73
|
+
modifiedPaths,
|
|
74
|
+
generationCounter,
|
|
75
|
+
addModifiedPaths,
|
|
76
|
+
clearPath,
|
|
77
|
+
clearAllPaths,
|
|
78
|
+
isPathModified
|
|
79
|
+
}}>
|
|
80
|
+
{children}
|
|
81
|
+
</AIModifiedPathsContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useAIModifiedPaths(): AIModifiedPathsContextType | null {
|
|
86
|
+
return useContext(AIModifiedPathsContext);
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import { EntityCollection, FieldCaption, IconForView, SearchIconsView, singular, toSnakeCase, } from "@firecms/core";
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { EntityCollection, FieldCaption, getFieldConfig, IconForView, Property, PropertyConfigBadge, resolveCollection, SearchIconsView, singular, toSnakeCase, unslugify, useAuthController, useCustomizationController } from "@firecms/core";
|
|
3
3
|
import {
|
|
4
4
|
BooleanSwitchWithLabel,
|
|
5
5
|
Chip,
|
|
@@ -8,11 +8,9 @@ import {
|
|
|
8
8
|
Container,
|
|
9
9
|
DebouncedTextField,
|
|
10
10
|
Dialog,
|
|
11
|
-
ExpandablePanel,
|
|
12
11
|
IconButton,
|
|
13
12
|
Select,
|
|
14
13
|
SelectItem,
|
|
15
|
-
SettingsIcon,
|
|
16
14
|
TextField,
|
|
17
15
|
Tooltip,
|
|
18
16
|
Typography,
|
|
@@ -22,16 +20,19 @@ import {
|
|
|
22
20
|
import { Field, getIn, useFormex } from "@firecms/formex";
|
|
23
21
|
import { useCollectionEditorController } from "../../useCollectionEditorController";
|
|
24
22
|
import { LayoutModeSwitch } from "./LayoutModeSwitch";
|
|
23
|
+
import { ViewModeSwitch } from "./ViewModeSwitch";
|
|
24
|
+
import { KanbanConfigSection } from "./KanbanConfigSection";
|
|
25
|
+
import { PropertyFormDialog } from "./PropertyEditView";
|
|
25
26
|
|
|
26
27
|
export function CollectionDetailsForm({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
isNewCollection,
|
|
29
|
+
reservedGroups,
|
|
30
|
+
existingPaths,
|
|
31
|
+
existingIds,
|
|
32
|
+
groups,
|
|
33
|
+
parentCollection,
|
|
34
|
+
expandKanban
|
|
35
|
+
}: {
|
|
35
36
|
isNewCollection: boolean,
|
|
36
37
|
reservedGroups?: string[];
|
|
37
38
|
existingPaths?: string[];
|
|
@@ -39,7 +40,7 @@ export function CollectionDetailsForm({
|
|
|
39
40
|
groups: string[] | null;
|
|
40
41
|
parentCollection?: EntityCollection;
|
|
41
42
|
parentCollectionIds?: string[];
|
|
42
|
-
|
|
43
|
+
expandKanban?: boolean;
|
|
43
44
|
}) {
|
|
44
45
|
|
|
45
46
|
const groupRef = React.useRef<HTMLInputElement>(null);
|
|
@@ -57,7 +58,35 @@ export function CollectionDetailsForm({
|
|
|
57
58
|
const collectionEditor = useCollectionEditorController();
|
|
58
59
|
|
|
59
60
|
const [iconDialogOpen, setIconDialogOpen] = useState(false);
|
|
60
|
-
const [
|
|
61
|
+
const [orderPropertyDialogOpen, setOrderPropertyDialogOpen] = useState(false);
|
|
62
|
+
|
|
63
|
+
const authController = useAuthController();
|
|
64
|
+
const customizationController = useCustomizationController();
|
|
65
|
+
|
|
66
|
+
// Resolve collection to get properties for order property select
|
|
67
|
+
const resolvedCollection = useMemo(() => resolveCollection({
|
|
68
|
+
collection: values,
|
|
69
|
+
path: values.path,
|
|
70
|
+
propertyConfigs: customizationController.propertyConfigs,
|
|
71
|
+
authController
|
|
72
|
+
}), [values, customizationController.propertyConfigs, authController]);
|
|
73
|
+
|
|
74
|
+
// Get number properties (for orderProperty)
|
|
75
|
+
const numberProperties = useMemo(() => {
|
|
76
|
+
const result: { key: string; label: string; property: Property; }[] = [];
|
|
77
|
+
if (!resolvedCollection.properties) return result;
|
|
78
|
+
|
|
79
|
+
Object.entries(resolvedCollection.properties).forEach(([key, prop]) => {
|
|
80
|
+
if (prop && 'dataType' in prop && prop.dataType === 'number') {
|
|
81
|
+
result.push({
|
|
82
|
+
key,
|
|
83
|
+
label: (prop as Property).name || key,
|
|
84
|
+
property: prop as Property
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return result;
|
|
89
|
+
}, [resolvedCollection.properties]);
|
|
61
90
|
|
|
62
91
|
const updateDatabaseId = (databaseId: string) => {
|
|
63
92
|
setFieldValue("databaseId", databaseId ?? undefined);
|
|
@@ -83,13 +112,7 @@ export function CollectionDetailsForm({
|
|
|
83
112
|
|
|
84
113
|
};
|
|
85
114
|
|
|
86
|
-
|
|
87
|
-
if (errors.id) {
|
|
88
|
-
setAdvancedPanelExpanded(true);
|
|
89
|
-
}
|
|
90
|
-
}, [errors.id]);
|
|
91
|
-
|
|
92
|
-
const collectionIcon = <IconForView collectionOrView={values}/>;
|
|
115
|
+
const collectionIcon = <IconForView collectionOrView={values} />;
|
|
93
116
|
|
|
94
117
|
const groupOptions = groups?.filter((group) => !reservedGroups?.includes(group));
|
|
95
118
|
|
|
@@ -103,17 +126,6 @@ export function CollectionDetailsForm({
|
|
|
103
126
|
|
|
104
127
|
const isSubcollection = !!parentCollection;
|
|
105
128
|
|
|
106
|
-
let customIdValue: "true" | "false" | "optional" | "code_defined" | undefined;
|
|
107
|
-
if (typeof values.customId === "object") {
|
|
108
|
-
customIdValue = "code_defined";
|
|
109
|
-
} else if (values.customId === true) {
|
|
110
|
-
customIdValue = "true";
|
|
111
|
-
} else if (values.customId === false) {
|
|
112
|
-
customIdValue = "false";
|
|
113
|
-
} else if (values.customId === "optional") {
|
|
114
|
-
customIdValue = "optional";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
129
|
const showErrors = submitCount > 0;
|
|
118
130
|
|
|
119
131
|
return (
|
|
@@ -127,10 +139,10 @@ export function CollectionDetailsForm({
|
|
|
127
139
|
{isNewCollection ? "New collection" : `${values?.name} collection`}
|
|
128
140
|
</Typography>
|
|
129
141
|
<DefaultDatabaseField databaseId={values.databaseId}
|
|
130
|
-
|
|
142
|
+
onDatabaseIdUpdate={updateDatabaseId} />
|
|
131
143
|
|
|
132
144
|
<Tooltip title={"Change icon"}
|
|
133
|
-
|
|
145
|
+
asChild={true}>
|
|
134
146
|
<IconButton
|
|
135
147
|
shape={"square"}
|
|
136
148
|
onClick={() => setIconDialogOpen(true)}>
|
|
@@ -155,7 +167,7 @@ export function CollectionDetailsForm({
|
|
|
155
167
|
label={"Name"}
|
|
156
168
|
autoFocus={true}
|
|
157
169
|
required
|
|
158
|
-
error={showErrors && Boolean(errors.name)}/>
|
|
170
|
+
error={showErrors && Boolean(errors.name)} />
|
|
159
171
|
<FieldCaption error={touched.name && Boolean(errors.name)}>
|
|
160
172
|
{touched.name && Boolean(errors.name) ? errors.name : "Name of this collection, usually a plural name (e.g. Products)"}
|
|
161
173
|
</FieldCaption>
|
|
@@ -163,11 +175,10 @@ export function CollectionDetailsForm({
|
|
|
163
175
|
|
|
164
176
|
<div className={cls("col-span-12 ")}>
|
|
165
177
|
<Field name={"path"}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
error={showErrors && Boolean(errors.path)}/>
|
|
178
|
+
as={DebouncedTextField}
|
|
179
|
+
label={"Path"}
|
|
180
|
+
required
|
|
181
|
+
error={showErrors && Boolean(errors.path)} />
|
|
171
182
|
|
|
172
183
|
<FieldCaption error={touched.path && Boolean(errors.path)}>
|
|
173
184
|
{touched.path && Boolean(errors.path)
|
|
@@ -215,7 +226,149 @@ export function CollectionDetailsForm({
|
|
|
215
226
|
<LayoutModeSwitch
|
|
216
227
|
className={"col-span-12"}
|
|
217
228
|
value={values.openEntityMode ?? "side_panel"}
|
|
218
|
-
onChange={(value) => setFieldValue("openEntityMode", value)}/>
|
|
229
|
+
onChange={(value) => setFieldValue("openEntityMode", value)} />
|
|
230
|
+
|
|
231
|
+
<ViewModeSwitch
|
|
232
|
+
className={"col-span-12"}
|
|
233
|
+
value={values.defaultViewMode ?? "table"}
|
|
234
|
+
onChange={(value) => setFieldValue("defaultViewMode", value)} />
|
|
235
|
+
|
|
236
|
+
<KanbanConfigSection className={"col-span-12"} forceExpanded={expandKanban} />
|
|
237
|
+
|
|
238
|
+
<div className={"col-span-12 mt-4"}>
|
|
239
|
+
{(() => {
|
|
240
|
+
// Check if orderProperty references a non-existent property
|
|
241
|
+
const orderPropertyMissing = Boolean(values.orderProperty) &&
|
|
242
|
+
!numberProperties.some(p => p.key === values.orderProperty);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<>
|
|
246
|
+
<Select
|
|
247
|
+
key={`order-select-${numberProperties.length}`}
|
|
248
|
+
name="orderProperty"
|
|
249
|
+
label="Order Property"
|
|
250
|
+
size={"large"}
|
|
251
|
+
fullWidth={true}
|
|
252
|
+
position={"item-aligned"}
|
|
253
|
+
disabled={numberProperties.length === 0}
|
|
254
|
+
error={orderPropertyMissing}
|
|
255
|
+
value={values.orderProperty ?? ""}
|
|
256
|
+
onValueChange={(v) => {
|
|
257
|
+
setFieldValue("orderProperty", v || undefined);
|
|
258
|
+
}}
|
|
259
|
+
renderValue={(value) => {
|
|
260
|
+
if (orderPropertyMissing) {
|
|
261
|
+
return <span className="text-red-500">{value} (not found)</span>;
|
|
262
|
+
}
|
|
263
|
+
const prop = numberProperties.find(p => p.key === value);
|
|
264
|
+
if (!prop) return "Select a property";
|
|
265
|
+
const fieldConfig = getFieldConfig(prop.property, customizationController.propertyConfigs);
|
|
266
|
+
return (
|
|
267
|
+
<div className="flex items-center gap-2">
|
|
268
|
+
<PropertyConfigBadge propertyConfig={fieldConfig} />
|
|
269
|
+
<span>{prop.label}</span>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}}
|
|
273
|
+
endAdornment={values.orderProperty ? (
|
|
274
|
+
<IconButton
|
|
275
|
+
size="small"
|
|
276
|
+
onClick={(e) => {
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
setFieldValue("orderProperty", undefined);
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
<CloseIcon size="small" />
|
|
282
|
+
</IconButton>
|
|
283
|
+
) : undefined}
|
|
284
|
+
>
|
|
285
|
+
{numberProperties.map((prop) => {
|
|
286
|
+
const fieldConfig = getFieldConfig(prop.property, customizationController.propertyConfigs);
|
|
287
|
+
return (
|
|
288
|
+
<SelectItem key={prop.key} value={prop.key}>
|
|
289
|
+
<div className="flex items-center gap-3">
|
|
290
|
+
<PropertyConfigBadge propertyConfig={fieldConfig} />
|
|
291
|
+
<div>
|
|
292
|
+
<div>{prop.label}</div>
|
|
293
|
+
<Typography variant="caption" color="secondary">
|
|
294
|
+
{fieldConfig?.name || "Number"}
|
|
295
|
+
</Typography>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</SelectItem>
|
|
299
|
+
);
|
|
300
|
+
})}
|
|
301
|
+
</Select>
|
|
302
|
+
<FieldCaption error={orderPropertyMissing}>
|
|
303
|
+
{orderPropertyMissing
|
|
304
|
+
? `Property "${values.orderProperty}" does not exist or is not a number property. Please select a valid property or clear the selection.`
|
|
305
|
+
: numberProperties.length === 0
|
|
306
|
+
? "No number properties found. Add a number property to enable ordering."
|
|
307
|
+
: "Select a number property to persist the order of items"
|
|
308
|
+
}
|
|
309
|
+
</FieldCaption>
|
|
310
|
+
</>
|
|
311
|
+
);
|
|
312
|
+
})()}
|
|
313
|
+
{(() => {
|
|
314
|
+
// Check if orderProperty references a non-existent property
|
|
315
|
+
const orderPropertyMissing = Boolean(values.orderProperty) &&
|
|
316
|
+
!numberProperties.some(p => p.key === values.orderProperty);
|
|
317
|
+
const showCreateButton = !values.orderProperty || orderPropertyMissing;
|
|
318
|
+
|
|
319
|
+
// Pre-fill with missing property id or default "__order"
|
|
320
|
+
const dialogPropertyKey = orderPropertyMissing && values.orderProperty
|
|
321
|
+
? values.orderProperty
|
|
322
|
+
: "__order";
|
|
323
|
+
const dialogPropertyName = orderPropertyMissing && values.orderProperty
|
|
324
|
+
? unslugify(values.orderProperty)
|
|
325
|
+
: "Order";
|
|
326
|
+
|
|
327
|
+
if (!showCreateButton) return null;
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<>
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
className="ml-3.5 text-sm text-primary hover:text-primary-dark mt-2"
|
|
334
|
+
onClick={() => setOrderPropertyDialogOpen(true)}
|
|
335
|
+
>
|
|
336
|
+
+ Create "{dialogPropertyKey}" property
|
|
337
|
+
</button>
|
|
338
|
+
<PropertyFormDialog
|
|
339
|
+
open={orderPropertyDialogOpen}
|
|
340
|
+
onCancel={() => setOrderPropertyDialogOpen(false)}
|
|
341
|
+
property={{
|
|
342
|
+
dataType: "number",
|
|
343
|
+
name: dialogPropertyName,
|
|
344
|
+
disabled: true,
|
|
345
|
+
hideFromCollection: true
|
|
346
|
+
}}
|
|
347
|
+
propertyKey={dialogPropertyKey}
|
|
348
|
+
existingProperty={false}
|
|
349
|
+
autoOpenTypeSelect={false}
|
|
350
|
+
autoUpdateId={false}
|
|
351
|
+
inArray={false}
|
|
352
|
+
allowDataInference={false}
|
|
353
|
+
propertyConfigs={customizationController.propertyConfigs}
|
|
354
|
+
collectionEditable={true}
|
|
355
|
+
existingPropertyKeys={Object.keys(values.properties ?? {})}
|
|
356
|
+
onPropertyChanged={({ id, property }) => {
|
|
357
|
+
const newProperties = {
|
|
358
|
+
...values.properties,
|
|
359
|
+
[id!]: property
|
|
360
|
+
};
|
|
361
|
+
const newPropertiesOrder = [...(values.propertiesOrder ?? Object.keys(values.properties ?? {})), id];
|
|
362
|
+
setFieldValue("properties", newProperties);
|
|
363
|
+
setFieldValue("propertiesOrder", newPropertiesOrder);
|
|
364
|
+
setFieldValue("orderProperty", id);
|
|
365
|
+
setOrderPropertyDialogOpen(false);
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
</>
|
|
369
|
+
);
|
|
370
|
+
})()}
|
|
371
|
+
</div>
|
|
219
372
|
|
|
220
373
|
<div className={"col-span-12"}>
|
|
221
374
|
<BooleanSwitchWithLabel
|
|
@@ -237,214 +390,12 @@ export function CollectionDetailsForm({
|
|
|
237
390
|
|
|
238
391
|
|
|
239
392
|
<div className={"col-span-12 mt-8"}>
|
|
240
|
-
<ExpandablePanel
|
|
241
|
-
expanded={advancedPanelExpanded}
|
|
242
|
-
onExpandedChange={setAdvancedPanelExpanded}
|
|
243
|
-
title={
|
|
244
|
-
<div className="flex flex-row text-surface-500">
|
|
245
|
-
<SettingsIcon/>
|
|
246
|
-
<Typography variant={"subtitle2"}
|
|
247
|
-
className="ml-2">
|
|
248
|
-
Advanced
|
|
249
|
-
</Typography>
|
|
250
|
-
</div>}
|
|
251
|
-
initiallyExpanded={false}>
|
|
252
|
-
<div className={"grid grid-cols-12 gap-4 p-4"}>
|
|
253
|
-
|
|
254
|
-
<div className={"col-span-12"}>
|
|
255
|
-
<Field name={"id"}
|
|
256
|
-
as={DebouncedTextField}
|
|
257
|
-
disabled={!isNewCollection}
|
|
258
|
-
label={"Collection id"}
|
|
259
|
-
error={showErrors && Boolean(errors.id)}/>
|
|
260
|
-
<FieldCaption error={touched.id && Boolean(errors.id)}>
|
|
261
|
-
{touched.id && Boolean(errors.id) ? errors.id : "This id identifies this collection. Typically the same as the path."}
|
|
262
|
-
</FieldCaption>
|
|
263
|
-
</div>
|
|
264
|
-
|
|
265
|
-
<div className={"col-span-12"}>
|
|
266
|
-
<TextField
|
|
267
|
-
error={showErrors && Boolean(errors.singularName)}
|
|
268
|
-
name={"singularName"}
|
|
269
|
-
aria-describedby={"singularName-helper"}
|
|
270
|
-
onChange={(e) => {
|
|
271
|
-
setFieldTouched("singularName", true);
|
|
272
|
-
return handleChange(e);
|
|
273
|
-
}}
|
|
274
|
-
value={values.singularName ?? ""}
|
|
275
|
-
label={"Singular name"}/>
|
|
276
|
-
<FieldCaption error={showErrors && Boolean(errors.singularName)}>
|
|
277
|
-
{showErrors && Boolean(errors.singularName) ? errors.singularName : "Optionally define a singular name for your entities"}
|
|
278
|
-
</FieldCaption>
|
|
279
|
-
</div>
|
|
280
|
-
<div className={"col-span-12"}>
|
|
281
|
-
<TextField
|
|
282
|
-
error={showErrors && Boolean(errors.sideDialogWidth)}
|
|
283
|
-
name={"sideDialogWidth"}
|
|
284
|
-
type={"number"}
|
|
285
|
-
aria-describedby={"sideDialogWidth-helper"}
|
|
286
|
-
onChange={(e) => {
|
|
287
|
-
setFieldTouched("sideDialogWidth", true);
|
|
288
|
-
const value = e.target.value;
|
|
289
|
-
if (!value) {
|
|
290
|
-
setFieldValue("sideDialogWidth", null);
|
|
291
|
-
} else if (!isNaN(Number(value))) {
|
|
292
|
-
setFieldValue("sideDialogWidth", Number(value));
|
|
293
|
-
}
|
|
294
|
-
}}
|
|
295
|
-
endAdornment={<IconButton
|
|
296
|
-
size={"small"}
|
|
297
|
-
onClick={() => {
|
|
298
|
-
setFieldValue("sideDialogWidth", null);
|
|
299
|
-
}}
|
|
300
|
-
disabled={!values.sideDialogWidth}>
|
|
301
|
-
<CloseIcon size={"small"}/>
|
|
302
|
-
</IconButton>}
|
|
303
|
-
value={values.sideDialogWidth ?? ""}
|
|
304
|
-
label={"Side dialog width"}/>
|
|
305
|
-
<FieldCaption error={showErrors && Boolean(errors.singularName)}>
|
|
306
|
-
{showErrors && Boolean(errors.singularName) ? errors.singularName : "Optionally define the width (in pixels) of entities side dialog. Default is 768px"}
|
|
307
|
-
</FieldCaption>
|
|
308
|
-
</div>
|
|
309
|
-
<div className={"col-span-12"}>
|
|
310
|
-
<TextField
|
|
311
|
-
error={showErrors && Boolean(errors.description)}
|
|
312
|
-
name="description"
|
|
313
|
-
value={values.description ?? ""}
|
|
314
|
-
onChange={handleChange}
|
|
315
|
-
multiline
|
|
316
|
-
minRows={2}
|
|
317
|
-
aria-describedby="description-helper-text"
|
|
318
|
-
label="Description"
|
|
319
|
-
/>
|
|
320
|
-
<FieldCaption error={showErrors && Boolean(errors.description)}>
|
|
321
|
-
{showErrors && Boolean(errors.description) ? errors.description : "Description of the collection, you can use markdown"}
|
|
322
|
-
</FieldCaption>
|
|
323
|
-
</div>
|
|
324
|
-
|
|
325
|
-
<div className={"col-span-12"}>
|
|
326
|
-
<Select
|
|
327
|
-
name="defaultSize"
|
|
328
|
-
size={"large"}
|
|
329
|
-
fullWidth={true}
|
|
330
|
-
label="Default row size"
|
|
331
|
-
position={"item-aligned"}
|
|
332
|
-
onChange={handleChange}
|
|
333
|
-
value={values.defaultSize ?? ""}
|
|
334
|
-
renderValue={(value: any) => value.toUpperCase()}
|
|
335
|
-
>
|
|
336
|
-
{["xs", "s", "m", "l", "xl"].map((value) => (
|
|
337
|
-
<SelectItem
|
|
338
|
-
key={`size-select-${value}`}
|
|
339
|
-
value={value}>
|
|
340
|
-
{value.toUpperCase()}
|
|
341
|
-
</SelectItem>
|
|
342
|
-
))}
|
|
343
|
-
</Select>
|
|
344
|
-
</div>
|
|
345
|
-
|
|
346
|
-
<div className={"col-span-12"}>
|
|
347
|
-
<BooleanSwitchWithLabel
|
|
348
|
-
position={"start"}
|
|
349
|
-
size={"large"}
|
|
350
|
-
label={values.includeJsonView === undefined || values.includeJsonView ? "Include JSON view" : "Do not include JSON view"}
|
|
351
|
-
onValueChange={(v) => setFieldValue("includeJsonView", v)}
|
|
352
|
-
value={values.includeJsonView === undefined ? true : values.includeJsonView}
|
|
353
|
-
/>
|
|
354
|
-
<FieldCaption>
|
|
355
|
-
Include the JSON representation of the document.
|
|
356
|
-
</FieldCaption>
|
|
357
|
-
</div>
|
|
358
|
-
|
|
359
|
-
<div className={"col-span-12"}>
|
|
360
|
-
<BooleanSwitchWithLabel
|
|
361
|
-
position={"start"}
|
|
362
|
-
size={"large"}
|
|
363
|
-
label={values.inlineEditing === undefined || values.inlineEditing ? "Data can be edited directly in the table view" : "Data can be edited only in the form view"}
|
|
364
|
-
onValueChange={(v) => setFieldValue("inlineEditing", v)}
|
|
365
|
-
value={values.inlineEditing === undefined ? true : values.inlineEditing}
|
|
366
|
-
/>
|
|
367
|
-
<FieldCaption>
|
|
368
|
-
Allow editing data directly in the table view, without opening the form view.
|
|
369
|
-
</FieldCaption>
|
|
370
|
-
</div>
|
|
371
|
-
|
|
372
|
-
<div className={"col-span-12"}>
|
|
373
|
-
<Select
|
|
374
|
-
name="customId"
|
|
375
|
-
label="Document IDs generation"
|
|
376
|
-
position={"item-aligned"}
|
|
377
|
-
size={"large"}
|
|
378
|
-
fullWidth={true}
|
|
379
|
-
disabled={customIdValue === "code_defined"}
|
|
380
|
-
onValueChange={(v) => {
|
|
381
|
-
if (v === "code_defined")
|
|
382
|
-
throw new Error("This should not happen");
|
|
383
|
-
setFieldValue("customId", v);
|
|
384
|
-
}}
|
|
385
|
-
value={customIdValue ?? ""}
|
|
386
|
-
renderValue={(value: any) => {
|
|
387
|
-
if (value === "code_defined")
|
|
388
|
-
return "Code defined";
|
|
389
|
-
else if (value === "true")
|
|
390
|
-
return "Users must define an ID";
|
|
391
|
-
else if (value === "optional")
|
|
392
|
-
return "Users can define an ID, but it is not required";
|
|
393
|
-
else
|
|
394
|
-
return "Document ID is generated automatically";
|
|
395
|
-
}}
|
|
396
|
-
>
|
|
397
|
-
<SelectItem value={"false"}>
|
|
398
|
-
Document ID is generated automatically
|
|
399
|
-
</SelectItem>
|
|
400
|
-
<SelectItem value={"true"}>
|
|
401
|
-
Users must define an ID
|
|
402
|
-
</SelectItem>
|
|
403
|
-
<SelectItem value={"optional"}>
|
|
404
|
-
Users can define an ID, but it is not required
|
|
405
|
-
</SelectItem>
|
|
406
|
-
</Select>
|
|
407
|
-
</div>
|
|
408
|
-
<div className={"col-span-12 mt-4"}>
|
|
409
|
-
<BooleanSwitchWithLabel
|
|
410
|
-
position={"start"}
|
|
411
|
-
size={"large"}
|
|
412
|
-
label="Collection group"
|
|
413
|
-
onValueChange={(v) => setFieldValue("collectionGroup", v)}
|
|
414
|
-
value={values.collectionGroup ?? false}
|
|
415
|
-
/>
|
|
416
|
-
<FieldCaption>
|
|
417
|
-
A collection group consists of all collections with the same path. This allows
|
|
418
|
-
you
|
|
419
|
-
to query over multiple collections at once.
|
|
420
|
-
</FieldCaption>
|
|
421
|
-
</div>
|
|
422
|
-
<div className={"col-span-12"}>
|
|
423
|
-
<BooleanSwitchWithLabel
|
|
424
|
-
position={"start"}
|
|
425
|
-
size={"large"}
|
|
426
|
-
label="Enable text search for this collection"
|
|
427
|
-
onValueChange={(v) => setFieldValue("textSearchEnabled", v)}
|
|
428
|
-
value={values.textSearchEnabled ?? false}
|
|
429
|
-
/>
|
|
430
|
-
<FieldCaption>
|
|
431
|
-
Allow text search for this collection. If you have not specified a text search
|
|
432
|
-
delegate, this will use the built-in local text search. This is not recommended
|
|
433
|
-
for large collections, as it may incur in performance and cost issues.
|
|
434
|
-
</FieldCaption>
|
|
435
|
-
</div>
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
</div>
|
|
439
|
-
</ExpandablePanel>
|
|
440
|
-
|
|
441
|
-
{children}
|
|
442
393
|
|
|
443
394
|
</div>
|
|
444
395
|
|
|
445
396
|
</div>
|
|
446
397
|
|
|
447
|
-
<div style={{ height: "52px" }}/>
|
|
398
|
+
<div style={{ height: "52px" }} />
|
|
448
399
|
|
|
449
400
|
<Dialog
|
|
450
401
|
open={iconDialogOpen}
|
|
@@ -454,10 +405,10 @@ export function CollectionDetailsForm({
|
|
|
454
405
|
>
|
|
455
406
|
<div className={"p-4 overflow-auto min-h-[200px]"}>
|
|
456
407
|
<SearchIconsView selectedIcon={typeof values.icon === "string" ? values.icon : undefined}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
408
|
+
onIconSelected={(icon: string) => {
|
|
409
|
+
setIconDialogOpen(false);
|
|
410
|
+
setFieldValue("icon", icon);
|
|
411
|
+
}} />
|
|
461
412
|
</div>
|
|
462
413
|
|
|
463
414
|
</Dialog>
|
|
@@ -468,18 +419,18 @@ export function CollectionDetailsForm({
|
|
|
468
419
|
}
|
|
469
420
|
|
|
470
421
|
function DefaultDatabaseField({
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
422
|
+
databaseId,
|
|
423
|
+
onDatabaseIdUpdate
|
|
424
|
+
}: { databaseId?: string, onDatabaseIdUpdate: (databaseId: string) => void }) {
|
|
474
425
|
|
|
475
426
|
return <Tooltip title={"Database ID"}
|
|
476
|
-
|
|
477
|
-
|
|
427
|
+
side={"top"}
|
|
428
|
+
align={"start"}>
|
|
478
429
|
<TextField size={"small"}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
430
|
+
invisible={true}
|
|
431
|
+
inputClassName={"text-end"}
|
|
432
|
+
value={databaseId ?? ""}
|
|
433
|
+
onChange={(e: any) => onDatabaseIdUpdate(e.target.value)}
|
|
434
|
+
placeholder={"(default)"}></TextField>
|
|
484
435
|
</Tooltip>
|
|
485
436
|
}
|