@firecms/core 3.0.0-canary.186 → 3.0.0-canary.188

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 (37) hide show
  1. package/dist/core/EntityEditView.d.ts +4 -15
  2. package/dist/core/EntityForm.d.ts +43 -0
  3. package/dist/form/components/FormEntry.d.ts +6 -0
  4. package/dist/form/components/FormLayout.d.ts +5 -0
  5. package/dist/form/components/index.d.ts +2 -0
  6. package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
  7. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  8. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  9. package/dist/index.es.js +12124 -11823
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/index.umd.js +12100 -11799
  12. package/dist/index.umd.js.map +1 -1
  13. package/dist/types/fields.d.ts +11 -0
  14. package/dist/types/navigation.d.ts +1 -0
  15. package/dist/types/plugins.d.ts +1 -1
  16. package/dist/types/properties.d.ts +1 -1
  17. package/package.json +5 -5
  18. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +4 -2
  19. package/src/core/EntityEditView.tsx +210 -1049
  20. package/src/core/EntityForm.tsx +956 -0
  21. package/src/core/NavigationRoutes.tsx +16 -12
  22. package/src/form/components/FormEntry.tsx +22 -0
  23. package/src/form/components/FormLayout.tsx +16 -0
  24. package/src/form/components/StorageUploadProgress.tsx +1 -1
  25. package/src/form/components/index.tsx +2 -0
  26. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +0 -2
  27. package/src/form/field_bindings/MapFieldBinding.tsx +0 -2
  28. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +8 -5
  29. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +5 -3
  30. package/src/preview/components/UrlComponentPreview.tsx +17 -18
  31. package/src/routes/FireCMSRoute.tsx +7 -2
  32. package/src/types/fields.tsx +16 -0
  33. package/src/types/navigation.ts +1 -0
  34. package/src/types/plugins.tsx +1 -1
  35. package/src/types/properties.ts +1 -1
  36. package/src/util/entity_cache.ts +9 -1
  37. package/src/util/useStorageUploadController.tsx +39 -21
@@ -68,24 +68,28 @@ export const NavigationRoutes = React.memo<NavigationRoutesProps>(
68
68
  });
69
69
  }
70
70
 
71
- const urlPath = navigation.buildUrlCollectionPath("");
72
- const collectionRoute = <Route path={urlPath + "/*"}
73
- key={`navigation_entity`}
74
- element={
75
- <ErrorBoundary>
76
- <FireCMSRoute/>
77
- </ErrorBoundary>
78
- }/>
71
+ const collectionUrlPath = navigation.buildUrlCollectionPath("");
72
+ const collectionRoute = (
73
+ <Route path={collectionUrlPath + "/*"}
74
+ key={`navigation_entity`}
75
+ element={
76
+ <ErrorBoundary>
77
+ <FireCMSRoute/>
78
+ </ErrorBoundary>
79
+ }/>
80
+ );
79
81
 
80
82
  const homeRoute = (
81
83
  <Route path={"/"}
82
84
  element={<HomePageRoute>{homePage}</HomePageRoute>}/>
83
85
  );
84
86
 
85
- const notFoundRoute = <Route path={"*"}
86
- element={
87
- <NotFoundPage/>
88
- }/>;
87
+ const notFoundRoute = (
88
+ <Route path={"*"}
89
+ element={
90
+ <NotFoundPage/>
91
+ }/>
92
+ );
89
93
 
90
94
  return (
91
95
  <Routes location={baseLocation}>
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { ErrorBoundary } from "../../components";
3
+
4
+ export function FormEntry({
5
+ propertyKey,
6
+ widthPercentage = 100,
7
+ children
8
+ }: {
9
+ propertyKey: string;
10
+ widthPercentage?: number;
11
+ children: React.ReactNode;
12
+ }) {
13
+ return (
14
+ <div id={`form_field_${propertyKey}`}
15
+ className={"relative"}
16
+ style={{ width: widthPercentage === 100 ? "100%" : `calc(${widthPercentage}% - 8px)` }}>
17
+ <ErrorBoundary>
18
+ {children}
19
+ </ErrorBoundary>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { cls } from "@firecms/ui";
3
+
4
+ export function FormLayout({
5
+ children,
6
+ className
7
+ }: {
8
+ children: React.ReactNode;
9
+ className?: string;
10
+ }) {
11
+ return (
12
+ <div className={cls("flex flex-wrap gap-x-4 w-full space-y-8", className)}>
13
+ {children}
14
+ </div>
15
+ );
16
+ }
@@ -92,7 +92,7 @@ export function StorageUploadProgress({
92
92
  `min-w-[${imageSize}px] min-h-[${imageSize}px]`)}>
93
93
 
94
94
  {loading &&
95
- <Skeleton className="w-full h-full"/>}
95
+ <Skeleton className="m-4 w-full h-full"/>}
96
96
 
97
97
  {error && <ErrorView title={"Error uploading file"}
98
98
  error={error}/>}
@@ -1,3 +1,5 @@
1
1
  export * from "./FieldHelperText";
2
2
  export * from "./LabelWithIcon";
3
3
  export * from "./LabelWithIconAndTooltip";
4
+ export * from "./FormEntry";
5
+ export * from "./FormLayout";
@@ -23,7 +23,6 @@ export function ArrayCustomShapedFieldBinding<T extends Array<any>>({
23
23
  minimalistView: minimalistViewProp,
24
24
  property,
25
25
  includeDescription,
26
- underlyingValueHasChanged,
27
26
  context,
28
27
  disabled
29
28
  }: FieldProps<T, any, any>) {
@@ -65,7 +64,6 @@ export function ArrayCustomShapedFieldBinding<T extends Array<any>>({
65
64
  disabled: disabled || thisDisabled,
66
65
  property: childProperty,
67
66
  includeDescription,
68
- underlyingValueHasChanged,
69
67
  context,
70
68
  partOfArray: true,
71
69
  minimalistView: false,
@@ -23,7 +23,6 @@ export function MapFieldBinding({
23
23
  property,
24
24
  minimalistView: minimalistViewProp,
25
25
  includeDescription,
26
- underlyingValueHasChanged,
27
26
  autoFocus,
28
27
  context,
29
28
  onPropertyChange
@@ -62,7 +61,6 @@ export function MapFieldBinding({
62
61
  disabled: disabled || thisDisabled,
63
62
  property: childProperty,
64
63
  includeDescription,
65
- underlyingValueHasChanged,
66
64
  context,
67
65
  partOfArray: false,
68
66
  minimalistView: false,
@@ -3,13 +3,14 @@ import {
3
3
  FieldHelperText,
4
4
  FieldProps,
5
5
  getIconForProperty,
6
- LabelWithIconAndTooltip, PropertyOrBuilder,
6
+ LabelWithIconAndTooltip,
7
+ PropertyOrBuilder,
7
8
  randomString,
8
9
  ResolvedArrayProperty,
9
10
  ResolvedStringProperty,
10
11
  useStorageSource
11
12
  } from "../../index";
12
- import { cls, fieldBackgroundHoverMixin, fieldBackgroundMixin } from "@firecms/ui";
13
+ import { cls, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundMixin } from "@firecms/ui";
13
14
  import { FireCMSEditor, FireCMSEditorProps } from "@firecms/editor";
14
15
  import { resolveProperty, resolveStorageFilenameString, resolveStoragePathString } from "../../util";
15
16
 
@@ -27,11 +28,13 @@ export function MarkdownEditorFieldBinding({
27
28
  showError,
28
29
  error,
29
30
  minimalistView,
31
+ disabled: disabledProp,
30
32
  isSubmitting,
31
33
  context,
32
34
  customProps,
33
35
  }: FieldProps<string, MarkdownEditorFieldProps>) {
34
36
 
37
+ const disabled = disabledProp || isSubmitting;
35
38
  const highlight = customProps?.highlight;
36
39
  const editorProps = customProps?.editorProps;
37
40
  const storageSource = useStorageSource();
@@ -54,11 +57,9 @@ export function MarkdownEditorFieldBinding({
54
57
  if (internalValue.current !== value) {
55
58
  internalValue.current = value;
56
59
  setFieldVersion(fieldVersion + 1);
57
- // fieldVersion.current = fieldVersion.current + 1;
58
60
  }
59
61
  }, [value]);
60
62
 
61
-
62
63
  const resolvedProperty = resolveProperty({
63
64
  propertyOrBuilder: property as PropertyOrBuilder,
64
65
  values: entityValues
@@ -107,6 +108,7 @@ export function MarkdownEditorFieldBinding({
107
108
  onMarkdownContentChange={onContentChange}
108
109
  version={context.formex.version + fieldVersion}
109
110
  highlight={highlight}
111
+ disabled={disabled}
110
112
  handleImageUpload={async (file: File) => {
111
113
  const storagePath = storagePathBuilder(file);
112
114
  const fileName = await fileNameBuilder(file);
@@ -136,7 +138,8 @@ export function MarkdownEditorFieldBinding({
136
138
  required={property.validation?.required}
137
139
  title={property.name}
138
140
  className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
139
- <div className={cls("rounded-md", fieldBackgroundMixin, fieldBackgroundHoverMixin)}>
141
+ <div
142
+ className={cls("rounded-md", fieldBackgroundMixin, disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin)}>
140
143
  {editor}
141
144
  </div>
142
145
  <FieldHelperText includeDescription={includeDescription}
@@ -28,7 +28,7 @@ import {
28
28
  import { useClearRestoreValue } from "../useClearRestoreValue";
29
29
 
30
30
  const dropZoneClasses = "box-border relative pt-[2px] items-center border border-transparent min-h-[254px] outline-none rounded-md duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] focus:border-primary-solid";
31
- const disabledClasses = "border-dotted-gray"
31
+ const disabledClasses = fieldBackgroundDisabledMixin;
32
32
  const nonActiveDropClasses = fieldBackgroundHoverMixin
33
33
  const activeDropClasses = "pt-0 border-2 border-solid"
34
34
  const acceptDropClasses = "transition-colors duration-200 ease-[cubic-bezier(0,0,0.2,1)] border-2 border-solid border-green-500"
@@ -206,6 +206,7 @@ function FileDropComponent({
206
206
  className={cls(
207
207
  fieldBackgroundMixin,
208
208
  disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
209
+ disabled ? "text-surface-accent-600 dark:text-surface-accent-500" : "",
209
210
  dropZoneClasses,
210
211
  multipleFilesSupported && internalValue.length ? "" : "flex",
211
212
  {
@@ -248,7 +249,7 @@ function FileDropComponent({
248
249
  metadata={metadata}
249
250
  storagePath={storagePathBuilder(entry.file)}
250
251
  onFileUploadComplete={onFileUploadComplete}
251
- imageSize={size === "medium" ? 220 : 118}
252
+ imageSize={size === "large" ? 220 : 118}
252
253
  simple={false}
253
254
  />
254
255
  );
@@ -285,7 +286,8 @@ function FileDropComponent({
285
286
  <div
286
287
  className="flex-grow min-h-[38px] box-border m-2 text-center">
287
288
  <Typography align={"center"}
288
- variant={"label"}>
289
+ variant={"label"}
290
+ className={disabled ? "text-surface-accent-600 dark:text-surface-accent-500" : ""}>
289
291
  {helpText}
290
292
  </Typography>
291
293
  </div>
@@ -55,24 +55,23 @@ export function UrlComponentPreview({
55
55
  return <VideoPreview size={size} src={url} interactive={interactive}/>;
56
56
  } else {
57
57
  return (
58
- <a
59
- href={url}
60
- rel="noopener noreferrer"
61
- target="_blank"
62
- onClick={(e) => e.stopPropagation()}
63
- className="flex flex-col items-center justify-center"
64
- style={{
65
- width: getThumbnailMeasure(size),
66
- height: getThumbnailMeasure(size)
67
- }}>
68
- <DescriptionIcon className="text-surface-700 dark:text-surface-300"/>
69
- {hint &&
70
- <Tooltip title={hint}>
71
- <Typography
72
- className="max-w-full truncate rtl text-left"
73
- variant={"caption"}>{hint}</Typography>
74
- </Tooltip>}
75
- </a>
58
+ <Tooltip title={hint}>
59
+ <a
60
+ href={url}
61
+ rel="noopener noreferrer"
62
+ target="_blank"
63
+ onClick={(e) => e.stopPropagation()}
64
+ className="flex flex-col items-center justify-center"
65
+ style={{
66
+ width: getThumbnailMeasure(size),
67
+ height: getThumbnailMeasure(size)
68
+ }}>
69
+ <DescriptionIcon className="text-surface-700 dark:text-surface-300"/>
70
+ {hint && <Typography
71
+ className="max-w-full truncate rtl text-left"
72
+ variant={"caption"}>{hint}</Typography>}
73
+ </a>
74
+ </Tooltip>
76
75
  );
77
76
  }
78
77
  }
@@ -190,10 +190,15 @@ function EntityFullScreenRoute({
190
190
  onValuesModified={(modified) => blocked.current = modified}
191
191
  onSaved={(params) => {
192
192
  console.log("Entity saved", params);
193
- navigate(`${basePath}/${params.entityId}`, { replace: true });
193
+ const newSelectedTab = params.selectedTab;
194
+ const newEntityId = params.entityId;
195
+ if (newSelectedTab) {
196
+ navigate(`${basePath}/${newEntityId}/${newSelectedTab}`, { replace: true });
197
+ } else {
198
+ navigate(`${basePath}/${newEntityId}`, { replace: true });
199
+ }
194
200
  }}
195
201
  onTabChange={(params) => {
196
- // updateUrl(params.entityId, params.selectedTab, !isNew, params.path, isNew);
197
202
  setSelectedTab(params.selectedTab);
198
203
  if (isNew) {
199
204
  return;
@@ -1,6 +1,7 @@
1
1
  import { CMSType, Property, PropertyOrBuilder } from "./properties";
2
2
  import { ResolvedEntityCollection, ResolvedProperty } from "./resolved_entities";
3
3
  import { FormexController } from "@firecms/formex";
4
+ import { Entity } from "./entities";
4
5
 
5
6
  /**
6
7
  * When building a custom field you need to create a React component that takes
@@ -161,6 +162,21 @@ export interface FormContext<M extends Record<string, any> = any> {
161
162
  */
162
163
  path?: string;
163
164
 
165
+ status: "new" | "existing" | "copy";
166
+
167
+ entity?: Entity<M>;
168
+
169
+ savingError?: Error;
170
+
171
+ openEntityMode: "side_panel" | "full_screen";
172
+
173
+ /**
174
+ * If pending close is set to true, the form will close when the user
175
+ * saves the entity
176
+ * @param pendingClose
177
+ */
178
+ setPendingClose?: (pendingClose: boolean) => void;
179
+
164
180
  /**
165
181
  * This is the underlying formex controller that powers the form
166
182
  */
@@ -5,6 +5,7 @@ import { EntityReference } from "./entities";
5
5
  /**
6
6
  * Controller that includes the resolved navigation and utility methods and
7
7
  * attributes.
8
+ * This controller holds the state of the navigation including the collections.
8
9
  * @group Models
9
10
  */
10
11
  export type NavigationController<EC extends EntityCollection = EntityCollection<any>> = {
@@ -195,7 +195,7 @@ export interface PluginFormActionProps<USER extends User = User, EC extends Enti
195
195
  formContext?: FormContext<any>;
196
196
  context: FireCMSContext<USER>;
197
197
  currentEntityId?: string;
198
- layout: "side_panel" | "full_screen";
198
+ openEntityMode: "side_panel" | "full_screen";
199
199
  }
200
200
 
201
201
  export type PluginFieldBuilderParams<T extends CMSType = CMSType, M extends Record<string, any> = any, EC extends EntityCollection<M> = EntityCollection<M>> = {
@@ -377,7 +377,7 @@ export interface StringProperty extends BaseProperty<string> {
377
377
 
378
378
  /**
379
379
  * You can specify a `Storage` configuration. It is used to
380
- * indicate that this string refers to a path in Google Cloud Storage.
380
+ * indicate that this string refers to a path in your storage provider.
381
381
  */
382
382
  storage?: StorageConfig;
383
383
 
@@ -10,23 +10,31 @@ const entityCache: Map<string, object> = new Map();
10
10
  const isLocalStorageAvailable = typeof localStorage !== "undefined";
11
11
 
12
12
  // Define custom replacer for JSON.stringify
13
- function customReplacer(key: string, value: any): any {
13
+ function customReplacer(key: string): any {
14
+
15
+ // @ts-ignore
16
+ const value = this[key];
17
+
14
18
  // Handle Date objects
19
+ // @ts-ignore
15
20
  if (value instanceof Date) {
16
21
  return { __type: "Date", value: value.toISOString() };
17
22
  }
18
23
 
19
24
  // Handle EntityReference
25
+ // @ts-ignore
20
26
  if (value instanceof EntityReference) {
21
27
  return { __type: "EntityReference", id: value.id, path: value.path };
22
28
  }
23
29
 
24
30
  // Handle GeoPoint
31
+ // @ts-ignore
25
32
  if (value instanceof GeoPoint) {
26
33
  return { __type: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
27
34
  }
28
35
 
29
36
  // Handle Vector
37
+ // @ts-ignore
30
38
  if (value instanceof Vector) {
31
39
  return { __type: "Vector", value: value.value };
32
40
  }
@@ -34,6 +34,7 @@ export interface StorageFieldItem {
34
34
  size: PreviewSize
35
35
  }
36
36
 
37
+
37
38
  export function useStorageUploadController<M extends object>({
38
39
  entityId,
39
40
  entityValues,
@@ -45,17 +46,17 @@ export function useStorageUploadController<M extends object>({
45
46
  disabled,
46
47
  onChange
47
48
  }:
48
- {
49
- entityId: string,
50
- entityValues: EntityValues<M>,
51
- value: string | string[] | null;
52
- path?: string,
53
- propertyKey: string,
54
- property: StringProperty | ArrayProperty<string[]> | ResolvedStringProperty | ResolvedArrayProperty<string[]>,
55
- storageSource: StorageSource,
56
- disabled: boolean,
57
- onChange: (value: string | string[] | null) => void
58
- }) {
49
+ {
50
+ entityId: string,
51
+ entityValues: EntityValues<M>,
52
+ value: string | string[] | null;
53
+ path?: string,
54
+ propertyKey: string,
55
+ property: StringProperty | ArrayProperty<string[]> | ResolvedStringProperty | ResolvedArrayProperty<string[]>,
56
+ storageSource: StorageSource,
57
+ disabled: boolean,
58
+ onChange: (value: string | string[] | null) => void
59
+ }) {
59
60
 
60
61
  const storage: StorageConfig | undefined = property.dataType === "string"
61
62
  ? property.storage
@@ -75,16 +76,7 @@ export function useStorageUploadController<M extends object>({
75
76
  const compression: ImageCompression | undefined = storage?.imageCompression;
76
77
 
77
78
  const internalInitialValue: StorageFieldItem[] =
78
- (multipleFilesSupported
79
- ? (value ?? []) as string[]
80
- : value ? [value as string] : []).map(entry => (
81
- {
82
- id: getRandomId(),
83
- storagePathOrDownloadUrl: entry,
84
- metadata,
85
- size
86
- }
87
- ));
79
+ getInternalInitialValue(multipleFilesSupported, value, metadata, size);
88
80
 
89
81
  const [initialValue, setInitialValue] = useState<string | string[] | null>(value);
90
82
  const [internalValue, setInternalValue] = useState<StorageFieldItem[]>(internalInitialValue);
@@ -226,6 +218,32 @@ export function useStorageUploadController<M extends object>({
226
218
  }
227
219
  }
228
220
 
221
+ function getInternalInitialValue(multipleFilesSupported: boolean,
222
+ value: string | string[] | null,
223
+ metadata: Record<string, any> | undefined,
224
+ size: PreviewSize): StorageFieldItem[] {
225
+ let strings: string[] = [];
226
+ if (multipleFilesSupported) {
227
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
228
+ strings = (value ?? []) as string[];
229
+ }
230
+ } else {
231
+ if (typeof value === "string") {
232
+ strings = value ? [value as string] : [];
233
+ }
234
+ }
235
+
236
+ return strings
237
+ .map(entry => (
238
+ {
239
+ id: getRandomId(),
240
+ storagePathOrDownloadUrl: entry,
241
+ metadata,
242
+ size
243
+ }
244
+ ));
245
+ }
246
+
229
247
  function removeDuplicates(items: StorageFieldItem[]) {
230
248
  return items.filter(
231
249
  (item, i) => {