@firecms/collection_editor 3.0.1 → 3.1.0-canary.9e89e98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +9418 -5587
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9413 -5582
- 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 +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 +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 +225 -0
- package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +209 -257
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +226 -167
- 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/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,335 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
EntityCollection,
|
|
4
|
+
FieldCaption,
|
|
5
|
+
IconForView,
|
|
6
|
+
SearchIconsView,
|
|
7
|
+
singular,
|
|
8
|
+
toSnakeCase,
|
|
9
|
+
useAuthController,
|
|
10
|
+
useCustomizationController
|
|
11
|
+
} from "@firecms/core";
|
|
12
|
+
import {
|
|
13
|
+
BooleanSwitchWithLabel,
|
|
14
|
+
Chip,
|
|
15
|
+
cls,
|
|
16
|
+
Container,
|
|
17
|
+
DebouncedTextField,
|
|
18
|
+
Dialog,
|
|
19
|
+
ExpandablePanel,
|
|
20
|
+
IconButton,
|
|
21
|
+
Select,
|
|
22
|
+
SelectItem,
|
|
23
|
+
TextField,
|
|
24
|
+
Tooltip,
|
|
25
|
+
Typography
|
|
26
|
+
} from "@firecms/ui";
|
|
27
|
+
|
|
28
|
+
import { Field, getIn, useFormex } from "@firecms/formex";
|
|
29
|
+
|
|
30
|
+
export function GeneralSettingsForm({
|
|
31
|
+
isNewCollection,
|
|
32
|
+
existingPaths,
|
|
33
|
+
existingIds,
|
|
34
|
+
parentCollection
|
|
35
|
+
}: {
|
|
36
|
+
isNewCollection: boolean;
|
|
37
|
+
existingPaths?: string[];
|
|
38
|
+
existingIds?: string[];
|
|
39
|
+
parentCollection?: EntityCollection;
|
|
40
|
+
}) {
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
values,
|
|
44
|
+
setFieldValue,
|
|
45
|
+
handleChange,
|
|
46
|
+
touched,
|
|
47
|
+
errors,
|
|
48
|
+
setFieldTouched,
|
|
49
|
+
submitCount
|
|
50
|
+
} = useFormex<EntityCollection>();
|
|
51
|
+
|
|
52
|
+
const [iconDialogOpen, setIconDialogOpen] = useState(false);
|
|
53
|
+
|
|
54
|
+
const authController = useAuthController();
|
|
55
|
+
const customizationController = useCustomizationController();
|
|
56
|
+
|
|
57
|
+
const updateDatabaseId = (databaseId: string) => {
|
|
58
|
+
setFieldValue("databaseId", databaseId ?? undefined);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const updateName = (name: string) => {
|
|
62
|
+
setFieldValue("name", name);
|
|
63
|
+
|
|
64
|
+
const pathTouched = getIn(touched, "path");
|
|
65
|
+
if (!pathTouched && isNewCollection && name) {
|
|
66
|
+
setFieldValue("path", toSnakeCase(name));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const idTouched = getIn(touched, "id");
|
|
70
|
+
if (!idTouched && isNewCollection && name) {
|
|
71
|
+
setFieldValue("id", toSnakeCase(name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const singularNameTouched = getIn(touched, "singularName");
|
|
75
|
+
if (!singularNameTouched && isNewCollection && name) {
|
|
76
|
+
setFieldValue("singularName", singular(name));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const collectionIcon = <IconForView collectionOrView={values} />;
|
|
81
|
+
const isSubcollection = !!parentCollection;
|
|
82
|
+
const showErrors = submitCount > 0;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className={"overflow-auto my-auto"}>
|
|
86
|
+
<Container maxWidth={"4xl"} className={"flex flex-col gap-4 p-8 m-auto"}>
|
|
87
|
+
|
|
88
|
+
<div>
|
|
89
|
+
<div className="flex flex-row gap-2 py-2 pt-3 items-center">
|
|
90
|
+
<Typography variant={!isNewCollection ? "h5" : "h4"} className={"flex-grow"}>
|
|
91
|
+
{isNewCollection ? "New collection" : `${values?.name} collection`}
|
|
92
|
+
</Typography>
|
|
93
|
+
<DefaultDatabaseField databaseId={values.databaseId}
|
|
94
|
+
onDatabaseIdUpdate={updateDatabaseId} />
|
|
95
|
+
|
|
96
|
+
<Tooltip title={"Change icon"}
|
|
97
|
+
asChild={true}>
|
|
98
|
+
<IconButton
|
|
99
|
+
shape={"square"}
|
|
100
|
+
onClick={() => setIconDialogOpen(true)}>
|
|
101
|
+
{collectionIcon}
|
|
102
|
+
</IconButton>
|
|
103
|
+
</Tooltip>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{parentCollection && <Chip colorScheme={"tealDarker"}>
|
|
107
|
+
<Typography variant={"caption"}>
|
|
108
|
+
This is a subcollection of <b>{parentCollection.name}</b>
|
|
109
|
+
</Typography>
|
|
110
|
+
</Chip>}
|
|
111
|
+
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className={"grid grid-cols-12 gap-4"}>
|
|
115
|
+
|
|
116
|
+
{/* Name */}
|
|
117
|
+
<div className={"col-span-12"}>
|
|
118
|
+
<TextField
|
|
119
|
+
value={values.name ?? ""}
|
|
120
|
+
onChange={(e: any) => updateName(e.target.value)}
|
|
121
|
+
label={"Name"}
|
|
122
|
+
autoFocus={true}
|
|
123
|
+
required
|
|
124
|
+
error={showErrors && Boolean(errors.name)} />
|
|
125
|
+
<FieldCaption error={touched.name && Boolean(errors.name)}>
|
|
126
|
+
{touched.name && Boolean(errors.name) ? errors.name : "Name of this collection, usually a plural name (e.g. Products)"}
|
|
127
|
+
</FieldCaption>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Path */}
|
|
131
|
+
<div className={cls("col-span-12")}>
|
|
132
|
+
<Field name={"path"}
|
|
133
|
+
as={DebouncedTextField}
|
|
134
|
+
label={"Path"}
|
|
135
|
+
required
|
|
136
|
+
error={showErrors && Boolean(errors.path)} />
|
|
137
|
+
|
|
138
|
+
<FieldCaption error={touched.path && Boolean(errors.path)}>
|
|
139
|
+
{touched.path && Boolean(errors.path)
|
|
140
|
+
? errors.path
|
|
141
|
+
: isSubcollection ? "Relative path to the parent (no need to include the parent path)" : "Path that this collection is stored in, in the database"}
|
|
142
|
+
</FieldCaption>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Singular Name */}
|
|
146
|
+
<div className={"col-span-12"}>
|
|
147
|
+
<TextField
|
|
148
|
+
error={showErrors && Boolean(errors.singularName)}
|
|
149
|
+
name={"singularName"}
|
|
150
|
+
aria-describedby={"singularName-helper"}
|
|
151
|
+
onChange={(e) => {
|
|
152
|
+
setFieldTouched("singularName", true);
|
|
153
|
+
return handleChange(e);
|
|
154
|
+
}}
|
|
155
|
+
value={values.singularName ?? ""}
|
|
156
|
+
label={"Singular name"} />
|
|
157
|
+
<FieldCaption error={showErrors && Boolean(errors.singularName)}>
|
|
158
|
+
{showErrors && Boolean(errors.singularName) ? errors.singularName : "Optionally define a singular name for your entities"}
|
|
159
|
+
</FieldCaption>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* Description */}
|
|
163
|
+
<div className={"col-span-12"}>
|
|
164
|
+
<TextField
|
|
165
|
+
error={showErrors && Boolean(errors.description)}
|
|
166
|
+
name="description"
|
|
167
|
+
value={values.description ?? ""}
|
|
168
|
+
onChange={handleChange}
|
|
169
|
+
multiline
|
|
170
|
+
minRows={2}
|
|
171
|
+
aria-describedby="description-helper-text"
|
|
172
|
+
label="Description"
|
|
173
|
+
/>
|
|
174
|
+
<FieldCaption error={showErrors && Boolean(errors.description)}>
|
|
175
|
+
{showErrors && Boolean(errors.description) ? errors.description : "Description of the collection, you can use markdown"}
|
|
176
|
+
</FieldCaption>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Collection ID */}
|
|
180
|
+
<div className={"col-span-12"}>
|
|
181
|
+
<Field name={"id"}
|
|
182
|
+
as={DebouncedTextField}
|
|
183
|
+
disabled={!isNewCollection}
|
|
184
|
+
label={"Collection ID"}
|
|
185
|
+
error={showErrors && Boolean(errors.id)} />
|
|
186
|
+
<FieldCaption error={touched.id && Boolean(errors.id)}>
|
|
187
|
+
{touched.id && Boolean(errors.id) ? errors.id : "This ID identifies this collection. Typically the same as the path."}
|
|
188
|
+
</FieldCaption>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Collection Group */}
|
|
192
|
+
<div className={"col-span-12"}>
|
|
193
|
+
<BooleanSwitchWithLabel
|
|
194
|
+
position={"start"}
|
|
195
|
+
size={"large"}
|
|
196
|
+
label="Collection group"
|
|
197
|
+
onValueChange={(v) => setFieldValue("collectionGroup", v)}
|
|
198
|
+
value={values.collectionGroup ?? false}
|
|
199
|
+
/>
|
|
200
|
+
<FieldCaption>
|
|
201
|
+
A collection group consists of all collections with the same path. This allows
|
|
202
|
+
you to query over multiple collections at once.
|
|
203
|
+
</FieldCaption>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{/* Advanced Settings */}
|
|
209
|
+
<ExpandablePanel
|
|
210
|
+
title={<Typography variant="subtitle2">Advanced settings</Typography>}
|
|
211
|
+
initiallyExpanded={false}
|
|
212
|
+
className="mt-4"
|
|
213
|
+
innerClassName="p-4 flex flex-col gap-4"
|
|
214
|
+
>
|
|
215
|
+
{/* History revisions */}
|
|
216
|
+
<div>
|
|
217
|
+
<BooleanSwitchWithLabel
|
|
218
|
+
position={"start"}
|
|
219
|
+
size={"large"}
|
|
220
|
+
allowIndeterminate={true}
|
|
221
|
+
label={values.history === null || values.history === undefined ? "Document history revisions enabled if enabled globally" : (
|
|
222
|
+
values.history ? "Document history revisions ENABLED" : "Document history revisions NOT enabled"
|
|
223
|
+
)}
|
|
224
|
+
onValueChange={(v) => setFieldValue("history", v)}
|
|
225
|
+
value={values.history === undefined ? null : values.history}
|
|
226
|
+
/>
|
|
227
|
+
<FieldCaption>
|
|
228
|
+
When enabled, each document in this collection will have a history of changes.
|
|
229
|
+
This is useful for auditing purposes. The data is stored in a subcollection of the document
|
|
230
|
+
in your database, called <b>__history</b>.
|
|
231
|
+
</FieldCaption>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Document ID generation */}
|
|
235
|
+
<div>
|
|
236
|
+
<Select
|
|
237
|
+
name="customId"
|
|
238
|
+
label="Document IDs generation"
|
|
239
|
+
position={"item-aligned"}
|
|
240
|
+
size={"large"}
|
|
241
|
+
fullWidth={true}
|
|
242
|
+
disabled={typeof values.customId === "object"}
|
|
243
|
+
onValueChange={(v) => {
|
|
244
|
+
if (v === "code_defined")
|
|
245
|
+
throw new Error("This should not happen");
|
|
246
|
+
if (v === "false") setFieldValue("customId", false);
|
|
247
|
+
else if (v === "true") setFieldValue("customId", true);
|
|
248
|
+
else if (v === "optional") setFieldValue("customId", "optional");
|
|
249
|
+
}}
|
|
250
|
+
value={
|
|
251
|
+
typeof values.customId === "object" ? "code_defined" :
|
|
252
|
+
values.customId === true ? "true" :
|
|
253
|
+
values.customId === "optional" ? "optional" : "false"
|
|
254
|
+
}
|
|
255
|
+
renderValue={(value: any) => {
|
|
256
|
+
if (value === "code_defined")
|
|
257
|
+
return "Code defined";
|
|
258
|
+
else if (value === "true")
|
|
259
|
+
return "Users must define an ID";
|
|
260
|
+
else if (value === "optional")
|
|
261
|
+
return "Users can define an ID, but it is not required";
|
|
262
|
+
else
|
|
263
|
+
return "Document ID is generated automatically";
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<SelectItem value={"false"}>
|
|
267
|
+
Document ID is generated automatically
|
|
268
|
+
</SelectItem>
|
|
269
|
+
<SelectItem value={"true"}>
|
|
270
|
+
Users must define an ID
|
|
271
|
+
</SelectItem>
|
|
272
|
+
<SelectItem value={"optional"}>
|
|
273
|
+
Users can define an ID, but it is not required
|
|
274
|
+
</SelectItem>
|
|
275
|
+
</Select>
|
|
276
|
+
<FieldCaption>
|
|
277
|
+
Configure how document IDs are generated when creating new entities.
|
|
278
|
+
</FieldCaption>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Text search */}
|
|
282
|
+
<div>
|
|
283
|
+
<BooleanSwitchWithLabel
|
|
284
|
+
position={"start"}
|
|
285
|
+
size={"large"}
|
|
286
|
+
label="Enable text search for this collection"
|
|
287
|
+
onValueChange={(v) => setFieldValue("textSearchEnabled", v)}
|
|
288
|
+
value={values.textSearchEnabled ?? false}
|
|
289
|
+
/>
|
|
290
|
+
<FieldCaption>
|
|
291
|
+
Allow text search for this collection. If you have not specified a text search
|
|
292
|
+
delegate, this will use the built-in local text search. This is not recommended
|
|
293
|
+
for large collections, as it may incur in performance and cost issues.
|
|
294
|
+
</FieldCaption>
|
|
295
|
+
</div>
|
|
296
|
+
</ExpandablePanel>
|
|
297
|
+
|
|
298
|
+
<div style={{ height: "52px" }} />
|
|
299
|
+
|
|
300
|
+
<Dialog
|
|
301
|
+
open={iconDialogOpen}
|
|
302
|
+
onOpenChange={setIconDialogOpen}
|
|
303
|
+
maxWidth={"xl"}
|
|
304
|
+
fullWidth
|
|
305
|
+
>
|
|
306
|
+
<div className={"p-4 overflow-auto min-h-[200px]"}>
|
|
307
|
+
<SearchIconsView selectedIcon={typeof values.icon === "string" ? values.icon : undefined}
|
|
308
|
+
onIconSelected={(icon: string) => {
|
|
309
|
+
setIconDialogOpen(false);
|
|
310
|
+
setFieldValue("icon", icon);
|
|
311
|
+
}} />
|
|
312
|
+
</div>
|
|
313
|
+
</Dialog>
|
|
314
|
+
|
|
315
|
+
</Container>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function DefaultDatabaseField({
|
|
321
|
+
databaseId,
|
|
322
|
+
onDatabaseIdUpdate
|
|
323
|
+
}: { databaseId?: string, onDatabaseIdUpdate: (databaseId: string) => void }) {
|
|
324
|
+
|
|
325
|
+
return <Tooltip title={"Database ID"}
|
|
326
|
+
side={"top"}
|
|
327
|
+
align={"start"}>
|
|
328
|
+
<TextField size={"small"}
|
|
329
|
+
invisible={true}
|
|
330
|
+
inputClassName={"text-end"}
|
|
331
|
+
value={databaseId ?? ""}
|
|
332
|
+
onChange={(e: any) => onDatabaseIdUpdate(e.target.value)}
|
|
333
|
+
placeholder={"(default)"}></TextField>
|
|
334
|
+
</Tooltip>
|
|
335
|
+
}
|
|
@@ -1,56 +1,76 @@
|
|
|
1
1
|
import { EntityCollection, isEmptyObject, useSnackbarController } from "@firecms/core";
|
|
2
|
-
import { Button, ContentCopyIcon, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@firecms/ui";
|
|
3
|
-
import React from "react";
|
|
2
|
+
import { Button, ContentCopyIcon, Dialog, DialogActions, DialogContent, DialogTitle, Tab, Tabs, Typography } from "@firecms/ui";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
4
|
import JSON5 from "json5";
|
|
5
5
|
import { Highlight, themes } from "prism-react-renderer"
|
|
6
6
|
import { camelCase } from "./utils/strings";
|
|
7
7
|
import { clone } from "@firecms/formex";
|
|
8
8
|
|
|
9
9
|
export function GetCodeDialog({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
collection,
|
|
11
|
+
onOpenChange,
|
|
12
|
+
open
|
|
13
|
+
}: { onOpenChange: (open: boolean) => void, collection: any, open: any }) {
|
|
14
14
|
|
|
15
15
|
const snackbarController = useSnackbarController();
|
|
16
|
+
const [format, setFormat] = useState<"ts" | "json">("json");
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const collectionData = collectionToCode({ ...collection });
|
|
19
|
+
|
|
20
|
+
const tsCode = collection
|
|
21
|
+
? "import { EntityCollection } from \"@firecms/core\";\n\nconst " + (collection?.name ? camelCase(collection.name) : "my") + "Collection:EntityCollection = " + JSON5.stringify(collectionData, null, "\t")
|
|
22
|
+
: "No collection selected";
|
|
23
|
+
|
|
24
|
+
const jsonCode = collection
|
|
25
|
+
? JSON.stringify(collectionData, null, 2)
|
|
19
26
|
: "No collection selected";
|
|
27
|
+
|
|
28
|
+
const code = format === "ts" ? tsCode : jsonCode;
|
|
29
|
+
|
|
20
30
|
return <Dialog open={open}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
31
|
+
onOpenChange={onOpenChange}
|
|
32
|
+
maxWidth={"4xl"}>
|
|
33
|
+
<div className="flex items-center justify-between pr-6">
|
|
34
|
+
<DialogTitle variant={"h6"}>Code for {collection.name}</DialogTitle>
|
|
35
|
+
<Tabs value={format} onValueChange={(v) => setFormat(v as "ts" | "json")}>
|
|
36
|
+
<Tab value="ts">TypeScript</Tab>
|
|
37
|
+
<Tab value="json">JSON</Tab>
|
|
38
|
+
</Tabs>
|
|
39
|
+
</div>
|
|
24
40
|
<DialogContent>
|
|
25
41
|
|
|
26
|
-
<Typography variant={"body2"} className={"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
<Typography variant={"body2"} className={"mb-4"}>
|
|
43
|
+
{format === "json"
|
|
44
|
+
? "Use this config to define the collection in JSON format."
|
|
45
|
+
: <>
|
|
46
|
+
If you want to customise the collection in code, you can add this collection code to your CMS
|
|
47
|
+
app configuration.
|
|
48
|
+
More info in the <a
|
|
49
|
+
rel="noopener noreferrer"
|
|
50
|
+
href={"https://firecms.co/docs/cloud/quickstart"}>docs</a>.
|
|
51
|
+
</>}
|
|
32
52
|
</Typography>
|
|
33
53
|
<Highlight
|
|
34
54
|
theme={themes.vsDark}
|
|
35
55
|
code={code}
|
|
36
|
-
language="typescript"
|
|
56
|
+
language={format === "ts" ? "typescript" : "json"}
|
|
37
57
|
>
|
|
38
58
|
{({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
className,
|
|
60
|
+
style,
|
|
61
|
+
tokens,
|
|
62
|
+
getLineProps,
|
|
63
|
+
getTokenProps
|
|
64
|
+
}) => (
|
|
45
65
|
<pre style={style} className={"p-4 rounded text-sm"}>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
{tokens.map((line, i) => (
|
|
67
|
+
<div key={i} {...getLineProps({ line })}>
|
|
68
|
+
{line.map((token, key) => (
|
|
69
|
+
<span key={key} {...getTokenProps({ token })} />
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</pre>
|
|
54
74
|
)}
|
|
55
75
|
</Highlight>
|
|
56
76
|
|
|
@@ -59,7 +79,6 @@ export function GetCodeDialog({
|
|
|
59
79
|
<Button
|
|
60
80
|
variant={"text"}
|
|
61
81
|
size={"small"}
|
|
62
|
-
color={"primary"}
|
|
63
82
|
onClick={(e) => {
|
|
64
83
|
e.stopPropagation();
|
|
65
84
|
e.preventDefault();
|
|
@@ -69,7 +88,7 @@ export function GetCodeDialog({
|
|
|
69
88
|
})
|
|
70
89
|
return navigator.clipboard.writeText(code);
|
|
71
90
|
}}>
|
|
72
|
-
<ContentCopyIcon size={"small"}/>
|
|
91
|
+
<ContentCopyIcon size={"small"} />
|
|
73
92
|
Copy to clipboard
|
|
74
93
|
</Button>
|
|
75
94
|
<Button onClick={() => onOpenChange(false)}>Close</Button>
|
|
@@ -134,8 +153,10 @@ function collectionToCode(collection: EntityCollection): object {
|
|
|
134
153
|
.map(([key, value]) => ({
|
|
135
154
|
[key]: propertyCleanup(value)
|
|
136
155
|
}))
|
|
137
|
-
.reduce((a, b) => ({
|
|
138
|
-
...
|
|
156
|
+
.reduce((a, b) => ({
|
|
157
|
+
...a,
|
|
158
|
+
...b
|
|
159
|
+
}), {}),
|
|
139
160
|
subcollections: (collection.subcollections ?? []).map(collectionToCode)
|
|
140
161
|
}
|
|
141
162
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React, { useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
EntityCollection,
|
|
4
|
+
FieldCaption,
|
|
5
|
+
getFieldConfig,
|
|
6
|
+
Property,
|
|
7
|
+
PropertyConfigBadge,
|
|
8
|
+
resolveCollection,
|
|
9
|
+
unslugify,
|
|
10
|
+
useAuthController,
|
|
11
|
+
useCustomizationController
|
|
12
|
+
} from "@firecms/core";
|
|
13
|
+
import {
|
|
14
|
+
CloseIcon,
|
|
15
|
+
IconButton,
|
|
16
|
+
Select,
|
|
17
|
+
SelectItem,
|
|
18
|
+
Typography
|
|
19
|
+
} from "@firecms/ui";
|
|
20
|
+
import { useFormex } from "@firecms/formex";
|
|
21
|
+
import { PropertyFormDialog } from "./PropertyEditView";
|
|
22
|
+
|
|
23
|
+
export function KanbanConfigSection({
|
|
24
|
+
className,
|
|
25
|
+
forceExpanded
|
|
26
|
+
}: {
|
|
27
|
+
className?: string;
|
|
28
|
+
forceExpanded?: boolean;
|
|
29
|
+
}) {
|
|
30
|
+
const authController = useAuthController();
|
|
31
|
+
const customizationController = useCustomizationController();
|
|
32
|
+
const { values, setFieldValue } = useFormex<EntityCollection>();
|
|
33
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
34
|
+
const [columnPropertyDialogOpen, setColumnPropertyDialogOpen] = useState(false);
|
|
35
|
+
|
|
36
|
+
// Resolve collection to get properties
|
|
37
|
+
const resolvedCollection = useMemo(() => resolveCollection({
|
|
38
|
+
collection: values,
|
|
39
|
+
path: values.path,
|
|
40
|
+
propertyConfigs: customizationController.propertyConfigs,
|
|
41
|
+
authController
|
|
42
|
+
}), [values, customizationController.propertyConfigs, authController]);
|
|
43
|
+
|
|
44
|
+
// Get enum string properties (for columnProperty)
|
|
45
|
+
const enumStringProperties = useMemo(() => {
|
|
46
|
+
const result: { key: string; label: string; property: Property }[] = [];
|
|
47
|
+
if (!resolvedCollection.properties) return result;
|
|
48
|
+
|
|
49
|
+
Object.entries(resolvedCollection.properties).forEach(([key, prop]) => {
|
|
50
|
+
if (prop && 'dataType' in prop && prop.dataType === 'string' && prop.enumValues) {
|
|
51
|
+
result.push({
|
|
52
|
+
key,
|
|
53
|
+
label: (prop as Property).name || key,
|
|
54
|
+
property: prop as Property
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
}, [resolvedCollection.properties]);
|
|
60
|
+
|
|
61
|
+
const kanbanConfig = values.kanban;
|
|
62
|
+
|
|
63
|
+
// Check if columnProperty references a non-existent property
|
|
64
|
+
const columnPropertyMissing = Boolean(kanbanConfig?.columnProperty) &&
|
|
65
|
+
!enumStringProperties.some(p => p.key === kanbanConfig?.columnProperty);
|
|
66
|
+
|
|
67
|
+
// Scroll to section when forceExpanded
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
if (forceExpanded && panelRef.current) {
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
panelRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
72
|
+
}, 100);
|
|
73
|
+
}
|
|
74
|
+
}, [forceExpanded]);
|
|
75
|
+
|
|
76
|
+
const showCreateButton = !kanbanConfig?.columnProperty || columnPropertyMissing;
|
|
77
|
+
const dialogPropertyKey = columnPropertyMissing && kanbanConfig?.columnProperty
|
|
78
|
+
? kanbanConfig.columnProperty
|
|
79
|
+
: "status";
|
|
80
|
+
const dialogPropertyName = columnPropertyMissing && kanbanConfig?.columnProperty
|
|
81
|
+
? unslugify(kanbanConfig.columnProperty)
|
|
82
|
+
: "Status";
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className={className} ref={panelRef}>
|
|
86
|
+
<Select
|
|
87
|
+
key={`column-select-${enumStringProperties.length}`}
|
|
88
|
+
name="kanban.columnProperty"
|
|
89
|
+
label="Kanban Column Property"
|
|
90
|
+
size={"large"}
|
|
91
|
+
fullWidth={true}
|
|
92
|
+
position={"item-aligned"}
|
|
93
|
+
disabled={enumStringProperties.length === 0}
|
|
94
|
+
error={columnPropertyMissing}
|
|
95
|
+
value={kanbanConfig?.columnProperty ?? ""}
|
|
96
|
+
onValueChange={(v) => {
|
|
97
|
+
if (v) {
|
|
98
|
+
setFieldValue("kanban", {
|
|
99
|
+
...kanbanConfig,
|
|
100
|
+
columnProperty: v
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
setFieldValue("kanban", undefined);
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
renderValue={(value) => {
|
|
107
|
+
if (columnPropertyMissing) {
|
|
108
|
+
return <span className="text-red-500">{value} (not found)</span>;
|
|
109
|
+
}
|
|
110
|
+
const prop = enumStringProperties.find(p => p.key === value);
|
|
111
|
+
if (!prop) return "Select a property";
|
|
112
|
+
const fieldConfig = getFieldConfig(prop.property, customizationController.propertyConfigs);
|
|
113
|
+
return (
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<PropertyConfigBadge propertyConfig={fieldConfig} />
|
|
116
|
+
<span>{prop.label}</span>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}}
|
|
120
|
+
endAdornment={kanbanConfig?.columnProperty ? (
|
|
121
|
+
<IconButton
|
|
122
|
+
size="small"
|
|
123
|
+
onClick={(e) => {
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
setFieldValue("kanban", undefined);
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<CloseIcon size="small" />
|
|
129
|
+
</IconButton>
|
|
130
|
+
) : undefined}
|
|
131
|
+
>
|
|
132
|
+
{enumStringProperties.map((prop) => {
|
|
133
|
+
const fieldConfig = getFieldConfig(prop.property, customizationController.propertyConfigs);
|
|
134
|
+
return (
|
|
135
|
+
<SelectItem key={prop.key} value={prop.key}>
|
|
136
|
+
<div className="flex items-center gap-3">
|
|
137
|
+
<PropertyConfigBadge propertyConfig={fieldConfig} />
|
|
138
|
+
<div>
|
|
139
|
+
<div>{prop.label}</div>
|
|
140
|
+
<Typography variant="caption" color="secondary">
|
|
141
|
+
{fieldConfig?.name || "Enum"}
|
|
142
|
+
</Typography>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</SelectItem>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</Select>
|
|
149
|
+
<FieldCaption error={columnPropertyMissing}>
|
|
150
|
+
{columnPropertyMissing
|
|
151
|
+
? `Property "${kanbanConfig?.columnProperty}" does not exist or is not an enum string property. Please select a valid property or clear the selection.`
|
|
152
|
+
: enumStringProperties.length === 0
|
|
153
|
+
? "No enum string properties found. Add a string property with enumValues to use Kanban view."
|
|
154
|
+
: "Select a string property with enum values to group entities into columns"
|
|
155
|
+
}
|
|
156
|
+
</FieldCaption>
|
|
157
|
+
|
|
158
|
+
{showCreateButton && (
|
|
159
|
+
<>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className="ml-3.5 text-sm text-primary hover:text-primary-dark mt-2"
|
|
163
|
+
onClick={() => setColumnPropertyDialogOpen(true)}
|
|
164
|
+
>
|
|
165
|
+
+ Create "{dialogPropertyKey}" property
|
|
166
|
+
</button>
|
|
167
|
+
<PropertyFormDialog
|
|
168
|
+
open={columnPropertyDialogOpen}
|
|
169
|
+
onCancel={() => setColumnPropertyDialogOpen(false)}
|
|
170
|
+
property={{
|
|
171
|
+
dataType: "string",
|
|
172
|
+
name: dialogPropertyName,
|
|
173
|
+
enumValues: [
|
|
174
|
+
{ id: "todo", label: "To Do" },
|
|
175
|
+
{ id: "in_progress", label: "In Progress" },
|
|
176
|
+
{ id: "done", label: "Done" }
|
|
177
|
+
]
|
|
178
|
+
}}
|
|
179
|
+
propertyKey={dialogPropertyKey}
|
|
180
|
+
existingProperty={false}
|
|
181
|
+
autoOpenTypeSelect={false}
|
|
182
|
+
autoUpdateId={false}
|
|
183
|
+
inArray={false}
|
|
184
|
+
allowDataInference={false}
|
|
185
|
+
propertyConfigs={customizationController.propertyConfigs}
|
|
186
|
+
collectionEditable={true}
|
|
187
|
+
existingPropertyKeys={Object.keys(values.properties ?? {})}
|
|
188
|
+
onPropertyChanged={({ id, property }) => {
|
|
189
|
+
const newProperties = {
|
|
190
|
+
...values.properties,
|
|
191
|
+
[id!]: property
|
|
192
|
+
};
|
|
193
|
+
const newPropertiesOrder = [...(values.propertiesOrder ?? Object.keys(values.properties ?? {})), id];
|
|
194
|
+
setFieldValue("properties", newProperties);
|
|
195
|
+
setFieldValue("propertiesOrder", newPropertiesOrder);
|
|
196
|
+
setFieldValue("kanban", {
|
|
197
|
+
...kanbanConfig,
|
|
198
|
+
columnProperty: id
|
|
199
|
+
});
|
|
200
|
+
setColumnPropertyDialogOpen(false);
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
</>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|