@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/locales/pt.ts
CHANGED
|
@@ -151,8 +151,8 @@ export const pt: FireCMSTranslations = {
|
|
|
151
151
|
// ─── Error states ─────────────────────────────────────────────
|
|
152
152
|
error: "Erro",
|
|
153
153
|
error_uploading_file: "Erro ao carregar ficheiro",
|
|
154
|
-
error_deleting: "Erro ao eliminar",
|
|
155
|
-
error_before_delete: "Erro antes de eliminar",
|
|
154
|
+
error_deleting: "Erro ao eliminar: {{message}}",
|
|
155
|
+
error_before_delete: "Erro antes de eliminar: {{message}}",
|
|
156
156
|
error_updating_asset: "Erro ao atualizar recurso",
|
|
157
157
|
error_deleting_asset: "Erro ao eliminar recurso",
|
|
158
158
|
error_firestore_index: "É necessário um índice Firestore para esta consulta.",
|
|
@@ -394,6 +394,7 @@ export const pt: FireCMSTranslations = {
|
|
|
394
394
|
download: "Descarregar",
|
|
395
395
|
large_number_of_documents: "Esta coleção tem um grande número de documentos ({{count}}).",
|
|
396
396
|
include_undefined_values: "Incluir valores indefinidos",
|
|
397
|
+
export_apply_filter_sort: "Exportar apenas os resultados que correspondem ao filtro/ordenação atual",
|
|
397
398
|
submit: "Submeter",
|
|
398
399
|
|
|
399
400
|
no_filterable_properties: "Não há propriedades filtráveis disponíveis",
|
|
@@ -595,6 +596,10 @@ export const pt: FireCMSTranslations = {
|
|
|
595
596
|
order_property_not_found: "A propriedade \"{{property}}\" não existe ou não é uma propriedade numérica. Selecione uma propriedade válida ou limpe a seleção.",
|
|
596
597
|
no_number_properties: "Nenhuma propriedade numérica encontrada. Adicione uma propriedade numérica para habilitar a ordenação.",
|
|
597
598
|
order_property_description: "Selecione uma propriedade numérica para manter a ordem dos itens",
|
|
599
|
+
image_property: "Propriedade de imagem",
|
|
600
|
+
image_property_not_found: "A propriedade \"{{property}}\" não existe ou não é uma propriedade de imagem. Selecione uma propriedade válida ou limpe a seleção.",
|
|
601
|
+
no_image_properties: "Nenhuma propriedade de imagem encontrada. Adicione uma propriedade de armazenamento ou URL de imagem para usar como miniatura.",
|
|
602
|
+
image_property_description: "Selecione a propriedade usada como miniatura nas visualizações de Cartões e Kanban. Se não for definida, a primeira propriedade de imagem correspondente será usada automaticamente.",
|
|
598
603
|
display_settings: "Configurações de exibição",
|
|
599
604
|
default_row_size: "Tamanho de linha padrão",
|
|
600
605
|
side_dialog_width: "Largura do diálogo lateral",
|
|
@@ -4,6 +4,7 @@ import equal from "react-fast-compare"
|
|
|
4
4
|
import {
|
|
5
5
|
CMSType,
|
|
6
6
|
EntityReference,
|
|
7
|
+
GeoPoint,
|
|
7
8
|
ResolvedArrayProperty,
|
|
8
9
|
ResolvedMapProperty,
|
|
9
10
|
ResolvedNumberProperty,
|
|
@@ -27,12 +28,14 @@ import { ArrayPropertyEnumPreview } from "./property_previews/ArrayPropertyEnumP
|
|
|
27
28
|
import { ArrayOfStringsPreview } from "./property_previews/ArrayOfStringsPreview";
|
|
28
29
|
import { ArrayOneOfPreview } from "./property_previews/ArrayOneOfPreview";
|
|
29
30
|
import { MapPropertyPreview } from "./property_previews/MapPropertyPreview";
|
|
31
|
+
import { GeopointPropertyPreview } from "./property_previews/GeopointPropertyPreview";
|
|
30
32
|
import { ReferencePreview } from "./components/ReferencePreview";
|
|
31
33
|
import { DatePreview } from "./components/DatePreview";
|
|
32
34
|
import { BooleanPreview } from "./components/BooleanPreview";
|
|
33
35
|
import { NumberPropertyPreview } from "./property_previews/NumberPropertyPreview";
|
|
34
36
|
import { ErrorView } from "../components";
|
|
35
37
|
import { UserPreview } from "./components/UserPreview";
|
|
38
|
+
import { getGeoPointCoordinates } from "../util";
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* @group Preview components
|
|
@@ -202,6 +205,15 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
|
|
|
202
205
|
} else {
|
|
203
206
|
content = buildWrongValueType(propertyKey, property.dataType, value);
|
|
204
207
|
}
|
|
208
|
+
} else if (property.dataType === "geopoint") {
|
|
209
|
+
const coordinates = getGeoPointCoordinates(value as GeoPoint);
|
|
210
|
+
if (coordinates) {
|
|
211
|
+
content = <GeopointPropertyPreview {...props}
|
|
212
|
+
property={property}
|
|
213
|
+
value={value as GeoPoint}/>;
|
|
214
|
+
} else {
|
|
215
|
+
content = buildWrongValueType(propertyKey, property.dataType, value);
|
|
216
|
+
}
|
|
205
217
|
} else if (property.dataType === "reference") {
|
|
206
218
|
if (typeof property.path === "string") {
|
|
207
219
|
if (typeof value === "object" && "isEntityReference" in value && value.isEntityReference()) {
|
|
@@ -6,6 +6,7 @@ import { useStorageSource } from "../../hooks";
|
|
|
6
6
|
import { DownloadConfig, FileType } from "../../types";
|
|
7
7
|
import { PreviewSize } from "../PropertyPreviewProps";
|
|
8
8
|
import { ErrorView } from "../../components";
|
|
9
|
+
import { getPreviewTypeFromUrl, isAbsoluteHttpUrl } from "../../util/urls";
|
|
9
10
|
|
|
10
11
|
type StorageThumbnailProps = {
|
|
11
12
|
storagePathOrDownloadUrl: string;
|
|
@@ -41,10 +42,20 @@ export function StorageThumbnailInternal({
|
|
|
41
42
|
const [error, setError] = React.useState<Error | undefined>(undefined);
|
|
42
43
|
const storage = useStorageSource();
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The saved value may already be a resolved download URL instead of a
|
|
47
|
+
* storage path: either because the property is configured with
|
|
48
|
+
* `storeUrl: true`, or because the value was written directly in the
|
|
49
|
+
* datasource pointing to an externally hosted file. Such values can be
|
|
50
|
+
* previewed as they are, and must not be handed over to the storage source,
|
|
51
|
+
* which can only resolve paths of its own bucket.
|
|
52
|
+
*/
|
|
53
|
+
const valueIsUrl = storeUrl || isAbsoluteHttpUrl(storagePathOrDownloadUrl);
|
|
54
|
+
|
|
44
55
|
const [downloadConfig, setDownloadConfig] = React.useState<DownloadConfig>(URL_CACHE[storagePathOrDownloadUrl]);
|
|
45
56
|
|
|
46
57
|
useEffect(() => {
|
|
47
|
-
if (!storagePathOrDownloadUrl)
|
|
58
|
+
if (!storagePathOrDownloadUrl || valueIsUrl)
|
|
48
59
|
return;
|
|
49
60
|
let unmounted = false;
|
|
50
61
|
storage.getDownloadURL(storagePathOrDownloadUrl)
|
|
@@ -57,10 +68,21 @@ export function StorageThumbnailInternal({
|
|
|
57
68
|
return () => {
|
|
58
69
|
unmounted = true;
|
|
59
70
|
};
|
|
60
|
-
}, [storagePathOrDownloadUrl]);
|
|
71
|
+
}, [storagePathOrDownloadUrl, valueIsUrl]);
|
|
61
72
|
|
|
62
73
|
if (!storagePathOrDownloadUrl) return null;
|
|
63
74
|
|
|
75
|
+
if (valueIsUrl) {
|
|
76
|
+
// There is no storage metadata for a value that is already a URL, so the
|
|
77
|
+
// preview type is inferred from its file extension when possible.
|
|
78
|
+
return <UrlComponentPreview previewType={getPreviewTypeFromUrl(storagePathOrDownloadUrl) ?? "file"}
|
|
79
|
+
url={storagePathOrDownloadUrl}
|
|
80
|
+
interactive={interactive}
|
|
81
|
+
size={size}
|
|
82
|
+
fill={fill}
|
|
83
|
+
hint={storagePathOrDownloadUrl} />;
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
const filetype = downloadConfig?.metadata ? getFiletype(downloadConfig?.metadata.contentType) : undefined;
|
|
65
87
|
const previewType = filetype?.startsWith("image")
|
|
66
88
|
? "image"
|
package/src/preview/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./property_previews/ArrayOfStringsPreview";
|
|
|
7
7
|
export * from "./property_previews/ArrayPropertyEnumPreview";
|
|
8
8
|
export * from "./property_previews/ArrayOfMapsPreview";
|
|
9
9
|
export * from "./property_previews/NumberPropertyPreview";
|
|
10
|
+
export * from "./property_previews/GeopointPropertyPreview";
|
|
10
11
|
export * from "./property_previews/StringPropertyPreview";
|
|
11
12
|
export * from "./property_previews/ArrayOfStorageComponentsPreview";
|
|
12
13
|
export * from "./property_previews/ArrayOfReferencesPreview";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { GeoPoint } from "../../types";
|
|
4
|
+
import { PropertyPreviewProps } from "../PropertyPreviewProps";
|
|
5
|
+
import { formatGeoPoint, getGeoPointCoordinates } from "../../util";
|
|
6
|
+
|
|
7
|
+
export function GeopointPropertyPreview({
|
|
8
|
+
value,
|
|
9
|
+
size
|
|
10
|
+
}: PropertyPreviewProps<GeoPoint>): React.ReactElement {
|
|
11
|
+
|
|
12
|
+
const coordinates = getGeoPointCoordinates(value);
|
|
13
|
+
|
|
14
|
+
if (!coordinates) {
|
|
15
|
+
return <span className={size === "small" ? "text-sm text-text-secondary dark:text-text-secondary-dark" : "text-text-secondary dark:text-text-secondary-dark"}>—</span>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<span className={size === "small" ? "text-sm font-mono" : "font-mono"}>
|
|
20
|
+
{formatGeoPoint(coordinates)}
|
|
21
|
+
</span>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from "../util/navigation_from_path";
|
|
11
11
|
import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
|
|
12
12
|
import { toArray } from "../util/arrays";
|
|
13
|
+
import { addInitialSlash, encodeEntityId } from "../util/navigation_utils";
|
|
14
|
+
import { shouldBlockEntityNavigation } from "../util/navigation_blocking";
|
|
13
15
|
import { NotFoundPage } from "../components";
|
|
14
16
|
import { lazyEager } from "../util/lazy_eager";
|
|
15
17
|
|
|
@@ -97,6 +99,7 @@ export function FireCMSRoute() {
|
|
|
97
99
|
isSubCollection={false}
|
|
98
100
|
parentCollectionIds={[]}
|
|
99
101
|
fullPath={collection.path}
|
|
102
|
+
pathSegments={navigationEntries[0].pathSegments}
|
|
100
103
|
fullIdPath={collection.id}
|
|
101
104
|
updateUrl={true}
|
|
102
105
|
{...collection}
|
|
@@ -121,6 +124,7 @@ export function FireCMSRoute() {
|
|
|
121
124
|
isSubCollection={false}
|
|
122
125
|
parentCollectionIds={[]}
|
|
123
126
|
fullPath={collection.path}
|
|
127
|
+
pathSegments={firstEntry.pathSegments}
|
|
124
128
|
updateUrl={true}
|
|
125
129
|
{...collection}
|
|
126
130
|
Actions={toArray(collection.Actions)} />
|
|
@@ -189,27 +193,51 @@ function EntityFullScreenRoute({
|
|
|
189
193
|
}
|
|
190
194
|
}, [urlTab]);
|
|
191
195
|
|
|
192
|
-
const
|
|
196
|
+
const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
|
|
197
|
+
|
|
198
|
+
// The entity id reaches the URL escaped AND percent-encoded, so it cannot be located by
|
|
199
|
+
// searching `pathname` for the raw id — that misses for any id needing encoding (a "/",
|
|
200
|
+
// but equally a space or a non-ASCII character). Rebuild the URLs from the navigation
|
|
201
|
+
// entries instead, which already carry the escaped chain in `fullPath`.
|
|
202
|
+
const entityEntryIndex = lastEntityEntry ? navigationEntries.indexOf(lastEntityEntry) : -1;
|
|
203
|
+
const parentCollectionEntry = entityEntryIndex > 0
|
|
204
|
+
? navigationEntries[entityEntryIndex - 1] as NavigationViewCollectionInternal<any>
|
|
205
|
+
: undefined;
|
|
206
|
+
|
|
207
|
+
const buildUrl = (escapedPath: string) => addInitialSlash(navigation.buildUrlCollectionPath(escapedPath));
|
|
208
|
+
|
|
209
|
+
const basePath = !entityId || isNew || !parentCollectionEntry
|
|
193
210
|
? pathname
|
|
194
|
-
:
|
|
211
|
+
: buildUrl(parentCollectionEntry.fullPath);
|
|
212
|
+
|
|
213
|
+
const entityPath = lastEntityEntry ? buildUrl(lastEntityEntry.fullPath) : basePath;
|
|
195
214
|
|
|
196
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Build the URL of an entity in the current collection. `id` is raw, so it is escaped
|
|
217
|
+
* before being joined; `buildUrlCollectionPath` then percent-encodes the whole path.
|
|
218
|
+
*/
|
|
219
|
+
const buildEntityUrl = (id: string, tab?: string) => {
|
|
220
|
+
const parentPath = parentCollectionEntry?.fullPath ?? lastCollectionEntry?.fullPath;
|
|
221
|
+
if (!parentPath) return pathname;
|
|
222
|
+
return buildUrl(`${parentPath}/${encodeEntityId(id)}${tab ? "/" + tab : ""}`);
|
|
223
|
+
};
|
|
197
224
|
|
|
198
225
|
let blocker: Blocker | undefined = undefined;
|
|
199
226
|
try {
|
|
200
227
|
blocker = useBlocker(({
|
|
228
|
+
currentLocation,
|
|
201
229
|
nextLocation
|
|
202
|
-
}) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
}) => shouldBlockEntityNavigation({
|
|
231
|
+
currentLocation,
|
|
232
|
+
nextLocation,
|
|
233
|
+
entityPath,
|
|
234
|
+
basePath,
|
|
235
|
+
blocked: blocked.current
|
|
236
|
+
}));
|
|
207
237
|
} catch (e) {
|
|
208
238
|
// console.warn("Blocker not available, navigation will not be blocked");
|
|
209
239
|
}
|
|
210
240
|
|
|
211
|
-
const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
|
|
212
|
-
|
|
213
241
|
if (isNew && !lastCollectionEntry) {
|
|
214
242
|
throw new Error("INTERNAL: No collection found in the navigation");
|
|
215
243
|
}
|
|
@@ -219,14 +247,17 @@ function EntityFullScreenRoute({
|
|
|
219
247
|
}
|
|
220
248
|
|
|
221
249
|
const collection = isNew ? lastCollectionEntry!.collection : lastEntityEntry!.parentCollection;
|
|
222
|
-
|
|
223
|
-
|
|
250
|
+
// `fullIdPath` is used downstream to build URLs, so it carries the escaped chain;
|
|
251
|
+
// `collectionPath` addresses the datasource, so it is resolved from the raw one.
|
|
252
|
+
const fullIdPath = isNew ? lastCollectionEntry!.fullPath : (parentCollectionEntry?.fullPath ?? lastEntityEntry!.fullPath);
|
|
253
|
+
const collectionPath = navigation.resolveIdsFrom(isNew ? lastCollectionEntry!.path : lastEntityEntry!.path);
|
|
224
254
|
return <>
|
|
225
255
|
<React.Suspense fallback={null}>
|
|
226
256
|
<EntityEditView
|
|
227
257
|
key={collection.id + "_" + (isNew ? "new" : (isCopy ? entityId + "_copy" : entityId))}
|
|
228
258
|
entityId={isNew ? undefined : entityId}
|
|
229
259
|
fullIdPath={fullIdPath}
|
|
260
|
+
pathSegments={isNew ? lastCollectionEntry!.pathSegments : lastEntityEntry!.pathSegments}
|
|
230
261
|
collection={collection}
|
|
231
262
|
layout={"full_screen"}
|
|
232
263
|
path={collectionPath}
|
|
@@ -234,25 +265,16 @@ function EntityFullScreenRoute({
|
|
|
234
265
|
selectedTab={selectedTab ?? undefined}
|
|
235
266
|
onValuesModified={(modified) => blocked.current = modified}
|
|
236
267
|
onSaved={(params) => {
|
|
237
|
-
const newSelectedTab = params.selectedTab;
|
|
238
268
|
const newEntityId = params.entityId;
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
} else {
|
|
242
|
-
navigate(`${basePath}/${newEntityId}`, { replace: true });
|
|
243
|
-
}
|
|
269
|
+
if (!newEntityId) return;
|
|
270
|
+
navigate(buildEntityUrl(newEntityId, params.selectedTab), { replace: true });
|
|
244
271
|
}}
|
|
245
272
|
onTabChange={(params) => {
|
|
246
273
|
setSelectedTab(params.selectedTab);
|
|
247
|
-
if (isNew) {
|
|
274
|
+
if (isNew || !entityId) {
|
|
248
275
|
return;
|
|
249
276
|
}
|
|
250
|
-
|
|
251
|
-
if (newSelectedTab) {
|
|
252
|
-
navigate(`${basePath}/${entityId}/${newSelectedTab}`, { replace: true });
|
|
253
|
-
} else {
|
|
254
|
-
navigate(`${basePath}/${entityId}`, { replace: true });
|
|
255
|
-
}
|
|
277
|
+
navigate(buildEntityUrl(entityId, params.selectedTab), { replace: true });
|
|
256
278
|
}}
|
|
257
279
|
parentCollectionIds={parentCollectionIds}
|
|
258
280
|
/>
|
package/src/types/collections.ts
CHANGED
|
@@ -101,6 +101,14 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
101
101
|
*/
|
|
102
102
|
titleProperty?: keyof M;
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Property used as the image thumbnail in Cards and Kanban view modes.
|
|
106
|
+
* Must reference a storage/image property (or an array of them), or a
|
|
107
|
+
* string property with `url: "image"`.
|
|
108
|
+
* If not specified, the first matching image property will be used.
|
|
109
|
+
*/
|
|
110
|
+
imageProperty?: keyof M;
|
|
111
|
+
|
|
104
112
|
/**
|
|
105
113
|
* When editing an entity, you can choose to open the entity in a side dialog
|
|
106
114
|
* or in a full screen dialog. Defaults to `full_screen`.
|
|
@@ -261,6 +269,22 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
261
269
|
*/
|
|
262
270
|
initialFilter?: FilterValues<Extract<keyof M, string>>; // setting FilterValues<M> can break defining collections by code
|
|
263
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Array of property keys that are allowed for filtering. Allowed filters will be displayed in the collection view, table row headers
|
|
274
|
+
* and will be used to filter data.
|
|
275
|
+
*
|
|
276
|
+
* If not specified, all properties that are filterable will be allowed.
|
|
277
|
+
*
|
|
278
|
+
* If you specify this prop, the filters will be displayed in the collection view if they are filterable.
|
|
279
|
+
*
|
|
280
|
+
* If you set it as an empty array, no filters will be displayed.
|
|
281
|
+
*
|
|
282
|
+
* e.g. `allowedFilters: ["name", "category"]`
|
|
283
|
+
*
|
|
284
|
+
* e.g. `allowedFilters: []`
|
|
285
|
+
*/
|
|
286
|
+
allowedFilters?: (keyof M)[];
|
|
287
|
+
|
|
264
288
|
/**
|
|
265
289
|
* Default sort applied to this collection.
|
|
266
290
|
* When setting this prop, entities will have a default order
|
|
@@ -725,6 +749,8 @@ export type EntityTableController<M extends Record<string, any> = any> = {
|
|
|
725
749
|
dataLoadingError?: Error;
|
|
726
750
|
filterValues?: FilterValues<Extract<keyof M, string>>;
|
|
727
751
|
setFilterValues?: (filterValues: FilterValues<Extract<keyof M, string>>) => void;
|
|
752
|
+
allowedFilters?: (keyof M)[];
|
|
753
|
+
forcedFilters?: (keyof M)[];
|
|
728
754
|
sortBy?: [Extract<keyof M, string>, "asc" | "desc"];
|
|
729
755
|
setSortBy?: (sortBy?: [Extract<keyof M, string>, "asc" | "desc"]) => void;
|
|
730
756
|
searchString?: string;
|
package/src/types/datasource.ts
CHANGED
|
@@ -8,6 +8,20 @@ import { FireCMSContext } from "./firecms_context";
|
|
|
8
8
|
*/
|
|
9
9
|
export interface FetchEntityProps<M extends Record<string, any> = any> {
|
|
10
10
|
path: string;
|
|
11
|
+
/**
|
|
12
|
+
* The path split at its real segment boundaries, e.g.
|
|
13
|
+
* `["nodes", "node/42", "edges"]`.
|
|
14
|
+
*
|
|
15
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
16
|
+
* recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
|
|
17
|
+
* nesting. This is the unambiguous form: one array element per segment, entity ids
|
|
18
|
+
* kept whole however many slashes they contain.
|
|
19
|
+
*
|
|
20
|
+
* For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
|
|
21
|
+
* delegates that do not care can keep using `path`. Delegates that do should prefer
|
|
22
|
+
* `pathSegments ?? path.split("/")`.
|
|
23
|
+
*/
|
|
24
|
+
pathSegments?: string[];
|
|
11
25
|
entityId: string;
|
|
12
26
|
databaseId?: string;
|
|
13
27
|
collection?: EntityCollection<M, any>
|
|
@@ -28,6 +42,20 @@ export type ListenEntityProps<M extends Record<string, any> = any> =
|
|
|
28
42
|
*/
|
|
29
43
|
export interface FetchCollectionProps<M extends Record<string, any> = any> {
|
|
30
44
|
path: string;
|
|
45
|
+
/**
|
|
46
|
+
* The path split at its real segment boundaries, e.g.
|
|
47
|
+
* `["nodes", "node/42", "edges"]`.
|
|
48
|
+
*
|
|
49
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
50
|
+
* recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
|
|
51
|
+
* nesting. This is the unambiguous form: one array element per segment, entity ids
|
|
52
|
+
* kept whole however many slashes they contain.
|
|
53
|
+
*
|
|
54
|
+
* For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
|
|
55
|
+
* delegates that do not care can keep using `path`. Delegates that do should prefer
|
|
56
|
+
* `pathSegments ?? path.split("/")`.
|
|
57
|
+
*/
|
|
58
|
+
pathSegments?: string[];
|
|
31
59
|
collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
|
|
32
60
|
filter?: FilterValues<Extract<keyof M, string>>,
|
|
33
61
|
limit?: number;
|
|
@@ -52,6 +80,20 @@ export type ListenCollectionProps<M extends Record<string, any> = any> =
|
|
|
52
80
|
*/
|
|
53
81
|
export interface SaveEntityProps<M extends Record<string, any> = any> {
|
|
54
82
|
path: string;
|
|
83
|
+
/**
|
|
84
|
+
* The path split at its real segment boundaries, e.g.
|
|
85
|
+
* `["nodes", "node/42", "edges"]`.
|
|
86
|
+
*
|
|
87
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
88
|
+
* recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
|
|
89
|
+
* nesting. This is the unambiguous form: one array element per segment, entity ids
|
|
90
|
+
* kept whole however many slashes they contain.
|
|
91
|
+
*
|
|
92
|
+
* For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
|
|
93
|
+
* delegates that do not care can keep using `path`. Delegates that do should prefer
|
|
94
|
+
* `pathSegments ?? path.split("/")`.
|
|
95
|
+
*/
|
|
96
|
+
pathSegments?: string[];
|
|
55
97
|
values: Partial<EntityValues<M>>;
|
|
56
98
|
entityId?: string; // can be empty for new entities
|
|
57
99
|
previousValues?: Partial<EntityValues<M>>;
|
|
@@ -64,6 +106,11 @@ export interface SaveEntityProps<M extends Record<string, any> = any> {
|
|
|
64
106
|
*/
|
|
65
107
|
export interface DeleteEntityProps<M extends Record<string, any> = any> {
|
|
66
108
|
entity: Entity<M>;
|
|
109
|
+
/**
|
|
110
|
+
* `entity.path` split at its real segment boundaries, when the caller knows them.
|
|
111
|
+
* Never derived from `entity.path` — see `pathSegments` on FetchCollectionProps.
|
|
112
|
+
*/
|
|
113
|
+
pathSegments?: string[];
|
|
67
114
|
collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
|
|
68
115
|
}
|
|
69
116
|
|
|
@@ -203,13 +250,19 @@ export interface DataSource {
|
|
|
203
250
|
name: string,
|
|
204
251
|
value: any,
|
|
205
252
|
entityId?: string,
|
|
206
|
-
collection?: EntityCollection
|
|
253
|
+
collection?: EntityCollection,
|
|
254
|
+
/**
|
|
255
|
+
* Appended last, and optional, so existing implementations and callers keep
|
|
256
|
+
* working unchanged — a function declaring fewer parameters stays assignable.
|
|
257
|
+
* See `pathSegments` on FetchCollectionProps.
|
|
258
|
+
*/
|
|
259
|
+
pathSegments?: string[]
|
|
207
260
|
): Promise<boolean>;
|
|
208
261
|
|
|
209
262
|
/**
|
|
210
263
|
* Generate an id for a new entity
|
|
211
264
|
*/
|
|
212
|
-
generateEntityId(path: string, collection: EntityCollection): string;
|
|
265
|
+
generateEntityId(path: string, collection: EntityCollection, pathSegments?: string[]): string;
|
|
213
266
|
|
|
214
267
|
/**
|
|
215
268
|
* Count the number of entities in a collection
|
|
@@ -237,6 +290,8 @@ export interface DataSource {
|
|
|
237
290
|
}
|
|
238
291
|
|
|
239
292
|
export type FilterCombinationValidProps = {
|
|
293
|
+
/** `path` split at its real segment boundaries — see `pathSegments` on FetchCollectionProps. */
|
|
294
|
+
pathSegments?: string[];
|
|
240
295
|
path: string;
|
|
241
296
|
collection: EntityCollection<any>;
|
|
242
297
|
filterValues: FilterValues<any>;
|
|
@@ -366,12 +421,12 @@ export interface DataSourceDelegate {
|
|
|
366
421
|
* @param collection
|
|
367
422
|
* @return `true` if there are no other fields besides the given entity
|
|
368
423
|
*/
|
|
369
|
-
checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
|
|
424
|
+
checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection, pathSegments?: string[]): Promise<boolean>;
|
|
370
425
|
|
|
371
426
|
/**
|
|
372
427
|
* Generate an id for a new entity
|
|
373
428
|
*/
|
|
374
|
-
generateEntityId(path: string, collection?: EntityCollection): string;
|
|
429
|
+
generateEntityId(path: string, collection?: EntityCollection, pathSegments?: string[]): string;
|
|
375
430
|
|
|
376
431
|
/**
|
|
377
432
|
* Count the number of entities in a collection
|
package/src/types/entities.ts
CHANGED
|
@@ -21,6 +21,17 @@ export interface Entity<M extends object = any> {
|
|
|
21
21
|
*/
|
|
22
22
|
path: string;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
26
|
+
*
|
|
27
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
28
|
+
* recovered from it. This keeps each segment whole.
|
|
29
|
+
*
|
|
30
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
31
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
32
|
+
*/
|
|
33
|
+
pathSegments?: string[];
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
36
|
* Current values
|
|
26
37
|
*/
|
|
@@ -55,10 +66,22 @@ export class EntityReference {
|
|
|
55
66
|
*/
|
|
56
67
|
readonly databaseId?: string;
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
/**
|
|
70
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
71
|
+
*
|
|
72
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
73
|
+
* recovered from it. This keeps each segment whole.
|
|
74
|
+
*
|
|
75
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
76
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
77
|
+
*/
|
|
78
|
+
readonly pathSegments?: string[];
|
|
79
|
+
|
|
80
|
+
constructor(id: string, path: string, databaseId?: string, pathSegments?: string[]) {
|
|
59
81
|
this.id = id;
|
|
60
82
|
this.path = path;
|
|
61
83
|
this.databaseId = databaseId;
|
|
84
|
+
this.pathSegments = pathSegments;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
get pathWithId() {
|
package/src/types/properties.ts
CHANGED
|
@@ -562,6 +562,22 @@ export type PropertyBuilderProps<M extends Record<string, any> = any> =
|
|
|
562
562
|
* Current value of this property
|
|
563
563
|
*/
|
|
564
564
|
propertyValue?: any;
|
|
565
|
+
/**
|
|
566
|
+
* Path of this property within the entity, in dot notation, e.g.
|
|
567
|
+
* `my_map.my_field` or `my_array.0.my_field`.
|
|
568
|
+
* Useful to look up sibling values in `values` with `getValueInPath`.
|
|
569
|
+
*
|
|
570
|
+
* When a property builder is used as the `of` prop of an array property,
|
|
571
|
+
* it is resolved twice: once per existing element, with an index
|
|
572
|
+
* qualified key (`my_array.0`), and once as the template for the element
|
|
573
|
+
* shape, with the array's own key (`my_array`). In the latter case
|
|
574
|
+
* `propertyValue` is the whole array, since the template is not bound to
|
|
575
|
+
* any element.
|
|
576
|
+
*
|
|
577
|
+
* It may be undefined if the property is resolved outside the context of
|
|
578
|
+
* an entity.
|
|
579
|
+
*/
|
|
580
|
+
propertyKey?: string;
|
|
565
581
|
/**
|
|
566
582
|
* Index of this property (only for arrays)
|
|
567
583
|
*/
|
|
@@ -592,6 +608,7 @@ export type PropertyBuilder<T extends CMSType = any, M extends Record<string, an
|
|
|
592
608
|
values,
|
|
593
609
|
previousValues,
|
|
594
610
|
propertyValue,
|
|
611
|
+
propertyKey,
|
|
595
612
|
index,
|
|
596
613
|
path,
|
|
597
614
|
entityId,
|
|
@@ -19,6 +19,12 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
|
|
|
19
19
|
* Full CMS path of the entity, including the collection and sub-collections.
|
|
20
20
|
*/
|
|
21
21
|
fullIdPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
24
|
+
* Optional: when omitted the panel falls back to splitting `path`, which is correct for
|
|
25
|
+
* any backend whose entity ids cannot contain "/".
|
|
26
|
+
*/
|
|
27
|
+
pathSegments?: string[];
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* ID of the entity, if not set, it means we are creating a new entity
|
|
@@ -419,6 +419,8 @@ export interface FireCMSTranslations {
|
|
|
419
419
|
download: string;
|
|
420
420
|
large_number_of_documents: string;
|
|
421
421
|
include_undefined_values: string;
|
|
422
|
+
/** Toggle in the export dialog, only shown when a filter or sort is active */
|
|
423
|
+
export_apply_filter_sort: string;
|
|
422
424
|
submit: string;
|
|
423
425
|
|
|
424
426
|
no_filterable_properties: string;
|
|
@@ -620,6 +622,10 @@ export interface FireCMSTranslations {
|
|
|
620
622
|
order_property_not_found: string;
|
|
621
623
|
no_number_properties: string;
|
|
622
624
|
order_property_description: string;
|
|
625
|
+
image_property: string;
|
|
626
|
+
image_property_not_found: string;
|
|
627
|
+
no_image_properties: string;
|
|
628
|
+
image_property_description: string;
|
|
623
629
|
display_settings: string;
|
|
624
630
|
default_row_size: string;
|
|
625
631
|
side_dialog_width: string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getLocalChangesBackup } from "../collections";
|
|
2
|
+
import { EntityCollection } from "../../types";
|
|
3
|
+
|
|
4
|
+
function collectionWith(localChangesBackup?: "manual_apply" | "auto_apply" | false): EntityCollection {
|
|
5
|
+
return {
|
|
6
|
+
id: "products",
|
|
7
|
+
name: "Products",
|
|
8
|
+
path: "products",
|
|
9
|
+
properties: {},
|
|
10
|
+
localChangesBackup
|
|
11
|
+
} as EntityCollection;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("getLocalChangesBackup", () => {
|
|
15
|
+
|
|
16
|
+
it("should default to manual_apply when not set", () => {
|
|
17
|
+
expect(getLocalChangesBackup(collectionWith(undefined))).toEqual("manual_apply");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should return manual_apply when explicitly set", () => {
|
|
21
|
+
expect(getLocalChangesBackup(collectionWith("manual_apply"))).toEqual("manual_apply");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should return auto_apply when explicitly set", () => {
|
|
25
|
+
expect(getLocalChangesBackup(collectionWith("auto_apply"))).toEqual("auto_apply");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return false when explicitly disabled, and not fall back to the default", () => {
|
|
29
|
+
expect(getLocalChangesBackup(collectionWith(false))).toEqual(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
});
|