@firecms/collection_editor 3.0.0-beta.13 → 3.0.0-beta.15

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 (44) hide show
  1. package/README.md +165 -1
  2. package/dist/ConfigControllerProvider.d.ts +0 -1
  3. package/dist/index.es.js +1620 -948
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +1616 -947
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/types/collection_editor_controller.d.ts +0 -1
  8. package/dist/types/collection_inference.d.ts +3 -0
  9. package/dist/types/config_controller.d.ts +3 -1
  10. package/dist/ui/EditorEntityAction.d.ts +2 -0
  11. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +1 -1
  12. package/dist/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +1 -1
  13. package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +4 -0
  14. package/dist/ui/collection_editor/EntityActionsSelectDialog.d.ts +4 -0
  15. package/dist/ui/collection_editor/PropertyTree.d.ts +2 -3
  16. package/dist/ui/collection_editor/properties/ReferencePropertyField.d.ts +2 -1
  17. package/dist/useCollectionEditorPlugin.d.ts +3 -3
  18. package/package.json +8 -9
  19. package/src/ConfigControllerProvider.tsx +0 -5
  20. package/src/types/collection_editor_controller.tsx +0 -2
  21. package/src/types/collection_inference.ts +3 -0
  22. package/src/types/config_controller.tsx +4 -1
  23. package/src/ui/EditorCollectionAction.tsx +2 -7
  24. package/src/ui/EditorEntityAction.tsx +51 -0
  25. package/src/ui/HomePageEditorCollectionAction.tsx +2 -1
  26. package/src/ui/collection_editor/CollectionDetailsForm.tsx +69 -37
  27. package/src/ui/collection_editor/CollectionEditorDialog.tsx +18 -5
  28. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +15 -25
  29. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +8 -6
  30. package/src/ui/collection_editor/EntityActionsEditTab.tsx +163 -0
  31. package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +41 -0
  32. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +5 -2
  33. package/src/ui/collection_editor/GetCodeDialog.tsx +5 -3
  34. package/src/ui/collection_editor/PropertyEditView.tsx +11 -3
  35. package/src/ui/collection_editor/PropertyFieldPreview.tsx +1 -0
  36. package/src/ui/collection_editor/PropertyTree.tsx +183 -139
  37. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +6 -2
  38. package/src/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
  39. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +5 -3
  40. package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +2 -0
  41. package/src/ui/collection_editor/utils/supported_fields.tsx +1 -0
  42. package/src/ui/collection_editor/utils/update_property_for_widget.ts +9 -0
  43. package/src/useCollectionEditorPlugin.tsx +12 -7
  44. package/src/utils/collections.ts +15 -5
@@ -37,18 +37,6 @@ export function CollectionEditorWelcomeView({
37
37
  }
38
38
  }, [existingCollectionPaths, path, pathSuggestions]);
39
39
 
40
- // const {
41
- // values,
42
- // setFieldValue,
43
- // setValues,
44
- // handleChange,
45
- // touched,
46
- // errors,
47
- // setFieldTouched,
48
- // isSubmitting,
49
- // submitCount
50
- // } = useFormex<EntityCollection>();
51
-
52
40
  const {
53
41
  values,
54
42
  setFieldValue,
@@ -56,6 +44,11 @@ export function CollectionEditorWelcomeView({
56
44
  submitCount
57
45
  } = useFormex<EntityCollection>();
58
46
 
47
+ const noSuggestions = !loadingPathSuggestions && (filteredPathSuggestions ?? [])?.length === 0;
48
+ if (!noSuggestions) {
49
+ return null;
50
+ }
51
+
59
52
  return (
60
53
  <div className={"overflow-auto my-auto"}>
61
54
  <Container maxWidth={"4xl"} className={"flex flex-col gap-4 p-8 m-auto"}>
@@ -96,12 +89,11 @@ export function CollectionEditorWelcomeView({
96
89
  {suggestion}
97
90
  </Chip>
98
91
  ))}
99
-
100
- {!loadingPathSuggestions && (filteredPathSuggestions ?? [])?.length === 0 &&
101
- <Typography variant={"caption"}>
102
- No suggestions
103
- </Typography>
104
- }
92
+ {(filteredPathSuggestions ?? []).length === 0 && !loadingPathSuggestions && <Typography
93
+ variant={"caption"}
94
+ color={"secondary"}>
95
+ No existing paths found
96
+ </Typography>}
105
97
 
106
98
  </div>
107
99
 
@@ -116,28 +108,29 @@ export function CollectionEditorWelcomeView({
116
108
  <div className={"flex gap-4"}>
117
109
  <TemplateButton title={"Products"}
118
110
  subtitle={"A collection of products with images, prices and stock"}
119
- icon={<Icon size={"small"} iconKey={productsCollectionTemplate.icon!}/>}
111
+ icon={<Icon size={"small"}
112
+ iconKey={productsCollectionTemplate.icon! as string}/>}
120
113
  onClick={() => {
121
114
  setValues(productsCollectionTemplate);
122
115
  onContinue();
123
116
  }}/>
124
117
  <TemplateButton title={"Users"}
125
118
  subtitle={"A collection of users with emails, names and roles"}
126
- icon={<Icon size={"small"} iconKey={usersCollectionTemplate.icon!}/>}
119
+ icon={<Icon size={"small"} iconKey={usersCollectionTemplate.icon! as string}/>}
127
120
  onClick={() => {
128
121
  setValues(usersCollectionTemplate);
129
122
  onContinue();
130
123
  }}/>
131
124
  <TemplateButton title={"Blog posts"}
132
125
  subtitle={"A collection of blog posts with images, authors and complex content"}
133
- icon={<Icon size={"small"} iconKey={blogCollectionTemplate.icon!}/>}
126
+ icon={<Icon size={"small"} iconKey={blogCollectionTemplate.icon! as string}/>}
134
127
  onClick={() => {
135
128
  setValues(blogCollectionTemplate);
136
129
  onContinue();
137
130
  }}/>
138
131
  <TemplateButton title={"Pages"}
139
132
  subtitle={"A collection of pages with images, authors and complex content"}
140
- icon={<Icon size={"small"} iconKey={pagesCollectionTemplate.icon!}/>}
133
+ icon={<Icon size={"small"} iconKey={pagesCollectionTemplate.icon! as string}/>}
141
134
  onClick={() => {
142
135
  setValues(pagesCollectionTemplate);
143
136
  onContinue();
@@ -202,9 +195,6 @@ export function TemplateButton({
202
195
  <Typography variant={"subtitle1"}>
203
196
  {title}
204
197
  </Typography>
205
- {/*<Typography>*/}
206
- {/* {subtitle}*/}
207
- {/*</Typography>*/}
208
198
 
209
199
  </div>
210
200
  </Card>
@@ -24,7 +24,6 @@ import {
24
24
  DebouncedTextField,
25
25
  defaultBorderMixin,
26
26
  IconButton,
27
- Paper,
28
27
  Tooltip,
29
28
  Typography,
30
29
  } from "@firecms/ui";
@@ -45,7 +44,7 @@ type CollectionEditorFormProps = {
45
44
  extraIcon: React.ReactNode;
46
45
  getUser?: (uid: string) => User | null;
47
46
  getData?: () => Promise<object[]>;
48
- doCollectionInference: (collection: PersistedCollection) => Promise<Partial<EntityCollection> | null> | undefined;
47
+ doCollectionInference?: (collection: PersistedCollection) => Promise<Partial<EntityCollection> | null> | undefined;
49
48
  propertyConfigs: Record<string, PropertyConfig>;
50
49
  collectionEditable: boolean;
51
50
  };
@@ -108,6 +107,8 @@ export function CollectionPropertiesEditorForm({
108
107
  return;
109
108
 
110
109
  setInferringProperties(true);
110
+
111
+ console.debug("CollectionEditor: inferring properties from data", doCollectionInference, values);
111
112
  // @ts-ignore
112
113
  doCollectionInference(values)
113
114
  .then((newCollection) => {
@@ -310,8 +311,9 @@ export function CollectionPropertiesEditorForm({
310
311
  };
311
312
 
312
313
  const body = (
313
- <div className={"grid grid-cols-12 gap-2 h-full bg-surface-50 dark:bg-surface-900"}>
314
+ <div className={"grid grid-cols-12 gap-2 h-full bg-white dark:bg-surface-950"}>
314
315
  <div className={cls(
316
+ "bg-surface-50 dark:bg-surface-900",
315
317
  "p-4 md:p-8 pb-20 md:pb-20",
316
318
  "col-span-12 lg:col-span-5 h-full overflow-auto",
317
319
  !asDialog && "border-r " + defaultBorderMixin
@@ -401,8 +403,8 @@ export function CollectionPropertiesEditorForm({
401
403
 
402
404
  {!asDialog &&
403
405
  <div className={"col-span-12 lg:col-span-7 p-4 md:py-8 md:px-4 h-full overflow-auto pb-20 md:pb-20"}>
404
- <Paper
405
- className="sticky top-8 p-4 min-h-full border border-transparent w-full flex flex-col justify-center ">
406
+ <div
407
+ className="sticky top-8 min-h-full w-full flex flex-col justify-center">
406
408
 
407
409
  {selectedPropertyFullId &&
408
410
  selectedProperty &&
@@ -446,7 +448,7 @@ export function CollectionPropertiesEditorForm({
446
448
  <Typography variant={"label"} className="flex items-center justify-center">
447
449
  {"This property is defined as a property builder in code"}
448
450
  </Typography>}
449
- </Paper>
451
+ </div>
450
452
  </div>}
451
453
 
452
454
  {asDialog && <PropertyFormDialog
@@ -0,0 +1,163 @@
1
+ import React from "react";
2
+ import {
3
+ ConfirmationDialog,
4
+ EntityAction,
5
+ EntityCollection,
6
+ resolveEntityAction,
7
+ useCustomizationController
8
+ } from "@firecms/core";
9
+ import {
10
+ AddIcon,
11
+ Alert,
12
+ Button,
13
+ Container,
14
+ DeleteIcon,
15
+ IconButton,
16
+ Paper,
17
+ Table,
18
+ TableBody,
19
+ TableCell,
20
+ TableRow,
21
+ Tooltip,
22
+ Typography,
23
+ } from "@firecms/ui";
24
+ import { PersistedCollection } from "../../types/persisted_collection";
25
+ import { useFormex } from "@firecms/formex";
26
+ import { EntityActionsSelectDialog } from "./EntityActionsSelectDialog";
27
+
28
+ export function EntityActionsEditTab({
29
+ collection,
30
+ }: {
31
+ collection: PersistedCollection,
32
+ }) {
33
+
34
+ const { entityActions: contextEntityActions } = useCustomizationController();
35
+
36
+ const [addEntityActionDialogOpen, setAddEntityActionDialogOpen] = React.useState<boolean>(false);
37
+ const [actionToDelete, setActionToDelete] = React.useState<string | undefined>();
38
+
39
+ const {
40
+ values,
41
+ setFieldValue
42
+ } = useFormex<EntityCollection>();
43
+
44
+ const resolvedEntityActions = values.entityActions?.filter((e): e is string => typeof e === "string")
45
+ .map(e => resolveEntityAction(e, contextEntityActions))
46
+ .filter(Boolean) as EntityAction<any>[] ?? [];
47
+ const hardCodedEntityActions = collection.entityActions?.filter((e): e is EntityAction<any> => typeof e !== "string") ?? [];
48
+ const totalEntityActions = resolvedEntityActions.length + hardCodedEntityActions.length;
49
+
50
+ return (
51
+ <div className={"overflow-auto my-auto"}>
52
+ <Container maxWidth={"2xl"} className={"flex flex-col gap-4 p-8 m-auto"}>
53
+ <div className={"flex flex-col gap-16"}>
54
+ <div className={"flex-grow flex flex-col gap-4 items-start"}>
55
+ <Typography variant={"h5"}>
56
+ Custom actions
57
+ </Typography>
58
+
59
+ {totalEntityActions === 0 &&
60
+ <Alert action={<Button variant="text"
61
+ size={"small"}
62
+ href={"https://firecms.co/docs/custom_actions"}
63
+ component={"a"}
64
+ rel="noopener noreferrer"
65
+ target="_blank">More info</Button>}>
66
+ Define your own custom actions by uploading them with the CLI.
67
+ </Alert>
68
+ }
69
+
70
+ {<>
71
+ <Paper className={"flex flex-col gap-4 p-2 w-full"}>
72
+ <Table>
73
+ <TableBody>
74
+ {resolvedEntityActions.map((action) => (
75
+ <TableRow key={action.key}>
76
+ <TableCell
77
+ align="left">
78
+ <Typography variant={"subtitle2"} className={"flex-grow"}>
79
+ {action.name}
80
+ </Typography>
81
+ </TableCell>
82
+ <TableCell
83
+ align="right">
84
+ <Tooltip title={"Remove"}
85
+ asChild={true}>
86
+ <IconButton size="small"
87
+ onClick={(e) => {
88
+ e.preventDefault();
89
+ e.stopPropagation();
90
+ setActionToDelete(action.key);
91
+ }}
92
+ color="inherit">
93
+ <DeleteIcon size={"small"}/>
94
+ </IconButton>
95
+ </Tooltip>
96
+ </TableCell>
97
+ </TableRow>
98
+ ))}
99
+ {hardCodedEntityActions.map((action) => (
100
+ <TableRow key={action.key}>
101
+ <TableCell
102
+ align="left">
103
+ <Typography variant={"subtitle2"} className={"flex-grow"}>
104
+ {action.name}
105
+ </Typography>
106
+ <Typography variant={"caption"} className={"flex-grow"}>
107
+ This action is defined in code with
108
+ key <code>{action.key}</code>
109
+ </Typography>
110
+ </TableCell>
111
+ </TableRow>
112
+ ))}
113
+ </TableBody>
114
+ </Table>
115
+
116
+ <Button
117
+ onClick={() => {
118
+ setAddEntityActionDialogOpen(true);
119
+ }}
120
+ variant={"text"}
121
+ startIcon={<AddIcon/>}>
122
+ Add custom entity action
123
+ </Button>
124
+ </Paper>
125
+
126
+ </>}
127
+
128
+
129
+ </div>
130
+
131
+ </div>
132
+ </Container>
133
+
134
+ <div style={{ height: "52px" }}/>
135
+
136
+ {actionToDelete &&
137
+ <ConfirmationDialog open={Boolean(actionToDelete)}
138
+ onAccept={() => {
139
+ setFieldValue("entityActions", values.entityActions?.filter(e => e !== actionToDelete));
140
+ setActionToDelete(undefined);
141
+ }}
142
+ onCancel={() => setActionToDelete(undefined)}
143
+ title={<>Remove this action?</>}
144
+ body={<>This will <b>not
145
+ delete any data</b>, only
146
+ the action in the CMS</>}/>}
147
+
148
+ <EntityActionsSelectDialog
149
+ open={addEntityActionDialogOpen}
150
+ onClose={(selectedActionKey) => {
151
+ if (selectedActionKey) {
152
+ console.log("Selected action key:", selectedActionKey);
153
+ const value = [...(values.entityActions ?? []), selectedActionKey]
154
+ // only actions that are defined in the registry
155
+ .filter((e): e is string => typeof e === "string" && (contextEntityActions ?? []).some(action => action.key === e));
156
+ ;
157
+ setFieldValue("entityActions", value);
158
+ }
159
+ setAddEntityActionDialogOpen(false);
160
+ }}/>
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,41 @@
1
+ import { useCustomizationController } from "@firecms/core";
2
+ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@firecms/ui";
3
+ import React from "react";
4
+
5
+ export function EntityActionsSelectDialog({
6
+ open,
7
+ onClose
8
+ }: { open: boolean, onClose: (selectedActionKey?: string) => void }) {
9
+ const {
10
+ entityActions
11
+ } = useCustomizationController();
12
+
13
+ return <Dialog
14
+ maxWidth={"md"}
15
+ open={open}>
16
+ <DialogTitle>Select custom action</DialogTitle>
17
+ <DialogContent className={"flex flex-col gap-4"}>
18
+ {entityActions?.map((action) => {
19
+ return <Button
20
+ key={action.key}
21
+ onClick={() => onClose(action.key)}
22
+ fullWidth
23
+ variant={"text"}
24
+ >
25
+ {action.name} ({action.key})
26
+ </Button>;
27
+ })}
28
+ {(entityActions ?? []).length === 0 &&
29
+ <Typography variant={"body2"}>
30
+ No custom actions defined. Define your custom actions in the customization settings, before using this
31
+ dialog.
32
+ </Typography>
33
+ }
34
+ </DialogContent>
35
+ <DialogActions>
36
+ <Button variant={"outlined"}
37
+ color={"primary"}
38
+ onClick={() => onClose()}>Cancel</Button>
39
+ </DialogActions>
40
+ </Dialog>
41
+ }
@@ -27,12 +27,15 @@ export function EntityCustomViewsSelectDialog({
27
27
  })}
28
28
  {(entityViews ?? []).length === 0 &&
29
29
  <Typography variant={"body2"}>
30
- No custom views defined
30
+ No custom views defined. Define your custom views in the customization settings, before using this
31
+ dialog.
31
32
  </Typography>
32
33
  }
33
34
  </DialogContent>
34
35
  <DialogActions>
35
- <Button variant={"outlined"} onClick={() => onClose()}>Cancel</Button>
36
+ <Button variant={"outlined"}
37
+ color={"primary"}
38
+ onClick={() => onClose()}>Cancel</Button>
36
39
  </DialogActions>
37
40
  </Dialog>
38
41
  }
@@ -1,5 +1,5 @@
1
1
  import { EntityCollection, isEmptyObject, useSnackbarController } from "@firecms/core";
2
- import { Button, ContentCopyIcon, Dialog, DialogActions, DialogContent, DialogTitle, Typography, } from "@firecms/ui";
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"
@@ -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: `Copied`
68
+ message: "Copied"
68
69
  })
69
70
  return navigator.clipboard.writeText(code);
70
71
  }}>
@@ -133,7 +134,8 @@ function collectionToCode(collection: EntityCollection): object {
133
134
  .map(([key, value]) => ({
134
135
  [key]: propertyCleanup(value)
135
136
  }))
136
- .reduce((a, b) => ({ ...a, ...b }), {}),
137
+ .reduce((a, b) => ({ ...a,
138
+ ...b }), {}),
137
139
  subcollections: (collection.subcollections ?? []).map(collectionToCode)
138
140
  }
139
141
 
@@ -7,7 +7,6 @@ import {
7
7
  DEFAULT_FIELD_CONFIGS,
8
8
  getFieldConfig,
9
9
  getFieldId,
10
- isEmptyObject,
11
10
  isPropertyBuilder,
12
11
  isValidRegExp,
13
12
  mergeDeep,
@@ -296,6 +295,7 @@ export function PropertyFormDialog({
296
295
 
297
296
  {onCancel && <Button
298
297
  variant={"text"}
298
+ color={"primary"}
299
299
  onClick={() => {
300
300
  onCancel();
301
301
  formexRef.current?.resetForm();
@@ -384,7 +384,7 @@ function PropertyEditFormFields({
384
384
  }, [deferredValues, includeIdAndTitle, propertyNamespace]);
385
385
 
386
386
  useEffect(() => {
387
- if (values?.id && onError && !isEmptyObject(errors)) {
387
+ if (values?.id && onError) {
388
388
  onError(values?.id, propertyNamespace, errors);
389
389
  }
390
390
  }, [errors, propertyNamespace, values?.id]);
@@ -462,6 +462,13 @@ function PropertyEditFormFields({
462
462
  existing={existing}
463
463
  multiple={false}
464
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}/>;
465
472
  } else if (selectedFieldConfigId === "date_time") {
466
473
  childComponent = <DateTimePropertyField disabled={disabled}/>;
467
474
  } else if (selectedFieldConfigId === "multi_references") {
@@ -606,6 +613,7 @@ const WIDGET_TYPE_MAP: Record<PropertyConfigId, string> = {
606
613
  file_upload: "File",
607
614
  multi_file_upload: "File",
608
615
  reference: "Reference",
616
+ reference_as_string: "Text",
609
617
  multi_references: "Reference",
610
618
  date_time: "Date",
611
619
  group: "Group",
@@ -763,7 +771,7 @@ export function WidgetSelectViewItem({
763
771
 
764
772
  return <Card
765
773
  onClick={onClick}
766
- className={"flex flex-row items-center px-4 py-2"}>
774
+ className={"flex flex-row items-center px-4 py-2 m-1"}>
767
775
  <div
768
776
  className={cls(
769
777
  "flex flex-row items-center text-base min-h-[48px]",
@@ -54,6 +54,7 @@ export function PropertyFieldPreview({
54
54
  </div>
55
55
  <Paper
56
56
  className={cls(
57
+ "m-1",
57
58
  "border",
58
59
  "pl-2 w-full flex flex-row gap-4 items-center",
59
60
  cardMixin,