@firecms/collection_editor 3.0.0 → 3.1.0-canary.02232f4

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 (119) 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 +15234 -8138
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +15199 -8103
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/locales/de.d.ts +120 -0
  10. package/dist/locales/en.d.ts +120 -0
  11. package/dist/locales/es.d.ts +120 -0
  12. package/dist/locales/fr.d.ts +120 -0
  13. package/dist/locales/hi.d.ts +120 -0
  14. package/dist/locales/it.d.ts +120 -0
  15. package/dist/locales/pt.d.ts +120 -0
  16. package/dist/types/collection_editor_controller.d.ts +14 -0
  17. package/dist/types/collection_inference.d.ts +8 -2
  18. package/dist/types/config_controller.d.ts +31 -1
  19. package/dist/ui/AddKanbanColumnAction.d.ts +11 -0
  20. package/dist/ui/KanbanSetupAction.d.ts +10 -0
  21. package/dist/ui/collection_editor/AICollectionGeneratorPopover.d.ts +37 -0
  22. package/dist/ui/collection_editor/AIModifiedPathsContext.d.ts +20 -0
  23. package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +2 -3
  24. package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +24 -0
  25. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +4 -1
  26. package/dist/ui/collection_editor/CollectionJsonImportDialog.d.ts +7 -0
  27. package/dist/ui/collection_editor/CollectionYupValidation.d.ts +9 -13
  28. package/dist/ui/collection_editor/DisplaySettingsForm.d.ts +3 -0
  29. package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +2 -1
  30. package/dist/ui/collection_editor/ExtendSettingsForm.d.ts +14 -0
  31. package/dist/ui/collection_editor/GeneralSettingsForm.d.ts +7 -0
  32. package/dist/ui/collection_editor/KanbanConfigSection.d.ts +4 -0
  33. package/dist/ui/collection_editor/PropertyEditView.d.ts +6 -1
  34. package/dist/ui/collection_editor/PropertyTree.d.ts +2 -1
  35. package/dist/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -1
  36. package/dist/ui/collection_editor/ViewModeSwitch.d.ts +6 -0
  37. package/dist/ui/collection_editor/properties/EnumPropertyField.d.ts +2 -1
  38. package/dist/ui/collection_editor/properties/conditions/ConditionsEditor.d.ts +10 -0
  39. package/dist/ui/collection_editor/properties/conditions/ConditionsPanel.d.ts +2 -0
  40. package/dist/ui/collection_editor/properties/conditions/EnumConditionsEditor.d.ts +6 -0
  41. package/dist/ui/collection_editor/properties/conditions/index.d.ts +6 -0
  42. package/dist/ui/collection_editor/properties/conditions/property_paths.d.ts +19 -0
  43. package/dist/useCollectionEditorPlugin.d.ts +7 -1
  44. package/dist/utils/validateCollectionJson.d.ts +22 -0
  45. package/package.json +15 -15
  46. package/src/ConfigControllerProvider.tsx +82 -47
  47. package/src/api/generateCollectionApi.ts +119 -0
  48. package/src/api/index.ts +1 -0
  49. package/src/index.ts +28 -1
  50. package/src/locales/de.ts +125 -0
  51. package/src/locales/en.ts +145 -0
  52. package/src/locales/es.ts +125 -0
  53. package/src/locales/fr.ts +125 -0
  54. package/src/locales/hi.ts +125 -0
  55. package/src/locales/it.ts +125 -0
  56. package/src/locales/pt.ts +125 -0
  57. package/src/types/collection_editor_controller.tsx +16 -3
  58. package/src/types/collection_inference.ts +15 -2
  59. package/src/types/config_controller.tsx +37 -1
  60. package/src/ui/AddKanbanColumnAction.tsx +203 -0
  61. package/src/ui/EditorCollectionAction.tsx +3 -3
  62. package/src/ui/EditorCollectionActionStart.tsx +1 -2
  63. package/src/ui/EditorEntityAction.tsx +3 -2
  64. package/src/ui/HomePageEditorCollectionAction.tsx +41 -13
  65. package/src/ui/KanbanSetupAction.tsx +38 -0
  66. package/src/ui/MissingReferenceWidget.tsx +1 -1
  67. package/src/ui/NewCollectionButton.tsx +4 -2
  68. package/src/ui/NewCollectionCard.tsx +7 -4
  69. package/src/ui/PropertyAddColumnComponent.tsx +4 -3
  70. package/src/ui/collection_editor/AICollectionGeneratorPopover.tsx +243 -0
  71. package/src/ui/collection_editor/AIModifiedPathsContext.tsx +88 -0
  72. package/src/ui/collection_editor/CollectionDetailsForm.tsx +222 -268
  73. package/src/ui/collection_editor/CollectionEditorDialog.tsx +270 -204
  74. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +138 -71
  75. package/src/ui/collection_editor/CollectionJsonImportDialog.tsx +171 -0
  76. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +202 -101
  77. package/src/ui/collection_editor/DisplaySettingsForm.tsx +335 -0
  78. package/src/ui/collection_editor/EntityActionsEditTab.tsx +106 -97
  79. package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +8 -10
  80. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +5 -7
  81. package/src/ui/collection_editor/EnumForm.tsx +153 -102
  82. package/src/ui/collection_editor/ExtendSettingsForm.tsx +94 -0
  83. package/src/ui/collection_editor/GeneralSettingsForm.tsx +335 -0
  84. package/src/ui/collection_editor/GetCodeDialog.tsx +63 -41
  85. package/src/ui/collection_editor/KanbanConfigSection.tsx +209 -0
  86. package/src/ui/collection_editor/LayoutModeSwitch.tsx +27 -43
  87. package/src/ui/collection_editor/PropertyEditView.tsx +272 -199
  88. package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -1
  89. package/src/ui/collection_editor/PropertyTree.tsx +130 -58
  90. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +169 -163
  91. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +0 -2
  92. package/src/ui/collection_editor/ViewModeSwitch.tsx +43 -0
  93. package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +6 -3
  94. package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +5 -2
  95. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +0 -2
  96. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +4 -1
  97. package/src/ui/collection_editor/properties/CommonPropertyFields.tsx +6 -4
  98. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +126 -42
  99. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +32 -24
  100. package/src/ui/collection_editor/properties/MapPropertyField.tsx +8 -9
  101. package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +128 -53
  102. package/src/ui/collection_editor/properties/NumberPropertyField.tsx +3 -1
  103. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +6 -9
  104. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +65 -49
  105. package/src/ui/collection_editor/properties/StringPropertyField.tsx +3 -1
  106. package/src/ui/collection_editor/properties/UrlPropertyField.tsx +12 -10
  107. package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +23 -4
  108. package/src/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +866 -0
  109. package/src/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +28 -0
  110. package/src/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +599 -0
  111. package/src/ui/collection_editor/properties/conditions/index.ts +6 -0
  112. package/src/ui/collection_editor/properties/conditions/property_paths.ts +92 -0
  113. package/src/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +5 -2
  114. package/src/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +7 -5
  115. package/src/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +10 -7
  116. package/src/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +11 -9
  117. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +5 -2
  118. package/src/useCollectionEditorPlugin.tsx +53 -22
  119. package/src/utils/validateCollectionJson.ts +380 -0
@@ -22,7 +22,8 @@ import {
22
22
  useCustomizationController,
23
23
  useNavigationController,
24
24
  User,
25
- useSnackbarController
25
+ useSnackbarController,
26
+ useTranslation,
26
27
  } from "@firecms/core";
27
28
  import {
28
29
  ArrowBackIcon,
@@ -42,10 +43,11 @@ import {
42
43
  Typography
43
44
  } from "@firecms/ui";
44
45
  import { YupSchema } from "./CollectionYupValidation";
45
- import { CollectionDetailsForm } from "./CollectionDetailsForm";
46
+ import { GeneralSettingsForm } from "./GeneralSettingsForm";
47
+ import { DisplaySettingsForm } from "./DisplaySettingsForm";
46
48
  import { CollectionPropertiesEditorForm } from "./CollectionPropertiesEditorForm";
47
49
  import { UnsavedChangesDialog } from "./UnsavedChangesDialog";
48
- import { SubcollectionsEditTab } from "./SubcollectionsEditTab";
50
+ import { ExtendSettingsForm } from "./ExtendSettingsForm";
49
51
  import { CollectionsConfigController } from "../../types/config_controller";
50
52
  import { CollectionEditorWelcomeView } from "./CollectionEditorWelcomeView";
51
53
  import { CollectionInference } from "../../types/collection_inference";
@@ -57,7 +59,9 @@ import { cleanPropertiesFromImport } from "./import/clean_import_data";
57
59
  import { PersistedCollection } from "../../types/persisted_collection";
58
60
  import { Formex, FormexController, useCreateFormex } from "@firecms/formex";
59
61
  import { getFullIdPath } from "./util";
60
- import { EntityActionsEditTab } from "./EntityActionsEditTab";
62
+ import { AICollectionGeneratorPopover } from "./AICollectionGeneratorPopover";
63
+ import { AIModifiedPathsProvider, useAIModifiedPaths } from "./AIModifiedPathsContext";
64
+ import { CollectionOperation, CollectionGenerationCallback } from "../../api/generateCollectionApi";
61
65
 
62
66
  export interface CollectionEditorDialogProps {
63
67
  open: boolean;
@@ -67,6 +71,11 @@ export interface CollectionEditorDialogProps {
67
71
  path?: string,
68
72
  name?: string,
69
73
  }
74
+ /**
75
+ * A collection to duplicate from. If provided, the new collection will be
76
+ * pre-populated with the same properties (but with empty name, path, and id).
77
+ */
78
+ copyFrom?: PersistedCollection;
70
79
  editedCollectionId?: string;
71
80
  fullPath?: string; // full path of this particular collection, like `products/123/locales`
72
81
  parentCollectionIds?: string[]; // path ids of the parent collection, like [`products`]
@@ -84,9 +93,29 @@ export interface CollectionEditorDialogProps {
84
93
  getData?: (path: string, parentPaths: string[]) => Promise<object[]>;
85
94
  parentCollection?: PersistedCollection;
86
95
  existingEntities?: Entity<any>[];
96
+ /**
97
+ * Initial view to open when editing: "general", "display", or "properties".
98
+ * For new collections, this is ignored.
99
+ */
100
+ initialView?: "general" | "display" | "properties";
101
+ /**
102
+ * If true, auto-expand the Kanban configuration section.
103
+ */
104
+ expandKanban?: boolean;
105
+ /**
106
+ * Callback function for generating/modifying collections.
107
+ * The plugin is API-agnostic - the consumer provides the implementation.
108
+ */
109
+ generateCollection?: CollectionGenerationCallback;
110
+ /**
111
+ * Optional analytics callback
112
+ */
113
+ onAnalyticsEvent?: (event: string, params?: object) => void;
87
114
  }
88
115
 
89
116
  export function CollectionEditorDialog(props: CollectionEditorDialogProps) {
117
+ const { t } = useTranslation();
118
+
90
119
 
91
120
  const open = props.open;
92
121
 
@@ -117,31 +146,32 @@ export function CollectionEditorDialog(props: CollectionEditorDialogProps) {
117
146
  maxWidth={"7xl"}
118
147
  onOpenChange={(open) => !open ? handleCancel() : undefined}
119
148
  >
120
- <DialogTitle hidden>Collection editor</DialogTitle>
121
- {open && <CollectionEditor {...props}
122
- handleCancel={handleCancel}
123
- setFormDirty={setFormDirty}/>}
124
-
125
- <UnsavedChangesDialog
126
- open={unsavedChangesDialogOpen}
127
- handleOk={() => props.handleClose(undefined)}
128
- handleCancel={() => setUnsavedChangesDialogOpen(false)}
129
- body={"There are unsaved changes in this collection"}/>
130
-
149
+ <DialogTitle hidden>{t("collection_editor")}</DialogTitle>
150
+ <AIModifiedPathsProvider>
151
+ {open && <CollectionEditor {...props}
152
+ handleCancel={handleCancel}
153
+ setFormDirty={setFormDirty} />}
154
+
155
+ <UnsavedChangesDialog
156
+ open={unsavedChangesDialogOpen}
157
+ handleOk={() => props.handleClose(undefined)}
158
+ handleCancel={() => setUnsavedChangesDialogOpen(false)}
159
+ body={t("unsaved_changes_in_collection")} />
160
+ </AIModifiedPathsProvider>
131
161
  </Dialog>
132
162
  );
133
163
  }
134
164
 
135
165
  type EditorView = "welcome"
136
- | "details"
166
+ | "general"
167
+ | "display"
137
168
  | "import_data_mapping"
138
169
  | "import_data_preview"
139
170
  | "import_data_saving"
140
171
  | "properties"
141
172
  | "loading"
142
173
  | "extra_view"
143
- | "subcollections"
144
- | "custom_actions";
174
+ | "extend";
145
175
 
146
176
  export function CollectionEditor(props: CollectionEditorDialogProps & {
147
177
  handleCancel: () => void,
@@ -157,7 +187,9 @@ export function CollectionEditor(props: CollectionEditorDialogProps & {
157
187
  } = navigation;
158
188
 
159
189
  const initialValuesProp = props.initialValues;
160
- const includeTemplates = !initialValuesProp?.path && (props.parentCollectionIds ?? []).length === 0;
190
+ const copyFromProp = props.copyFrom;
191
+ // Skip templates when duplicating (copyFrom is provided)
192
+ const includeTemplates = !copyFromProp && !initialValuesProp?.path && (props.parentCollectionIds ?? []).length === 0;
161
193
  const collectionsInThisLevel = (props.parentCollection ? props.parentCollection.subcollections : collections) ?? [];
162
194
  const existingPaths = collectionsInThisLevel.map(col => col.path.trim().toLowerCase());
163
195
  const existingIds = collectionsInThisLevel.map(col => col.id?.trim().toLowerCase()).filter(Boolean) as string[];
@@ -194,25 +226,36 @@ export function CollectionEditor(props: CollectionEditorDialogProps & {
194
226
  }
195
227
  : undefined;
196
228
 
229
+ // Build initial values - handle copyFrom for duplication
197
230
  const initialValues: PersistedCollection<any> = initialCollection
198
231
  ? applyPropertyConfigs(initialCollection, propertyConfigs)
199
- : {
200
- id: initialValuesProp?.path ?? randomString(16),
201
- path: initialValuesProp?.path ?? "",
202
- name: initialValuesProp?.name ?? "",
203
- group: initialValuesProp?.group ?? "",
204
- properties: {} as PropertiesOrBuilders,
205
- propertiesOrder: [],
206
- icon: coolIconKeys[Math.floor(Math.random() * coolIconKeys.length)],
207
- ownerId: authController.user?.uid ?? ""
208
- };
232
+ : copyFromProp
233
+ ? {
234
+ // When duplicating, copy all properties but clear identifiers
235
+ ...copyFromProp,
236
+ id: randomString(16),
237
+ path: "",
238
+ name: "",
239
+ subcollections: undefined, // Don't copy subcollections
240
+ ownerId: authController.user?.uid ?? ""
241
+ }
242
+ : {
243
+ id: initialValuesProp?.path ?? randomString(16),
244
+ path: initialValuesProp?.path ?? "",
245
+ name: initialValuesProp?.name ?? "",
246
+ group: initialValuesProp?.group ?? "",
247
+ properties: {} as PropertiesOrBuilders,
248
+ propertiesOrder: [],
249
+ icon: coolIconKeys[Math.floor(Math.random() * coolIconKeys.length)],
250
+ ownerId: authController.user?.uid ?? ""
251
+ };
209
252
 
210
253
  if (!initialLoadingCompleted) {
211
- return <CircularProgressCenter/>;
254
+ return <CircularProgressCenter />;
212
255
  }
213
256
 
214
257
  if (!props.isNewCollection && (!navigation.initialised || !initialLoadingCompleted)) {
215
- return <CircularProgressCenter/>;
258
+ return <CircularProgressCenter />;
216
259
  }
217
260
 
218
261
  return <CollectionEditorInternal
@@ -224,48 +267,53 @@ export function CollectionEditor(props: CollectionEditorDialogProps & {
224
267
  collection={collection}
225
268
  setCollection={setCollection}
226
269
  groups={groups}
227
- propertyConfigs={propertyConfigs}/>
270
+ propertyConfigs={propertyConfigs} />
228
271
 
229
272
  }
230
273
 
231
274
  function CollectionEditorInternal<M extends Record<string, any>>({
232
- isNewCollection,
233
- configController,
234
- editedCollectionId,
235
- parentCollectionIds,
236
- fullPath,
237
- collectionInference,
238
- handleClose,
239
- reservedGroups,
240
- extraView,
241
- handleCancel,
242
- setFormDirty,
243
- getUser,
244
- parentCollection,
245
- getData,
246
- existingPaths,
247
- existingIds,
248
- includeTemplates,
249
- collection,
250
- setCollection,
251
- initialValues,
252
- propertyConfigs,
253
- groups,
254
- existingEntities
255
- }: CollectionEditorDialogProps & {
256
- handleCancel: () => void,
257
- setFormDirty: (dirty: boolean) => void,
258
- initialValues: PersistedCollection<M>,
259
- existingPaths: string[],
260
- existingIds: string[],
261
- includeTemplates: boolean,
262
- collection: PersistedCollection<M> | undefined,
263
- setCollection: (collection: PersistedCollection<M>) => void,
264
- propertyConfigs: Record<string, PropertyConfig<any>>,
265
- groups: string[],
266
- }
275
+ isNewCollection,
276
+ configController,
277
+ editedCollectionId,
278
+ parentCollectionIds,
279
+ fullPath,
280
+ collectionInference,
281
+ handleClose,
282
+ reservedGroups,
283
+ extraView,
284
+ handleCancel,
285
+ setFormDirty,
286
+ getUser,
287
+ parentCollection,
288
+ getData,
289
+ existingPaths,
290
+ existingIds,
291
+ includeTemplates,
292
+ collection,
293
+ setCollection,
294
+ initialValues,
295
+ propertyConfigs,
296
+ groups,
297
+ existingEntities,
298
+ initialView: initialViewProp,
299
+ expandKanban,
300
+ generateCollection,
301
+ onAnalyticsEvent
302
+ }: CollectionEditorDialogProps & {
303
+ handleCancel: () => void,
304
+ setFormDirty: (dirty: boolean) => void,
305
+ initialValues: PersistedCollection<M>,
306
+ existingPaths: string[],
307
+ existingIds: string[],
308
+ includeTemplates: boolean,
309
+ collection: PersistedCollection<M> | undefined,
310
+ setCollection: (collection: PersistedCollection<M>) => void,
311
+ propertyConfigs: Record<string, PropertyConfig<any>>,
312
+ groups: (string | null)[],
313
+ }
267
314
  ) {
268
315
 
316
+ const { t } = useTranslation();
269
317
  const importConfig = useImportConfig();
270
318
  const navigation = useNavigationController();
271
319
  const snackbarController = useSnackbarController();
@@ -273,7 +321,9 @@ function CollectionEditorInternal<M extends Record<string, any>>({
273
321
  // Use this ref to store which properties have errors
274
322
  const propertyErrorsRef = useRef({});
275
323
 
276
- const initialView = isNewCollection ? (includeTemplates ? "welcome" : "details") : "properties";
324
+ const initialView = isNewCollection
325
+ ? (includeTemplates ? "welcome" : "general")
326
+ : (initialViewProp ?? "properties");
277
327
  const [currentView, setCurrentView] = useState<EditorView>(initialView); // this view can edit either the details view or the properties one
278
328
 
279
329
  const [error, setError] = React.useState<Error | undefined>();
@@ -296,14 +346,14 @@ function CollectionEditorInternal<M extends Record<string, any>>({
296
346
  console.error(e);
297
347
  snackbarController.open({
298
348
  type: "error",
299
- message: "Error persisting collection: " + (e.message ?? "Details in the console")
349
+ message: t("error_persisting_collection", { error: e.message ?? t("details_in_console") })
300
350
  });
301
351
  return false;
302
352
  });
303
353
  };
304
354
 
305
355
  const setNextMode = () => {
306
- if (currentView === "details") {
356
+ if (currentView === "general") {
307
357
  if (importConfig.inUse) {
308
358
  setCurrentView("import_data_saving");
309
359
  } else if (extraView) {
@@ -312,22 +362,29 @@ function CollectionEditorInternal<M extends Record<string, any>>({
312
362
  setCurrentView("properties");
313
363
  }
314
364
  } else if (currentView === "welcome") {
315
- setCurrentView("details");
365
+ setCurrentView("general");
316
366
  } else if (currentView === "import_data_mapping") {
317
367
  setCurrentView("import_data_preview");
318
368
  } else if (currentView === "import_data_preview") {
319
- setCurrentView("details");
369
+ setCurrentView("general");
320
370
  } else if (currentView === "extra_view") {
321
371
  setCurrentView("properties");
322
372
  } else {
323
- setCurrentView("details");
373
+ setCurrentView("general");
324
374
  }
325
375
 
326
376
  };
327
377
 
328
378
  const doCollectionInference = collectionInference ? (collection: PersistedCollection<any>) => {
329
379
  if (!collectionInference) return undefined;
330
- return collectionInference?.(collection.path, collection.collectionGroup ?? false, parentPaths ?? [], collection.databaseId);
380
+ return collectionInference?.(
381
+ collection.path,
382
+ collection.collectionGroup ?? false,
383
+ parentPaths ?? [],
384
+ collection.databaseId,
385
+ collection.initialFilter,
386
+ collection.initialSort
387
+ );
331
388
  } : undefined;
332
389
 
333
390
  const inferCollectionFromData = async (newCollection: PersistedCollection<M>) => {
@@ -370,7 +427,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
370
427
  console.error(e);
371
428
  snackbarController.open({
372
429
  type: "error",
373
- message: "Error inferring collection: " + (e.message ?? "Details in the console")
430
+ message: t("error_inferring_collection", { error: e.message ?? t("details_in_console") })
374
431
  });
375
432
  return newCollection;
376
433
  }
@@ -382,6 +439,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
382
439
 
383
440
  if (!isNewCollection) {
384
441
  saveCollection(newCollectionState).then(() => {
442
+ aiModifiedPaths?.clearAllPaths();
385
443
  formexController.resetForm();
386
444
  handleClose(newCollectionState);
387
445
  });
@@ -391,7 +449,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
391
449
  if (currentView === "welcome") {
392
450
  setNextMode();
393
451
  formexController.resetForm({ values: newCollectionState });
394
- } else if (currentView === "details") {
452
+ } else if (currentView === "general") {
395
453
  if (extraView || importConfig.inUse) {
396
454
  formexController.resetForm({ values: newCollectionState });
397
455
  setNextMode();
@@ -406,8 +464,8 @@ function CollectionEditorInternal<M extends Record<string, any>>({
406
464
  }
407
465
  });
408
466
  }).finally(() => {
409
- setNextMode();
410
- });
467
+ setNextMode();
468
+ });
411
469
  } else {
412
470
  formexController.resetForm({ values: newCollectionState });
413
471
  setNextMode();
@@ -432,7 +490,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
432
490
  } catch (e: any) {
433
491
  snackbarController.open({
434
492
  type: "error",
435
- message: "Error persisting collection: " + (e.message ?? "Details in the console")
493
+ message: t("error_persisting_collection", { error: e.message ?? t("details_in_console") })
436
494
  });
437
495
  console.error(e);
438
496
  formexController.resetForm({ values: newCollectionState });
@@ -442,7 +500,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
442
500
  const validation = (col: PersistedCollection) => {
443
501
 
444
502
  let errors: Record<string, any> = {};
445
- const schema = (currentView === "properties" || currentView === "subcollections" || currentView === "details") && YupSchema;
503
+ const schema = (currentView === "properties" || currentView === "extend" || currentView === "general") && YupSchema;
446
504
  if (schema) {
447
505
  try {
448
506
  schema.validateSync(col, { abortEarly: false });
@@ -455,12 +513,12 @@ function CollectionEditorInternal<M extends Record<string, any>>({
455
513
  if (currentView === "properties") {
456
514
  errors = { ...errors, ...propertyErrorsRef.current };
457
515
  }
458
- if (currentView === "details") {
459
- const pathError = validatePath(col.path, isNewCollection, existingPaths, col.id);
516
+ if (currentView === "general") {
517
+ const pathError = validatePath(t, col.path, isNewCollection, existingPaths, col.id);
460
518
  if (pathError) {
461
519
  errors.path = pathError;
462
520
  }
463
- const idError = validateId(col.id, isNewCollection, existingPaths, existingIds);
521
+ const idError = validateId(t, col.id, isNewCollection, existingPaths, existingIds);
464
522
  if (idError) {
465
523
  errors.id = idError;
466
524
  }
@@ -483,15 +541,15 @@ function CollectionEditorInternal<M extends Record<string, any>>({
483
541
  submitCount
484
542
  } = formController;
485
543
 
486
- // TODO: getting data is only working in root collections with this code
487
544
  const path = values.path;
488
545
  const updatedFullPath = fullPath?.includes("/") ? fullPath?.split("/").slice(0, -1).join("/") + "/" + path : path; // TODO: this path is wrong
489
- const pathError = validatePath(path, isNewCollection, existingPaths, values.id);
546
+ const pathError = validatePath(t, path, isNewCollection, existingPaths, values.id);
490
547
 
491
548
  const parentPaths = !pathError && parentCollectionIds ? navigation.convertIdsToPaths(parentCollectionIds) : undefined;
492
549
  const resolvedPath = !pathError ? navigation.resolveIdsFrom(updatedFullPath) : undefined;
550
+
493
551
  const getDataWithPath = resolvedPath && getData ? async () => {
494
- const data = await getData(resolvedPath, parentPaths ?? []);
552
+ const data = await getData!(resolvedPath, parentPaths ?? []);
495
553
  if (existingEntities) {
496
554
  const existingData = existingEntities.map(e => e.values);
497
555
  data.push(...existingData);
@@ -547,7 +605,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
547
605
  setDeleteRequested(false);
548
606
  handleCancel();
549
607
  snackbarController.open({
550
- message: "Collection deleted",
608
+ message: t("collection_deleted"),
551
609
  type: "success"
552
610
  });
553
611
  });
@@ -558,7 +616,16 @@ function CollectionEditorInternal<M extends Record<string, any>>({
558
616
  onImportDataSet(importData, propertiesOrder);
559
617
  setCurrentView("import_data_mapping");
560
618
  } else {
561
- setCurrentView("details");
619
+ setCurrentView("general");
620
+ }
621
+ };
622
+
623
+ const aiModifiedPaths = useAIModifiedPaths();
624
+
625
+ const handleAIGenerated = (generatedCollection: EntityCollection, operations?: CollectionOperation[]) => {
626
+ formController.setValues(generatedCollection as PersistedCollection<M>);
627
+ if (operations && aiModifiedPaths) {
628
+ aiModifiedPaths.addModifiedPaths(operations);
562
629
  }
563
630
  };
564
631
 
@@ -566,99 +633,102 @@ function CollectionEditorInternal<M extends Record<string, any>>({
566
633
  <Formex value={formController}>
567
634
 
568
635
  <>
569
- {!isNewCollection && <Tabs value={currentView}
570
- innerClassName={cls(defaultBorderMixin, "px-4 h-14 w-full justify-end bg-surface-50 dark:bg-surface-950 border-b")}
571
- onValueChange={(v) => setCurrentView(v as EditorView)}>
572
- <Tab value={"details"}>
573
- Details
574
- </Tab>
575
- <Tab value={"properties"}>
576
- Properties
577
- </Tab>
578
- <Tab value={"subcollections"}>
579
- Additional views
580
- </Tab>
581
- <Tab value={"custom_actions"}>
582
- Custom actions
583
- </Tab>
584
- </Tabs>}
636
+ {!isNewCollection && <div className={cls("px-4 py-2 w-full flex items-center justify-end gap-2 bg-surface-50 dark:bg-surface-950 border-b", defaultBorderMixin)}>
637
+ {generateCollection && (
638
+ <AICollectionGeneratorPopover
639
+ existingCollection={values}
640
+ onGenerated={handleAIGenerated}
641
+ generateCollection={generateCollection}
642
+ onAnalyticsEvent={onAnalyticsEvent}
643
+ />
644
+ )}
645
+ <Tabs value={currentView}
646
+ onValueChange={(v) => setCurrentView(v as EditorView)}>
647
+ <Tab value={"general"}>
648
+ {t("tab_general")}
649
+ </Tab>
650
+ <Tab value={"display"}>
651
+ {t("tab_display")}
652
+ </Tab>
653
+ <Tab value={"properties"}>
654
+ {t("tab_properties")}
655
+ </Tab>
656
+ <Tab value={"extend"}>
657
+ {t("tab_extend")}
658
+ </Tab>
659
+ </Tabs>
660
+ </div>}
585
661
 
586
662
  <form noValidate
587
- onSubmit={formController.handleSubmit}
588
- className={cls(
589
- isNewCollection ? "h-full" : "h-[calc(100%-48px)]",
590
- "flex-grow flex flex-col relative")}>
663
+ onSubmit={formController.handleSubmit}
664
+ className={cls(
665
+ isNewCollection ? "h-full" : "h-[calc(100%-48px)]",
666
+ "flex-grow flex flex-col relative")}>
591
667
 
592
668
  {currentView === "loading" &&
593
- <CircularProgressCenter/>}
669
+ <CircularProgressCenter />}
594
670
 
595
671
  {currentView === "extra_view" &&
596
672
  path &&
597
673
  extraView?.View &&
598
- <extraView.View path={path}/>}
674
+ <extraView.View path={path} />}
599
675
 
600
676
  {currentView === "welcome" &&
601
677
  <CollectionEditorWelcomeView
602
678
  path={path}
603
679
  onContinue={onWelcomeScreenContinue}
604
680
  existingCollectionPaths={existingPaths}
605
- parentCollection={parentCollection}/>}
681
+ parentCollection={parentCollection}
682
+ generateCollection={generateCollection}
683
+ onAnalyticsEvent={onAnalyticsEvent} />}
606
684
 
607
685
  {currentView === "import_data_mapping" && importConfig &&
608
686
  <CollectionEditorImportMapping importConfig={importConfig}
609
- collectionEditable={collectionEditable}
610
- propertyConfigs={propertyConfigs}/>}
687
+ collectionEditable={collectionEditable}
688
+ propertyConfigs={propertyConfigs} />}
611
689
 
612
690
  {currentView === "import_data_preview" && importConfig &&
613
691
  <CollectionEditorImportDataPreview importConfig={importConfig}
614
- properties={values.properties as Properties}
615
- propertiesOrder={values.propertiesOrder as string[]}/>}
692
+ properties={values.properties as Properties}
693
+ propertiesOrder={values.propertiesOrder as string[]} />}
616
694
 
617
695
  {currentView === "import_data_saving" && importConfig &&
618
696
  <ImportSaveInProgress importConfig={importConfig}
619
- collection={values}
620
- path={path}
621
- onImportSuccess={async (importedCollection) => {
622
- snackbarController.open({
623
- type: "info",
624
- message: "Data imported successfully"
625
- });
626
- await saveCollection(values);
627
- handleClose(importedCollection);
628
- }}
697
+ collection={values}
698
+ path={path}
699
+ onImportSuccess={async (importedCollection) => {
700
+ snackbarController.open({
701
+ type: "info",
702
+ message: t("data_imported_successfully_msg")
703
+ });
704
+ await saveCollection(values);
705
+ handleClose(importedCollection);
706
+ }}
629
707
  />}
630
708
 
631
- {currentView === "details" &&
632
- <CollectionDetailsForm
709
+ {currentView === "general" &&
710
+ <GeneralSettingsForm
633
711
  existingPaths={existingPaths}
634
712
  existingIds={existingIds}
635
- groups={groups}
636
- parentCollectionIds={parentCollectionIds}
637
713
  parentCollection={parentCollection}
638
- isNewCollection={isNewCollection}>
639
- {!isNewCollection && isMergedCollection && <div className={"flex flex-col gap-4 mt-8"}>
640
- <Typography variant={"body2"} color={"secondary"}>This collection is defined in code.
641
- The changes done in this editor will override the properties defined in code.
642
- You can delete the overridden values to revert to the state defined in code.
643
- </Typography>
644
- <Button color={"neutral"}
645
- onClick={() => {
646
- setDeleteRequested(true);
647
- }}>Reset to code</Button>
648
- </div>}
649
- </CollectionDetailsForm>}
650
-
651
- {currentView === "custom_actions" && collection &&
652
- <EntityActionsEditTab collection={collection}/>}
653
-
654
- {currentView === "subcollections" && collection &&
655
- <SubcollectionsEditTab
714
+ isNewCollection={isNewCollection} />
715
+ }
716
+
717
+ {currentView === "display" &&
718
+ <DisplaySettingsForm expandKanban={expandKanban} />
719
+ }
720
+
721
+ {currentView === "extend" && collection &&
722
+ <ExtendSettingsForm
723
+ collection={collection}
656
724
  parentCollection={parentCollection}
657
725
  configController={configController}
658
- getUser={getUser}
659
726
  collectionInference={collectionInference}
727
+ getUser={getUser}
660
728
  parentCollectionIds={parentCollectionIds}
661
- collection={collection}/>}
729
+ isMergedCollection={!isNewCollection && isMergedCollection}
730
+ onResetToCode={() => setDeleteRequested(true)} />
731
+ }
662
732
 
663
733
  {currentView === "properties" &&
664
734
  <CollectionPropertiesEditorForm
@@ -683,64 +753,58 @@ function CollectionEditorInternal<M extends Record<string, any>>({
683
753
  color={"primary"}
684
754
  onClick={() => setCurrentView("extra_view")}>
685
755
  {extraView.icon}
686
- </IconButton>}/>
756
+ </IconButton>} />
687
757
  }
688
758
 
689
759
  <DialogActions
690
760
  position={"absolute"}>
691
- {error && <ErrorView error={error}/>}
761
+ {error && <ErrorView error={error} />}
692
762
 
693
763
  {isNewCollection && includeTemplates && currentView === "import_data_mapping" &&
694
764
  <Button variant={"text"}
695
- type="button"
696
- color={"primary"}
697
- onClick={() => {
698
- importConfig.setInUse(false);
699
- return setCurrentView("welcome");
700
- }}>
701
- <ArrowBackIcon/>
702
- Back
765
+ type="button"
766
+ onClick={() => {
767
+ importConfig.setInUse(false);
768
+ return setCurrentView("welcome");
769
+ }}>
770
+ {t("back")}
703
771
  </Button>}
704
772
 
705
773
  {isNewCollection && includeTemplates && currentView === "import_data_preview" &&
706
774
  <Button variant={"text"}
707
- type="button"
708
- color={"primary"}
709
- onClick={() => {
710
- setCurrentView("import_data_mapping");
711
- }}>
712
- <ArrowBackIcon/>
713
- Back
775
+ type="button"
776
+ onClick={() => {
777
+ setCurrentView("import_data_mapping");
778
+ }}>
779
+ {t("back")}
714
780
  </Button>}
715
781
 
716
- {isNewCollection && includeTemplates && currentView === "details" &&
782
+ {isNewCollection && includeTemplates && currentView === "general" &&
717
783
  <Button variant={"text"}
718
- color={"neutral"}
719
- type="button"
720
- onClick={() => setCurrentView("welcome")}>
721
- <ArrowBackIcon/>
722
- Back
784
+ type="button"
785
+ onClick={() => setCurrentView("welcome")}>
786
+ {t("back")}
723
787
  </Button>}
724
788
 
725
789
  {isNewCollection && currentView === "properties" && <Button variant={"text"}
726
- type="button"
727
- color={"neutral"}
728
- onClick={() => setCurrentView("details")}>
729
- <ArrowBackIcon/>
730
- Back
790
+ type="button"
791
+ color={"neutral"}
792
+ onClick={() => setCurrentView("general")}>
793
+ <ArrowBackIcon />
794
+ {t("back")}
731
795
  </Button>}
732
796
 
733
797
  <Button variant={"text"}
734
- color={"neutral"}
735
- onClick={() => {
736
- handleCancel();
737
- }}>
738
- Cancel
798
+ color={"neutral"}
799
+ onClick={() => {
800
+ handleCancel();
801
+ }}>
802
+ {t("cancel")}
739
803
  </Button>
740
804
 
741
805
  {currentView === "welcome" &&
742
806
  <Button variant={"text"} onClick={() => onWelcomeScreenContinue()}>
743
- Continue from scratch
807
+ {t("continue_from_scratch")}
744
808
  </Button>}
745
809
 
746
810
  {isNewCollection && currentView === "import_data_mapping" &&
@@ -749,7 +813,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
749
813
  color="primary"
750
814
  onClick={onImportMappingComplete}
751
815
  >
752
- Next
816
+ {t("next")}
753
817
  </Button>}
754
818
 
755
819
  {isNewCollection && currentView === "import_data_preview" &&
@@ -760,22 +824,22 @@ function CollectionEditorInternal<M extends Record<string, any>>({
760
824
  setNextMode();
761
825
  }}
762
826
  >
763
- Next
827
+ {t("next")}
764
828
  </Button>}
765
829
 
766
- {isNewCollection && (currentView === "details" || currentView === "properties") &&
830
+ {isNewCollection && (currentView === "general" || currentView === "properties") &&
767
831
  <LoadingButton
768
832
  variant={"filled"}
769
833
  color="primary"
770
834
  type="submit"
771
835
  loading={isSubmitting}
772
- disabled={isSubmitting || (currentView === "details" && !validValues)}
836
+ disabled={isSubmitting || (currentView === "general" && !validValues)}
773
837
  startIcon={currentView === "properties"
774
- ? <CheckIcon/>
838
+ ? <CheckIcon />
775
839
  : undefined}
776
840
  >
777
- {currentView === "details" && "Next"}
778
- {currentView === "properties" && "Create collection"}
841
+ {currentView === "general" && t("next")}
842
+ {currentView === "properties" && t("create_collection")}
779
843
  </LoadingButton>}
780
844
 
781
845
  {!isNewCollection && <LoadingButton
@@ -784,7 +848,7 @@ function CollectionEditorInternal<M extends Record<string, any>>({
784
848
  type="submit"
785
849
  loading={isSubmitting}
786
850
  >
787
- Update collection
851
+ {t("update_collection")}
788
852
  </LoadingButton>}
789
853
 
790
854
  </DialogActions>
@@ -797,10 +861,8 @@ function CollectionEditorInternal<M extends Record<string, any>>({
797
861
  open={deleteRequested}
798
862
  onAccept={deleteCollection}
799
863
  onCancel={() => setDeleteRequested(false)}
800
- title={<>Delete the stored config?</>}
801
- body={<> This will <b>not
802
- delete any data</b>, only
803
- the stored config, and reset to the code state.</>}/>
864
+ title={<>{t("delete_stored_config")}</>}
865
+ body={<>{t("delete_stored_config_body")}</>} />
804
866
 
805
867
  </DialogContent>
806
868
 
@@ -814,7 +876,9 @@ function applyPropertyConfigs<M extends Record<string, any> = any>(collection: P
814
876
  const propertiesResult: PropertiesOrBuilders<any> = {};
815
877
  if (properties) {
816
878
  Object.keys(properties).forEach((key) => {
817
- propertiesResult[key] = applyPropertiesConfig(properties[key] as PropertyOrBuilder, propertyConfigs);
879
+ const prop = properties[key];
880
+ if (prop == null) return;
881
+ propertiesResult[key] = applyPropertiesConfig(prop as PropertyOrBuilder, propertyConfigs);
818
882
  });
819
883
  }
820
884
 
@@ -826,7 +890,7 @@ function applyPropertyConfigs<M extends Record<string, any> = any>(collection: P
826
890
 
827
891
  function applyPropertiesConfig(property: PropertyOrBuilder, propertyConfigs: Record<string, PropertyConfig<any>>) {
828
892
  let internalProperty = property;
829
- if (propertyConfigs && typeof internalProperty === "object" && internalProperty.propertyConfig) {
893
+ if (propertyConfigs && internalProperty && typeof internalProperty === "object" && internalProperty.propertyConfig) {
830
894
  const propertyConfig = propertyConfigs[internalProperty.propertyConfig];
831
895
  if (propertyConfig && isPropertyBuilder(propertyConfig.property)) {
832
896
  internalProperty = propertyConfig.property;
@@ -853,30 +917,32 @@ function applyPropertiesConfig(property: PropertyOrBuilder, propertyConfigs: Rec
853
917
 
854
918
  }
855
919
 
856
- const validatePath = (value: string, isNewCollection: boolean, existingPaths: string[], idValue?: string) => {
920
+ type TranslateFn = (key: string, params?: Record<string, string>) => string;
921
+
922
+ const validatePath = (t: TranslateFn, value: string, isNewCollection: boolean, existingPaths: string[], idValue?: string) => {
857
923
  let error;
858
924
  if (!value) {
859
- error = "You must specify a path in the database for this collection";
925
+ error = t("must_specify_path");
860
926
  }
861
927
  // if (isNewCollection && existingIds?.includes(value.trim().toLowerCase()))
862
928
  // error = "There is already a collection which uses this path as an id";
863
929
  if (isNewCollection && existingPaths?.includes(value.trim().toLowerCase()) && !idValue)
864
- error = "There is already a collection with the specified path. If you want to have multiple collections referring to the same database path, make sure the have different ids";
930
+ error = t("collection_path_already_exists");
865
931
 
866
932
  const subpaths = removeInitialAndTrailingSlashes(value).split("/");
867
933
  if (subpaths.length % 2 === 0) {
868
- error = `Collection paths must have an odd number of segments: ${value}`;
934
+ error = t("collection_path_odd_segments", { path: value });
869
935
  }
870
936
  return error;
871
937
  };
872
938
 
873
- const validateId = (value: string, isNewCollection: boolean, existingPaths: string[], existingIds: string[]) => {
939
+ const validateId = (t: TranslateFn, value: string, isNewCollection: boolean, existingPaths: string[], existingIds: string[]) => {
874
940
  if (!value) return undefined;
875
941
  let error;
876
942
  if (isNewCollection && existingPaths?.includes(value.trim().toLowerCase()))
877
- error = "There is already a collection that uses this value as a path";
943
+ error = t("collection_uses_path_as_id");
878
944
  if (isNewCollection && existingIds?.includes(value.trim().toLowerCase()))
879
- error = "There is already a collection which uses this id";
945
+ error = t("collection_uses_this_id");
880
946
  // if (error) {
881
947
  // setAdvancedPanelExpanded(true);
882
948
  // }