@firecms/core 3.3.0 → 3.4.0-canary.0ef7442
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 +7 -0
- 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 +7 -1
- package/dist/components/common/useTableSearchHelper.d.ts +7 -1
- package/dist/core/DefaultDrawer.d.ts +10 -3
- package/dist/core/EntityEditView.d.ts +7 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +6 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- 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 +6 -1
- package/dist/index.es.js +2915 -1330
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2913 -1328
- 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 +24 -0
- package/dist/types/datasource.d.ts +59 -4
- package/dist/types/entities.d.ts +21 -1
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +6 -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/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 +23 -1
- package/dist/util/parent_references_from_path.d.ts +1 -0
- 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/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 +4 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +29 -3
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityCollectionView/useBoardDataController.tsx +11 -0
- package/src/components/EntityPreview.tsx +41 -40
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +11 -0
- 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/table_url_params.ts +172 -0
- package/src/components/common/useDataSourceTableController.tsx +104 -159
- package/src/components/common/useTableSearchHelper.ts +21 -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 +78 -32
- package/src/core/EntitySidePanel.tsx +2 -0
- package/src/core/field_configs.tsx +15 -0
- package/src/form/EntityForm.tsx +9 -3
- 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 +2 -0
- package/src/hooks/data/save.ts +2 -0
- package/src/hooks/data/useCollectionFetch.tsx +13 -0
- package/src/hooks/data/useEntityFetch.tsx +18 -0
- package/src/hooks/useResolvedNavigationFrom.tsx +2 -0
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +2 -0
- package/src/internal/useBuildDataSource.ts +25 -4
- package/src/internal/useBuildSideEntityController.tsx +8 -1
- 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/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 +47 -25
- package/src/types/collections.ts +26 -0
- package/src/types/datasource.ts +59 -4
- package/src/types/entities.ts +24 -1
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +6 -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/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 +41 -6
- package/src/util/navigation_utils.ts +40 -2
- package/src/util/parent_references_from_path.ts +14 -4
- package/src/util/previews.ts +8 -1
- package/src/util/resolutions.ts +8 -0
- package/src/util/urls.ts +52 -0
package/src/form/EntityForm.tsx
CHANGED
|
@@ -71,6 +71,11 @@ export type OnUpdateParams = {
|
|
|
71
71
|
|
|
72
72
|
export type EntityFormProps<M extends Record<string, any>> = {
|
|
73
73
|
path: string;
|
|
74
|
+
/**
|
|
75
|
+
* `path` split at its real segment boundaries; forwarded to the datasource for
|
|
76
|
+
* generateEntityId and checkUniqueField. Never derived from `path`.
|
|
77
|
+
*/
|
|
78
|
+
pathSegments?: string[];
|
|
74
79
|
fullIdPath?: string;
|
|
75
80
|
collection: EntityCollection<M>;
|
|
76
81
|
entityId?: string;
|
|
@@ -214,6 +219,7 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
|
|
|
214
219
|
|
|
215
220
|
export function EntityForm<M extends Record<string, any>>({
|
|
216
221
|
path,
|
|
222
|
+
pathSegments,
|
|
217
223
|
fullIdPath,
|
|
218
224
|
entityId: entityIdProp,
|
|
219
225
|
collection,
|
|
@@ -292,7 +298,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
292
298
|
if (mustSetCustomId) {
|
|
293
299
|
return undefined;
|
|
294
300
|
} else {
|
|
295
|
-
return dataSource.generateEntityId(path, collection);
|
|
301
|
+
return dataSource.generateEntityId(path, collection, pathSegments);
|
|
296
302
|
}
|
|
297
303
|
} else {
|
|
298
304
|
return entityIdProp;
|
|
@@ -388,7 +394,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
388
394
|
},
|
|
389
395
|
onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
|
|
390
396
|
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
391
|
-
if (controller.dirty) {
|
|
397
|
+
if (controller.dirty && localChangesBackup !== false) {
|
|
392
398
|
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
393
399
|
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
394
400
|
saveEntityToCache(key, touchedValues);
|
|
@@ -692,7 +698,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
692
698
|
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
693
699
|
name,
|
|
694
700
|
value
|
|
695
|
-
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
701
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId, collection, pathSegments),
|
|
696
702
|
[dataSource, path, entityId]);
|
|
697
703
|
|
|
698
704
|
const validationSchema = useMemo(() => entityId
|
|
@@ -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
|
@@ -81,6 +81,8 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
return dataSource.deleteEntity({
|
|
84
|
+
// Carried by the entity from the fetch that produced it; undefined if unknown.
|
|
85
|
+
pathSegments: entity.pathSegments,
|
|
84
86
|
entity,
|
|
85
87
|
collection
|
|
86
88
|
}).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,
|
|
@@ -103,6 +104,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
103
104
|
return dataSource.saveEntity({
|
|
104
105
|
collection,
|
|
105
106
|
path: resolvedPath,
|
|
107
|
+
pathSegments,
|
|
106
108
|
entityId,
|
|
107
109
|
values: updatedValues,
|
|
108
110
|
previousValues,
|
|
@@ -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,
|
|
66
73
|
collection,
|
|
67
74
|
filterValues,
|
|
68
75
|
sortBy,
|
|
@@ -91,6 +98,10 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
91
98
|
setDataLoading(true);
|
|
92
99
|
|
|
93
100
|
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
101
|
+
// See useEntityFetch: entities carry the segments they were loaded with.
|
|
102
|
+
if (pathSegments) {
|
|
103
|
+
entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments });
|
|
104
|
+
}
|
|
94
105
|
if (collection.callbacks?.onFetch) {
|
|
95
106
|
try {
|
|
96
107
|
entities = await Promise.all(
|
|
@@ -124,6 +135,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
124
135
|
if (dataSource.listenCollection) {
|
|
125
136
|
return dataSource.listenCollection<M>({
|
|
126
137
|
path,
|
|
138
|
+
pathSegments,
|
|
127
139
|
collection,
|
|
128
140
|
onUpdate: onEntitiesUpdate,
|
|
129
141
|
onError,
|
|
@@ -137,6 +149,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
137
149
|
} else {
|
|
138
150
|
dataSource.fetchCollection<M>({
|
|
139
151
|
path,
|
|
152
|
+
pathSegments,
|
|
140
153
|
collection,
|
|
141
154
|
searchString,
|
|
142
155
|
filter: filterValues,
|
|
@@ -9,6 +9,11 @@ import { useFireCMSContext } from "../useFireCMSContext";
|
|
|
9
9
|
*/
|
|
10
10
|
export interface EntityFetchProps<M extends Record<string, any>, USER extends User = User> {
|
|
11
11
|
path: string;
|
|
12
|
+
/**
|
|
13
|
+
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
14
|
+
* entity id containing "/" stays unambiguous. Defaults to splitting the resolved path.
|
|
15
|
+
*/
|
|
16
|
+
pathSegments?: string[];
|
|
12
17
|
entityId?: string;
|
|
13
18
|
databaseId?: string;
|
|
14
19
|
collection: EntityCollection<M, USER>;
|
|
@@ -39,6 +44,7 @@ const CACHE: Record<string, Entity<any> | undefined> = {};
|
|
|
39
44
|
export function useEntityFetch<M extends Record<string, any>, USER extends User>(
|
|
40
45
|
{
|
|
41
46
|
path: inputPath,
|
|
47
|
+
pathSegments: inputPathSegments,
|
|
42
48
|
entityId,
|
|
43
49
|
collection,
|
|
44
50
|
databaseId,
|
|
@@ -49,6 +55,10 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
49
55
|
const navigationController = useNavigationController();
|
|
50
56
|
|
|
51
57
|
const path = navigationController.resolveIdsFrom(inputPath);
|
|
58
|
+
// Never fabricate segments. If the caller did not thread them, pass undefined so a
|
|
59
|
+
// delegate can tell "not provided" from a real answer — splitting `path` here would
|
|
60
|
+
// produce a confidently wrong array for any id containing "/".
|
|
61
|
+
const pathSegments = inputPathSegments;
|
|
52
62
|
|
|
53
63
|
const context: FireCMSContext<USER> = useFireCMSContext();
|
|
54
64
|
|
|
@@ -61,6 +71,12 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
61
71
|
setDataLoading(true);
|
|
62
72
|
|
|
63
73
|
const onEntityUpdate = async (updatedEntity: Entity<M> | undefined) => {
|
|
74
|
+
// Attach the segments this entity was loaded with, so anything derived from it
|
|
75
|
+
// later — a reference, a delete — stays resolvable without re-deriving them
|
|
76
|
+
// from the flattened path. Delegates are not required to set this themselves.
|
|
77
|
+
if (updatedEntity && pathSegments && !updatedEntity.pathSegments) {
|
|
78
|
+
updatedEntity = { ...updatedEntity, pathSegments };
|
|
79
|
+
}
|
|
64
80
|
if (collection.callbacks?.onFetch && updatedEntity) {
|
|
65
81
|
try {
|
|
66
82
|
updatedEntity = await collection.callbacks.onFetch({
|
|
@@ -97,6 +113,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
97
113
|
if (dataSource.listenEntity) {
|
|
98
114
|
return dataSource.listenEntity<M>({
|
|
99
115
|
path,
|
|
116
|
+
pathSegments,
|
|
100
117
|
entityId,
|
|
101
118
|
databaseId,
|
|
102
119
|
collection,
|
|
@@ -106,6 +123,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
106
123
|
} else {
|
|
107
124
|
dataSource.fetchEntity<M>({
|
|
108
125
|
path,
|
|
126
|
+
pathSegments,
|
|
109
127
|
entityId,
|
|
110
128
|
databaseId,
|
|
111
129
|
collection
|
|
@@ -86,6 +86,8 @@ export function resolveNavigationFrom<M extends Record<string, any>, USER extend
|
|
|
86
86
|
}
|
|
87
87
|
return dataSource.fetchEntity({
|
|
88
88
|
path: entry.path,
|
|
89
|
+
// The navigation entry already knows the real segment boundaries.
|
|
90
|
+
pathSegments: entry.pathSegments,
|
|
89
91
|
entityId: entry.entityId,
|
|
90
92
|
collection
|
|
91
93
|
})
|
|
@@ -74,6 +74,10 @@ export function useValidateAuthenticator<USER extends User = any>
|
|
|
74
74
|
|
|
75
75
|
if (authenticator instanceof Function && delegateUser && !equal(checkedUserRef.current?.uid, delegateUser.uid)) {
|
|
76
76
|
setAuthLoading(true);
|
|
77
|
+
// Reset the error of any previous authentication attempt. Otherwise, a
|
|
78
|
+
// rejected user would keep `canAccessMainView` to false for every user
|
|
79
|
+
// logging in afterwards, locking them out of the CMS until a page reload.
|
|
80
|
+
setNotAllowedError(false);
|
|
77
81
|
try {
|
|
78
82
|
const allowed = await authenticator({
|
|
79
83
|
user: delegateUser,
|
|
@@ -8,6 +8,7 @@ import { fr } from "../locales/fr";
|
|
|
8
8
|
import { it } from "../locales/it";
|
|
9
9
|
import { hi } from "../locales/hi";
|
|
10
10
|
import { pt } from "../locales/pt";
|
|
11
|
+
import { pl } from "../locales/pl";
|
|
11
12
|
import { FireCMSTranslations } from "../types/translations";
|
|
12
13
|
|
|
13
14
|
const FIRECMS_NS = "firecms_core";
|
|
@@ -139,6 +140,7 @@ function buildResources(
|
|
|
139
140
|
it: { [FIRECMS_NS]: { ...it } },
|
|
140
141
|
hi: { [FIRECMS_NS]: { ...hi } },
|
|
141
142
|
pt: { [FIRECMS_NS]: { ...pt } },
|
|
143
|
+
pl: { [FIRECMS_NS]: { ...pl } },
|
|
142
144
|
};
|
|
143
145
|
|
|
144
146
|
if (!translations) return resources;
|
|
@@ -55,6 +55,7 @@ export function useBuildDataSource({
|
|
|
55
55
|
*/
|
|
56
56
|
fetchCollection: useCallback(<M extends Record<string, any>>({
|
|
57
57
|
path,
|
|
58
|
+
pathSegments,
|
|
58
59
|
collection,
|
|
59
60
|
filter,
|
|
60
61
|
limit,
|
|
@@ -67,6 +68,7 @@ export function useBuildDataSource({
|
|
|
67
68
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
68
69
|
return usedDelegate.fetchCollection<M>({
|
|
69
70
|
path,
|
|
71
|
+
pathSegments,
|
|
70
72
|
filter,
|
|
71
73
|
limit,
|
|
72
74
|
startAfter,
|
|
@@ -97,6 +99,7 @@ export function useBuildDataSource({
|
|
|
97
99
|
? useCallback(<M extends Record<string, any>>(
|
|
98
100
|
{
|
|
99
101
|
path,
|
|
102
|
+
pathSegments,
|
|
100
103
|
collection: collectionProp,
|
|
101
104
|
filter,
|
|
102
105
|
limit,
|
|
@@ -117,6 +120,7 @@ export function useBuildDataSource({
|
|
|
117
120
|
|
|
118
121
|
return usedDelegate.listenCollection<M>({
|
|
119
122
|
path,
|
|
123
|
+
pathSegments,
|
|
120
124
|
filter,
|
|
121
125
|
limit,
|
|
122
126
|
startAfter,
|
|
@@ -139,6 +143,7 @@ export function useBuildDataSource({
|
|
|
139
143
|
*/
|
|
140
144
|
fetchEntity: useCallback(<M extends Record<string, any>>({
|
|
141
145
|
path,
|
|
146
|
+
pathSegments,
|
|
142
147
|
entityId,
|
|
143
148
|
collection
|
|
144
149
|
}: FetchEntityProps<M>
|
|
@@ -146,6 +151,7 @@ export function useBuildDataSource({
|
|
|
146
151
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
147
152
|
return usedDelegate.fetchEntity({
|
|
148
153
|
path,
|
|
154
|
+
pathSegments,
|
|
149
155
|
entityId,
|
|
150
156
|
collection
|
|
151
157
|
});
|
|
@@ -165,6 +171,7 @@ export function useBuildDataSource({
|
|
|
165
171
|
? useCallback(<M extends Record<string, any>>(
|
|
166
172
|
{
|
|
167
173
|
path,
|
|
174
|
+
pathSegments,
|
|
168
175
|
entityId,
|
|
169
176
|
collection,
|
|
170
177
|
onUpdate,
|
|
@@ -177,6 +184,7 @@ export function useBuildDataSource({
|
|
|
177
184
|
|
|
178
185
|
return usedDelegate.listenEntity<M>({
|
|
179
186
|
path,
|
|
187
|
+
pathSegments,
|
|
180
188
|
entityId,
|
|
181
189
|
onUpdate,
|
|
182
190
|
onError,
|
|
@@ -198,6 +206,7 @@ export function useBuildDataSource({
|
|
|
198
206
|
saveEntity: useCallback(async <M extends Record<string, any>>(
|
|
199
207
|
{
|
|
200
208
|
path,
|
|
209
|
+
pathSegments,
|
|
201
210
|
entityId,
|
|
202
211
|
values,
|
|
203
212
|
collection: collectionProp,
|
|
@@ -244,6 +253,7 @@ export function useBuildDataSource({
|
|
|
244
253
|
try {
|
|
245
254
|
const entities = await usedDelegate.fetchCollection({
|
|
246
255
|
path,
|
|
256
|
+
pathSegments,
|
|
247
257
|
orderBy: orderProperty,
|
|
248
258
|
order: "asc",
|
|
249
259
|
limit: 1,
|
|
@@ -270,6 +280,7 @@ export function useBuildDataSource({
|
|
|
270
280
|
|
|
271
281
|
return usedDelegate.saveEntity({
|
|
272
282
|
path,
|
|
283
|
+
pathSegments,
|
|
273
284
|
collection,
|
|
274
285
|
entityId,
|
|
275
286
|
values: finalValues,
|
|
@@ -292,12 +303,15 @@ export function useBuildDataSource({
|
|
|
292
303
|
deleteEntity: useCallback(<M extends Record<string, any>>(
|
|
293
304
|
{
|
|
294
305
|
entity,
|
|
306
|
+
pathSegments,
|
|
295
307
|
collection
|
|
296
308
|
}: DeleteEntityProps<M>
|
|
297
309
|
): Promise<void> => {
|
|
298
310
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
299
311
|
return usedDelegate.deleteEntity({
|
|
300
312
|
entity,
|
|
313
|
+
// Falls back to what the entity carries from the fetch that produced it.
|
|
314
|
+
pathSegments: pathSegments ?? entity.pathSegments,
|
|
301
315
|
collection
|
|
302
316
|
});
|
|
303
317
|
}, [delegate.deleteEntity]),
|
|
@@ -317,25 +331,28 @@ export function useBuildDataSource({
|
|
|
317
331
|
name: string,
|
|
318
332
|
value: any,
|
|
319
333
|
entityId?: string,
|
|
320
|
-
collection?: EntityCollection
|
|
334
|
+
collection?: EntityCollection,
|
|
335
|
+
pathSegments?: string[]
|
|
321
336
|
): Promise<boolean> => {
|
|
322
337
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
323
|
-
return usedDelegate.checkUniqueField(path, name, value, entityId, collection);
|
|
338
|
+
return usedDelegate.checkUniqueField(path, name, value, entityId, collection, pathSegments);
|
|
324
339
|
}, [delegate.checkUniqueField]),
|
|
325
340
|
|
|
326
|
-
generateEntityId: useCallback((path: string, collection: EntityCollection): string => {
|
|
341
|
+
generateEntityId: useCallback((path: string, collection: EntityCollection, pathSegments?: string[]): string => {
|
|
327
342
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
328
|
-
return usedDelegate.generateEntityId(path, collection);
|
|
343
|
+
return usedDelegate.generateEntityId(path, collection, pathSegments);
|
|
329
344
|
}, [delegate.generateEntityId]),
|
|
330
345
|
|
|
331
346
|
countEntities: delegate.countEntities ? async ({
|
|
332
347
|
path,
|
|
348
|
+
pathSegments,
|
|
333
349
|
collection,
|
|
334
350
|
filter,
|
|
335
351
|
order,
|
|
336
352
|
orderBy
|
|
337
353
|
}: {
|
|
338
354
|
path: string,
|
|
355
|
+
pathSegments?: string[],
|
|
339
356
|
collection: EntityCollection<any>,
|
|
340
357
|
filter?: FilterValues<Extract<keyof any, string>>,
|
|
341
358
|
orderBy?: string,
|
|
@@ -344,6 +361,7 @@ export function useBuildDataSource({
|
|
|
344
361
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
345
362
|
return usedDelegate.countEntities!({
|
|
346
363
|
path,
|
|
364
|
+
pathSegments,
|
|
347
365
|
filter,
|
|
348
366
|
orderBy,
|
|
349
367
|
order,
|
|
@@ -353,11 +371,13 @@ export function useBuildDataSource({
|
|
|
353
371
|
|
|
354
372
|
isFilterCombinationValid: useCallback(({
|
|
355
373
|
path,
|
|
374
|
+
pathSegments,
|
|
356
375
|
databaseId,
|
|
357
376
|
filterValues,
|
|
358
377
|
sortBy
|
|
359
378
|
}: {
|
|
360
379
|
path: string,
|
|
380
|
+
pathSegments?: string[],
|
|
361
381
|
databaseId?: string,
|
|
362
382
|
filterValues: FilterValues<any>,
|
|
363
383
|
sortBy?: [string, "asc" | "desc"]
|
|
@@ -367,6 +387,7 @@ export function useBuildDataSource({
|
|
|
367
387
|
return delegate.isFilterCombinationValid(
|
|
368
388
|
{
|
|
369
389
|
path,
|
|
390
|
+
pathSegments,
|
|
370
391
|
databaseId,
|
|
371
392
|
filterValues,
|
|
372
393
|
sortBy
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { getNavigationEntriesFromPath, NavigationViewInternal } from "../util/navigation_from_path";
|
|
14
14
|
import { useLocation } from "react-router-dom";
|
|
15
15
|
import {
|
|
16
|
+
encodeEntityId,
|
|
16
17
|
removeInitialAndTrailingSlashes,
|
|
17
18
|
resolveCollection,
|
|
18
19
|
resolveDefaultSelectedView,
|
|
@@ -223,12 +224,14 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
|
|
|
223
224
|
let sidePanel: EntitySidePanelProps<any> | undefined = undefined;
|
|
224
225
|
let lastCollectionPath = "";
|
|
225
226
|
let lastCollectionId: string | undefined = undefined;
|
|
227
|
+
let lastCollectionSegments: string[] | undefined = undefined;
|
|
226
228
|
for (let i = 0; i < navigationViewsForPath.length; i++) {
|
|
227
229
|
const navigationEntry = navigationViewsForPath[i];
|
|
228
230
|
|
|
229
231
|
if (navigationEntry.type === "collection") {
|
|
230
232
|
lastCollectionPath = navigationEntry.path;
|
|
231
233
|
lastCollectionId = navigationEntry.collection.id;
|
|
234
|
+
lastCollectionSegments = navigationEntry.pathSegments;
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
const previousEntry = navigationViewsForPath[i - 1];
|
|
@@ -236,6 +239,7 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
|
|
|
236
239
|
sidePanel = {
|
|
237
240
|
path: navigationEntry.path,
|
|
238
241
|
fullIdPath: navigationEntry.fullIdPath,
|
|
242
|
+
pathSegments: navigationEntry.pathSegments,
|
|
239
243
|
entityId: navigationEntry.entityId,
|
|
240
244
|
copy: false,
|
|
241
245
|
width: navigationEntry.parentCollection?.sideDialogWidth
|
|
@@ -258,6 +262,9 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
|
|
|
258
262
|
sidePanel = {
|
|
259
263
|
path: lastCollectionPath,
|
|
260
264
|
fullIdPath: lastCollectionId,
|
|
265
|
+
// A new entity still needs unambiguous segments: it may be created inside a
|
|
266
|
+
// subcollection whose parent id contains "/".
|
|
267
|
+
pathSegments: lastCollectionSegments,
|
|
261
268
|
copy: false
|
|
262
269
|
}
|
|
263
270
|
}
|
|
@@ -277,7 +284,7 @@ const propsToSidePanel = (props: EntitySidePanelProps,
|
|
|
277
284
|
const collectionPath = removeInitialAndTrailingSlashes(props.path);
|
|
278
285
|
|
|
279
286
|
const urlPath = props.entityId
|
|
280
|
-
? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}${locationSearch}#${SIDE_URL_HASH}`)
|
|
287
|
+
? buildUrlCollectionPath(`${collectionPath}/${encodeEntityId(props.entityId)}${props.selectedTab ? "/" + props.selectedTab : ""}${locationSearch}#${SIDE_URL_HASH}`)
|
|
281
288
|
: buildUrlCollectionPath(`${collectionPath}${locationSearch}#${NEW_URL_HASH}`);
|
|
282
289
|
|
|
283
290
|
const resolvedPanelProps: EntitySidePanelProps<any> = {
|
package/src/locales/de.ts
CHANGED
|
@@ -146,8 +146,8 @@ export const de: FireCMSTranslations = {
|
|
|
146
146
|
// ─── Error states ─────────────────────────────────────────────
|
|
147
147
|
error: "Fehler",
|
|
148
148
|
error_uploading_file: "Fehler beim Hochladen der Datei",
|
|
149
|
-
error_deleting: "Fehler beim Löschen",
|
|
150
|
-
error_before_delete: "Fehler vor dem Löschen",
|
|
149
|
+
error_deleting: "Fehler beim Löschen: {{message}}",
|
|
150
|
+
error_before_delete: "Fehler vor dem Löschen: {{message}}",
|
|
151
151
|
error_updating_asset: "Fehler beim Aktualisieren des Assets",
|
|
152
152
|
error_deleting_asset: "Fehler beim Löschen des Assets",
|
|
153
153
|
error_firestore_index: "Für diese Abfrage ist ein Firestore-Index erforderlich.",
|
|
@@ -389,6 +389,7 @@ export const de: FireCMSTranslations = {
|
|
|
389
389
|
download: "Herunterladen",
|
|
390
390
|
large_number_of_documents: "Diese Sammlung hat eine große Anzahl von Dokumenten ({{count}}).",
|
|
391
391
|
include_undefined_values: "Undefinierte Werte einschließen",
|
|
392
|
+
export_apply_filter_sort: "Nur Ergebnisse exportieren, die dem aktuellen Filter/der aktuellen Sortierung entsprechen",
|
|
392
393
|
submit: "Einreichen",
|
|
393
394
|
|
|
394
395
|
no_filterable_properties: "Keine filterbaren Eigenschaften verfügbar",
|
|
@@ -586,6 +587,10 @@ export const de: FireCMSTranslations = {
|
|
|
586
587
|
order_property_not_found: "Eigenschaft \"{{property}}\" existiert nicht oder ist keine Zahlen-Eigenschaft. Bitte wählen Sie eine gültige Eigenschaft oder löschen Sie die Auswahl.",
|
|
587
588
|
no_number_properties: "Keine Zahlen-Eigenschaften gefunden. Fügen Sie eine Zahlen-Eigenschaft hinzu, um die Sortierung zu aktivieren.",
|
|
588
589
|
order_property_description: "Wählen Sie eine Zahlen-Eigenschaft, um die Reihenfolge der Elemente beizubehalten",
|
|
590
|
+
image_property: "Bild-Eigenschaft",
|
|
591
|
+
image_property_not_found: "Eigenschaft \"{{property}}\" existiert nicht oder ist keine Bild-Eigenschaft. Bitte wählen Sie eine gültige Eigenschaft oder löschen Sie die Auswahl.",
|
|
592
|
+
no_image_properties: "Keine Bild-Eigenschaften gefunden. Fügen Sie eine Speicher- oder Bild-URL-Eigenschaft hinzu, um sie als Vorschaubild zu verwenden.",
|
|
593
|
+
image_property_description: "Wählen Sie die Eigenschaft, die als Vorschaubild in den Ansichten Karten und Kanban verwendet wird. Wenn nicht festgelegt, wird automatisch die erste passende Bild-Eigenschaft verwendet.",
|
|
589
594
|
display_settings: "Anzeigeeinstellungen",
|
|
590
595
|
default_row_size: "Standard-Zeilengröße",
|
|
591
596
|
side_dialog_width: "Seitendialog-Breite",
|
package/src/locales/en.ts
CHANGED
|
@@ -154,8 +154,8 @@ export const en: FireCMSTranslations = {
|
|
|
154
154
|
// ─── Error states ─────────────────────────────────────────────
|
|
155
155
|
error: "Error",
|
|
156
156
|
error_uploading_file: "Error uploading file",
|
|
157
|
-
error_deleting: "Error deleting",
|
|
158
|
-
error_before_delete: "Error before delete",
|
|
157
|
+
error_deleting: "Error deleting: {{message}}",
|
|
158
|
+
error_before_delete: "Error before delete: {{message}}",
|
|
159
159
|
error_updating_asset: "Error updating asset",
|
|
160
160
|
error_deleting_asset: "Error deleting asset",
|
|
161
161
|
error_firestore_index: "A Firestore index is required for this query.",
|
|
@@ -397,6 +397,7 @@ export const en: FireCMSTranslations = {
|
|
|
397
397
|
download: "Download",
|
|
398
398
|
large_number_of_documents: "This collections has a large number of documents ({{count}}).",
|
|
399
399
|
include_undefined_values: "Include undefined values",
|
|
400
|
+
export_apply_filter_sort: "Only export results matching the current filter/sort",
|
|
400
401
|
submit: "Submit",
|
|
401
402
|
|
|
402
403
|
no_filterable_properties: "No filterable properties available",
|
|
@@ -598,6 +599,10 @@ export const en: FireCMSTranslations = {
|
|
|
598
599
|
order_property_not_found: "Property \"{{property}}\" does not exist or is not a number property. Please select a valid property or clear the selection.",
|
|
599
600
|
no_number_properties: "No number properties found. Add a number property to enable ordering.",
|
|
600
601
|
order_property_description: "Select a number property to persist the order of items",
|
|
602
|
+
image_property: "Image Property",
|
|
603
|
+
image_property_not_found: "Property \"{{property}}\" does not exist or is not an image property. Please select a valid property or clear the selection.",
|
|
604
|
+
no_image_properties: "No image properties found. Add a storage or URL image property to use as a thumbnail.",
|
|
605
|
+
image_property_description: "Select the property used as the thumbnail image in Cards and Kanban view modes. If not set, the first matching image property will be used automatically.",
|
|
601
606
|
display_settings: "Display settings",
|
|
602
607
|
default_row_size: "Default row size",
|
|
603
608
|
side_dialog_width: "Side dialog width",
|