@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
|
@@ -26,6 +26,8 @@ import { OnCellValueChangeParams } from "../../../common";
|
|
|
26
26
|
interface PopupFormFieldProps<M extends Record<string, any>> {
|
|
27
27
|
customFieldValidator?: CustomFieldValidator;
|
|
28
28
|
path: string;
|
|
29
|
+
/** `path` split at its real segment boundaries; forwarded to the datasource. */
|
|
30
|
+
pathSegments?: string[];
|
|
29
31
|
entityId: string;
|
|
30
32
|
tableKey: string;
|
|
31
33
|
propertyKey?: Extract<keyof M, string>;
|
|
@@ -54,6 +56,7 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
|
|
|
54
56
|
propertyKey,
|
|
55
57
|
collection: inputCollection,
|
|
56
58
|
path,
|
|
59
|
+
pathSegments,
|
|
57
60
|
cellRect,
|
|
58
61
|
open,
|
|
59
62
|
onClose,
|
|
@@ -66,11 +69,12 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
|
|
|
66
69
|
if (entityId && inputCollection) {
|
|
67
70
|
dataSource.fetchEntity({
|
|
68
71
|
path,
|
|
72
|
+
pathSegments,
|
|
69
73
|
entityId,
|
|
70
74
|
collection: inputCollection
|
|
71
75
|
}).then(setEntity);
|
|
72
76
|
}
|
|
73
|
-
}, [entityId, inputCollection, dataSource, path]);
|
|
77
|
+
}, [entityId, inputCollection, dataSource, path, pathSegments]);
|
|
74
78
|
|
|
75
79
|
if (!entity) return null;
|
|
76
80
|
return <PopupFormFieldInternal {...{
|
|
@@ -42,6 +42,8 @@ export type EntityCollectionBoardViewProps<M extends Record<string, any> = any>
|
|
|
42
42
|
collection: EntityCollection<M>;
|
|
43
43
|
tableController: EntityTableController<M>;
|
|
44
44
|
fullPath: string;
|
|
45
|
+
/** `fullPath` split at its real segment boundaries; forwarded to the datasource. */
|
|
46
|
+
pathSegments?: string[];
|
|
45
47
|
parentCollectionIds?: string[];
|
|
46
48
|
columnProperty: string;
|
|
47
49
|
onEntityClick?: (entity: Entity<M>) => void;
|
|
@@ -60,6 +62,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
60
62
|
collection,
|
|
61
63
|
tableController,
|
|
62
64
|
fullPath,
|
|
65
|
+
pathSegments,
|
|
63
66
|
parentCollectionIds = [],
|
|
64
67
|
columnProperty,
|
|
65
68
|
onEntityClick,
|
|
@@ -169,6 +172,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
169
172
|
// Use the new per-column data controller
|
|
170
173
|
const boardDataController = useBoardDataController<M>({
|
|
171
174
|
fullPath,
|
|
175
|
+
pathSegments,
|
|
172
176
|
collection,
|
|
173
177
|
columnProperty,
|
|
174
178
|
columns,
|
|
@@ -270,6 +274,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
270
274
|
|
|
271
275
|
currentDataSource.countEntities({
|
|
272
276
|
path: fullPath,
|
|
277
|
+
pathSegments,
|
|
273
278
|
collection: currentCollection
|
|
274
279
|
}).then(count => {
|
|
275
280
|
totalCount = count;
|
|
@@ -281,6 +286,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
281
286
|
|
|
282
287
|
currentDataSource.countEntities({
|
|
283
288
|
path: fullPath,
|
|
289
|
+
pathSegments,
|
|
284
290
|
collection: currentCollection,
|
|
285
291
|
filter: { [orderProperty]: ["!=", null] } as FilterValues<string>
|
|
286
292
|
}).then(count => {
|
|
@@ -432,6 +438,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
432
438
|
|
|
433
439
|
const saveProps: SaveEntityProps = {
|
|
434
440
|
path: entity.path,
|
|
441
|
+
// Paired with `entity.path`: the segments the entity was loaded with.
|
|
442
|
+
pathSegments: entity.pathSegments,
|
|
435
443
|
entityId: entity.id,
|
|
436
444
|
values: updatedValues as M,
|
|
437
445
|
previousValues: entity.values,
|
|
@@ -472,6 +480,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
472
480
|
console.log("Fetching all documents from collection...");
|
|
473
481
|
const allDocs = await dataSource.fetchCollection<M>({
|
|
474
482
|
path: fullPath,
|
|
483
|
+
pathSegments,
|
|
475
484
|
collection,
|
|
476
485
|
limit: 10000 // Fetch all
|
|
477
486
|
});
|
|
@@ -492,6 +501,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
492
501
|
|
|
493
502
|
const saveProps: SaveEntityProps = {
|
|
494
503
|
path: entity.path,
|
|
504
|
+
// Paired with `entity.path`: the segments the entity was loaded with.
|
|
505
|
+
pathSegments: entity.pathSegments,
|
|
495
506
|
entityId: entity.id,
|
|
496
507
|
values: updatedValues as M,
|
|
497
508
|
previousValues: entity.values,
|
|
@@ -663,6 +674,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
663
674
|
});
|
|
664
675
|
sideEntityController.open({
|
|
665
676
|
path: fullPath,
|
|
677
|
+
pathSegments,
|
|
666
678
|
collection,
|
|
667
679
|
entityId: undefined,
|
|
668
680
|
updateUrl: true,
|
|
@@ -90,6 +90,7 @@ import { useSelectionController } from "./useSelectionController";
|
|
|
90
90
|
import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
|
|
91
91
|
import { addRecentId, getRecentIds } from "./utils";
|
|
92
92
|
import { useScrollRestoration } from "../common/useScrollRestoration";
|
|
93
|
+
import { entityCacheKey } from "../../util/entity_cache";
|
|
93
94
|
|
|
94
95
|
const DEFAULT_ENTITY_OPEN_MODE: "side_panel" | "full_screen" = "side_panel";
|
|
95
96
|
|
|
@@ -106,6 +107,14 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
|
|
|
106
107
|
* Full path using navigation ids.
|
|
107
108
|
*/
|
|
108
109
|
fullIdPath?: string;
|
|
110
|
+
/**
|
|
111
|
+
* `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
112
|
+
* Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
|
|
113
|
+
*
|
|
114
|
+
* Optional, and never derived by splitting `fullPath` — a guess would be wrong in
|
|
115
|
+
* exactly the case the field exists for. Absent means "not known here", not "no slashes".
|
|
116
|
+
*/
|
|
117
|
+
pathSegments?: string[];
|
|
109
118
|
/**
|
|
110
119
|
* If this is a subcollection, specify the parent collection ids.
|
|
111
120
|
*/
|
|
@@ -152,6 +161,7 @@ export const EntityCollectionView = React.memo(
|
|
|
152
161
|
function EntityCollectionView<M extends Record<string, any>>({
|
|
153
162
|
fullPath: fullPathProp,
|
|
154
163
|
fullIdPath,
|
|
164
|
+
pathSegments,
|
|
155
165
|
parentCollectionIds,
|
|
156
166
|
isSubCollection,
|
|
157
167
|
className,
|
|
@@ -190,7 +200,7 @@ export const EntityCollectionView = React.memo(
|
|
|
190
200
|
collectionRef.current = collection;
|
|
191
201
|
}, [collection]);
|
|
192
202
|
|
|
193
|
-
const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
|
|
203
|
+
const canCreateEntities = canCreateEntity(collection, authController, fullPath, null, pathSegments);
|
|
194
204
|
const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
|
|
195
205
|
const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
|
|
196
206
|
|
|
@@ -220,7 +230,7 @@ export const EntityCollectionView = React.memo(
|
|
|
220
230
|
|
|
221
231
|
const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
|
|
222
232
|
const collection = collectionRef.current;
|
|
223
|
-
if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
|
|
233
|
+
if (!canEditEntity(collection, authController, fullPath, entity ?? null, pathSegments)) {
|
|
224
234
|
return false;
|
|
225
235
|
}
|
|
226
236
|
return collection.inlineEditing === undefined || collection.inlineEditing;
|
|
@@ -317,6 +327,7 @@ export const EntityCollectionView = React.memo(
|
|
|
317
327
|
|
|
318
328
|
const tableController = useDataSourceTableController<M>({
|
|
319
329
|
fullPath,
|
|
330
|
+
pathSegments,
|
|
320
331
|
collection,
|
|
321
332
|
lastDeleteTimestamp,
|
|
322
333
|
scrollRestoration,
|
|
@@ -342,10 +353,16 @@ export const EntityCollectionView = React.memo(
|
|
|
342
353
|
addRecentId(collection.id, clickedEntity.id);
|
|
343
354
|
}
|
|
344
355
|
|
|
345
|
-
|
|
356
|
+
// In a collection group the row's own path is the real one, so the row's segments
|
|
357
|
+
// are the matching ones; otherwise both come from this view. `path` and
|
|
358
|
+
// `pathSegments` are picked from the same source so they cannot describe
|
|
359
|
+
// different chains.
|
|
360
|
+
const isCollectionGroup = Boolean(collection?.collectionGroup);
|
|
361
|
+
const path = isCollectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
|
|
346
362
|
navigateToEntity({
|
|
347
363
|
navigation,
|
|
348
364
|
path,
|
|
365
|
+
pathSegments: isCollectionGroup ? clickedEntity.pathSegments : (pathSegments ?? clickedEntity.pathSegments),
|
|
349
366
|
fullIdPath,
|
|
350
367
|
sideEntityController,
|
|
351
368
|
openEntityMode,
|
|
@@ -365,6 +382,7 @@ export const EntityCollectionView = React.memo(
|
|
|
365
382
|
collection,
|
|
366
383
|
entityId: undefined,
|
|
367
384
|
path: fullPath,
|
|
385
|
+
pathSegments,
|
|
368
386
|
fullIdPath,
|
|
369
387
|
sideEntityController,
|
|
370
388
|
navigation,
|
|
@@ -440,6 +458,7 @@ export const EntityCollectionView = React.memo(
|
|
|
440
458
|
const onViewModeChange = useCallback((mode: ViewMode) => {
|
|
441
459
|
analyticsController.onAnalyticsEvent?.("view_mode_changed", {
|
|
442
460
|
path: fullPath,
|
|
461
|
+
pathSegments,
|
|
443
462
|
from: viewMode,
|
|
444
463
|
to: mode
|
|
445
464
|
});
|
|
@@ -450,7 +469,7 @@ export const EntityCollectionView = React.memo(
|
|
|
450
469
|
}
|
|
451
470
|
}, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
|
|
452
471
|
|
|
453
|
-
const createEnabled = canCreateEntity(collection, authController, fullPath, null);
|
|
472
|
+
const createEnabled = canCreateEntity(collection, authController, fullPath, null, pathSegments);
|
|
454
473
|
|
|
455
474
|
const uniqueFieldValidator: UniqueFieldValidator = useCallback(
|
|
456
475
|
({
|
|
@@ -458,8 +477,8 @@ export const EntityCollectionView = React.memo(
|
|
|
458
477
|
value,
|
|
459
478
|
property,
|
|
460
479
|
entityId
|
|
461
|
-
}) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
|
|
462
|
-
[fullPath]);
|
|
480
|
+
}) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection, pathSegments),
|
|
481
|
+
[fullPath, pathSegments]);
|
|
463
482
|
|
|
464
483
|
const onValueChange: OnCellValueChange<any, any> = ({
|
|
465
484
|
value,
|
|
@@ -473,6 +492,8 @@ export const EntityCollectionView = React.memo(
|
|
|
473
492
|
|
|
474
493
|
const saveProps: SaveEntityProps = {
|
|
475
494
|
path: entity.path ?? fullPath,
|
|
495
|
+
// Paired with the `path` above: the row's segments describe the row's path.
|
|
496
|
+
pathSegments: entity.path ? entity.pathSegments : pathSegments,
|
|
476
497
|
entityId: entity.id,
|
|
477
498
|
values: updatedValues,
|
|
478
499
|
previousValues: entity.values,
|
|
@@ -503,7 +524,7 @@ export const EntityCollectionView = React.memo(
|
|
|
503
524
|
|
|
504
525
|
};
|
|
505
526
|
|
|
506
|
-
const resolvedFullPath = navigation.resolveIdsFrom(fullPath);
|
|
527
|
+
const resolvedFullPath = navigation.resolveIdsFrom(fullPath, pathSegments);
|
|
507
528
|
const resolvedCollection = useMemo(() => resolveCollection<M>({
|
|
508
529
|
collection,
|
|
509
530
|
path: fullPath,
|
|
@@ -581,6 +602,7 @@ export const EntityCollectionView = React.memo(
|
|
|
581
602
|
const onKanbanPropertyChange = useCallback((property: string) => {
|
|
582
603
|
analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
|
|
583
604
|
path: fullPath,
|
|
605
|
+
pathSegments,
|
|
584
606
|
property
|
|
585
607
|
});
|
|
586
608
|
setSelectedKanbanProperty(property);
|
|
@@ -645,6 +667,7 @@ export const EntityCollectionView = React.memo(
|
|
|
645
667
|
entityId: entity.id,
|
|
646
668
|
selectedTab: subcollection.id ?? subcollection.path,
|
|
647
669
|
path: fullPath,
|
|
670
|
+
pathSegments,
|
|
648
671
|
fullIdPath,
|
|
649
672
|
navigation,
|
|
650
673
|
sideEntityController
|
|
@@ -663,13 +686,15 @@ export const EntityCollectionView = React.memo(
|
|
|
663
686
|
width: 260,
|
|
664
687
|
dependencies: [],
|
|
665
688
|
Builder: ({ entity }) => {
|
|
666
|
-
|
|
689
|
+
// `entity.path` is a RAW datasource path, so its segments are passed:
|
|
690
|
+
// splitting it would mis-read any parent id containing "/".
|
|
691
|
+
const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path, entity.pathSegments);
|
|
667
692
|
return (
|
|
668
693
|
<div className={"flex flex-col gap-2 w-full"}>
|
|
669
694
|
{collectionsWithPath.map((reference) => {
|
|
670
695
|
return (
|
|
671
696
|
<ReferencePreview
|
|
672
|
-
key={reference.path
|
|
697
|
+
key={entityCacheKey(reference.path, reference.id)}
|
|
673
698
|
reference={reference}
|
|
674
699
|
size={"small"} />
|
|
675
700
|
);
|
|
@@ -700,7 +725,7 @@ export const EntityCollectionView = React.memo(
|
|
|
700
725
|
entity?: Entity<M>,
|
|
701
726
|
customEntityActions?: EntityAction[]
|
|
702
727
|
}): EntityAction[] => {
|
|
703
|
-
const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
|
|
728
|
+
const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity, entity.pathSegments ?? pathSegments) : true;
|
|
704
729
|
const actions: EntityAction[] = [
|
|
705
730
|
{ ...editEntityAction, name: t("edit") }
|
|
706
731
|
];
|
|
@@ -776,6 +801,7 @@ export const EntityCollectionView = React.memo(
|
|
|
776
801
|
// EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
|
|
777
802
|
const countFetcher = <EntitiesCount
|
|
778
803
|
fullPath={fullPath}
|
|
804
|
+
pathSegments={pathSegments}
|
|
779
805
|
collection={collection}
|
|
780
806
|
filter={tableController.filterValues}
|
|
781
807
|
sortBy={tableController.sortBy}
|
|
@@ -829,9 +855,14 @@ export const EntityCollectionView = React.memo(
|
|
|
829
855
|
onTextSearchClick,
|
|
830
856
|
textSearchEnabled
|
|
831
857
|
} = useTableSearchHelper({
|
|
858
|
+
pathSegments,
|
|
832
859
|
collection,
|
|
833
860
|
fullPath: resolvedFullPath,
|
|
834
|
-
parentCollectionIds
|
|
861
|
+
parentCollectionIds,
|
|
862
|
+
// When the search term comes from the URL instead of the search bar, text search
|
|
863
|
+
// has never been initialised for this collection. Initialise it so the search bar
|
|
864
|
+
// is usable (editable and clearable) right away.
|
|
865
|
+
initialSearchString: tableController.searchString
|
|
835
866
|
});
|
|
836
867
|
|
|
837
868
|
// Popover open state managed at parent level to prevent closing when view changes
|
|
@@ -881,6 +912,7 @@ export const EntityCollectionView = React.memo(
|
|
|
881
912
|
loading={tableController.dataLoading}
|
|
882
913
|
onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
|
|
883
914
|
onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
|
|
915
|
+
initialSearchString={tableController.searchString}
|
|
884
916
|
textSearchLoading={textSearchLoading}
|
|
885
917
|
viewModeToggle={viewModeToggleElement}
|
|
886
918
|
actionsStart={<EntityCollectionViewStartActions
|
|
@@ -888,6 +920,7 @@ export const EntityCollectionView = React.memo(
|
|
|
888
920
|
collection={collection}
|
|
889
921
|
tableController={tableController}
|
|
890
922
|
path={fullPath}
|
|
923
|
+
pathSegments={pathSegments}
|
|
891
924
|
relativePath={collection.path}
|
|
892
925
|
selectionController={usedSelectionController}
|
|
893
926
|
collectionEntitiesCount={docsCount}
|
|
@@ -899,6 +932,7 @@ export const EntityCollectionView = React.memo(
|
|
|
899
932
|
onMultipleDeleteClick={onMultipleDeleteClick}
|
|
900
933
|
onNewClick={onNewClick}
|
|
901
934
|
path={fullPath}
|
|
935
|
+
pathSegments={pathSegments}
|
|
902
936
|
relativePath={collection.path}
|
|
903
937
|
selectionController={usedSelectionController}
|
|
904
938
|
selectionEnabled={selectionEnabled}
|
|
@@ -915,6 +949,7 @@ export const EntityCollectionView = React.memo(
|
|
|
915
949
|
collection={collection}
|
|
916
950
|
tableController={tableController}
|
|
917
951
|
fullPath={fullPath}
|
|
952
|
+
pathSegments={pathSegments}
|
|
918
953
|
parentCollectionIds={parentCollectionIds}
|
|
919
954
|
columnProperty={selectedKanbanProperty}
|
|
920
955
|
onEntityClick={onEntityClick}
|
|
@@ -1006,6 +1041,7 @@ export const EntityCollectionView = React.memo(
|
|
|
1006
1041
|
getIdColumnWidth={getIdColumnWidth}
|
|
1007
1042
|
additionalIDHeaderWidget={<EntityIdHeaderWidget
|
|
1008
1043
|
path={fullPath}
|
|
1044
|
+
pathSegments={pathSegments}
|
|
1009
1045
|
fullIdPath={fullIdPath ?? fullPath}
|
|
1010
1046
|
collection={collection} />}
|
|
1011
1047
|
openEntityMode={openEntityMode}
|
|
@@ -1045,6 +1081,7 @@ export const EntityCollectionView = React.memo(
|
|
|
1045
1081
|
|
|
1046
1082
|
{popupCell && <PopupFormField
|
|
1047
1083
|
key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
|
|
1084
|
+
pathSegments={pathSegments}
|
|
1048
1085
|
open={Boolean(popupCell)}
|
|
1049
1086
|
onClose={onPopupClose}
|
|
1050
1087
|
cellRect={popupCell?.cellRect}
|
|
@@ -1100,13 +1137,16 @@ function EntitiesCount({
|
|
|
1100
1137
|
collection,
|
|
1101
1138
|
filter,
|
|
1102
1139
|
sortBy,
|
|
1103
|
-
onCountChange
|
|
1140
|
+
onCountChange,
|
|
1141
|
+
pathSegments
|
|
1104
1142
|
}: {
|
|
1105
1143
|
fullPath: string,
|
|
1106
1144
|
collection: EntityCollection,
|
|
1107
1145
|
filter?: FilterValues<any>,
|
|
1108
1146
|
sortBy?: [string, "asc" | "desc"],
|
|
1109
1147
|
onCountChange?: (count: number) => void,
|
|
1148
|
+
/** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
|
|
1149
|
+
pathSegments?: string[],
|
|
1110
1150
|
}) {
|
|
1111
1151
|
|
|
1112
1152
|
const dataSource = useDataSource(collection);
|
|
@@ -1116,18 +1156,24 @@ function EntitiesCount({
|
|
|
1116
1156
|
|
|
1117
1157
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
1118
1158
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
1119
|
-
|
|
1159
|
+
// Resolved together so the two representations always describe the same chain.
|
|
1160
|
+
// Segments are resolved, never derived (see useEntityFetch).
|
|
1161
|
+
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
|
|
1162
|
+
const resolvedPathSegments = useMemo(() => pathSegments
|
|
1163
|
+
? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
|
|
1164
|
+
: undefined, [pathSegments, navigation.resolveSegmentsFrom]);
|
|
1120
1165
|
|
|
1121
1166
|
useEffect(() => {
|
|
1122
1167
|
if (dataSource.countEntities)
|
|
1123
1168
|
dataSource.countEntities({
|
|
1124
1169
|
path: resolvedPath,
|
|
1170
|
+
pathSegments: resolvedPathSegments,
|
|
1125
1171
|
collection,
|
|
1126
1172
|
filter,
|
|
1127
1173
|
orderBy: sortByProperty,
|
|
1128
1174
|
order: currentSort
|
|
1129
1175
|
}).then(setCount).catch(setError);
|
|
1130
|
-
}, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
|
|
1176
|
+
}, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
|
|
1131
1177
|
|
|
1132
1178
|
useEffect(() => {
|
|
1133
1179
|
if (onCountChange && count !== undefined) {
|
|
@@ -1156,10 +1202,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
|
|
|
1156
1202
|
function EntityIdHeaderWidget({
|
|
1157
1203
|
collection,
|
|
1158
1204
|
path,
|
|
1205
|
+
pathSegments,
|
|
1159
1206
|
fullIdPath
|
|
1160
1207
|
}: {
|
|
1161
1208
|
collection: EntityCollection,
|
|
1162
1209
|
path: string,
|
|
1210
|
+
/** `path` split at its real segment boundaries, when the caller knows them. */
|
|
1211
|
+
pathSegments?: string[],
|
|
1163
1212
|
fullIdPath: string
|
|
1164
1213
|
}) {
|
|
1165
1214
|
|
|
@@ -1199,6 +1248,7 @@ function EntityIdHeaderWidget({
|
|
|
1199
1248
|
collection,
|
|
1200
1249
|
entityId,
|
|
1201
1250
|
path,
|
|
1251
|
+
pathSegments,
|
|
1202
1252
|
fullIdPath,
|
|
1203
1253
|
sideEntityController,
|
|
1204
1254
|
navigation
|
|
@@ -1224,7 +1274,7 @@ function EntityIdHeaderWidget({
|
|
|
1224
1274
|
</form>
|
|
1225
1275
|
{recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
|
|
1226
1276
|
{recentIds.map(id => (
|
|
1227
|
-
<ReferencePreview reference={new EntityReference(id, path)}
|
|
1277
|
+
<ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
|
|
1228
1278
|
key={id}
|
|
1229
1279
|
hover={true}
|
|
1230
1280
|
onClick={() => {
|
|
@@ -1234,6 +1284,7 @@ function EntityIdHeaderWidget({
|
|
|
1234
1284
|
collection,
|
|
1235
1285
|
entityId: id,
|
|
1236
1286
|
path,
|
|
1287
|
+
pathSegments,
|
|
1237
1288
|
fullIdPath,
|
|
1238
1289
|
sideEntityController,
|
|
1239
1290
|
navigation
|
|
@@ -22,6 +22,8 @@ import { useTranslation } from "../../hooks/useTranslation";
|
|
|
22
22
|
export type EntityCollectionViewActionsProps<M extends Record<string, any>> = {
|
|
23
23
|
collection: EntityCollection<M>;
|
|
24
24
|
path: string;
|
|
25
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
26
|
+
pathSegments?: string[];
|
|
25
27
|
relativePath: string;
|
|
26
28
|
parentCollectionIds: string[];
|
|
27
29
|
selectionEnabled: boolean;
|
|
@@ -40,6 +42,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
40
42
|
onMultipleDeleteClick,
|
|
41
43
|
selectionEnabled,
|
|
42
44
|
path,
|
|
45
|
+
pathSegments,
|
|
43
46
|
selectionController,
|
|
44
47
|
tableController,
|
|
45
48
|
collectionEntitiesCount,
|
|
@@ -56,7 +59,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
56
59
|
|
|
57
60
|
const selectedEntities = selectionController.selectedEntities;
|
|
58
61
|
|
|
59
|
-
const addButton = canCreateEntity(collection, authController, path, null) &&
|
|
62
|
+
const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
|
|
60
63
|
onNewClick && (largeLayout
|
|
61
64
|
? <Button
|
|
62
65
|
id={`add_entity_${path}`}
|
|
@@ -75,7 +78,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
75
78
|
<AddIcon size={"small"} />
|
|
76
79
|
</Button>);
|
|
77
80
|
|
|
78
|
-
const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null);
|
|
81
|
+
const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null, pathSegments);
|
|
79
82
|
|
|
80
83
|
let multipleDeleteButton: React.ReactNode | undefined;
|
|
81
84
|
if (selectionEnabled) {
|
|
@@ -107,6 +110,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
107
110
|
|
|
108
111
|
const actionProps: CollectionActionsProps = {
|
|
109
112
|
path,
|
|
113
|
+
pathSegments,
|
|
110
114
|
relativePath,
|
|
111
115
|
parentCollectionIds,
|
|
112
116
|
collection,
|
|
@@ -17,6 +17,8 @@ import { useTranslation } from "../../hooks/useTranslation";
|
|
|
17
17
|
export type EntityCollectionViewStartActionsProps<M extends Record<string, any>> = {
|
|
18
18
|
collection: EntityCollection<M>;
|
|
19
19
|
path: string;
|
|
20
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
21
|
+
pathSegments?: string[];
|
|
20
22
|
relativePath: string;
|
|
21
23
|
parentCollectionIds: string[];
|
|
22
24
|
selectionController: SelectionController<M>;
|
|
@@ -33,6 +35,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
33
35
|
relativePath,
|
|
34
36
|
parentCollectionIds,
|
|
35
37
|
path,
|
|
38
|
+
pathSegments,
|
|
36
39
|
selectionController,
|
|
37
40
|
tableController,
|
|
38
41
|
collectionEntitiesCount,
|
|
@@ -57,6 +60,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
57
60
|
|
|
58
61
|
const actionProps: CollectionActionsProps = {
|
|
59
62
|
path,
|
|
63
|
+
pathSegments,
|
|
60
64
|
relativePath,
|
|
61
65
|
parentCollectionIds,
|
|
62
66
|
collection,
|
|
@@ -66,8 +70,10 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
66
70
|
collectionEntitiesCount
|
|
67
71
|
};
|
|
68
72
|
|
|
73
|
+
const hasAnyAllowedFilters = !tableController.allowedFilters || tableController.allowedFilters.length > 0;
|
|
74
|
+
|
|
69
75
|
// Filters button
|
|
70
|
-
const filtersButton = resolvedProperties && tableController.setFilterValues && (
|
|
76
|
+
const filtersButton = resolvedProperties && tableController.setFilterValues && hasAnyAllowedFilters && (
|
|
71
77
|
<Tooltip title={t("filters")}
|
|
72
78
|
key={"filters_tooltip"}>
|
|
73
79
|
<Badge
|
|
@@ -131,6 +137,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
131
137
|
filterValues={tableController.filterValues}
|
|
132
138
|
setFilterValues={(filterValues) => tableController.setFilterValues?.(filterValues ?? {})}
|
|
133
139
|
forceFilter={collection.forceFilter}
|
|
140
|
+
allowedFilters={tableController.allowedFilters?.map(key => key.toString())}
|
|
134
141
|
/>
|
|
135
142
|
)}
|
|
136
143
|
</>
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
DialogActions,
|
|
13
13
|
DialogContent,
|
|
14
14
|
DialogTitle,
|
|
15
|
-
FilterListIcon,
|
|
16
15
|
Typography
|
|
17
16
|
} from "@firecms/ui";
|
|
18
17
|
import { useTranslation } from "../../hooks/useTranslation";
|
|
@@ -30,6 +29,7 @@ export interface FiltersDialogProps {
|
|
|
30
29
|
filterValues: FilterValues<any> | undefined;
|
|
31
30
|
setFilterValues: (filterValues?: FilterValues<any>) => void;
|
|
32
31
|
forceFilter?: FilterValues<any>;
|
|
32
|
+
allowedFilters?: string[];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -42,7 +42,8 @@ export function FiltersDialog({
|
|
|
42
42
|
properties,
|
|
43
43
|
filterValues,
|
|
44
44
|
setFilterValues,
|
|
45
|
-
forceFilter
|
|
45
|
+
forceFilter,
|
|
46
|
+
allowedFilters
|
|
46
47
|
}: FiltersDialogProps) {
|
|
47
48
|
const { t } = useTranslation();
|
|
48
49
|
|
|
@@ -59,18 +60,18 @@ export function FiltersDialog({
|
|
|
59
60
|
}
|
|
60
61
|
}, [open, filterValues]);
|
|
61
62
|
|
|
62
|
-
// Get list of
|
|
63
|
-
const
|
|
64
|
-
return Object.entries(properties).filter(([key
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!baseProperty) return false;
|
|
71
|
-
return ["string", "number", "boolean", "date", "reference"].includes(baseProperty.dataType);
|
|
63
|
+
// Get list of editable filter properties
|
|
64
|
+
const editableFilterProperties = useMemo(() => {
|
|
65
|
+
return Object.entries(properties).filter(([key]) => {
|
|
66
|
+
const isFilterAllowed = !allowedFilters || allowedFilters.includes(key);
|
|
67
|
+
|
|
68
|
+
const isFilterForced = Boolean(forceFilter && Object.keys(forceFilter).includes(key));
|
|
69
|
+
|
|
70
|
+
return isFilterAllowed && !isFilterForced;
|
|
72
71
|
});
|
|
73
|
-
}, [properties, forceFilter]);
|
|
72
|
+
}, [properties, allowedFilters, forceFilter]);
|
|
73
|
+
|
|
74
|
+
const hasEditableFilterProperties = editableFilterProperties.length > 0;
|
|
74
75
|
|
|
75
76
|
const handleFilterChange = useCallback((propertyKey: string, value?: [VirtualTableWhereFilterOp, any]) => {
|
|
76
77
|
setLocalFilters(prev => {
|
|
@@ -103,7 +104,13 @@ export function FiltersDialog({
|
|
|
103
104
|
|
|
104
105
|
// Check if any reference field's dialog is currently open (should hide this dialog)
|
|
105
106
|
const isAnyFieldHidden = Object.values(hiddenFields).some(hidden => hidden);
|
|
106
|
-
|
|
107
|
+
|
|
108
|
+
const getActiveFilterCount = () => {
|
|
109
|
+
const editableLocalFilters = Object.keys(localFilters).filter((key) => !forceFilter || !(key in forceFilter));
|
|
110
|
+
return editableLocalFilters.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const activeFilterCount = getActiveFilterCount();
|
|
107
114
|
|
|
108
115
|
const renderFilterField = useCallback((propertyKey: string, property: ResolvedProperty) => {
|
|
109
116
|
const isArray = property.dataType === "array";
|
|
@@ -185,14 +192,14 @@ export function FiltersDialog({
|
|
|
185
192
|
</DialogTitle>
|
|
186
193
|
|
|
187
194
|
<DialogContent >
|
|
188
|
-
{
|
|
195
|
+
{!hasEditableFilterProperties ? (
|
|
189
196
|
<Typography color="secondary" className="py-8 text-center">
|
|
190
197
|
{t("no_filterable_properties")}
|
|
191
198
|
</Typography>
|
|
192
199
|
) : (
|
|
193
200
|
<table className="w-full border-collapse">
|
|
194
201
|
<tbody>
|
|
195
|
-
{
|
|
202
|
+
{editableFilterProperties.map(([propertyKey, property], index) => {
|
|
196
203
|
const hasFilter = propertyKey in localFilters;
|
|
197
204
|
|
|
198
205
|
return (
|
|
@@ -234,18 +241,22 @@ export function FiltersDialog({
|
|
|
234
241
|
{t("clear")}
|
|
235
242
|
</Button>
|
|
236
243
|
<div className="flex-grow" />
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
{hasEditableFilterProperties && (
|
|
245
|
+
<>
|
|
246
|
+
<Button
|
|
247
|
+
variant="text"
|
|
248
|
+
onClick={() => onOpenChange(false)}
|
|
249
|
+
>
|
|
250
|
+
{t("cancel")}
|
|
251
|
+
</Button>
|
|
252
|
+
<Button
|
|
253
|
+
variant="filled"
|
|
254
|
+
onClick={handleApply}
|
|
255
|
+
>
|
|
256
|
+
{t("apply_filters")}
|
|
257
|
+
</Button>
|
|
258
|
+
</>
|
|
259
|
+
)}
|
|
249
260
|
</DialogActions>
|
|
250
261
|
</Dialog>
|
|
251
262
|
);
|