@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.
- package/dist/core/EntityEditView.d.ts +2 -1
- package/dist/index.es.js +166 -143
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +166 -143
- package/dist/index.umd.js.map +1 -1
- package/dist/types/fields.d.ts +3 -1
- package/dist/types/properties.d.ts +6 -0
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/objects.d.ts +1 -1
- package/dist/util/resolutions.d.ts +4 -4
- package/package.json +5 -5
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +1 -1
- package/src/components/EntityJsonPreview.tsx +3 -3
- package/src/core/EntityEditView.tsx +106 -60
- package/src/form/EntityForm.tsx +5 -3
- package/src/hooks/useBuildNavigationController.tsx +1 -1
- package/src/routes/FireCMSRoute.tsx +1 -1
- package/src/types/fields.tsx +3 -1
- package/src/types/properties.ts +7 -0
- package/src/util/createFormexStub.tsx +62 -0
- package/src/util/join_collections.ts +3 -1
- package/src/util/objects.ts +5 -1
- package/src/util/resolutions.ts +4 -2
- package/src/util/useStorageUploadController.tsx +17 -0
package/dist/types/fields.d.ts
CHANGED
|
@@ -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.
|
package/dist/util/objects.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
54
|
-
"@firecms/formex": "^3.0.0-canary.
|
|
55
|
-
"@firecms/ui": "^3.0.0-canary.
|
|
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": "
|
|
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.
|
|
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
|
-
|
|
51
|
+
backgroundColor: "inherit"
|
|
52
52
|
}}
|
|
53
|
-
className="
|
|
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
|
-
{
|
|
250
|
+
{usedFormContext && <Builder
|
|
224
251
|
collection={collection}
|
|
225
252
|
entity={usedEntity}
|
|
226
|
-
modifiedValues={
|
|
227
|
-
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -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={
|
|
682
|
+
<Formex value={formex}>
|
|
682
683
|
<form
|
|
683
|
-
onSubmit={
|
|
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
|
-
{
|
|
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");
|
package/src/types/fields.tsx
CHANGED
|
@@ -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
|
}
|
package/src/types/properties.ts
CHANGED
|
@@ -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
|
|
package/src/util/objects.ts
CHANGED
|
@@ -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()) });
|
package/src/util/resolutions.ts
CHANGED
|
@@ -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(
|
|
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 => {
|