@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.
Files changed (79) hide show
  1. package/README.md +165 -1
  2. package/dist/ConfigControllerProvider.d.ts +0 -1
  3. package/dist/index.es.js +9957 -4962
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +9949 -4958
  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 +4 -1
  9. package/dist/types/config_controller.d.ts +3 -1
  10. package/dist/types/config_permissions.d.ts +2 -2
  11. package/dist/types/persisted_collection.d.ts +1 -1
  12. package/dist/ui/EditorEntityAction.d.ts +2 -0
  13. package/dist/ui/collection_editor/CollectionDetailsForm.d.ts +3 -1
  14. package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +1 -1
  15. package/dist/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +1 -1
  16. package/dist/ui/collection_editor/EntityActionsEditTab.d.ts +4 -0
  17. package/dist/ui/collection_editor/EntityActionsSelectDialog.d.ts +4 -0
  18. package/dist/ui/collection_editor/LayoutModeSwitch.d.ts +5 -0
  19. package/dist/ui/collection_editor/PropertyEditView.d.ts +8 -0
  20. package/dist/ui/collection_editor/PropertyTree.d.ts +2 -3
  21. package/dist/ui/collection_editor/import/CollectionEditorImportDataPreview.d.ts +1 -1
  22. package/dist/ui/collection_editor/import/CollectionEditorImportMapping.d.ts +8 -1
  23. package/dist/ui/collection_editor/import/clean_import_data.d.ts +1 -1
  24. package/dist/ui/collection_editor/properties/MarkdownPropertyField.d.ts +4 -0
  25. package/dist/ui/collection_editor/properties/ReferencePropertyField.d.ts +2 -1
  26. package/dist/ui/collection_editor/properties/StringPropertyField.d.ts +1 -1
  27. package/dist/useCollectionEditorPlugin.d.ts +6 -6
  28. package/dist/utils/collections.d.ts +1 -1
  29. package/package.json +25 -23
  30. package/src/ConfigControllerProvider.tsx +3 -8
  31. package/src/types/collection_editor_controller.tsx +1 -2
  32. package/src/types/collection_inference.ts +4 -1
  33. package/src/types/config_controller.tsx +4 -1
  34. package/src/types/config_permissions.ts +1 -1
  35. package/src/types/persisted_collection.ts +2 -3
  36. package/src/ui/CollectionViewHeaderAction.tsx +4 -2
  37. package/src/ui/EditorCollectionAction.tsx +3 -7
  38. package/src/ui/EditorCollectionActionStart.tsx +1 -1
  39. package/src/ui/EditorEntityAction.tsx +51 -0
  40. package/src/ui/HomePageEditorCollectionAction.tsx +5 -3
  41. package/src/ui/NewCollectionButton.tsx +1 -1
  42. package/src/ui/PropertyAddColumnComponent.tsx +5 -3
  43. package/src/ui/collection_editor/CollectionDetailsForm.tsx +126 -49
  44. package/src/ui/collection_editor/CollectionEditorDialog.tsx +71 -16
  45. package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +20 -29
  46. package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +19 -17
  47. package/src/ui/collection_editor/EntityActionsEditTab.tsx +163 -0
  48. package/src/ui/collection_editor/EntityActionsSelectDialog.tsx +41 -0
  49. package/src/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +11 -7
  50. package/src/ui/collection_editor/EnumForm.tsx +11 -7
  51. package/src/ui/collection_editor/GetCodeDialog.tsx +46 -26
  52. package/src/ui/collection_editor/LayoutModeSwitch.tsx +54 -0
  53. package/src/ui/collection_editor/PropertyEditView.tsx +263 -76
  54. package/src/ui/collection_editor/PropertyFieldPreview.tsx +7 -6
  55. package/src/ui/collection_editor/PropertyTree.tsx +184 -138
  56. package/src/ui/collection_editor/SubcollectionsEditTab.tsx +24 -17
  57. package/src/ui/collection_editor/UnsavedChangesDialog.tsx +9 -7
  58. package/src/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +20 -4
  59. package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +34 -3
  60. package/src/ui/collection_editor/import/clean_import_data.ts +1 -1
  61. package/src/ui/collection_editor/properties/BlockPropertyField.tsx +18 -12
  62. package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +54 -47
  63. package/src/ui/collection_editor/properties/EnumPropertyField.tsx +2 -0
  64. package/src/ui/collection_editor/properties/MapPropertyField.tsx +3 -2
  65. package/src/ui/collection_editor/properties/MarkdownPropertyField.tsx +139 -0
  66. package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +7 -3
  67. package/src/ui/collection_editor/properties/StoragePropertyField.tsx +13 -18
  68. package/src/ui/collection_editor/properties/StringPropertyField.tsx +4 -9
  69. package/src/ui/collection_editor/properties/UrlPropertyField.tsx +1 -0
  70. package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +2 -0
  71. package/src/ui/collection_editor/properties/validation/ValidationPanel.tsx +2 -2
  72. package/src/ui/collection_editor/templates/pages_template.ts +1 -6
  73. package/src/ui/collection_editor/utils/strings.ts +13 -6
  74. package/src/ui/collection_editor/utils/supported_fields.tsx +2 -0
  75. package/src/ui/collection_editor/utils/update_property_for_widget.ts +37 -6
  76. package/src/useCollectionEditorPlugin.tsx +20 -15
  77. package/src/utils/collections.ts +23 -7
  78. package/dist/ui/collection_editor/PropertySelectItem.d.ts +0 -8
  79. 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, Typography, } from "@firecms/ui";
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
- <Typography variant={"h6"} className={"my-4"}>
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/customization_quickstart"}>docs</a>.
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: `Copied`
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 = (property: any) => {
82
-
83
- const updatedProperty = {
84
- ...property
85
- };
86
-
87
- delete updatedProperty.fromBuilder;
88
- delete updatedProperty.resolved;
89
- delete updatedProperty.propertiesOrder;
90
- delete updatedProperty.editable;
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
- return updatedProperty;
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(collection.properties ?? {})
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, ...b }), {}),
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
- Select,
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: { ...property, editable: property.editable ?? true }
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 === "markdown" ||
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
- <Select
486
- // className={"w-full"}
487
- error={Boolean(selectedWidgetError)}
488
- value={selectedFieldConfigId ?? ""}
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={setSelectOpen}
492
- position={"item-aligned"}
493
- disabled={disabled}
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
- const key = value as PropertyConfigId;
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
- onValueChange={(value) => {
528
- onWidgetSelectChanged(value as PropertyConfigId);
529
- }}>
530
- {displayedWidgets.map(([key, propertyConfig]) => {
531
- const baseProperty = propertyConfig.property;
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
- <DeleteConfirmationDialog open={deleteDialogOpen}
580
- onAccept={() => onDelete(values?.id, propertyNamespace)}
581
- onCancel={() => setDeleteDialogOpen(false)}
582
- title={<div>Delete this property?</div>}
583
- body={
584
- <div> This will <b>not delete any
585
- data</b>, only modify the
586
- collection.</div>
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
+ }