@firecms/core 3.3.0 → 3.4.0-canary.2575a08
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/app/useApp.d.ts +1 -0
- package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
- package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/EntityPreview.d.ts +3 -1
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
- package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
- package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
- package/dist/components/common/table_url_params.d.ts +24 -0
- package/dist/components/common/useDataSourceTableController.d.ts +9 -1
- package/dist/components/common/useTableSearchHelper.d.ts +9 -1
- package/dist/core/DefaultDrawer.d.ts +10 -3
- package/dist/core/EntityEditView.d.ts +9 -1
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +8 -1
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/save.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/hooks/data/useEntityFetch.d.ts +9 -1
- package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
- package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
- package/dist/index.es.js +3658 -1820
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +3655 -1817
- package/dist/index.umd.js.map +1 -1
- package/dist/locales/pl.d.ts +2 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
- package/dist/types/collections.d.ts +31 -0
- package/dist/types/datasource.d.ts +67 -4
- package/dist/types/entities.d.ts +21 -1
- package/dist/types/entity_actions.d.ts +6 -0
- package/dist/types/entity_callbacks.d.ts +21 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +13 -0
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +7 -0
- package/dist/types/translations.d.ts +6 -0
- package/dist/util/__tests__/collections.test.d.ts +1 -0
- package/dist/util/__tests__/urls.test.d.ts +1 -0
- package/dist/util/collections.d.ts +1 -1
- package/dist/util/entities.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/geopoint.d.ts +22 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/navigation_blocking.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +17 -0
- package/dist/util/navigation_utils.d.ts +107 -3
- package/dist/util/parent_references_from_path.d.ts +16 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- package/dist/util/urls.d.ts +22 -0
- package/package.json +17 -10
- package/src/app/Scaffold.tsx +34 -44
- package/src/app/useApp.tsx +1 -0
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
- package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityPreview.tsx +45 -41
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
- package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
- package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
- package/src/components/VirtualTable/VirtualTable.tsx +19 -8
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/table_url_params.ts +172 -0
- package/src/components/common/useDataSourceTableController.tsx +126 -163
- package/src/components/common/useTableSearchHelper.ts +26 -2
- package/src/contexts/SnackbarProvider.tsx +14 -1
- package/src/core/DefaultDrawer.tsx +150 -64
- package/src/core/DrawerNavigationGroup.tsx +27 -29
- package/src/core/DrawerNavigationItem.tsx +10 -12
- package/src/core/EntityEditView.tsx +77 -35
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +8 -4
- package/src/core/field_configs.tsx +15 -0
- package/src/form/EntityForm.tsx +31 -10
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
- package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
- package/src/form/index.tsx +1 -0
- package/src/hooks/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +6 -1
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +35 -4
- package/src/hooks/useBuildNavigationController.tsx +23 -8
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +50 -4
- package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
- package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
- package/src/internal/useBuildDataSource.ts +37 -15
- package/src/internal/useBuildSideEntityController.tsx +20 -5
- package/src/locales/de.ts +7 -2
- package/src/locales/en.ts +7 -2
- package/src/locales/es.ts +7 -2
- package/src/locales/fr.ts +7 -2
- package/src/locales/hi.ts +7 -2
- package/src/locales/it.ts +7 -2
- package/src/locales/pl.ts +735 -0
- package/src/locales/pt.ts +7 -2
- package/src/preview/PropertyPreview.tsx +12 -0
- package/src/preview/components/ReferencePreview.tsx +7 -3
- package/src/preview/components/StorageThumbnail.tsx +24 -2
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
- package/src/routes/FireCMSRoute.tsx +54 -27
- package/src/types/collections.ts +34 -0
- package/src/types/datasource.ts +67 -4
- package/src/types/entities.ts +24 -1
- package/src/types/entity_actions.tsx +6 -0
- package/src/types/entity_callbacks.ts +24 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +13 -0
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +7 -0
- package/src/types/translations.ts +6 -0
- package/src/util/__tests__/collections.test.ts +32 -0
- package/src/util/__tests__/urls.test.ts +131 -0
- package/src/util/collections.ts +1 -1
- package/src/util/entities.ts +16 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/geopoint.ts +81 -0
- package/src/util/index.ts +2 -0
- package/src/util/navigation_blocking.ts +45 -0
- package/src/util/navigation_from_path.ts +47 -7
- package/src/util/navigation_utils.ts +244 -4
- package/src/util/parent_references_from_path.ts +45 -4
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
- package/src/util/previews.ts +8 -1
- package/src/util/resolutions.ts +8 -0
- package/src/util/urls.ts +52 -0
|
@@ -8,7 +8,7 @@ import { EntityEditView, OnUpdateParams } from "./EntityEditView";
|
|
|
8
8
|
import { useSideDialogContext } from "./SideDialogs";
|
|
9
9
|
import { CloseIcon, IconButton, OpenInFullIcon } from "@firecms/ui";
|
|
10
10
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
11
|
-
import { saveEntityToMemoryCache } from "../util/entity_cache";
|
|
11
|
+
import { entityCacheKey, saveEntityToMemoryCache } from "../util/entity_cache";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* This is the component in charge of rendering the side dialog used
|
|
@@ -25,6 +25,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
25
25
|
path,
|
|
26
26
|
entityId,
|
|
27
27
|
fullIdPath,
|
|
28
|
+
pathSegments,
|
|
28
29
|
formProps,
|
|
29
30
|
} = props;
|
|
30
31
|
|
|
@@ -59,6 +60,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
59
60
|
if (params.status !== "existing") {
|
|
60
61
|
sideEntityController.replace({
|
|
61
62
|
path: params.path,
|
|
63
|
+
pathSegments: params.pathSegments ?? pathSegments,
|
|
62
64
|
entityId: params.entityId,
|
|
63
65
|
fullIdPath: props.fullIdPath,
|
|
64
66
|
selectedTab: params.selectedTab,
|
|
@@ -75,10 +77,10 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
const parentCollectionIds = useMemo(() => {
|
|
78
|
-
return navigationController.getParentCollectionIds(path);
|
|
80
|
+
return navigationController.getParentCollectionIds(path, pathSegments);
|
|
79
81
|
}, [navigationController, path]);
|
|
80
82
|
|
|
81
|
-
const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path);
|
|
83
|
+
const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path, false, pathSegments);
|
|
82
84
|
|
|
83
85
|
useEffect(() => {
|
|
84
86
|
function beforeunload(e: any) {
|
|
@@ -116,6 +118,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
116
118
|
<EntityEditView
|
|
117
119
|
{...props}
|
|
118
120
|
fullIdPath={fullIdPath}
|
|
121
|
+
pathSegments={pathSegments}
|
|
119
122
|
layout={"side_panel"}
|
|
120
123
|
collection={collection as EntityCollection}
|
|
121
124
|
parentCollectionIds={parentCollectionIds}
|
|
@@ -135,7 +138,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
135
138
|
className="self-center"
|
|
136
139
|
size={"smallest"}
|
|
137
140
|
onClick={() => {
|
|
138
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path
|
|
141
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
139
142
|
saveEntityToMemoryCache(key, values);
|
|
140
143
|
if (entityId)
|
|
141
144
|
navigate(location.pathname + location.search);
|
|
@@ -152,6 +155,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
152
155
|
}) => {
|
|
153
156
|
sideEntityController.replace({
|
|
154
157
|
path,
|
|
158
|
+
pathSegments,
|
|
155
159
|
entityId,
|
|
156
160
|
fullIdPath: props.fullIdPath,
|
|
157
161
|
selectedTab,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ArrayOfReferencesFieldBinding,
|
|
7
7
|
BlockFieldBinding,
|
|
8
8
|
DateTimeFieldBinding,
|
|
9
|
+
GeopointFieldBinding,
|
|
9
10
|
KeyValueFieldBinding,
|
|
10
11
|
MapFieldBinding,
|
|
11
12
|
MultiSelectFieldBinding,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
ListAltIcon,
|
|
35
36
|
ListIcon,
|
|
36
37
|
MailIcon,
|
|
38
|
+
LocationOnIcon,
|
|
37
39
|
NumbersIcon, PersonIcon,
|
|
38
40
|
RepeatIcon,
|
|
39
41
|
ScheduleIcon,
|
|
@@ -273,6 +275,17 @@ export const DEFAULT_FIELD_CONFIGS: Record<string, PropertyConfig<any>> = {
|
|
|
273
275
|
Field: DateTimeFieldBinding
|
|
274
276
|
}
|
|
275
277
|
},
|
|
278
|
+
geopoint: {
|
|
279
|
+
key: "geopoint",
|
|
280
|
+
name: "Geopoint",
|
|
281
|
+
description: "Latitude and longitude pair",
|
|
282
|
+
Icon: LocationOnIcon,
|
|
283
|
+
color: "#0ea5e9",
|
|
284
|
+
property: {
|
|
285
|
+
dataType: "geopoint",
|
|
286
|
+
Field: GeopointFieldBinding
|
|
287
|
+
}
|
|
288
|
+
},
|
|
276
289
|
group: {
|
|
277
290
|
key: "group",
|
|
278
291
|
name: "Group",
|
|
@@ -419,6 +432,8 @@ export function getDefaultFieldId(property: Property | ResolvedProperty) {
|
|
|
419
432
|
return "switch";
|
|
420
433
|
} else if (property.dataType === "date") {
|
|
421
434
|
return "date_time";
|
|
435
|
+
} else if (property.dataType === "geopoint") {
|
|
436
|
+
return "geopoint";
|
|
422
437
|
} else if (property.dataType === "reference") {
|
|
423
438
|
return "reference";
|
|
424
439
|
}
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -48,6 +48,7 @@ import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
|
48
48
|
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
49
49
|
import { ValidationError } from "yup";
|
|
50
50
|
import {
|
|
51
|
+
entityCacheKey,
|
|
51
52
|
flattenKeys,
|
|
52
53
|
getEntityFromCache,
|
|
53
54
|
removeEntityFromCache,
|
|
@@ -64,6 +65,8 @@ export type OnUpdateParams = {
|
|
|
64
65
|
entity: Entity<any>,
|
|
65
66
|
status: EntityStatus,
|
|
66
67
|
path: string,
|
|
68
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
69
|
+
pathSegments?: string[],
|
|
67
70
|
entityId?: string;
|
|
68
71
|
selectedTab?: string;
|
|
69
72
|
collection: EntityCollection<any>
|
|
@@ -71,6 +74,11 @@ export type OnUpdateParams = {
|
|
|
71
74
|
|
|
72
75
|
export type EntityFormProps<M extends Record<string, any>> = {
|
|
73
76
|
path: string;
|
|
77
|
+
/**
|
|
78
|
+
* `path` split at its real segment boundaries; forwarded to the datasource for
|
|
79
|
+
* generateEntityId and checkUniqueField. Never derived from `path`.
|
|
80
|
+
*/
|
|
81
|
+
pathSegments?: string[];
|
|
74
82
|
fullIdPath?: string;
|
|
75
83
|
collection: EntityCollection<M>;
|
|
76
84
|
entityId?: string;
|
|
@@ -214,6 +222,7 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
|
|
|
214
222
|
|
|
215
223
|
export function EntityForm<M extends Record<string, any>>({
|
|
216
224
|
path,
|
|
225
|
+
pathSegments,
|
|
217
226
|
fullIdPath,
|
|
218
227
|
entityId: entityIdProp,
|
|
219
228
|
collection,
|
|
@@ -270,6 +279,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
270
279
|
entityId: entityIdProp,
|
|
271
280
|
collection,
|
|
272
281
|
path,
|
|
282
|
+
pathSegments,
|
|
273
283
|
values: valuesToBeSaved
|
|
274
284
|
});
|
|
275
285
|
}, false, 2000);
|
|
@@ -292,7 +302,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
292
302
|
if (mustSetCustomId) {
|
|
293
303
|
return undefined;
|
|
294
304
|
} else {
|
|
295
|
-
return dataSource.generateEntityId(path, collection);
|
|
305
|
+
return dataSource.generateEntityId(path, collection, pathSegments);
|
|
296
306
|
}
|
|
297
307
|
} else {
|
|
298
308
|
return entityIdProp;
|
|
@@ -308,7 +318,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
308
318
|
const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [authController, collection, path, status, entity, customizationController.propertyConfigs]);
|
|
309
319
|
|
|
310
320
|
const localChangesDataRaw = useMemo(() => entityId
|
|
311
|
-
? getEntityFromCache(path
|
|
321
|
+
? getEntityFromCache(entityCacheKey(path, entityId))
|
|
312
322
|
: getEntityFromCache(path + "#new"), [entityId, path]);
|
|
313
323
|
|
|
314
324
|
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
@@ -387,8 +397,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
387
397
|
onValuesModified?.(false, initialValues as M);
|
|
388
398
|
},
|
|
389
399
|
onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
|
|
390
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path
|
|
391
|
-
if (controller.dirty) {
|
|
400
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
401
|
+
if (controller.dirty && localChangesBackup !== false) {
|
|
392
402
|
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
393
403
|
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
394
404
|
saveEntityToCache(key, touchedValues);
|
|
@@ -468,8 +478,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
468
478
|
removeEntityFromMemoryCache(path + "#new");
|
|
469
479
|
removeEntityFromCache(path + "#new");
|
|
470
480
|
} else {
|
|
471
|
-
removeEntityFromMemoryCache(path
|
|
472
|
-
removeEntityFromCache(path
|
|
481
|
+
removeEntityFromMemoryCache(entityCacheKey(path, entityId));
|
|
482
|
+
removeEntityFromCache(entityCacheKey(path, entityId));
|
|
473
483
|
}
|
|
474
484
|
}
|
|
475
485
|
|
|
@@ -491,6 +501,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
491
501
|
entity: updatedEntity,
|
|
492
502
|
status,
|
|
493
503
|
path,
|
|
504
|
+
pathSegments,
|
|
494
505
|
entityId: updatedEntity.id,
|
|
495
506
|
collection
|
|
496
507
|
});
|
|
@@ -511,16 +522,20 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
511
522
|
previousValues,
|
|
512
523
|
entityId,
|
|
513
524
|
collection,
|
|
514
|
-
path
|
|
525
|
+
path,
|
|
526
|
+
pathSegments
|
|
515
527
|
}: {
|
|
516
528
|
collection: EntityCollection<M>,
|
|
517
529
|
path: string,
|
|
530
|
+
/** `path` split at its real segment boundaries, travelling with the `path` above. */
|
|
531
|
+
pathSegments?: string[],
|
|
518
532
|
entityId: string | undefined,
|
|
519
533
|
values: M,
|
|
520
534
|
previousValues?: M,
|
|
521
535
|
}) => {
|
|
522
536
|
return saveEntityWithCallbacks({
|
|
523
537
|
path,
|
|
538
|
+
pathSegments,
|
|
524
539
|
entityId,
|
|
525
540
|
values,
|
|
526
541
|
previousValues,
|
|
@@ -538,6 +553,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
538
553
|
type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
539
554
|
collection: ResolvedEntityCollection<M>,
|
|
540
555
|
path: string,
|
|
556
|
+
pathSegments?: string[],
|
|
541
557
|
entityId: string | undefined,
|
|
542
558
|
values: EntityValues<M>,
|
|
543
559
|
previousValues?: EntityValues<M>,
|
|
@@ -547,6 +563,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
547
563
|
const onSaveEntityRequest = async ({
|
|
548
564
|
collection,
|
|
549
565
|
path,
|
|
566
|
+
pathSegments,
|
|
550
567
|
entityId,
|
|
551
568
|
values,
|
|
552
569
|
previousValues,
|
|
@@ -560,6 +577,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
560
577
|
return saveEntity({
|
|
561
578
|
collection,
|
|
562
579
|
path,
|
|
580
|
+
pathSegments,
|
|
563
581
|
entityId,
|
|
564
582
|
values,
|
|
565
583
|
previousValues
|
|
@@ -573,6 +591,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
573
591
|
return onSaveEntityRequest({
|
|
574
592
|
collection: resolvedCollection,
|
|
575
593
|
path,
|
|
594
|
+
pathSegments,
|
|
576
595
|
entityId,
|
|
577
596
|
values,
|
|
578
597
|
previousValues: entity?.values,
|
|
@@ -623,13 +642,14 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
623
642
|
const plugins = customizationController.plugins;
|
|
624
643
|
|
|
625
644
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
626
|
-
const parentCollectionIds = navigationController.getParentCollectionIds(path);
|
|
645
|
+
const parentCollectionIds = navigationController.getParentCollectionIds(path, pathSegments);
|
|
627
646
|
|
|
628
647
|
if (plugins && collection) {
|
|
629
648
|
const actionProps: PluginFormActionProps = {
|
|
630
649
|
entityId,
|
|
631
650
|
parentCollectionIds,
|
|
632
651
|
path,
|
|
652
|
+
pathSegments,
|
|
633
653
|
status,
|
|
634
654
|
collection,
|
|
635
655
|
context,
|
|
@@ -692,7 +712,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
692
712
|
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
693
713
|
name,
|
|
694
714
|
value
|
|
695
|
-
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
715
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId, collection, pathSegments),
|
|
696
716
|
[dataSource, path, entityId]);
|
|
697
717
|
|
|
698
718
|
const validationSchema = useMemo(() => entityId
|
|
@@ -890,6 +910,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
890
910
|
const dialogActions = <EntityFormActionsComponent
|
|
891
911
|
collection={resolvedCollection}
|
|
892
912
|
path={path}
|
|
913
|
+
pathSegments={pathSegments}
|
|
893
914
|
fullPath={path}
|
|
894
915
|
fullIdPath={fullIdPath}
|
|
895
916
|
entity={entity}
|
|
@@ -924,7 +945,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
924
945
|
|
|
925
946
|
{manualApplyLocalChanges && hasLocalChanges &&
|
|
926
947
|
<LocalChangesMenu
|
|
927
|
-
cacheKey={status === "new" || status === "copy" ? path + "#new" : path
|
|
948
|
+
cacheKey={status === "new" || status === "copy" ? path + "#new" : entityCacheKey(path, entityId)}
|
|
928
949
|
properties={resolvedCollection.properties}
|
|
929
950
|
cachedData={localChangesDataRaw as Partial<M>}
|
|
930
951
|
formex={formex}
|
|
@@ -26,6 +26,8 @@ export interface EntityFormActionsProps {
|
|
|
26
26
|
fullIdPath?: string;
|
|
27
27
|
collection: ResolvedEntityCollection;
|
|
28
28
|
path: string;
|
|
29
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
30
|
+
pathSegments?: string[];
|
|
29
31
|
entity?: Entity;
|
|
30
32
|
layout: "bottom" | "side";
|
|
31
33
|
savingError?: Error;
|
|
@@ -58,7 +58,7 @@ export function ArrayOfReferencesFieldBinding({
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
|
|
61
|
-
setValue(entities.map(e => getReferenceFrom(e)));
|
|
61
|
+
setValue(entities.filter(Boolean).map(e => getReferenceFrom(e)));
|
|
62
62
|
}, [setValue]);
|
|
63
63
|
|
|
64
64
|
const referenceDialogController = useReferenceDialog({
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { CloseIcon, IconButton, TextField } from "@firecms/ui";
|
|
4
|
+
import { FieldProps, GeoPoint } from "../../types";
|
|
5
|
+
import { FieldHelperText, LabelWithIcon } from "../components";
|
|
6
|
+
import { PropertyIdCopyTooltip } from "../../components";
|
|
7
|
+
import { useClearRestoreValue } from "../useClearRestoreValue";
|
|
8
|
+
import { getIconForProperty } from "../../util";
|
|
9
|
+
import { formatGeoPoint, getGeoPointCoordinates, parseGeoPoint } from "../../util/geopoint";
|
|
10
|
+
|
|
11
|
+
type GeopointFieldBindingProps = FieldProps<GeoPoint>;
|
|
12
|
+
|
|
13
|
+
export function GeopointFieldBinding({
|
|
14
|
+
propertyKey,
|
|
15
|
+
value,
|
|
16
|
+
setValue,
|
|
17
|
+
error,
|
|
18
|
+
showError,
|
|
19
|
+
disabled,
|
|
20
|
+
autoFocus,
|
|
21
|
+
property,
|
|
22
|
+
includeDescription,
|
|
23
|
+
size = "large"
|
|
24
|
+
}: GeopointFieldBindingProps) {
|
|
25
|
+
|
|
26
|
+
const coordinates = getGeoPointCoordinates(value);
|
|
27
|
+
const canClear = Boolean((property as any).clearable);
|
|
28
|
+
const [latitude, setLatitude] = useState<string>(coordinates ? coordinates.latitude.toString() : "");
|
|
29
|
+
const [longitude, setLongitude] = useState<string>(coordinates ? coordinates.longitude.toString() : "");
|
|
30
|
+
const [localError, setLocalError] = useState<string | undefined>();
|
|
31
|
+
const skipSyncRef = useRef(false);
|
|
32
|
+
|
|
33
|
+
useClearRestoreValue({
|
|
34
|
+
property,
|
|
35
|
+
value,
|
|
36
|
+
setValue
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (skipSyncRef.current) {
|
|
41
|
+
skipSyncRef.current = false;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const nextCoordinates = getGeoPointCoordinates(value);
|
|
45
|
+
setLatitude(nextCoordinates ? nextCoordinates.latitude.toString() : "");
|
|
46
|
+
setLongitude(nextCoordinates ? nextCoordinates.longitude.toString() : "");
|
|
47
|
+
}, [value]);
|
|
48
|
+
|
|
49
|
+
const updateGeoPoint = (nextLatitude: string, nextLongitude: string) => {
|
|
50
|
+
skipSyncRef.current = true;
|
|
51
|
+
setLatitude(nextLatitude);
|
|
52
|
+
setLongitude(nextLongitude);
|
|
53
|
+
|
|
54
|
+
const trimmedLatitude = nextLatitude.trim();
|
|
55
|
+
const trimmedLongitude = nextLongitude.trim();
|
|
56
|
+
|
|
57
|
+
if (!trimmedLatitude && !trimmedLongitude) {
|
|
58
|
+
setLocalError(undefined);
|
|
59
|
+
setValue(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const parsed = parseGeoPoint(`${trimmedLatitude}, ${trimmedLongitude}`);
|
|
64
|
+
|
|
65
|
+
if (parsed.error) {
|
|
66
|
+
setLocalError(parsed.error);
|
|
67
|
+
setValue(null);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setLocalError(undefined);
|
|
72
|
+
setValue(parsed.point);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleClear = (event?: React.MouseEvent) => {
|
|
76
|
+
if (event) {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
}
|
|
80
|
+
updateGeoPoint("", "");
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const resolvedError = localError ?? error;
|
|
84
|
+
const shouldShowError = Boolean(resolvedError) || Boolean(showError && error);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<PropertyIdCopyTooltip propertyKey={propertyKey}>
|
|
89
|
+
<div className="flex flex-col gap-2">
|
|
90
|
+
<div className="mt-1">
|
|
91
|
+
<LabelWithIcon
|
|
92
|
+
icon={getIconForProperty(property, "small")}
|
|
93
|
+
required={property.validation?.required}
|
|
94
|
+
title={property.name}
|
|
95
|
+
className={shouldShowError ? "text-red-500 dark:text-red-500" : "text-text-secondary dark:text-text-secondary-dark"}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
|
99
|
+
<TextField
|
|
100
|
+
size={size}
|
|
101
|
+
value={latitude}
|
|
102
|
+
onChange={(event) => updateGeoPoint(event.target.value, longitude)}
|
|
103
|
+
autoFocus={autoFocus}
|
|
104
|
+
label={"Latitude"}
|
|
105
|
+
type="number"
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
endAdornment={canClear ? (
|
|
108
|
+
<IconButton onClick={handleClear}>
|
|
109
|
+
<CloseIcon />
|
|
110
|
+
</IconButton>
|
|
111
|
+
) : undefined}
|
|
112
|
+
error={shouldShowError && Boolean(resolvedError)}
|
|
113
|
+
/>
|
|
114
|
+
<TextField
|
|
115
|
+
size={size}
|
|
116
|
+
value={longitude}
|
|
117
|
+
onChange={(event) => updateGeoPoint(latitude, event.target.value)}
|
|
118
|
+
label={"Longitude"}
|
|
119
|
+
type="number"
|
|
120
|
+
disabled={disabled}
|
|
121
|
+
error={shouldShowError && Boolean(resolvedError)}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
{value && !resolvedError && (
|
|
125
|
+
<div className="text-xs text-text-secondary dark:text-text-secondary-dark font-mono">
|
|
126
|
+
{formatGeoPoint(value)}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</PropertyIdCopyTooltip>
|
|
131
|
+
|
|
132
|
+
<FieldHelperText includeDescription={includeDescription}
|
|
133
|
+
showError={shouldShowError}
|
|
134
|
+
error={resolvedError}
|
|
135
|
+
disabled={disabled}
|
|
136
|
+
property={property}/>
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|
package/src/form/index.tsx
CHANGED
|
@@ -11,6 +11,7 @@ export { StorageUploadFieldBinding } from "./field_bindings/StorageUploadFieldBi
|
|
|
11
11
|
export { TextFieldBinding } from "./field_bindings/TextFieldBinding";
|
|
12
12
|
export { SwitchFieldBinding } from "./field_bindings/SwitchFieldBinding";
|
|
13
13
|
export { DateTimeFieldBinding } from "./field_bindings/DateTimeFieldBinding";
|
|
14
|
+
export { GeopointFieldBinding } from "./field_bindings/GeopointFieldBinding";
|
|
14
15
|
export { ReferenceFieldBinding } from "./field_bindings/ReferenceFieldBinding";
|
|
15
16
|
export { ReferenceAsStringFieldBinding } from "./field_bindings/ReferenceAsStringFieldBinding";
|
|
16
17
|
export { MapFieldBinding } from "./field_bindings/MapFieldBinding";
|
package/src/hooks/data/delete.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type DeleteEntityWithCallbacksProps<M extends Record<string, any>, USER e
|
|
|
46
46
|
export async function deleteEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
|
|
47
47
|
dataSource,
|
|
48
48
|
entity,
|
|
49
|
+
pathSegments,
|
|
49
50
|
collection,
|
|
50
51
|
callbacks,
|
|
51
52
|
onDeleteSuccess,
|
|
@@ -62,11 +63,17 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
62
63
|
|
|
63
64
|
console.debug("Deleting entity", entity.path, entity.id);
|
|
64
65
|
|
|
66
|
+
// An explicit `pathSegments` wins, then the ones the entity was loaded with. The prop
|
|
67
|
+
// used to be accepted and silently ignored, which is the same class of bug this field
|
|
68
|
+
// exists to prevent.
|
|
69
|
+
const resolvedPathSegments = pathSegments ?? entity.pathSegments;
|
|
70
|
+
|
|
65
71
|
const entityDeleteProps: EntityOnDeleteProps<M, any> = {
|
|
66
72
|
entity,
|
|
67
73
|
collection,
|
|
68
74
|
entityId: entity.id,
|
|
69
75
|
path: entity.path,
|
|
76
|
+
pathSegments: resolvedPathSegments,
|
|
70
77
|
context
|
|
71
78
|
};
|
|
72
79
|
|
|
@@ -81,6 +88,7 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
return dataSource.deleteEntity({
|
|
91
|
+
pathSegments: resolvedPathSegments,
|
|
84
92
|
entity,
|
|
85
93
|
collection
|
|
86
94
|
}).then(() => {
|
package/src/hooks/data/save.ts
CHANGED
|
@@ -43,6 +43,7 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, any>> =
|
|
|
43
43
|
export async function saveEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
|
|
44
44
|
collection,
|
|
45
45
|
path,
|
|
46
|
+
pathSegments,
|
|
46
47
|
entityId,
|
|
47
48
|
values,
|
|
48
49
|
previousValues,
|
|
@@ -67,7 +68,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
67
68
|
|
|
68
69
|
const customizationController = context.customizationController;
|
|
69
70
|
|
|
70
|
-
const resolvedPath = context.navigation.resolveIdsFrom(path);
|
|
71
|
+
const resolvedPath = context.navigation.resolveIdsFrom(path, pathSegments);
|
|
71
72
|
|
|
72
73
|
const callbacks = collection.callbacks;
|
|
73
74
|
if (callbacks?.onPreSave) {
|
|
@@ -83,6 +84,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
83
84
|
updatedValues = await callbacks.onPreSave({
|
|
84
85
|
collection: resolvedCollection,
|
|
85
86
|
path,
|
|
87
|
+
pathSegments,
|
|
86
88
|
resolvedPath,
|
|
87
89
|
entityId,
|
|
88
90
|
values,
|
|
@@ -103,6 +105,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
103
105
|
return dataSource.saveEntity({
|
|
104
106
|
collection,
|
|
105
107
|
path: resolvedPath,
|
|
108
|
+
pathSegments,
|
|
106
109
|
entityId,
|
|
107
110
|
values: updatedValues,
|
|
108
111
|
previousValues,
|
|
@@ -121,6 +124,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
121
124
|
callbacks.onSaveSuccess({
|
|
122
125
|
collection: resolvedCollection,
|
|
123
126
|
path,
|
|
127
|
+
pathSegments,
|
|
124
128
|
resolvedPath,
|
|
125
129
|
entityId: entity.id,
|
|
126
130
|
values: updatedValues,
|
|
@@ -150,6 +154,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
150
154
|
callbacks.onSaveFailure({
|
|
151
155
|
collection: resolvedCollection,
|
|
152
156
|
path,
|
|
157
|
+
pathSegments,
|
|
153
158
|
resolvedPath,
|
|
154
159
|
entityId,
|
|
155
160
|
values: updatedValues,
|
|
@@ -13,6 +13,12 @@ export interface CollectionFetchProps<M extends Record<string, any>> {
|
|
|
13
13
|
* Absolute collection path
|
|
14
14
|
*/
|
|
15
15
|
path: string;
|
|
16
|
+
/**
|
|
17
|
+
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
18
|
+
* entity id containing "/" stays unambiguous. Never derived from `path` — see
|
|
19
|
+
* `pathSegments` on the datasource props.
|
|
20
|
+
*/
|
|
21
|
+
pathSegments?: string[];
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* collection of the entity displayed by this collection
|
|
@@ -63,6 +69,7 @@ export interface CollectionFetchResult<M extends Record<string, any>> {
|
|
|
63
69
|
export function useCollectionFetch<M extends Record<string, any>, USER extends User>(
|
|
64
70
|
{
|
|
65
71
|
path: inputPath,
|
|
72
|
+
pathSegments: inputPathSegments,
|
|
66
73
|
collection,
|
|
67
74
|
filterValues,
|
|
68
75
|
sortBy,
|
|
@@ -73,7 +80,13 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
73
80
|
const dataSource = useDataSource(collection);
|
|
74
81
|
const navigationController = useNavigationController();
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
// Resolved together so the two representations always describe the same chain.
|
|
84
|
+
// Segments are resolved, never derived: only collection ids are swapped for their
|
|
85
|
+
// real paths, and entity ids are carried through whole (see useEntityFetch).
|
|
86
|
+
const pathSegments = inputPathSegments
|
|
87
|
+
? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
|
|
88
|
+
: undefined;
|
|
89
|
+
const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
|
|
77
90
|
|
|
78
91
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
79
92
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
@@ -91,6 +104,10 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
91
104
|
setDataLoading(true);
|
|
92
105
|
|
|
93
106
|
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
107
|
+
// See useEntityFetch: entities carry the segments they were loaded with.
|
|
108
|
+
if (pathSegments) {
|
|
109
|
+
entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments });
|
|
110
|
+
}
|
|
94
111
|
if (collection.callbacks?.onFetch) {
|
|
95
112
|
try {
|
|
96
113
|
entities = await Promise.all(
|
|
@@ -98,6 +115,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
98
115
|
collection.callbacks!.onFetch!({
|
|
99
116
|
collection,
|
|
100
117
|
path,
|
|
118
|
+
pathSegments,
|
|
101
119
|
entity,
|
|
102
120
|
context
|
|
103
121
|
})));
|
|
@@ -124,6 +142,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
124
142
|
if (dataSource.listenCollection) {
|
|
125
143
|
return dataSource.listenCollection<M>({
|
|
126
144
|
path,
|
|
145
|
+
pathSegments,
|
|
127
146
|
collection,
|
|
128
147
|
onUpdate: onEntitiesUpdate,
|
|
129
148
|
onError,
|
|
@@ -137,6 +156,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
137
156
|
} else {
|
|
138
157
|
dataSource.fetchCollection<M>({
|
|
139
158
|
path,
|
|
159
|
+
pathSegments,
|
|
140
160
|
collection,
|
|
141
161
|
searchString,
|
|
142
162
|
filter: filterValues,
|