@firecms/core 3.0.0-canary.215 → 3.0.0-canary.217

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.
@@ -138,7 +138,9 @@ export interface FormContext<M extends Record<string, any> = any> {
138
138
  savingError?: Error;
139
139
  openEntityMode: "side_panel" | "full_screen";
140
140
  /**
141
- * This is the underlying formex controller that powers the form
141
+ * This is the underlying formex controller that powers the form.
142
+ * If you are in a red only mode, the formex controller is there, but you can't
143
+ * operate with it
142
144
  */
143
145
  formex: FormexController<M>;
144
146
  }
@@ -653,6 +653,12 @@ export type StorageConfig = {
653
653
  * Define maximal file size in bytes
654
654
  */
655
655
  maxSize?: number;
656
+ /**
657
+ * Use this callback to process the file before uploading it to the storage.
658
+ * If nothing is returned, the file is uploaded as it is.
659
+ * @param file
660
+ */
661
+ processFile?: (file: File) => Promise<File> | undefined;
656
662
  /**
657
663
  * Postprocess the saved value (storage path or URL)
658
664
  * after it has been resolved.
@@ -0,0 +1,2 @@
1
+ import { FormexController } from "@firecms/formex";
2
+ export declare function createFormexStub<T extends object>(values: T): FormexController<T>;
@@ -1,6 +1,6 @@
1
1
  export declare const pick: <T>(obj: T, ...args: any[]) => T;
2
2
  export declare function isObject(item: any): any;
3
- export declare function mergeDeep<T extends Record<any, any>, U extends Record<any, any>>(target: T, source: U): T & U;
3
+ export declare function mergeDeep<T extends Record<any, any>, U extends Record<any, any>>(target: T, source: U, ignoreUndefined?: boolean): T & U;
4
4
  export declare function getValueInPath(o: object | undefined, path: string): any;
5
5
  export declare function removeInPath(o: object, path: string): object | undefined;
6
6
  export declare function removeFunctions(o: object | undefined): any;
@@ -81,8 +81,8 @@ export declare function resolveProperties<M extends Record<string, any>>({ prope
81
81
  export declare function resolvePropertyEnum(property: StringProperty | NumberProperty, fromBuilder?: boolean): ResolvedStringProperty | ResolvedNumberProperty;
82
82
  export declare function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefined;
83
83
  export declare function resolveEntityView(entityView: string | EntityCustomView<any>, contextEntityViews?: EntityCustomView<any>[]): EntityCustomView<any> | undefined;
84
- export declare function resolvedSelectedEntityView<M extends Record<string, any>>(customViews: (string | EntityCustomView<M>)[] | undefined, customizationController: CustomizationController, selectedTab?: string): {
85
- resolvedEntityViews: EntityCustomView[];
86
- selectedEntityView: EntityCustomView | undefined;
87
- selectedSecondaryForm: EntityCustomView | undefined;
84
+ export declare function resolvedSelectedEntityView<M extends Record<string, any>>(customViews: (string | EntityCustomView<M>)[] | undefined, customizationController: CustomizationController, selectedTab?: string, canEdit?: boolean): {
85
+ resolvedEntityViews: EntityCustomView<M>[];
86
+ selectedEntityView: EntityCustomView<M> | undefined;
87
+ selectedSecondaryForm: EntityCustomView<M> | undefined;
88
88
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.215",
4
+ "version": "3.0.0-canary.217",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -50,9 +50,9 @@
50
50
  "./package.json": "./package.json"
51
51
  },
52
52
  "dependencies": {
53
- "@firecms/editor": "^3.0.0-canary.215",
54
- "@firecms/formex": "^3.0.0-canary.215",
55
- "@firecms/ui": "^3.0.0-canary.215",
53
+ "@firecms/editor": "^3.0.0-canary.217",
54
+ "@firecms/formex": "^3.0.0-canary.217",
55
+ "@firecms/ui": "^3.0.0-canary.217",
56
56
  "@hello-pangea/dnd": "^17.0.0",
57
57
  "@radix-ui/react-portal": "^1.1.3",
58
58
  "clsx": "^2.1.1",
@@ -105,7 +105,7 @@
105
105
  "dist",
106
106
  "src"
107
107
  ],
108
- "gitHead": "e04e9ae0e4c6b351f6a77f30295445bededef042",
108
+ "gitHead": "3a1bf3d652342f4352ff6153a9f30eb80016a037",
109
109
  "publishConfig": {
110
110
  "access": "public"
111
111
  },
@@ -165,7 +165,7 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
165
165
 
166
166
  {!hideId && size !== "xs" && (
167
167
  <div
168
- className="w-[138px] overflow-hidden truncate font-mono text-xs text-text-secondary dark:text-text-secondary-dark max-w-full text-ellipsis px-2 align-center flex items-center gap-1"
168
+ className="w-[138px] overflow-hidden truncate font-mono text-xs text-text-secondary dark:text-text-secondary-dark max-w-full text-ellipsis px-2 align-center justify-center flex items-center gap-1"
169
169
  onClick={(event) => {
170
170
  event.stopPropagation();
171
171
  }}>
@@ -38,7 +38,7 @@ export function EntityJsonPreview({ values }: { values: object }) {
38
38
 
39
39
  return (
40
40
  <Highlight
41
- theme={mode === "dark" ? themes.vsDark : themes.vsLight}
41
+ theme={mode === "dark" ? themes.vsDark : themes.github}
42
42
  code={code}
43
43
  language="json"
44
44
  >
@@ -48,9 +48,9 @@ export function EntityJsonPreview({ values }: { values: object }) {
48
48
  ref={preRef}
49
49
  style={{
50
50
  ...style,
51
- background: "inherit"
51
+ backgroundColor: "inherit"
52
52
  }}
53
- className="container mx-auto p-8 rounded text-sm"
53
+ className="max-w-6xl mx-auto p-8 rounded text-sm"
54
54
  >
55
55
  {tokens.map((line, i) => (
56
56
  <div key={i} {...getLineProps({ line })} className="text-wrap">
@@ -5,6 +5,7 @@ import { CircularProgressCenter, EntityCollectionView, EntityView, ErrorBoundary
5
5
  import {
6
6
  canEditEntity,
7
7
  removeInitialAndTrailingSlashes,
8
+ resolveCollection,
8
9
  resolveDefaultSelectedView,
9
10
  resolvedSelectedEntityView
10
11
  } from "../util";
@@ -21,6 +22,7 @@ import { getEntityFromCache } from "../util/entity_cache";
21
22
  import { EntityForm, EntityFormProps } from "../form";
22
23
  import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
23
24
  import { EntityJsonPreview } from "../components/EntityJsonPreview";
25
+ import { createFormexStub } from "../util/createFormexStub";
24
26
 
25
27
  export const MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
26
28
  export const JSON_TAB_VALUE = "__json";
@@ -54,7 +56,7 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
54
56
  onTabChange?: (props: OnTabChangeParams<M>) => void;
55
57
  layout?: "side_panel" | "full_screen";
56
58
  barActions?: React.ReactNode;
57
- formProps?: Partial<EntityFormProps<M>>
59
+ formProps?: Partial<EntityFormProps<M>>,
58
60
  }
59
61
 
60
62
  /**
@@ -104,19 +106,6 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
104
106
  console.error(`Entity with id ${entityId} not found in collection ${props.path}`);
105
107
  }
106
108
 
107
- if (!canEdit) {
108
- return <div className={"flex flex-col"}>
109
- <Typography className={"mt-16 mb-8 mx-8"} variant={"h4"}>
110
- {props.collection.singularName ?? props.collection.name}
111
- </Typography>
112
- <EntityView
113
- className={"px-8"}
114
- entity={entity as Entity<M>}
115
- path={props.path}
116
- collection={props.collection}/>
117
- </div>
118
- }
119
-
120
109
  return <EntityEditViewInner<M> {...props}
121
110
  entityId={entityId}
122
111
  entity={entity}
@@ -124,9 +113,11 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
124
113
  dataLoading={dataLoading}
125
114
  status={status}
126
115
  setStatus={setStatus}
116
+ canEdit={canEdit}
127
117
  />;
128
118
  }
129
119
 
120
+
130
121
  export function EntityEditViewInner<M extends Record<string, any>>({
131
122
  path,
132
123
  entityId,
@@ -143,13 +134,15 @@ export function EntityEditViewInner<M extends Record<string, any>>({
143
134
  barActions,
144
135
  status,
145
136
  setStatus,
146
- formProps
137
+ formProps,
138
+ canEdit
147
139
  }: EntityEditViewProps<M> & {
148
140
  entity?: Entity<M>,
149
141
  cachedDirtyValues?: Partial<M>, // dirty cached entity in memory
150
142
  dataLoading: boolean,
151
143
  status: EntityStatus,
152
144
  setStatus: (status: EntityStatus) => void,
145
+ canEdit?: boolean,
153
146
  }) {
154
147
 
155
148
  const context = useFireCMSContext();
@@ -177,10 +170,9 @@ export function EntityEditViewInner<M extends Record<string, any>>({
177
170
  ), []);
178
171
 
179
172
  const [selectedTab, setSelectedTab] = useState<string>(selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
180
-
181
173
  useEffect(() => {
182
- if ((selectedTabProp ?? MAIN_TAB_VALUE) !== selectedTab) {
183
- setSelectedTab(selectedTabProp ?? MAIN_TAB_VALUE);
174
+ if ((selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE) !== selectedTab) {
175
+ setSelectedTab(selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
184
176
  }
185
177
  }, [selectedTabProp]);
186
178
 
@@ -195,23 +187,58 @@ export function EntityEditViewInner<M extends Record<string, any>>({
195
187
  resolvedEntityViews,
196
188
  selectedEntityView,
197
189
  selectedSecondaryForm
198
- } = resolvedSelectedEntityView(customViews, customizationController, selectedTab);
190
+ } = resolvedSelectedEntityView(customViews, customizationController, selectedTab, canEdit);
199
191
 
200
192
  const actionsAtTheBottom = !largeLayout || layout === "side_panel" || selectedEntityView?.includeActions === "bottom";
201
193
 
202
194
  const mainViewVisible = selectedTab === MAIN_TAB_VALUE || Boolean(selectedSecondaryForm);
203
195
 
196
+ const authController = useAuthController();
197
+
204
198
  const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
205
199
  .filter(e => !e.includeActions)
206
200
  .map((customView) => {
201
+
207
202
  if (!customView)
208
203
  return null;
209
204
  const Builder = customView.Builder;
210
205
  if (!Builder) {
211
- console.error("customView.Builder is not defined");
206
+ console.error("INTERNAL: customView.Builder is not defined");
212
207
  return null;
213
208
  }
214
209
 
210
+ if (!entityId) {
211
+ console.error("INTERNAL: entityId is not defined");
212
+ return null;
213
+ }
214
+
215
+ const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
216
+ const usedFormContext: FormContext = formContext ?? {
217
+ entityId,
218
+ openEntityMode: layout,
219
+ status: status,
220
+ values: usedEntity?.values ?? {},
221
+ setFieldValue: (key: string, value: any) => {
222
+ throw new Error("You can't update values in read only mode");
223
+ },
224
+ save: () => {
225
+ throw new Error("You can't save in read only mode");
226
+ },
227
+ collection: resolveCollection<M>({
228
+ collection,
229
+ path,
230
+ entityId,
231
+ values: usedEntity?.values ?? {},
232
+ previousValues: usedEntity?.values ?? {},
233
+ propertyConfigs: customizationController.propertyConfigs,
234
+ authController
235
+ }),
236
+ path,
237
+ entity: usedEntity,
238
+ savingError: undefined,
239
+ formex: formexStub
240
+ };
241
+
215
242
  return <div
216
243
  className={cls(defaultBorderMixin,
217
244
  "relative flex-1 w-full h-full overflow-auto",
@@ -220,11 +247,11 @@ export function EntityEditViewInner<M extends Record<string, any>>({
220
247
  key={`custom_view_${customView.key}`}
221
248
  role="tabpanel">
222
249
  <ErrorBoundary>
223
- {formContext && <Builder
250
+ {usedFormContext && <Builder
224
251
  collection={collection}
225
252
  entity={usedEntity}
226
- modifiedValues={formContext.formex.values ?? usedEntity?.values}
227
- formContext={formContext}
253
+ modifiedValues={usedFormContext?.formex?.values ?? usedEntity?.values}
254
+ formContext={usedFormContext}
228
255
  />}
229
256
  </ErrorBoundary>
230
257
  </div>;
@@ -239,7 +266,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
239
266
  role="tabpanel">
240
267
  <ErrorBoundary>
241
268
  <EntityJsonPreview
242
- values={formContext?.values ?? {}}/>
269
+ values={formContext?.values ?? entity?.values ?? {}}/>
243
270
  </ErrorBoundary>
244
271
  </div>;
245
272
 
@@ -288,42 +315,61 @@ export function EntityEditViewInner<M extends Record<string, any>>({
288
315
  }
289
316
  };
290
317
 
291
- // Render the main entity form view (or a read-only view if the user cannot edit)
292
- const entityView = <EntityForm<M>
293
- collection={collection}
294
- path={path}
295
- entityId={entityId ?? usedEntity?.id}
296
- onValuesModified={onValuesModified}
297
- entity={entity}
298
- initialDirtyValues={cachedDirtyValues}
299
- openEntityMode={layout}
300
- forceActionsAtTheBottom={actionsAtTheBottom}
301
- initialStatus={status}
302
- className={cls(!mainViewVisible ? "hidden" : "", formProps?.className)}
303
- EntityFormActionsComponent={EntityEditViewFormActions}
304
- {...formProps}
305
- onEntityChange={(entity) => {
306
- setUsedEntity(entity);
307
- formProps?.onEntityChange?.(entity);
308
- }}
309
- onStatusChange={(status) => {
310
- setStatus(status);
311
- formProps?.onStatusChange?.(status);
312
- }}
313
- onFormContextReady={(formContext) => {
314
- setFormContext(formContext);
315
- formProps?.onFormContextReady?.(formContext);
316
- }}
317
- onSaved={(params) => {
318
- const res = {
319
- ...params,
320
- selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab
321
- };
322
- onSaved?.(res);
323
- formProps?.onSaved?.(res);
324
- }}
325
- Builder={selectedSecondaryForm?.Builder}
326
- />;
318
+ const entityView = !canEdit ?
319
+ (
320
+ <div
321
+ className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", !mainViewVisible ? "hidden" : "")}>
322
+ <div
323
+ className={cls("relative flex flex-col max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit")}>
324
+ <Typography className={"mt-16 mb-8 mx-8"} variant={"h4"}>
325
+ {collection.singularName ?? collection.name}
326
+ </Typography>
327
+ <EntityView
328
+ className={"px-8 h-full overflow-auto"}
329
+ entity={entity as Entity<M>}
330
+ path={path}
331
+ collection={collection}/>
332
+ </div>
333
+ </div>
334
+ )
335
+ :
336
+ (
337
+ <EntityForm<M>
338
+ collection={collection}
339
+ path={path}
340
+ entityId={entityId ?? usedEntity?.id}
341
+ onValuesModified={onValuesModified}
342
+ entity={entity}
343
+ initialDirtyValues={cachedDirtyValues}
344
+ openEntityMode={layout}
345
+ forceActionsAtTheBottom={actionsAtTheBottom}
346
+ initialStatus={status}
347
+ className={cls(!mainViewVisible ? "hidden" : "", formProps?.className)}
348
+ EntityFormActionsComponent={EntityEditViewFormActions}
349
+ {...formProps}
350
+ onEntityChange={(entity) => {
351
+ setUsedEntity(entity);
352
+ formProps?.onEntityChange?.(entity);
353
+ }}
354
+ onStatusChange={(status) => {
355
+ setStatus(status);
356
+ formProps?.onStatusChange?.(status);
357
+ }}
358
+ onFormContextReady={(formContext) => {
359
+ setFormContext(formContext);
360
+ formProps?.onFormContextReady?.(formContext);
361
+ }}
362
+ onSaved={(params) => {
363
+ const res = {
364
+ ...params,
365
+ selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab
366
+ };
367
+ onSaved?.(res);
368
+ formProps?.onSaved?.(res);
369
+ }}
370
+ Builder={selectedSecondaryForm?.Builder}
371
+ />
372
+ );
327
373
 
328
374
  const subcollectionTabs = subcollections && subcollections.map((subcollection) =>
329
375
  <Tab
@@ -90,6 +90,7 @@ export type EntityFormProps<M extends Record<string, any>> = {
90
90
  children?: React.ReactNode;
91
91
  };
92
92
 
93
+
93
94
  export function EntityForm<M extends Record<string, any>>({
94
95
  path,
95
96
  entityId: entityIdProp,
@@ -678,9 +679,9 @@ export function EntityForm<M extends Record<string, any>>({
678
679
  />;
679
680
 
680
681
  return (
681
- <Formex value={formContext.formex}>
682
+ <Formex value={formex}>
682
683
  <form
683
- onSubmit={formContext.formex.handleSubmit}
684
+ onSubmit={formex.handleSubmit}
684
685
  onReset={() => formex.resetForm({
685
686
  values: getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs) as M
686
687
  })}
@@ -692,7 +693,7 @@ export function EntityForm<M extends Record<string, any>>({
692
693
 
693
694
  <div className={cls("flex flex-col w-full pt-12 pb-16 px-4 sm:px-8 md:px-10")}>
694
695
 
695
- {formContext.formex.dirty
696
+ {formex.dirty
696
697
  ? <Tooltip title={"Local unsaved changes"}
697
698
  className={"self-end sticky top-4 z-10"}>
698
699
  <Chip size={"small"} colorScheme={"orangeDarker"}>
@@ -777,3 +778,4 @@ function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<a
777
778
  }
778
779
  }, [formex.values]);
779
780
  }
781
+
@@ -528,7 +528,7 @@ function useCustomBlocker(): NavigationBlocker {
528
528
  return shouldBlock;
529
529
  });
530
530
  } catch (e) {
531
- console.warn("Blocker not available, navigation will not be blocked");
531
+ // console.warn("Blocker not available, navigation will not be blocked");
532
532
  }
533
533
 
534
534
  const updateBlockListener = (path: string, block: boolean, basePath?: string) => {
@@ -175,7 +175,7 @@ function EntityFullScreenRoute({
175
175
  return blocked.current;
176
176
  });
177
177
  } catch (e) {
178
- console.warn("Blocker not available, navigation will not be blocked");
178
+ // console.warn("Blocker not available, navigation will not be blocked");
179
179
  }
180
180
 
181
181
  const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
@@ -171,7 +171,9 @@ export interface FormContext<M extends Record<string, any> = any> {
171
171
  openEntityMode: "side_panel" | "full_screen";
172
172
 
173
173
  /**
174
- * This is the underlying formex controller that powers the form
174
+ * This is the underlying formex controller that powers the form.
175
+ * If you are in a red only mode, the formex controller is there, but you can't
176
+ * operate with it
175
177
  */
176
178
  formex: FormexController<M>;
177
179
  }
@@ -815,6 +815,13 @@ export type StorageConfig = {
815
815
  */
816
816
  maxSize?: number,
817
817
 
818
+ /**
819
+ * Use this callback to process the file before uploading it to the storage.
820
+ * If nothing is returned, the file is uploaded as it is.
821
+ * @param file
822
+ */
823
+ processFile?: (file: File) => Promise<File> | undefined;
824
+
818
825
  /**
819
826
  * Postprocess the saved value (storage path or URL)
820
827
  * after it has been resolved.
@@ -0,0 +1,62 @@
1
+ import { FormexController } from "@firecms/formex";
2
+
3
+ export function createFormexStub<T extends object>(values: T): FormexController<T> {
4
+ const errorMessage = "You are in a read-only context. You cannot modify the formex controller.";
5
+
6
+ return {
7
+ values,
8
+ initialValues: values,
9
+ touched: {} as Record<string, boolean>,
10
+ dirty: false,
11
+ errors: {} as Record<string, string>,
12
+ submitCount: 0,
13
+ isSubmitting: false,
14
+ isValidating: false,
15
+ version: 0,
16
+ canUndo: false,
17
+ canRedo: false,
18
+
19
+ setValues: () => {
20
+ throw new Error(errorMessage);
21
+ },
22
+ setFieldValue: () => {
23
+ throw new Error(errorMessage);
24
+ },
25
+ setFieldTouched: () => {
26
+ throw new Error(errorMessage);
27
+ },
28
+ setDirty: () => {
29
+ throw new Error(errorMessage);
30
+ },
31
+ setSubmitCount: () => {
32
+ throw new Error(errorMessage);
33
+ },
34
+ setFieldError: () => {
35
+ throw new Error(errorMessage);
36
+ },
37
+ handleChange: () => {
38
+ throw new Error(errorMessage);
39
+ },
40
+ handleBlur: () => {
41
+ throw new Error(errorMessage);
42
+ },
43
+ handleSubmit: () => {
44
+ throw new Error(errorMessage);
45
+ },
46
+ validate: () => {
47
+ throw new Error(errorMessage);
48
+ },
49
+ resetForm: () => {
50
+ throw new Error(errorMessage);
51
+ },
52
+ setSubmitting: () => {
53
+ throw new Error(errorMessage);
54
+ },
55
+ undo: () => {
56
+ throw new Error(errorMessage);
57
+ },
58
+ redo: () => {
59
+ throw new Error(errorMessage);
60
+ }
61
+ };
62
+ }
@@ -78,6 +78,7 @@ export function mergeCollection(target: EntityCollection,
78
78
  modifyCollection?: (props: ModifyCollectionProps) => EntityCollection | void
79
79
  ): EntityCollection {
80
80
 
81
+
81
82
  const subcollectionsMerged = joinCollectionLists(
82
83
  target?.subcollections ?? [],
83
84
  source?.subcollections ?? [],
@@ -94,7 +95,7 @@ export function mergeCollection(target: EntityCollection,
94
95
  propertiesMerged[key] = source.properties[key] as PropertyOrBuilder;
95
96
  });
96
97
 
97
- const mergedCollection = mergeDeep(target, source);
98
+ const mergedCollection = mergeDeep(target, source, true);
98
99
  const targetPropertiesOrder = getCollectionKeys(target);
99
100
  const sourcePropertiesOrder = getCollectionKeys(source);
100
101
  const mergedPropertiesOrder = [...new Set([...sourcePropertiesOrder, ...targetPropertiesOrder])];
@@ -118,6 +119,7 @@ export function mergeCollection(target: EntityCollection,
118
119
 
119
120
  // @ts-ignore
120
121
  resultCollection["merged"] = true;
122
+
121
123
  return resultCollection
122
124
  }
123
125
 
@@ -12,12 +12,16 @@ export function isObject(item: any) {
12
12
  return item && typeof item === "object" && !Array.isArray(item);
13
13
  }
14
14
 
15
- export function mergeDeep<T extends Record<any, any>, U extends Record<any, any>>(target: T, source: U): T & U {
15
+ export function mergeDeep<T extends Record<any, any>, U extends Record<any, any>>(target: T, source: U, ignoreUndefined: boolean = false): T & U {
16
16
  const targetIsObject = isObject(target);
17
17
  const output = targetIsObject ? { ...target } : target;
18
18
  if (targetIsObject && isObject(source)) {
19
19
  Object.keys(source).forEach(key => {
20
20
  const sourceElement = source[key];
21
+ // Skip undefined values when ignoreUndefined is true
22
+ if (ignoreUndefined && sourceElement === undefined) {
23
+ return;
24
+ }
21
25
  if (sourceElement instanceof Date) {
22
26
  // Assign a new Date instance with the same time value
23
27
  Object.assign(output, { [key]: new Date(sourceElement.getTime()) });
@@ -441,11 +441,13 @@ export function resolveEntityView(entityView: string | EntityCustomView<any>, co
441
441
  export function resolvedSelectedEntityView<M extends Record<string, any>>(
442
442
  customViews: (string | EntityCustomView<M>)[] | undefined,
443
443
  customizationController: CustomizationController,
444
- selectedTab?: string
444
+ selectedTab?: string,
445
+ canEdit?: boolean,
445
446
  ) {
446
447
  const resolvedEntityViews = customViews ? customViews
447
448
  .map(e => resolveEntityView(e, customizationController.entityViews))
448
- .filter(Boolean) as EntityCustomView[]
449
+ .filter((e): e is EntityCustomView<M> => Boolean(e))
450
+ .filter((e) => canEdit || !e.includeActions)
449
451
  : [];
450
452
 
451
453
  const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTab);
@@ -71,6 +71,8 @@ export function useStorageUploadController<M extends object>({
71
71
  if (!storage)
72
72
  throw Error("Storage meta must be specified");
73
73
 
74
+ const processFile = storage?.processFile;
75
+
74
76
  const metadata: Record<string, any> | undefined = storage?.metadata;
75
77
  const size = multipleFilesSupported ? "medium" : "large";
76
78
 
@@ -172,7 +174,22 @@ export function useStorageUploadController<M extends object>({
172
174
  if (!acceptedFiles.length || disabled)
173
175
  return;
174
176
 
177
+ if (processFile) {
178
+ try {
179
+ acceptedFiles = await Promise.all(acceptedFiles.map(async file => {
180
+ const processedFile = await processFile(file);
181
+ if (!processedFile) {
182
+ return file;
183
+ }
184
+ return processedFile;
185
+ }));
186
+ } catch (e) {
187
+ console.error("Error processing file with custom code. Attempting to continue uploading.", e);
188
+ }
189
+ }
190
+
175
191
  let newInternalValue: StorageFieldItem[];
192
+
176
193
  if (multipleFilesSupported) {
177
194
  newInternalValue = [...internalValue,
178
195
  ...(await Promise.all(acceptedFiles.map(async file => {