@firecms/collection_editor 3.0.0-alpha.9 → 3.0.0-beta.2-pre.1

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 (145) hide show
  1. package/dist/ConfigControllerProvider.d.ts +3 -2
  2. package/dist/index.d.ts +3 -2
  3. package/dist/index.es.js +3128 -4094
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +3 -1
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/types/collection_editor_controller.d.ts +22 -7
  8. package/dist/types/collection_inference.d.ts +1 -1
  9. package/dist/types/config_controller.d.ts +32 -5
  10. package/dist/types/persisted_collection.d.ts +3 -1
  11. package/dist/ui/CollectionViewHeaderAction.d.ts +10 -0
  12. package/dist/{components → ui}/EditorCollectionAction.d.ts +1 -1
  13. package/dist/ui/MissingReferenceWidget.d.ts +3 -0
  14. package/dist/ui/NewCollectionButton.d.ts +1 -0
  15. package/dist/ui/PropertyAddColumnComponent.d.ts +6 -0
  16. package/dist/ui/RootCollectionSuggestions.d.ts +1 -0
  17. package/dist/{components → ui}/collection_editor/CollectionDetailsForm.d.ts +3 -2
  18. package/dist/{components → ui}/collection_editor/CollectionEditorDialog.d.ts +11 -6
  19. package/dist/{components → ui}/collection_editor/CollectionPropertiesEditorForm.d.ts +6 -3
  20. package/dist/{components → ui}/collection_editor/CollectionYupValidation.d.ts +3 -0
  21. package/dist/ui/collection_editor/EntityCustomViewsSelectDialog.d.ts +4 -0
  22. package/dist/ui/collection_editor/GetCodeDialog.d.ts +5 -0
  23. package/dist/{components → ui}/collection_editor/PropertyEditView.d.ts +8 -6
  24. package/dist/{components → ui}/collection_editor/PropertyFieldPreview.d.ts +4 -3
  25. package/dist/ui/collection_editor/PropertySelectItem.d.ts +8 -0
  26. package/dist/{components → ui}/collection_editor/PropertyTree.d.ts +8 -5
  27. package/dist/{components → ui}/collection_editor/SubcollectionsEditTab.d.ts +2 -2
  28. package/dist/{components → ui}/collection_editor/import/CollectionEditorImportDataPreview.d.ts +1 -1
  29. package/dist/ui/collection_editor/import/CollectionEditorImportMapping.d.ts +7 -0
  30. package/dist/{components → ui}/collection_editor/import/clean_import_data.d.ts +1 -1
  31. package/dist/{components → ui}/collection_editor/properties/BlockPropertyField.d.ts +4 -1
  32. package/dist/{components → ui}/collection_editor/properties/CommonPropertyFields.d.ts +1 -0
  33. package/dist/{components → ui}/collection_editor/properties/MapPropertyField.d.ts +4 -1
  34. package/dist/{components → ui}/collection_editor/properties/RepeatPropertyField.d.ts +4 -1
  35. package/dist/{components → ui}/collection_editor/properties/StringPropertyField.d.ts +1 -1
  36. package/dist/ui/collection_editor/properties/UrlPropertyField.d.ts +4 -0
  37. package/dist/ui/collection_editor/templates/blog_template.d.ts +2 -0
  38. package/dist/ui/collection_editor/templates/pages_template.d.ts +2 -0
  39. package/dist/ui/collection_editor/templates/products_template.d.ts +2 -0
  40. package/dist/ui/collection_editor/templates/users_template.d.ts +2 -0
  41. package/dist/ui/collection_editor/utils/strings.d.ts +1 -0
  42. package/dist/ui/collection_editor/utils/supported_fields.d.ts +3 -0
  43. package/dist/ui/collection_editor/utils/update_property_for_widget.d.ts +2 -0
  44. package/dist/useCollectionEditorPlugin.d.ts +5 -3
  45. package/dist/utils/entities.d.ts +3 -4
  46. package/package.json +22 -19
  47. package/src/ConfigControllerProvider.tsx +336 -0
  48. package/src/index.ts +35 -0
  49. package/src/types/collection_editor_controller.tsx +42 -0
  50. package/src/types/collection_inference.ts +3 -0
  51. package/src/types/config_controller.tsx +60 -0
  52. package/src/types/config_permissions.ts +20 -0
  53. package/src/types/persisted_collection.ts +9 -0
  54. package/src/ui/CollectionViewHeaderAction.tsx +43 -0
  55. package/src/ui/EditorCollectionAction.tsx +109 -0
  56. package/src/ui/HomePageEditorCollectionAction.tsx +84 -0
  57. package/src/ui/MissingReferenceWidget.tsx +35 -0
  58. package/src/ui/NewCollectionButton.tsx +16 -0
  59. package/src/ui/NewCollectionCard.tsx +47 -0
  60. package/src/ui/PropertyAddColumnComponent.tsx +42 -0
  61. package/src/ui/RootCollectionSuggestions.tsx +55 -0
  62. package/src/ui/collection_editor/CollectionDetailsForm.tsx +366 -0
  63. package/src/ui/collection_editor/CollectionEditorDialog.tsx +754 -0
  64. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +206 -0
  65. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +481 -0
  66. package/src/ui/collection_editor/CollectionYupValidation.tsx +7 -0
  67. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +37 -0
  68. package/src/ui/collection_editor/EnumForm.tsx +354 -0
  69. package/src/ui/collection_editor/GetCodeDialog.tsx +110 -0
  70. package/src/ui/collection_editor/PropertyEditView.tsx +558 -0
  71. package/src/ui/collection_editor/PropertyFieldPreview.tsx +203 -0
  72. package/src/ui/collection_editor/PropertySelectItem.tsx +32 -0
  73. package/src/ui/collection_editor/PropertyTree.tsx +233 -0
  74. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +253 -0
  75. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +47 -0
  76. package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +37 -0
  77. package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +260 -0
  78. package/src/ui/collection_editor/import/clean_import_data.ts +53 -0
  79. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +135 -0
  80. package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +36 -0
  81. package/src/ui/collection_editor/properties/CommonPropertyFields.tsx +137 -0
  82. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +87 -0
  83. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +117 -0
  84. package/src/ui/collection_editor/properties/FieldHelperView.tsx +13 -0
  85. package/src/ui/collection_editor/properties/KeyValuePropertyField.tsx +20 -0
  86. package/src/ui/collection_editor/properties/MapPropertyField.tsx +149 -0
  87. package/src/ui/collection_editor/properties/NumberPropertyField.tsx +38 -0
  88. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +165 -0
  89. package/src/ui/collection_editor/properties/RepeatPropertyField.tsx +108 -0
  90. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +194 -0
  91. package/src/ui/collection_editor/properties/StringPropertyField.tsx +79 -0
  92. package/src/ui/collection_editor/properties/UrlPropertyField.tsx +89 -0
  93. package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +36 -0
  94. package/src/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +50 -0
  95. package/src/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +50 -0
  96. package/src/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +100 -0
  97. package/src/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +132 -0
  98. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +28 -0
  99. package/src/ui/collection_editor/templates/blog_template.ts +115 -0
  100. package/src/ui/collection_editor/templates/pages_template.ts +188 -0
  101. package/src/ui/collection_editor/templates/products_template.ts +88 -0
  102. package/src/ui/collection_editor/templates/users_template.ts +42 -0
  103. package/src/ui/collection_editor/util.ts +21 -0
  104. package/src/ui/collection_editor/utils/strings.ts +8 -0
  105. package/src/ui/collection_editor/utils/supported_fields.tsx +29 -0
  106. package/src/ui/collection_editor/utils/update_property_for_widget.ts +271 -0
  107. package/src/ui/collection_editor/utils/useTraceUpdate.tsx +23 -0
  108. package/src/useCollectionEditorController.tsx +9 -0
  109. package/src/useCollectionEditorPlugin.tsx +137 -0
  110. package/src/useCollectionsConfigController.tsx +9 -0
  111. package/src/utils/arrays.ts +3 -0
  112. package/src/utils/entities.ts +38 -0
  113. package/src/vite-env.d.ts +1 -0
  114. package/dist/components/collection_editor/PropertySelectItem.d.ts +0 -8
  115. package/dist/components/collection_editor/SelectIcons.d.ts +0 -6
  116. package/dist/components/collection_editor/import/CollectionEditorImportMapping.d.ts +0 -4
  117. package/dist/components/collection_editor/templates/blog_template.d.ts +0 -10
  118. package/dist/components/collection_editor/templates/products_template.d.ts +0 -12
  119. package/dist/components/collection_editor/templates/users_template.d.ts +0 -7
  120. package/dist/components/collection_editor/utils/supported_fields.d.ts +0 -3
  121. package/dist/components/collection_editor/utils/update_property_for_widget.d.ts +0 -3
  122. package/dist/types/editable_properties.d.ts +0 -10
  123. package/dist/utils/icons.d.ts +0 -2
  124. package/dist/utils/synonyms.d.ts +0 -1951
  125. /package/dist/{components → ui}/HomePageEditorCollectionAction.d.ts +0 -0
  126. /package/dist/{components → ui}/NewCollectionCard.d.ts +0 -0
  127. /package/dist/{components → ui}/collection_editor/CollectionEditorWelcomeView.d.ts +0 -0
  128. /package/dist/{components → ui}/collection_editor/EnumForm.d.ts +0 -0
  129. /package/dist/{components → ui}/collection_editor/UnsavedChangesDialog.d.ts +0 -0
  130. /package/dist/{components → ui}/collection_editor/properties/BooleanPropertyField.d.ts +0 -0
  131. /package/dist/{components → ui}/collection_editor/properties/DateTimePropertyField.d.ts +0 -0
  132. /package/dist/{components → ui}/collection_editor/properties/EnumPropertyField.d.ts +0 -0
  133. /package/dist/{components → ui}/collection_editor/properties/FieldHelperView.d.ts +0 -0
  134. /package/dist/{components → ui}/collection_editor/properties/KeyValuePropertyField.d.ts +0 -0
  135. /package/dist/{components → ui}/collection_editor/properties/NumberPropertyField.d.ts +0 -0
  136. /package/dist/{components → ui}/collection_editor/properties/ReferencePropertyField.d.ts +0 -0
  137. /package/dist/{components → ui}/collection_editor/properties/StoragePropertyField.d.ts +0 -0
  138. /package/dist/{components → ui}/collection_editor/properties/advanced/AdvancedPropertyValidation.d.ts +0 -0
  139. /package/dist/{components → ui}/collection_editor/properties/validation/ArrayPropertyValidation.d.ts +0 -0
  140. /package/dist/{components → ui}/collection_editor/properties/validation/GeneralPropertyValidation.d.ts +0 -0
  141. /package/dist/{components → ui}/collection_editor/properties/validation/NumberPropertyValidation.d.ts +0 -0
  142. /package/dist/{components → ui}/collection_editor/properties/validation/StringPropertyValidation.d.ts +0 -0
  143. /package/dist/{components → ui}/collection_editor/properties/validation/ValidationPanel.d.ts +0 -0
  144. /package/dist/{components → ui}/collection_editor/util.d.ts +0 -0
  145. /package/dist/{components → ui}/collection_editor/utils/useTraceUpdate.d.ts +0 -0
@@ -0,0 +1,206 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { EntityCollection, unslugify, } from "@firecms/core";
3
+ import { Button, Card, Chip, CircularProgress, cn, Container, Icon, Tooltip, Typography, } from "@firecms/ui";
4
+ import { useFormikContext } from "formik";
5
+
6
+ import { productsCollectionTemplate } from "./templates/products_template";
7
+ import { blogCollectionTemplate } from "./templates/blog_template";
8
+ import { usersCollectionTemplate } from "./templates/users_template";
9
+ import { ImportFileUpload } from "@firecms/data_import_export";
10
+ import { pagesCollectionTemplate } from "./templates/pages_template";
11
+
12
+ export function CollectionEditorWelcomeView({
13
+ path,
14
+ pathSuggestions,
15
+ parentCollection,
16
+ onContinue,
17
+ collections
18
+ }: {
19
+ path: string;
20
+ pathSuggestions?: (path: string) => Promise<string[]>;
21
+ parentCollection?: EntityCollection;
22
+ onContinue: (importData?: object[]) => void;
23
+ collections?: EntityCollection[];
24
+ }) {
25
+
26
+ const [loadingPathSuggestions, setLoadingPathSuggestions] = useState(false);
27
+ const [filteredPathSuggestions, setFilteredPathSuggestions] = useState<string[] | undefined>();
28
+ useEffect(() => {
29
+ if (pathSuggestions && collections) {
30
+ setLoadingPathSuggestions(true);
31
+ pathSuggestions(path)
32
+ .then(suggestions => {
33
+ const filteredSuggestions = suggestions.filter(s => !collections.find(c => c.path.trim().toLowerCase() === s.trim().toLowerCase()));
34
+ setFilteredPathSuggestions(filteredSuggestions);
35
+ })
36
+ .finally(() => setLoadingPathSuggestions(false));
37
+ }
38
+ }, [collections, path, pathSuggestions]);
39
+
40
+ const {
41
+ values,
42
+ setFieldValue,
43
+ setValues,
44
+ handleChange,
45
+ touched,
46
+ errors,
47
+ setFieldTouched,
48
+ isSubmitting,
49
+ submitCount
50
+ } = useFormikContext<EntityCollection>();
51
+
52
+ return (
53
+ <div className={"overflow-auto my-auto"}>
54
+ <Container maxWidth={"4xl"} className={"flex flex-col gap-4 p-8 m-auto"}>
55
+
56
+ <div
57
+ className="flex flex-row py-2 pt-3 items-center">
58
+ <Typography variant={"h4"} className={"flex-grow"}>
59
+ New collection
60
+ </Typography>
61
+ </div>
62
+
63
+ {parentCollection && <Chip colorScheme={"tealDarker"}>
64
+ <Typography variant={"caption"}>
65
+ This is a subcollection of <b>{parentCollection.name}</b>
66
+ </Typography>
67
+ </Chip>}
68
+
69
+ <div className={"my-2"}>
70
+ <Typography variant={"caption"}
71
+ color={"secondary"}>
72
+ ● Use one of the existing paths in your database:
73
+ </Typography>
74
+ <div className={"flex flex-wrap gap-x-2 gap-y-1 items-center my-2 min-h-7"}>
75
+
76
+ {loadingPathSuggestions && !filteredPathSuggestions && <CircularProgress size={"small"}/>}
77
+
78
+ {filteredPathSuggestions?.map((suggestion, index) => (
79
+ <Chip key={suggestion}
80
+ colorScheme={"cyanLighter"}
81
+ onClick={() => {
82
+ setFieldValue("name", unslugify(suggestion));
83
+ setFieldValue("id", suggestion);
84
+ setFieldValue("path", suggestion);
85
+ setFieldValue("properties", undefined);
86
+ onContinue();
87
+ }}
88
+ size="small">
89
+ {suggestion}
90
+ </Chip>
91
+ ))}
92
+
93
+ {!loadingPathSuggestions && (filteredPathSuggestions ?? [])?.length === 0 &&
94
+ <Typography variant={"caption"}>
95
+ No suggestions
96
+ </Typography>
97
+ }
98
+
99
+ </div>
100
+
101
+ </div>
102
+
103
+ <div className={"my-2"}>
104
+ <Typography variant={"caption"}
105
+ color={"secondary"}>
106
+ ● Select a template:
107
+ </Typography>
108
+
109
+ <div className={"flex gap-4"}>
110
+ <TemplateButton title={"Products"}
111
+ subtitle={"A collection of products with images, prices and stock"}
112
+ icon={<Icon size={"small"} iconKey={productsCollectionTemplate.icon!}/>}
113
+ onClick={() => {
114
+ setValues(productsCollectionTemplate);
115
+ onContinue();
116
+ }}/>
117
+ <TemplateButton title={"Users"}
118
+ subtitle={"A collection of users with emails, names and roles"}
119
+ icon={<Icon size={"small"} iconKey={usersCollectionTemplate.icon!}/>}
120
+ onClick={() => {
121
+ setValues(usersCollectionTemplate);
122
+ onContinue();
123
+ }}/>
124
+ <TemplateButton title={"Blog posts"}
125
+ subtitle={"A collection of blog posts with images, authors and complex content"}
126
+ icon={<Icon size={"small"} iconKey={blogCollectionTemplate.icon!}/>}
127
+ onClick={() => {
128
+ setValues(blogCollectionTemplate);
129
+ onContinue();
130
+ }}/>
131
+ <TemplateButton title={"Pages"}
132
+ subtitle={"A collection of pages with images, authors and complex content"}
133
+ icon={<Icon size={"small"} iconKey={pagesCollectionTemplate.icon!}/>}
134
+ onClick={() => {
135
+ setValues(pagesCollectionTemplate);
136
+ onContinue();
137
+ }}/>
138
+ </div>
139
+
140
+ </div>
141
+
142
+ {!parentCollection && <div>
143
+
144
+ <Typography variant={"caption"}
145
+ color={"secondary"}
146
+ className={"mb-2"}>
147
+ ● Create a collection from a file (csv, json, xls, xslx...)
148
+ </Typography>
149
+
150
+ <ImportFileUpload onDataAdded={(data) => onContinue(data)}/>
151
+
152
+ </div>}
153
+
154
+ <div>
155
+
156
+ <Button variant={"text"} onClick={() => onContinue()} className={"my-2"}>
157
+ Continue from scratch
158
+ </Button>
159
+ </div>
160
+
161
+ {/*<div style={{ height: "52px" }}/>*/}
162
+
163
+ </Container>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ export function TemplateButton({
169
+ title,
170
+ subtitle,
171
+ icon,
172
+ onClick
173
+ }: {
174
+ title: string,
175
+ icon: React.ReactNode,
176
+ subtitle: string,
177
+ onClick?: () => void
178
+ }) {
179
+
180
+ return (
181
+ <Tooltip title={subtitle}>
182
+ <Card
183
+ onClick={onClick}
184
+ className={cn(
185
+ "my-2 rounded-md border mx-0 p-6 px-4 focus:outline-none transition ease-in-out duration-150 flex flex-row gap-4 items-center",
186
+ "text-gray-700 dark:text-gray-300",
187
+ "hover:border-primary-dark hover:text-primary-dark dark:hover:text-primary focus:ring-primary hover:ring-1 hover:ring-primary",
188
+ "border-gray-400 dark:border-gray-600 "
189
+ )}
190
+ >
191
+ {icon}
192
+ <div className={"flex flex-col items-start"}>
193
+
194
+ <Typography variant={"subtitle1"}>
195
+ {title}
196
+ </Typography>
197
+ {/*<Typography>*/}
198
+ {/* {subtitle}*/}
199
+ {/*</Typography>*/}
200
+
201
+ </div>
202
+ </Card>
203
+ </Tooltip>
204
+ );
205
+
206
+ }
@@ -0,0 +1,481 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+
3
+ import { Field, FormikErrors, getIn, useFormikContext } from "formik";
4
+ import {
5
+ EntityCollection,
6
+ ErrorBoundary,
7
+ isPropertyBuilder,
8
+ makePropertiesEditable,
9
+ Properties,
10
+ Property,
11
+ PropertyConfig,
12
+ PropertyOrBuilder,
13
+ useLargeLayout,
14
+ User,
15
+ useSnackbarController
16
+ } from "@firecms/core";
17
+ import {
18
+ AddIcon,
19
+ AutoAwesomeIcon,
20
+ Button,
21
+ CircularProgress,
22
+ cn,
23
+ CodeIcon,
24
+ DebouncedTextField,
25
+ defaultBorderMixin,
26
+ IconButton,
27
+ Paper,
28
+ Tooltip,
29
+ Typography,
30
+ } from "@firecms/ui";
31
+
32
+ import { getFullId, idToPropertiesPath, namespaceToPropertiesOrderPath } from "./util";
33
+ import { OnPropertyChangedParams, PropertyForm, PropertyFormDialog } from "./PropertyEditView";
34
+ import { PropertyTree } from "./PropertyTree";
35
+ import { PersistedCollection } from "../../types/persisted_collection";
36
+ import { GetCodeDialog } from "./GetCodeDialog";
37
+
38
+ type CollectionEditorFormProps = {
39
+ showErrors: boolean;
40
+ isNewCollection: boolean;
41
+ propertyErrorsRef?: React.MutableRefObject<any>;
42
+ onPropertyError: (propertyKey: string, namespace: string | undefined, error?: FormikErrors<any>) => void;
43
+ setDirty?: (dirty: boolean) => void;
44
+ reservedGroups?: string[];
45
+ extraIcon: React.ReactNode;
46
+ getUser: (uid: string) => User | null;
47
+ getData?: () => Promise<object[]>;
48
+ doCollectionInference: (collection: PersistedCollection) => Promise<Partial<EntityCollection> | null> | undefined;
49
+ propertyConfigs: Record<string, PropertyConfig>;
50
+ collectionEditable: boolean;
51
+ };
52
+
53
+ export function CollectionPropertiesEditorForm({
54
+ showErrors,
55
+ isNewCollection,
56
+ propertyErrorsRef,
57
+ onPropertyError,
58
+ setDirty,
59
+ reservedGroups,
60
+ extraIcon,
61
+ getUser,
62
+ getData,
63
+ doCollectionInference,
64
+ propertyConfigs,
65
+ collectionEditable
66
+ }: CollectionEditorFormProps) {
67
+
68
+ const {
69
+ values,
70
+ setFieldValue,
71
+ setFieldError,
72
+ setFieldTouched,
73
+ errors,
74
+ dirty
75
+ } = useFormikContext<PersistedCollection>();
76
+
77
+ const snackbarController = useSnackbarController();
78
+
79
+ const largeLayout = useLargeLayout();
80
+ const asDialog = !largeLayout
81
+
82
+ // index of the selected property within the namespace
83
+ const [selectedPropertyIndex, setSelectedPropertyIndex] = useState<number | undefined>();
84
+ const [selectedPropertyKey, setSelectedPropertyKey] = useState<string | undefined>();
85
+ const [selectedPropertyNamespace, setSelectedPropertyNamespace] = useState<string | undefined>();
86
+
87
+ const selectedPropertyFullId = selectedPropertyKey ? getFullId(selectedPropertyKey, selectedPropertyNamespace) : undefined;
88
+ const selectedProperty = selectedPropertyFullId ? getIn(values.properties, selectedPropertyFullId.replaceAll(".", ".properties.")) : undefined;
89
+ const [codeDialogOpen, setCodeDialogOpen] = useState<boolean>(false);
90
+
91
+ const [inferringProperties, setInferringProperties] = useState<boolean>(false);
92
+
93
+ const [newPropertyDialogOpen, setNewPropertyDialogOpen] = useState<boolean>(false);
94
+ const [inferredPropertyKeys, setInferredPropertyKeys] = useState<string[]>([]);
95
+
96
+ const currentPropertiesOrderRef = React.useRef<{
97
+ [key: string]: string[]
98
+ }>(values.propertiesOrder ? { "": values.propertiesOrder } : {});
99
+
100
+ useEffect(() => {
101
+ if (setDirty)
102
+ setDirty(dirty);
103
+ }, [dirty]);
104
+
105
+ const inferPropertiesFromData = doCollectionInference
106
+ ? (): void => {
107
+ if (!doCollectionInference)
108
+ return;
109
+
110
+ setInferringProperties(true);
111
+ // @ts-ignore
112
+ doCollectionInference(values)
113
+ .then((newCollection) => {
114
+
115
+ if (newCollection)
116
+ makePropertiesEditable(newCollection.properties as Properties);
117
+
118
+ if (!newCollection) {
119
+ snackbarController.open({
120
+ type: "error",
121
+ message: "Could not infer properties from data"
122
+ });
123
+ return;
124
+ }
125
+ // find properties in the new collection, not present in the current one
126
+ const newPropertyKeys = (newCollection.properties ? Object.keys(newCollection.properties) : [])
127
+ .filter((propertyKey) => !values.properties[propertyKey]);
128
+ if (newPropertyKeys.length === 0) {
129
+ snackbarController.open({
130
+ type: "info",
131
+ message: "No new properties found in existing data"
132
+ });
133
+ return;
134
+ }
135
+ // add them to the current collection
136
+ const updatedProperties = {
137
+ ...newPropertyKeys.reduce((acc, propertyKey) => {
138
+ acc[propertyKey] = (newCollection.properties ?? {})[propertyKey];
139
+ return acc;
140
+ }, {} as { [key: string]: PropertyOrBuilder }),
141
+ ...values.properties
142
+ };
143
+ const updatedPropertiesOrder = [
144
+ ...newPropertyKeys,
145
+ ...(values.propertiesOrder ?? [])
146
+ ];
147
+ setFieldValue("properties", updatedProperties, false);
148
+
149
+ updatePropertiesOrder(updatedPropertiesOrder);
150
+
151
+ setInferredPropertyKeys(newPropertyKeys);
152
+ })
153
+ .finally(() => {
154
+ setInferringProperties(false);
155
+ })
156
+ }
157
+ : undefined;
158
+
159
+ const getCurrentPropertiesOrder = (namespace?: string) => {
160
+ if (!namespace) return currentPropertiesOrderRef.current[""];
161
+ return currentPropertiesOrderRef.current[namespace] ?? getIn(values, namespaceToPropertiesOrderPath(namespace));
162
+ }
163
+
164
+ const updatePropertiesOrder = (newPropertiesOrder: string[], namespace?: string) => {
165
+ const propertiesOrderPath = namespaceToPropertiesOrderPath(namespace);
166
+
167
+ setFieldValue(propertiesOrderPath, newPropertiesOrder, false);
168
+ currentPropertiesOrderRef.current[namespace ?? ""] = newPropertiesOrder;
169
+
170
+ };
171
+
172
+ const deleteProperty = (propertyKey?: string, namespace?: string) => {
173
+ const fullId = propertyKey ? getFullId(propertyKey, namespace) : undefined;
174
+ if (!fullId)
175
+ throw Error("collection editor miss config");
176
+
177
+ setFieldValue(idToPropertiesPath(fullId), undefined, false);
178
+
179
+ const currentPropertiesOrder = getCurrentPropertiesOrder(namespace);
180
+ const newPropertiesOrder = currentPropertiesOrder.filter((p) => p !== propertyKey);
181
+ updatePropertiesOrder(newPropertiesOrder, namespace);
182
+
183
+ setNewPropertyDialogOpen(false);
184
+
185
+ setSelectedPropertyIndex(undefined);
186
+ setSelectedPropertyKey(undefined);
187
+ setSelectedPropertyNamespace(undefined);
188
+ };
189
+
190
+ const onPropertyMove = (propertiesOrder: string[], namespace?: string) => {
191
+ setFieldValue(namespaceToPropertiesOrderPath(namespace), propertiesOrder, false);
192
+ };
193
+
194
+ const onPropertyCreated = ({
195
+ id,
196
+ property
197
+ }: {
198
+ id?: string,
199
+ property: Property
200
+ }) => {
201
+ if (!id) {
202
+ throw Error("Need to include an ID when creating a new property")
203
+ }
204
+ setFieldValue("properties", {
205
+ ...(values.properties ?? {}),
206
+ [id]: property
207
+ }, false);
208
+ const newPropertiesOrder = [...(values.propertiesOrder ?? Object.keys(values.properties)), id];
209
+
210
+ updatePropertiesOrder(newPropertiesOrder);
211
+
212
+ setNewPropertyDialogOpen(false);
213
+ if (largeLayout) {
214
+ setSelectedPropertyIndex(newPropertiesOrder.indexOf(id));
215
+ setSelectedPropertyKey(id);
216
+ }
217
+ setSelectedPropertyNamespace(undefined);
218
+ };
219
+
220
+ const onPropertyChanged = ({
221
+ id,
222
+ property,
223
+ previousId,
224
+ namespace
225
+ }: OnPropertyChangedParams) => {
226
+ const fullId = id ? getFullId(id, namespace) : undefined;
227
+ const propertyPath = fullId ? idToPropertiesPath(fullId) : undefined;
228
+
229
+ // If the id has changed we need to a little cleanup
230
+ if (previousId && previousId !== id) {
231
+ const previousFullId = getFullId(previousId, namespace);
232
+ const previousPropertyPath = idToPropertiesPath(previousFullId);
233
+
234
+ const currentPropertiesOrder = getCurrentPropertiesOrder(namespace);
235
+
236
+ // replace previousId with id in propertiesOrder
237
+ const newPropertiesOrder = currentPropertiesOrder
238
+ .map((p) => p === previousId ? id : p)
239
+ .filter((p) => p !== undefined) as string[];
240
+
241
+ updatePropertiesOrder(newPropertiesOrder, namespace);
242
+
243
+ if (id) {
244
+ setSelectedPropertyIndex(newPropertiesOrder.indexOf(id));
245
+ setSelectedPropertyKey(id);
246
+ }
247
+ setFieldValue(previousPropertyPath, undefined, false);
248
+ setFieldTouched(previousPropertyPath, false, false);
249
+ }
250
+
251
+ console.debug("onPropertyChanged", {
252
+ id,
253
+ property,
254
+ previousId,
255
+ namespace,
256
+ propertyPath
257
+ })
258
+
259
+ if (propertyPath) {
260
+ setFieldValue(propertyPath, property, false);
261
+ setFieldTouched(propertyPath, true, false);
262
+ }
263
+
264
+ };
265
+
266
+ const onPropertyErrorInternal = useCallback((id: string, namespace?: string, error?: FormikErrors<any>) => {
267
+ const propertyPath = id ? getFullId(id, namespace) : undefined;
268
+ console.warn("onPropertyErrorInternal", {
269
+ id,
270
+ namespace,
271
+ error,
272
+ propertyPath
273
+ });
274
+ if (propertyPath) {
275
+ const hasError = error && Object.keys(error).length > 0;
276
+ onPropertyError(id, namespace, hasError ? error : undefined);
277
+ setFieldError(idToPropertiesPath(propertyPath), hasError ? "Property error" : undefined);
278
+ }
279
+ }, [])
280
+
281
+ const closePropertyDialog = () => {
282
+ setSelectedPropertyIndex(undefined);
283
+ setSelectedPropertyKey(undefined);
284
+ };
285
+
286
+ const initialErrors = selectedPropertyKey && propertyErrorsRef?.current?.properties ? propertyErrorsRef.current.properties[selectedPropertyKey] : undefined;
287
+
288
+ const emptyCollection = values?.propertiesOrder === undefined || values.propertiesOrder.length === 0;
289
+
290
+ const usedPropertiesOrder = (values.propertiesOrder
291
+ ? values.propertiesOrder
292
+ : Object.keys(values.properties)) as string[];
293
+
294
+ const owner = useMemo(() => getUser(values.ownerId), [getUser, values.ownerId]);
295
+ const body = (
296
+ <div className={"grid grid-cols-12 gap-2 h-full bg-gray-50 dark:bg-gray-900"}>
297
+ <div className={cn(
298
+ "p-4 md:p-8 pb-20 md:pb-20",
299
+ "col-span-12 lg:col-span-5 h-full overflow-auto",
300
+ !asDialog && "border-r " + defaultBorderMixin
301
+ )}>
302
+
303
+ <div className="flex my-2">
304
+
305
+ <div className="flex-grow mb-4">
306
+
307
+ <Field
308
+ name={"name"}
309
+ as={DebouncedTextField}
310
+ invisible={true}
311
+ className="-ml-1"
312
+ inputClassName="text-2xl font-headers"
313
+ placeholder={"Collection name"}
314
+ size={"small"}
315
+ required
316
+ error={Boolean(errors?.name)}/>
317
+
318
+ {owner &&
319
+ <Typography variant={"body2"}
320
+ className={"ml-2"}
321
+ color={"secondary"}>
322
+ Created by {owner.displayName}
323
+ </Typography>}
324
+ </div>
325
+
326
+ {extraIcon && <div className="ml-4">
327
+ {extraIcon}
328
+ </div>}
329
+
330
+ <div className="ml-1 mt-2 flex flex-row gap-2">
331
+ <Tooltip title={"Get the code for this collection"}>
332
+ <IconButton
333
+ variant={"filled"}
334
+ disabled={inferringProperties}
335
+ onClick={() => setCodeDialogOpen(true)}>
336
+ <CodeIcon/>
337
+ </IconButton>
338
+ </Tooltip>
339
+ {inferPropertiesFromData && <Tooltip title={"Add new properties based on data"}>
340
+ <IconButton
341
+ variant={"filled"}
342
+ disabled={inferringProperties}
343
+ onClick={inferPropertiesFromData}>
344
+ {inferringProperties ? <CircularProgress size={"small"}/> : <AutoAwesomeIcon/>}
345
+ </IconButton>
346
+ </Tooltip>}
347
+ <Tooltip title={"Add new property"}>
348
+ <Button
349
+ variant={"outlined"}
350
+ onClick={() => setNewPropertyDialogOpen(true)}>
351
+ <AddIcon/>
352
+ </Button>
353
+ </Tooltip>
354
+ </div>
355
+ </div>
356
+
357
+ <ErrorBoundary>
358
+ <PropertyTree
359
+ className={"pl-8"}
360
+ onPropertyClick={(propertyKey, namespace) => {
361
+ setSelectedPropertyIndex(usedPropertiesOrder.indexOf(propertyKey));
362
+ setSelectedPropertyKey(propertyKey);
363
+ setSelectedPropertyNamespace(namespace);
364
+ }}
365
+ inferredPropertyKeys={inferredPropertyKeys}
366
+ selectedPropertyKey={selectedPropertyKey ? getFullId(selectedPropertyKey, selectedPropertyNamespace) : undefined}
367
+ properties={values.properties}
368
+ additionalFields={values.additionalFields}
369
+ propertiesOrder={usedPropertiesOrder}
370
+ onPropertyMove={onPropertyMove}
371
+ onPropertyRemove={isNewCollection ? deleteProperty : undefined}
372
+ collectionEditable={collectionEditable}
373
+ errors={showErrors ? errors : {}}/>
374
+ </ErrorBoundary>
375
+
376
+ <Button className={"mt-8 w-full"}
377
+ color="primary"
378
+ variant={"outlined"}
379
+ size={"large"}
380
+ onClick={() => setNewPropertyDialogOpen(true)}
381
+ startIcon={<AddIcon/>}>
382
+ Add new property
383
+ </Button>
384
+ </div>
385
+
386
+ {!asDialog &&
387
+ <div className={"col-span-12 lg:col-span-7 ml-2 p-4 md:p-8 h-full overflow-auto pb-20 md:pb-20"}>
388
+ <Paper
389
+ className="sticky top-8 p-4 min-h-full border border-transparent w-full flex flex-col justify-center ">
390
+
391
+ {selectedPropertyFullId &&
392
+ selectedProperty &&
393
+ !isPropertyBuilder(selectedProperty) &&
394
+ <PropertyForm
395
+ inArray={false}
396
+ key={`edit_view_${selectedPropertyIndex}`}
397
+ existingProperty={!isNewCollection}
398
+ autoUpdateId={false}
399
+ allowDataInference={!isNewCollection}
400
+ autoOpenTypeSelect={false}
401
+ propertyKey={selectedPropertyKey}
402
+ propertyNamespace={selectedPropertyNamespace}
403
+ property={selectedProperty}
404
+ onPropertyChanged={onPropertyChanged}
405
+ onDelete={deleteProperty}
406
+ onError={onPropertyErrorInternal}
407
+ forceShowErrors={showErrors}
408
+ initialErrors={initialErrors}
409
+ getData={getData}
410
+ propertyConfigs={propertyConfigs}
411
+ collectionEditable={collectionEditable}
412
+ />}
413
+
414
+ {!selectedProperty &&
415
+ <Typography variant={"label"} className="flex items-center justify-center h-full">
416
+ {emptyCollection
417
+ ? "Now you can add your first property"
418
+ : "Select a property to edit it"}
419
+ </Typography>}
420
+
421
+ {selectedProperty && isPropertyBuilder(selectedProperty) &&
422
+ <Typography variant={"label"} className="flex items-center justify-center">
423
+ {"This property is defined as a property builder in code"}
424
+ </Typography>}
425
+ </Paper>
426
+ </div>}
427
+
428
+ {asDialog && <PropertyFormDialog
429
+ inArray={false}
430
+ open={selectedPropertyIndex !== undefined}
431
+ key={`edit_view_${selectedPropertyIndex}`}
432
+ autoUpdateId={!selectedProperty}
433
+ allowDataInference={!isNewCollection}
434
+ existingProperty={true}
435
+ autoOpenTypeSelect={false}
436
+ propertyKey={selectedPropertyKey}
437
+ propertyNamespace={selectedPropertyNamespace}
438
+ property={selectedProperty}
439
+ onPropertyChanged={onPropertyChanged}
440
+ onDelete={deleteProperty}
441
+ onError={onPropertyErrorInternal}
442
+ forceShowErrors={showErrors}
443
+ initialErrors={initialErrors}
444
+ getData={getData}
445
+ propertyConfigs={propertyConfigs}
446
+ collectionEditable={collectionEditable}
447
+ onOkClicked={asDialog
448
+ ? closePropertyDialog
449
+ : undefined
450
+ }/>}
451
+
452
+ </div>);
453
+
454
+ return (<>
455
+
456
+ {body}
457
+
458
+ {/* This is the dialog used for new properties*/}
459
+ <PropertyFormDialog
460
+ inArray={false}
461
+ existingProperty={false}
462
+ autoOpenTypeSelect={true}
463
+ autoUpdateId={true}
464
+ forceShowErrors={showErrors}
465
+ open={newPropertyDialogOpen}
466
+ onCancel={() => setNewPropertyDialogOpen(false)}
467
+ onPropertyChanged={onPropertyCreated}
468
+ getData={getData}
469
+ allowDataInference={!isNewCollection}
470
+ propertyConfigs={propertyConfigs}
471
+ collectionEditable={collectionEditable}
472
+ existingPropertyKeys={values.propertiesOrder as string[]}/>
473
+
474
+ <GetCodeDialog
475
+ collection={values}
476
+ open={codeDialogOpen}
477
+ onOpenChange={setCodeDialogOpen}/>
478
+
479
+ </>
480
+ );
481
+ }