@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.
Files changed (90) hide show
  1. package/dist/ConfigControllerProvider.d.ts +6 -0
  2. package/dist/api/generateCollectionApi.d.ts +71 -0
  3. package/dist/api/index.d.ts +1 -0
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.es.js +9418 -5587
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +9413 -5582
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/types/collection_editor_controller.d.ts +14 -0
  10. package/dist/types/collection_inference.d.ts +8 -2
  11. package/dist/types/config_controller.d.ts +23 -2
  12. package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
  13. package/dist/ui/KanbanSetupAction.d.ts +10 -0
  14. package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +33 -0
  15. package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
  16. package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
  17. package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +20 -0
  18. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +3 -1
  19. package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
  20. package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
  21. package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
  22. package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
  23. package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
  24. package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
  25. package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
  26. package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
  27. package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
  28. package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
  29. package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
  30. package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
  31. package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
  32. package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
  33. package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
  34. package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
  35. package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
  36. package/dist/useCollectionEditorPlugin.d.ts +7 -1
  37. package/dist/utils/validateCollectionJson.d.ts +22 -0
  38. package/package.json +11 -11
  39. package/src/ConfigControllerProvider.tsx +81 -47
  40. package/src/api/generateCollectionApi.ts +119 -0
  41. package/src/api/index.ts +1 -0
  42. package/src/index.ts +28 -1
  43. package/src/types/collection_editor_controller.tsx +16 -3
  44. package/src/types/collection_inference.ts +15 -2
  45. package/src/types/config_controller.tsx +27 -2
  46. package/src/ui/AddKanbanColumnAction.tsx +203 -0
  47. package/src/ui/EditorCollectionActionStart.tsx +1 -2
  48. package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
  49. package/src/ui/KanbanSetupAction.tsx +38 -0
  50. package/src/ui/MissingReferenceWidget.tsx +1 -1
  51. package/src/ui/NewCollectionButton.tsx +1 -1
  52. package/src/ui/PropertyAddColumnComponent.tsx +1 -1
  53. package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +225 -0
  54. package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
  55. package/src/ui/collection_editor/CollectionDetailsForm.tsx +209 -257
  56. package/src/ui/collection_editor/CollectionEditorDialog.tsx +226 -167
  57. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +130 -67
  58. package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
  59. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +190 -91
  60. package/src/ui/collection_editor/DisplaySettingsForm.tsx +333 -0
  61. package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -96
  62. package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +6 -7
  63. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +1 -3
  64. package/src/ui/collection_editor/EnumForm.tsx +147 -100
  65. package/src/ui/collection_editor/ExtendSettingsForm.tsx +93 -0
  66. package/src/ui/collection_editor/GeneralSettingsForm.tsx +335 -0
  67. package/src/ui/collection_editor/GetCodeDialog.tsx +57 -36
  68. package/src/ui/collection_editor/KanbanConfigSection.tsx +207 -0
  69. package/src/ui/collection_editor/LayoutModeSwitch.tsx +22 -41
  70. package/src/ui/collection_editor/PropertyEditView.tsx +205 -141
  71. package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
  72. package/src/ui/collection_editor/PropertyTree.tsx +130 -58
  73. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +171 -162
  74. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
  75. package/src/ui/collection_editor/ViewModeSwitch.tsx +41 -0
  76. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
  77. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +1 -0
  78. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +117 -35
  79. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +28 -21
  80. package/src/ui/collection_editor/properties/MapPropertyField.tsx +0 -2
  81. package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +115 -39
  82. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +1 -1
  83. package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +861 -0
  84. package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
  85. package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
  86. package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
  87. package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
  88. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +1 -1
  89. package/src/useCollectionEditorPlugin.tsx +32 -17
  90. 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
- collection,
11
- onOpenChange,
12
- open
13
- }: { onOpenChange: (open: boolean) => void, collection: any, open: any }) {
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 code = collection
18
- ? "import { EntityCollection } from \"@firecms/core\";\n\nconst " + (collection?.name ? camelCase(collection.name) : "my") + "Collection:EntityCollection = " + JSON5.stringify(collectionToCode({ ...collection }), null, "\t")
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
- onOpenChange={onOpenChange}
22
- maxWidth={"4xl"}>
23
- <DialogTitle variant={"h6"}>Code for {collection.name}</DialogTitle>
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={"my-4 mb-8"}>
27
- If you want to customise the collection in code, you can add this collection code to your CMS
28
- app configuration.
29
- More info in the <a
30
- rel="noopener noreferrer"
31
- href={"https://firecms.co/docs/cloud/quickstart"}>docs</a>.
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
- className,
40
- style,
41
- tokens,
42
- getLineProps,
43
- getTokenProps
44
- }) => (
59
+ className,
60
+ style,
61
+ tokens,
62
+ getLineProps,
63
+ getTokenProps
64
+ }) => (
45
65
  <pre style={style} className={"p-4 rounded text-sm"}>
46
- {tokens.map((line, i) => (
47
- <div key={i} {...getLineProps({ line })}>
48
- {line.map((token, key) => (
49
- <span key={key} {...getTokenProps({ token })} />
50
- ))}
51
- </div>
52
- ))}
53
- </pre>
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) => ({ ...a,
138
- ...b }), {}),
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
+ }