@firecms/collection_editor 3.0.0-canary.99 → 3.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -1
- package/dist/ConfigControllerProvider.d.ts +0 -1
- package/dist/index.es.js +9957 -4962
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9949 -4958
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collection_editor_controller.d.ts +0 -1
- package/dist/types/collection_inference.d.ts +4 -1
- package/dist/types/config_controller.d.ts +3 -1
- package/dist/types/config_permissions.d.ts +2 -2
- package/dist/types/persisted_collection.d.ts +1 -1
- package/dist/ui/EditorEntityAction.d.ts +2 -0
- package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +3 -1
- package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +1 -1
- package/dist/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +1 -1
- package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +4 -0
- package/dist/ui/collection_editor/EntityActionsSelectDialog.d.ts +4 -0
- package/dist/ui/collection_editor/LayoutModeSwitch.d.ts +5 -0
- package/dist/ui/collection_editor/PropertyEditView.d.ts +8 -0
- package/dist/ui/collection_editor/PropertyTree.d.ts +2 -3
- package/dist/ui/collection_editor/import/CollectionEditorImportDataPreview.d.ts +1 -1
- package/dist/ui/collection_editor/import/CollectionEditorImportMapping.d.ts +8 -1
- package/dist/ui/collection_editor/import/clean_import_data.d.ts +1 -1
- package/dist/ui/collection_editor/properties/MarkdownPropertyField.d.ts +4 -0
- package/dist/ui/collection_editor/properties/ReferencePropertyField.d.ts +2 -1
- package/dist/ui/collection_editor/properties/StringPropertyField.d.ts +1 -1
- package/dist/useCollectionEditorPlugin.d.ts +6 -6
- package/dist/utils/collections.d.ts +1 -1
- package/package.json +25 -23
- package/src/ConfigControllerProvider.tsx +3 -8
- package/src/types/collection_editor_controller.tsx +1 -2
- package/src/types/collection_inference.ts +4 -1
- package/src/types/config_controller.tsx +4 -1
- package/src/types/config_permissions.ts +1 -1
- package/src/types/persisted_collection.ts +2 -3
- package/src/ui/CollectionViewHeaderAction.tsx +4 -2
- package/src/ui/EditorCollectionAction.tsx +3 -7
- package/src/ui/EditorCollectionActionStart.tsx +1 -1
- package/src/ui/EditorEntityAction.tsx +51 -0
- package/src/ui/HomePageEditorCollectionAction.tsx +5 -3
- package/src/ui/NewCollectionButton.tsx +1 -1
- package/src/ui/PropertyAddColumnComponent.tsx +5 -3
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +126 -49
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +71 -16
- package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +20 -29
- package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +19 -17
- package/src/ui/collection_editor/EntityActionsEditTab.tsx +163 -0
- package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +41 -0
- package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +11 -7
- package/src/ui/collection_editor/EnumForm.tsx +11 -7
- package/src/ui/collection_editor/GetCodeDialog.tsx +46 -26
- package/src/ui/collection_editor/LayoutModeSwitch.tsx +54 -0
- package/src/ui/collection_editor/PropertyEditView.tsx +263 -76
- package/src/ui/collection_editor/PropertyFieldPreview.tsx +7 -6
- package/src/ui/collection_editor/PropertyTree.tsx +184 -138
- package/src/ui/collection_editor/SubcollectionsEditTab.tsx +24 -17
- package/src/ui/collection_editor/UnsavedChangesDialog.tsx +9 -7
- package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +20 -4
- package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +34 -3
- package/src/ui/collection_editor/import/clean_import_data.ts +1 -1
- package/src/ui/collection_editor/properties/BlockPropertyField.tsx +18 -12
- package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +54 -47
- package/src/ui/collection_editor/properties/EnumPropertyField.tsx +2 -0
- package/src/ui/collection_editor/properties/MapPropertyField.tsx +3 -2
- package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +139 -0
- package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +7 -3
- package/src/ui/collection_editor/properties/StoragePropertyField.tsx +13 -18
- package/src/ui/collection_editor/properties/StringPropertyField.tsx +4 -9
- package/src/ui/collection_editor/properties/UrlPropertyField.tsx +1 -0
- package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +2 -0
- package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +2 -2
- package/src/ui/collection_editor/templates/pages_template.ts +1 -6
- package/src/ui/collection_editor/utils/strings.ts +13 -6
- package/src/ui/collection_editor/utils/supported_fields.tsx +2 -0
- package/src/ui/collection_editor/utils/update_property_for_widget.ts +37 -6
- package/src/useCollectionEditorPlugin.tsx +20 -15
- package/src/utils/collections.ts +23 -7
- package/dist/ui/collection_editor/PropertySelectItem.d.ts +0 -8
- package/src/ui/collection_editor/PropertySelectItem.tsx +0 -32
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { EntityCollection, useSnackbarController } from "@firecms/core";
|
|
2
|
-
import { Button, ContentCopyIcon, Dialog, DialogActions, DialogContent,
|
|
1
|
+
import { EntityCollection, isEmptyObject, useSnackbarController } from "@firecms/core";
|
|
2
|
+
import { Button, ContentCopyIcon, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@firecms/ui";
|
|
3
3
|
import React 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
|
+
import { clone } from "@firecms/formex";
|
|
7
8
|
|
|
8
9
|
export function GetCodeDialog({
|
|
9
10
|
collection,
|
|
@@ -14,21 +15,20 @@ export function GetCodeDialog({
|
|
|
14
15
|
const snackbarController = useSnackbarController();
|
|
15
16
|
|
|
16
17
|
const code = collection
|
|
17
|
-
? "import { EntityCollection } from \"firecms\";\n\nconst " + (collection?.name ? camelCase(collection.name) : "my") + "Collection:EntityCollection = " + JSON5.stringify(collectionToCode(collection), null, "\t")
|
|
18
|
+
? "import { EntityCollection } from \"@firecms/core\";\n\nconst " + (collection?.name ? camelCase(collection.name) : "my") + "Collection:EntityCollection = " + JSON5.stringify(collectionToCode({ ...collection }), null, "\t")
|
|
18
19
|
: "No collection selected";
|
|
19
20
|
return <Dialog open={open}
|
|
20
21
|
onOpenChange={onOpenChange}
|
|
21
22
|
maxWidth={"4xl"}>
|
|
23
|
+
<DialogTitle variant={"h6"}>Code for {collection.name}</DialogTitle>
|
|
22
24
|
<DialogContent>
|
|
23
|
-
|
|
24
|
-
Code for {collection.name}
|
|
25
|
-
</Typography>
|
|
25
|
+
|
|
26
26
|
<Typography variant={"body2"} className={"my-4 mb-8"}>
|
|
27
27
|
If you want to customise the collection in code, you can add this collection code to your CMS
|
|
28
28
|
app configuration.
|
|
29
29
|
More info in the <a
|
|
30
30
|
rel="noopener noreferrer"
|
|
31
|
-
href={"https://firecms.co/docs/
|
|
31
|
+
href={"https://firecms.co/docs/cloud/quickstart"}>docs</a>.
|
|
32
32
|
</Typography>
|
|
33
33
|
<Highlight
|
|
34
34
|
theme={themes.vsDark}
|
|
@@ -59,12 +59,13 @@ export function GetCodeDialog({
|
|
|
59
59
|
<Button
|
|
60
60
|
variant={"text"}
|
|
61
61
|
size={"small"}
|
|
62
|
+
color={"primary"}
|
|
62
63
|
onClick={(e) => {
|
|
63
64
|
e.stopPropagation();
|
|
64
65
|
e.preventDefault();
|
|
65
66
|
snackbarController.open({
|
|
66
67
|
type: "success",
|
|
67
|
-
message:
|
|
68
|
+
message: "Copied"
|
|
68
69
|
})
|
|
69
70
|
return navigator.clipboard.writeText(code);
|
|
70
71
|
}}>
|
|
@@ -78,24 +79,40 @@ export function GetCodeDialog({
|
|
|
78
79
|
|
|
79
80
|
function collectionToCode(collection: EntityCollection): object {
|
|
80
81
|
|
|
81
|
-
const propertyCleanup = (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
const propertyCleanup = (value: any): any => {
|
|
83
|
+
if (value === undefined || value === null) {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
const valueCopy = clone(value);
|
|
87
|
+
if (typeof valueCopy === "function") {
|
|
88
|
+
return valueCopy;
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(valueCopy)) {
|
|
91
|
+
return valueCopy.map((v: any) => propertyCleanup(v));
|
|
92
|
+
}
|
|
93
|
+
if (typeof valueCopy === "object") {
|
|
94
|
+
if (valueCopy === null)
|
|
95
|
+
return valueCopy;
|
|
96
|
+
Object.keys(valueCopy).forEach((key) => {
|
|
97
|
+
if (!isEmptyObject(valueCopy)) {
|
|
98
|
+
const childRes = propertyCleanup(valueCopy[key]);
|
|
99
|
+
if (childRes !== null && childRes !== undefined && childRes !== false && !isEmptyObject(childRes)) {
|
|
100
|
+
valueCopy[key] = childRes;
|
|
101
|
+
} else {
|
|
102
|
+
delete valueCopy[key];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
delete valueCopy.fromBuilder;
|
|
107
|
+
delete valueCopy.resolved;
|
|
108
|
+
delete valueCopy.propertiesOrder;
|
|
109
|
+
delete valueCopy.propertyConfig;
|
|
110
|
+
delete valueCopy.resolvedProperties;
|
|
111
|
+
delete valueCopy.editable;
|
|
91
112
|
|
|
92
|
-
if (updatedProperty.type === "map") {
|
|
93
|
-
return {
|
|
94
|
-
...updatedProperty,
|
|
95
|
-
properties: updatedProperty.properties.map(propertyCleanup)
|
|
96
|
-
}
|
|
97
113
|
}
|
|
98
|
-
|
|
114
|
+
|
|
115
|
+
return valueCopy;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
return {
|
|
@@ -111,11 +128,14 @@ function collectionToCode(collection: EntityCollection): object {
|
|
|
111
128
|
customId: collection.customId,
|
|
112
129
|
initialFilter: collection.initialFilter,
|
|
113
130
|
initialSort: collection.initialSort,
|
|
114
|
-
properties: Object.entries(
|
|
131
|
+
properties: Object.entries({
|
|
132
|
+
...(collection.properties ?? {})
|
|
133
|
+
})
|
|
115
134
|
.map(([key, value]) => ({
|
|
116
135
|
[key]: propertyCleanup(value)
|
|
117
136
|
}))
|
|
118
|
-
.reduce((a, b) => ({ ...a,
|
|
137
|
+
.reduce((a, b) => ({ ...a,
|
|
138
|
+
...b }), {}),
|
|
119
139
|
subcollections: (collection.subcollections ?? []).map(collectionToCode)
|
|
120
140
|
}
|
|
121
141
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Card, cls, SquareIcon, Tooltip, Typography, VerticalSplitIcon } from "@firecms/ui";
|
|
2
|
+
|
|
3
|
+
export function LayoutModeSwitch({
|
|
4
|
+
value,
|
|
5
|
+
onChange,
|
|
6
|
+
className
|
|
7
|
+
}: {
|
|
8
|
+
value: "side_panel" | "full_screen";
|
|
9
|
+
onChange: (value: "side_panel" | "full_screen") => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}) {
|
|
12
|
+
|
|
13
|
+
return <div className={cls(className)}>
|
|
14
|
+
<Typography variant={"label"} color={"secondary"} className={"ml-3.5"}>Document view</Typography>
|
|
15
|
+
<div className={cls("flex flex-row gap-4")}>
|
|
16
|
+
|
|
17
|
+
<Tooltip title={"Documents are open in a side panel"}>
|
|
18
|
+
<Card
|
|
19
|
+
onClick={() => onChange("side_panel")}
|
|
20
|
+
className={cls(
|
|
21
|
+
"my-2 rounded-md mx-0 p-4 focus:outline-none transition ease-in-out duration-150 flex flex-row gap-4 items-center",
|
|
22
|
+
"text-surface-700 dark:text-surface-accent-300",
|
|
23
|
+
"hover:text-primary-dark dark:hover:text-primary focus:ring-primary hover:ring-1 hover:ring-primary",
|
|
24
|
+
value === "side_panel" ? "border-primary dark:border-primary" : "border-surface-400 dark:border-surface-600",
|
|
25
|
+
)}
|
|
26
|
+
>
|
|
27
|
+
<VerticalSplitIcon/>
|
|
28
|
+
<Typography variant={"label"}>
|
|
29
|
+
Side panel
|
|
30
|
+
</Typography>
|
|
31
|
+
</Card>
|
|
32
|
+
</Tooltip>
|
|
33
|
+
|
|
34
|
+
<Tooltip title={"Documents are open full-screen"}>
|
|
35
|
+
<Card
|
|
36
|
+
onClick={() => onChange("full_screen")}
|
|
37
|
+
className={cls(
|
|
38
|
+
"my-2 rounded-md mx-0 p-4 focus:outline-none transition ease-in-out duration-150 flex flex-row gap-4 items-center",
|
|
39
|
+
"text-surface-700 dark:text-surface-accent-300",
|
|
40
|
+
"hover:text-primary-dark dark:hover:text-primary focus:ring-primary hover:ring-1 hover:ring-primary",
|
|
41
|
+
value === "full_screen" ? "border-primary dark:border-primary" : "border-surface-400 dark:border-surface-600",
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
<SquareIcon/>
|
|
45
|
+
<Typography variant={"label"}>
|
|
46
|
+
Full screen
|
|
47
|
+
</Typography>
|
|
48
|
+
</Card>
|
|
49
|
+
</Tooltip>
|
|
50
|
+
|
|
51
|
+
</div>
|
|
52
|
+
<Typography variant={"caption"} color={"secondary"} className={"ml-3.5"}>Should documents be opened full screen or in an inline side dialog</Typography>
|
|
53
|
+
</div>
|
|
54
|
+
}
|
|
@@ -3,9 +3,8 @@ import equal from "react-fast-compare"
|
|
|
3
3
|
|
|
4
4
|
import { Formex, FormexController, getIn, useCreateFormex } from "@firecms/formex";
|
|
5
5
|
import {
|
|
6
|
+
ConfirmationDialog,
|
|
6
7
|
DEFAULT_FIELD_CONFIGS,
|
|
7
|
-
DeleteConfirmationDialog,
|
|
8
|
-
PropertyConfigId,
|
|
9
8
|
getFieldConfig,
|
|
10
9
|
getFieldId,
|
|
11
10
|
isPropertyBuilder,
|
|
@@ -14,18 +13,25 @@ import {
|
|
|
14
13
|
Property,
|
|
15
14
|
PropertyConfig,
|
|
16
15
|
PropertyConfigBadge,
|
|
16
|
+
PropertyConfigId,
|
|
17
17
|
} from "@firecms/core";
|
|
18
18
|
import {
|
|
19
19
|
Button,
|
|
20
|
+
Card,
|
|
20
21
|
cls,
|
|
21
22
|
DeleteIcon,
|
|
22
23
|
Dialog,
|
|
23
24
|
DialogActions,
|
|
24
25
|
DialogContent,
|
|
26
|
+
DialogTitle,
|
|
27
|
+
fieldBackgroundDisabledMixin,
|
|
28
|
+
fieldBackgroundHoverMixin,
|
|
29
|
+
fieldBackgroundMixin,
|
|
25
30
|
IconButton,
|
|
26
31
|
InfoLabel,
|
|
27
|
-
|
|
28
|
-
Typography
|
|
32
|
+
Tooltip,
|
|
33
|
+
Typography,
|
|
34
|
+
WarningIcon
|
|
29
35
|
} from "@firecms/ui";
|
|
30
36
|
import { EnumPropertyField } from "./properties/EnumPropertyField";
|
|
31
37
|
import { StoragePropertyField } from "./properties/StoragePropertyField";
|
|
@@ -42,9 +48,9 @@ import { AdvancedPropertyValidation } from "./properties/advanced/AdvancedProper
|
|
|
42
48
|
import { editableProperty } from "../../utils/entities";
|
|
43
49
|
import { KeyValuePropertyField } from "./properties/KeyValuePropertyField";
|
|
44
50
|
import { updatePropertyFromWidget } from "./utils/update_property_for_widget";
|
|
45
|
-
import { PropertySelectItem } from "./PropertySelectItem";
|
|
46
51
|
import { UrlPropertyField } from "./properties/UrlPropertyField";
|
|
47
52
|
import { supportedFields } from "./utils/supported_fields";
|
|
53
|
+
import { MarkdownPropertyField } from "./properties/MarkdownPropertyField";
|
|
48
54
|
|
|
49
55
|
export type PropertyWithId = Property & {
|
|
50
56
|
id?: string
|
|
@@ -68,6 +74,7 @@ export type PropertyFormProps = {
|
|
|
68
74
|
property?: Property;
|
|
69
75
|
onPropertyChanged?: (params: OnPropertyChangedParams) => void;
|
|
70
76
|
onPropertyChangedImmediate?: boolean;
|
|
77
|
+
onDismiss?: () => void;
|
|
71
78
|
onDelete?: (id?: string, namespace?: string) => void;
|
|
72
79
|
onError?: (id: string, namespace?: string, error?: Record<string, any>) => void;
|
|
73
80
|
initialErrors?: Record<string, any>;
|
|
@@ -95,6 +102,7 @@ export const PropertyForm = React.memo(
|
|
|
95
102
|
property,
|
|
96
103
|
onPropertyChanged,
|
|
97
104
|
onPropertyChangedImmediate = true,
|
|
105
|
+
onDismiss,
|
|
98
106
|
onDelete,
|
|
99
107
|
onError,
|
|
100
108
|
initialErrors,
|
|
@@ -134,6 +142,7 @@ export const PropertyForm = React.memo(
|
|
|
134
142
|
};
|
|
135
143
|
|
|
136
144
|
const formexController = useCreateFormex<PropertyWithId>({
|
|
145
|
+
debugId: "PROPERTY_FORM",
|
|
137
146
|
initialValues: property
|
|
138
147
|
? { id: propertyKey, ...property } as PropertyWithId
|
|
139
148
|
: initialValue,
|
|
@@ -141,14 +150,16 @@ export const PropertyForm = React.memo(
|
|
|
141
150
|
validateOnChange: true,
|
|
142
151
|
validateOnInitialRender: true,
|
|
143
152
|
onSubmit: (newPropertyWithId, controller) => {
|
|
144
|
-
console.debug("onSubmit", newPropertyWithId);
|
|
145
153
|
const {
|
|
146
154
|
id,
|
|
147
155
|
...property
|
|
148
156
|
} = newPropertyWithId;
|
|
149
157
|
doOnPropertyChanged({
|
|
150
158
|
id,
|
|
151
|
-
property: {
|
|
159
|
+
property: {
|
|
160
|
+
...property,
|
|
161
|
+
editable: property.editable ?? true
|
|
162
|
+
}
|
|
152
163
|
});
|
|
153
164
|
if (!existingProperty)
|
|
154
165
|
controller.resetForm({ values: initialValue });
|
|
@@ -209,6 +220,7 @@ export const PropertyForm = React.memo(
|
|
|
209
220
|
includeIdAndTitle={includeIdAndName}
|
|
210
221
|
propertyNamespace={propertyNamespace}
|
|
211
222
|
onError={onError}
|
|
223
|
+
onDismiss={onDismiss}
|
|
212
224
|
showErrors={forceShowErrors || formexController.submitCount > 0}
|
|
213
225
|
existing={existingProperty}
|
|
214
226
|
autoUpdateId={autoUpdateId}
|
|
@@ -262,8 +274,11 @@ export function PropertyFormDialog({
|
|
|
262
274
|
e.stopPropagation();
|
|
263
275
|
formexRef.current?.handleSubmit(e)
|
|
264
276
|
}}>
|
|
277
|
+
<DialogTitle hidden>Property edit view</DialogTitle>
|
|
265
278
|
<DialogContent>
|
|
279
|
+
|
|
266
280
|
<PropertyForm {...formProps}
|
|
281
|
+
onDismiss={onCancel}
|
|
267
282
|
onPropertyChanged={(params) => {
|
|
268
283
|
onPropertyChanged?.(params);
|
|
269
284
|
onOkClicked?.();
|
|
@@ -279,6 +294,7 @@ export function PropertyFormDialog({
|
|
|
279
294
|
|
|
280
295
|
{onCancel && <Button
|
|
281
296
|
variant={"text"}
|
|
297
|
+
color={"primary"}
|
|
282
298
|
onClick={() => {
|
|
283
299
|
onCancel();
|
|
284
300
|
formexRef.current?.resetForm();
|
|
@@ -308,6 +324,7 @@ function PropertyEditFormFields({
|
|
|
308
324
|
onPropertyChanged,
|
|
309
325
|
onDelete,
|
|
310
326
|
propertyNamespace,
|
|
327
|
+
onDismiss,
|
|
311
328
|
onError,
|
|
312
329
|
showErrors,
|
|
313
330
|
disabled,
|
|
@@ -322,6 +339,7 @@ function PropertyEditFormFields({
|
|
|
322
339
|
autoUpdateId?: boolean;
|
|
323
340
|
autoOpenTypeSelect: boolean;
|
|
324
341
|
propertyNamespace?: string;
|
|
342
|
+
onDismiss?: () => void;
|
|
325
343
|
onPropertyChanged?: (params: OnPropertyChangedParams) => void;
|
|
326
344
|
onDelete?: (id?: string, namespace?: string) => void;
|
|
327
345
|
onError?: (id: string, namespace?: string, error?: Record<string, any>) => void;
|
|
@@ -338,12 +356,6 @@ function PropertyEditFormFields({
|
|
|
338
356
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
339
357
|
const [selectedFieldConfigId, setSelectedFieldConfigId] = useState<string | undefined>(values?.dataType ? getFieldId(values) : undefined);
|
|
340
358
|
|
|
341
|
-
const allSupportedFields = Object.entries(supportedFields).concat(Object.entries(propertyConfigs));
|
|
342
|
-
|
|
343
|
-
const displayedWidgets = inArray
|
|
344
|
-
? allSupportedFields.filter(([_, propertyConfig]) => !isPropertyBuilder(propertyConfig.property) && propertyConfig.property?.dataType !== "array")
|
|
345
|
-
: allSupportedFields;
|
|
346
|
-
|
|
347
359
|
const deferredValues = useDeferredValue(values);
|
|
348
360
|
const nameFieldRef = useRef<HTMLInputElement>(null);
|
|
349
361
|
|
|
@@ -388,7 +400,7 @@ function PropertyEditFormFields({
|
|
|
388
400
|
let childComponent;
|
|
389
401
|
if (selectedFieldConfigId === "text_field" ||
|
|
390
402
|
selectedFieldConfigId === "multiline" ||
|
|
391
|
-
selectedFieldConfigId === "
|
|
403
|
+
selectedFieldConfigId === "user_select" ||
|
|
392
404
|
selectedFieldConfigId === "email") {
|
|
393
405
|
childComponent =
|
|
394
406
|
<StringPropertyField widgetId={selectedFieldConfigId}
|
|
@@ -398,6 +410,10 @@ function PropertyEditFormFields({
|
|
|
398
410
|
childComponent =
|
|
399
411
|
<UrlPropertyField disabled={disabled}
|
|
400
412
|
showErrors={showErrors}/>;
|
|
413
|
+
} else if (selectedFieldConfigId === "markdown") {
|
|
414
|
+
childComponent =
|
|
415
|
+
<MarkdownPropertyField disabled={disabled}
|
|
416
|
+
showErrors={showErrors}/>;
|
|
401
417
|
} else if (selectedFieldConfigId === "select" ||
|
|
402
418
|
selectedFieldConfigId === "number_select") {
|
|
403
419
|
childComponent = <EnumPropertyField
|
|
@@ -446,6 +462,13 @@ function PropertyEditFormFields({
|
|
|
446
462
|
existing={existing}
|
|
447
463
|
multiple={false}
|
|
448
464
|
disabled={disabled}/>;
|
|
465
|
+
} else if (selectedFieldConfigId === "reference_as_string") {
|
|
466
|
+
childComponent =
|
|
467
|
+
<ReferencePropertyField showErrors={showErrors}
|
|
468
|
+
existing={existing}
|
|
469
|
+
asString={true}
|
|
470
|
+
multiple={false}
|
|
471
|
+
disabled={disabled}/>;
|
|
449
472
|
} else if (selectedFieldConfigId === "date_time") {
|
|
450
473
|
childComponent = <DateTimePropertyField disabled={disabled}/>;
|
|
451
474
|
} else if (selectedFieldConfigId === "multi_references") {
|
|
@@ -482,62 +505,22 @@ function PropertyEditFormFields({
|
|
|
482
505
|
|
|
483
506
|
<div className="flex mt-2 justify-between">
|
|
484
507
|
<div className={"w-full flex flex-col gap-2"}>
|
|
485
|
-
<
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
placeholder={"Select a property widget"}
|
|
508
|
+
<WidgetSelectView
|
|
509
|
+
initialProperty={values}
|
|
510
|
+
value={selectedFieldConfigId as PropertyConfigId}
|
|
511
|
+
onValueChange={(value) => onWidgetSelectChanged(value as PropertyConfigId)}
|
|
490
512
|
open={selectOpen}
|
|
491
|
-
onOpenChange={
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
renderValue={(value) => {
|
|
495
|
-
if (!value) {
|
|
496
|
-
return <em>Select a property
|
|
497
|
-
widget</em>;
|
|
513
|
+
onOpenChange={(open, hasValue) => {
|
|
514
|
+
if (!hasValue) {
|
|
515
|
+
onDismiss?.();
|
|
498
516
|
}
|
|
499
|
-
|
|
500
|
-
const propertyConfig = DEFAULT_FIELD_CONFIGS[key] ?? propertyConfigs[key];
|
|
501
|
-
const baseProperty = propertyConfig.property;
|
|
502
|
-
const baseFieldConfig = baseProperty && !isPropertyBuilder(baseProperty) ? getFieldConfig(baseProperty, propertyConfigs) : undefined;
|
|
503
|
-
const optionDisabled = isPropertyBuilder(baseProperty) || (existing && baseProperty.dataType !== values?.dataType);
|
|
504
|
-
const computedFieldConfig = baseFieldConfig ? mergeDeep(baseFieldConfig, propertyConfig) : propertyConfig;
|
|
505
|
-
return <div
|
|
506
|
-
onClick={(e) => {
|
|
507
|
-
if (optionDisabled) {
|
|
508
|
-
e.stopPropagation();
|
|
509
|
-
e.preventDefault();
|
|
510
|
-
}
|
|
511
|
-
}}
|
|
512
|
-
className={cls(
|
|
513
|
-
"flex items-center",
|
|
514
|
-
optionDisabled ? "w-full pointer-events-none opacity-50" : "")}>
|
|
515
|
-
<div className={"mr-8"}>
|
|
516
|
-
<PropertyConfigBadge propertyConfig={computedFieldConfig}/>
|
|
517
|
-
</div>
|
|
518
|
-
<div className={"flex flex-col items-start text-base text-left"}>
|
|
519
|
-
<div>{computedFieldConfig.name}</div>
|
|
520
|
-
<Typography variant={"caption"}
|
|
521
|
-
color={"disabled"}>
|
|
522
|
-
{optionDisabled ? "You can only switch to widgets that use the same data type" : computedFieldConfig.description}
|
|
523
|
-
</Typography>
|
|
524
|
-
</div>
|
|
525
|
-
</div>
|
|
517
|
+
setSelectOpen(open);
|
|
526
518
|
}}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
{
|
|
531
|
-
|
|
532
|
-
const optionDisabled = existing && !isPropertyBuilder(baseProperty) && baseProperty.dataType !== values?.dataType;
|
|
533
|
-
return <PropertySelectItem
|
|
534
|
-
key={key}
|
|
535
|
-
value={key}
|
|
536
|
-
optionDisabled={optionDisabled}
|
|
537
|
-
propertyConfig={propertyConfig}
|
|
538
|
-
existing={existing}/>;
|
|
539
|
-
})}
|
|
540
|
-
</Select>
|
|
519
|
+
disabled={disabled}
|
|
520
|
+
showError={Boolean(selectedWidgetError)}
|
|
521
|
+
existing={existing}
|
|
522
|
+
propertyConfigs={propertyConfigs}
|
|
523
|
+
inArray={inArray}/>
|
|
541
524
|
|
|
542
525
|
{selectedWidgetError &&
|
|
543
526
|
<Typography variant="caption"
|
|
@@ -576,15 +559,15 @@ function PropertyEditFormFields({
|
|
|
576
559
|
</div>
|
|
577
560
|
|
|
578
561
|
{onDelete &&
|
|
579
|
-
<
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
562
|
+
<ConfirmationDialog open={deleteDialogOpen}
|
|
563
|
+
onAccept={() => onDelete(values?.id, propertyNamespace)}
|
|
564
|
+
onCancel={() => setDeleteDialogOpen(false)}
|
|
565
|
+
title={<div>Delete this property?</div>}
|
|
566
|
+
body={
|
|
567
|
+
<div> This will <b>not delete any
|
|
568
|
+
data</b>, only modify the
|
|
569
|
+
collection.</div>
|
|
570
|
+
}/>}
|
|
588
571
|
|
|
589
572
|
</>
|
|
590
573
|
);
|
|
@@ -614,3 +597,207 @@ function validateName(value: string) {
|
|
|
614
597
|
}
|
|
615
598
|
return error;
|
|
616
599
|
}
|
|
600
|
+
|
|
601
|
+
const WIDGET_TYPE_MAP: Record<PropertyConfigId, string> = {
|
|
602
|
+
text_field: "Text",
|
|
603
|
+
multiline: "Text",
|
|
604
|
+
markdown: "Text",
|
|
605
|
+
url: "Text",
|
|
606
|
+
email: "Text",
|
|
607
|
+
switch: "Boolean",
|
|
608
|
+
user_select: "Users",
|
|
609
|
+
select: "Select",
|
|
610
|
+
multi_select: "Select",
|
|
611
|
+
number_input: "Number",
|
|
612
|
+
number_select: "Select",
|
|
613
|
+
multi_number_select: "Select",
|
|
614
|
+
file_upload: "File",
|
|
615
|
+
multi_file_upload: "File",
|
|
616
|
+
reference: "Reference",
|
|
617
|
+
reference_as_string: "Text",
|
|
618
|
+
multi_references: "Reference",
|
|
619
|
+
date_time: "Date",
|
|
620
|
+
group: "Group",
|
|
621
|
+
key_value: "Group",
|
|
622
|
+
repeat: "Array",
|
|
623
|
+
custom_array: "Array",
|
|
624
|
+
block: "Group"
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
function WidgetSelectView({
|
|
628
|
+
initialProperty,
|
|
629
|
+
value,
|
|
630
|
+
onValueChange,
|
|
631
|
+
open,
|
|
632
|
+
onOpenChange,
|
|
633
|
+
disabled,
|
|
634
|
+
showError,
|
|
635
|
+
existing,
|
|
636
|
+
propertyConfigs,
|
|
637
|
+
inArray
|
|
638
|
+
}: {
|
|
639
|
+
initialProperty?: PropertyWithId,
|
|
640
|
+
value?: PropertyConfigId,
|
|
641
|
+
onValueChange: (value: string) => void,
|
|
642
|
+
showError: boolean,
|
|
643
|
+
open: boolean,
|
|
644
|
+
onOpenChange: (open: boolean, hasValue: boolean) => void,
|
|
645
|
+
disabled: boolean,
|
|
646
|
+
existing: boolean,
|
|
647
|
+
propertyConfigs: Record<string, PropertyConfig>,
|
|
648
|
+
inArray?: boolean
|
|
649
|
+
}) {
|
|
650
|
+
|
|
651
|
+
const allSupportedFields = Object.entries(supportedFields).concat(Object.entries(propertyConfigs));
|
|
652
|
+
|
|
653
|
+
const displayedWidgets = (inArray
|
|
654
|
+
? allSupportedFields.filter(([_, propertyConfig]) => !isPropertyBuilder(propertyConfig.property) && propertyConfig.property?.dataType !== "array")
|
|
655
|
+
: allSupportedFields)
|
|
656
|
+
.map(([key, propertyConfig]) => ({
|
|
657
|
+
[key]: propertyConfig
|
|
658
|
+
}))
|
|
659
|
+
.reduce((a, b) => {
|
|
660
|
+
return {
|
|
661
|
+
...a,
|
|
662
|
+
...b
|
|
663
|
+
}
|
|
664
|
+
}, {});
|
|
665
|
+
|
|
666
|
+
const key = value;
|
|
667
|
+
const propertyConfig = key ? (DEFAULT_FIELD_CONFIGS[key] ?? propertyConfigs[key]) : undefined;
|
|
668
|
+
const baseProperty = propertyConfig?.property;
|
|
669
|
+
const baseFieldConfig = baseProperty && !isPropertyBuilder(baseProperty) ? getFieldConfig(baseProperty, propertyConfigs) : undefined;
|
|
670
|
+
const computedFieldConfig = baseFieldConfig && propertyConfig ? mergeDeep(baseFieldConfig, propertyConfig) : propertyConfig;
|
|
671
|
+
|
|
672
|
+
const groups: string[] = [...new Set(Object.keys(displayedWidgets).map(key => {
|
|
673
|
+
const group = WIDGET_TYPE_MAP[key as PropertyConfigId];
|
|
674
|
+
if (group) {
|
|
675
|
+
return group;
|
|
676
|
+
}
|
|
677
|
+
return "Custom/Other"
|
|
678
|
+
}))];
|
|
679
|
+
|
|
680
|
+
return <>
|
|
681
|
+
<div
|
|
682
|
+
onClick={() => {
|
|
683
|
+
if (!disabled) {
|
|
684
|
+
onOpenChange(!open, Boolean(value));
|
|
685
|
+
}
|
|
686
|
+
}}
|
|
687
|
+
className={cls(
|
|
688
|
+
"select-none rounded-md text-sm p-4",
|
|
689
|
+
fieldBackgroundMixin,
|
|
690
|
+
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
691
|
+
"relative flex items-center",
|
|
692
|
+
)}>
|
|
693
|
+
{!value && <em>Select a property widget</em>}
|
|
694
|
+
{value && computedFieldConfig && <div
|
|
695
|
+
className={cls(
|
|
696
|
+
"flex items-center")}>
|
|
697
|
+
<div className={"mr-8"}>
|
|
698
|
+
<PropertyConfigBadge propertyConfig={computedFieldConfig}/>
|
|
699
|
+
</div>
|
|
700
|
+
<div className={"flex flex-col items-start text-base text-left"}>
|
|
701
|
+
<div>{computedFieldConfig.name}</div>
|
|
702
|
+
<Typography variant={"caption"}
|
|
703
|
+
color={"secondary"}>
|
|
704
|
+
{computedFieldConfig.description}
|
|
705
|
+
</Typography>
|
|
706
|
+
</div>
|
|
707
|
+
</div>}
|
|
708
|
+
</div>
|
|
709
|
+
<Dialog open={open}
|
|
710
|
+
onOpenChange={(open) => onOpenChange(open, Boolean(value))}
|
|
711
|
+
maxWidth={"4xl"}>
|
|
712
|
+
<DialogTitle>
|
|
713
|
+
Select a property widget
|
|
714
|
+
</DialogTitle>
|
|
715
|
+
<DialogContent>
|
|
716
|
+
<div>
|
|
717
|
+
{groups.map(group => {
|
|
718
|
+
return <div key={group} className={"mt-4"}>
|
|
719
|
+
<Typography variant={"label"}>{group}</Typography>
|
|
720
|
+
<div className={"grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2 mt-4"}>
|
|
721
|
+
{Object.entries(displayedWidgets).map(([key, propertyConfig]) => {
|
|
722
|
+
const groupKey = WIDGET_TYPE_MAP[key as PropertyConfigId];
|
|
723
|
+
if (groupKey === group) {
|
|
724
|
+
return <WidgetSelectViewItem
|
|
725
|
+
key={key}
|
|
726
|
+
initialProperty={initialProperty}
|
|
727
|
+
onClick={() => {
|
|
728
|
+
onValueChange(key);
|
|
729
|
+
onOpenChange(false, true);
|
|
730
|
+
}}
|
|
731
|
+
propertyConfig={propertyConfig}
|
|
732
|
+
existing={existing}/>;
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
})}
|
|
736
|
+
</div>
|
|
737
|
+
</div>;
|
|
738
|
+
})}
|
|
739
|
+
{/*{displayedWidgets.map(([key, propertyConfig]) => {*/}
|
|
740
|
+
{/* return <WidgetSelectViewItem*/}
|
|
741
|
+
{/* key={key}*/}
|
|
742
|
+
{/* initialProperty={initialProperty}*/}
|
|
743
|
+
{/* onClick={() => {*/}
|
|
744
|
+
{/* onValueChange(key);*/}
|
|
745
|
+
{/* onOpenChange(false);*/}
|
|
746
|
+
{/* }}*/}
|
|
747
|
+
{/* propertyConfig={propertyConfig}*/}
|
|
748
|
+
{/* existing={existing}/>;*/}
|
|
749
|
+
{/*})}*/}
|
|
750
|
+
</div>
|
|
751
|
+
</DialogContent>
|
|
752
|
+
</Dialog>
|
|
753
|
+
</>;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export interface PropertySelectItemProps {
|
|
757
|
+
onClick?: () => void;
|
|
758
|
+
initialProperty?: PropertyWithId;
|
|
759
|
+
propertyConfig: PropertyConfig;
|
|
760
|
+
existing: boolean;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export function WidgetSelectViewItem({
|
|
764
|
+
onClick,
|
|
765
|
+
initialProperty,
|
|
766
|
+
// optionDisabled,
|
|
767
|
+
propertyConfig,
|
|
768
|
+
existing
|
|
769
|
+
}: PropertySelectItemProps) {
|
|
770
|
+
const baseProperty = propertyConfig.property;
|
|
771
|
+
const shouldWarnChangingDataType = existing && !isPropertyBuilder(baseProperty) && baseProperty.dataType !== initialProperty?.dataType;
|
|
772
|
+
|
|
773
|
+
return <Card
|
|
774
|
+
onClick={onClick}
|
|
775
|
+
className={"flex flex-row items-center px-4 py-2 m-1"}>
|
|
776
|
+
<div
|
|
777
|
+
className={cls(
|
|
778
|
+
"flex flex-row items-center text-base min-h-[48px]",
|
|
779
|
+
)}>
|
|
780
|
+
<div className={"mr-8"}>
|
|
781
|
+
<PropertyConfigBadge propertyConfig={propertyConfig} disabled={shouldWarnChangingDataType}/>
|
|
782
|
+
</div>
|
|
783
|
+
<div>
|
|
784
|
+
<div className={"flex flex-row gap-2 items-center"}>
|
|
785
|
+
{shouldWarnChangingDataType && <Tooltip
|
|
786
|
+
title={"This widget uses a different data type than the initially selected widget. This can cause errors with existing data."}>
|
|
787
|
+
<WarningIcon size="smallest" className={"w-4"}/>
|
|
788
|
+
</Tooltip>}
|
|
789
|
+
<Typography
|
|
790
|
+
variant={"label"}
|
|
791
|
+
color={shouldWarnChangingDataType ? "secondary" : undefined}>{propertyConfig.name}</Typography>
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
<Typography variant={"caption"}
|
|
795
|
+
color={"secondary"}
|
|
796
|
+
className={"max-w-sm"}>
|
|
797
|
+
{propertyConfig.description}
|
|
798
|
+
</Typography>
|
|
799
|
+
|
|
800
|
+
</div>
|
|
801
|
+
</div>
|
|
802
|
+
</Card>
|
|
803
|
+
}
|