@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/app/Scaffold.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { PropsWithChildren, useCallback } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { cls, defaultBorderMixin, IconButton, MenuIcon, Sheet } from "@firecms/ui";
|
|
3
3
|
import equal from "react-fast-compare"
|
|
4
4
|
import { useLargeLayout } from "../hooks";
|
|
5
5
|
import { ErrorBoundary } from "../components";
|
|
@@ -80,7 +80,11 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
|
|
|
80
80
|
const [onHover, setOnHover] = React.useState(false);
|
|
81
81
|
|
|
82
82
|
const setOnHoverTrue = useCallback(() => setOnHover(true), []);
|
|
83
|
-
const setOnHoverFalse = useCallback(() =>
|
|
83
|
+
const setOnHoverFalse = useCallback(() => {
|
|
84
|
+
// Don't collapse the drawer while a popover/dropdown is open
|
|
85
|
+
if (typeof document !== "undefined" && document.querySelector("[data-radix-popper-content-wrapper]")) return;
|
|
86
|
+
setOnHover(false);
|
|
87
|
+
}, []);
|
|
84
88
|
|
|
85
89
|
const handleDrawerOpen = useCallback(() => {
|
|
86
90
|
setDrawerOpen(true);
|
|
@@ -96,17 +100,21 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
|
|
|
96
100
|
} catch { /* ignore */ }
|
|
97
101
|
}, []);
|
|
98
102
|
|
|
99
|
-
|
|
103
|
+
// Pinned-open state only; hover is tracked separately so it can *float*
|
|
104
|
+
// over the content instead of pushing it.
|
|
105
|
+
const computedDrawerOpen: boolean = drawerOpen;
|
|
106
|
+
const computedDrawerHovered: boolean = Boolean(largeLayout && onHover);
|
|
100
107
|
|
|
101
108
|
const hasAppBar = Boolean(appBarChildren.length > 0);
|
|
102
109
|
return (
|
|
103
110
|
<AppContext.Provider value={{
|
|
104
111
|
logo,
|
|
105
112
|
hasDrawer: includeDrawer,
|
|
106
|
-
drawerHovered:
|
|
113
|
+
drawerHovered: computedDrawerHovered,
|
|
107
114
|
drawerOpen: computedDrawerOpen,
|
|
108
115
|
closeDrawer: handleDrawerClose,
|
|
109
116
|
openDrawer: handleDrawerOpen,
|
|
117
|
+
closeHover: setOnHoverFalse,
|
|
110
118
|
autoOpenDrawer
|
|
111
119
|
}}>
|
|
112
120
|
<div
|
|
@@ -128,7 +136,7 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
|
|
|
128
136
|
onMouseMove={setOnHoverTrue}
|
|
129
137
|
onMouseLeave={setOnHoverFalse}
|
|
130
138
|
open={computedDrawerOpen}
|
|
131
|
-
hovered={
|
|
139
|
+
hovered={computedDrawerHovered}
|
|
132
140
|
setDrawerOpen={setDrawerOpen}>
|
|
133
141
|
{includeDrawer && drawerChildren}
|
|
134
142
|
</DrawerWrapper>
|
|
@@ -177,49 +185,30 @@ function DrawerWrapper(props: {
|
|
|
177
185
|
}) {
|
|
178
186
|
|
|
179
187
|
const { t } = useTranslation();
|
|
180
|
-
|
|
188
|
+
|
|
189
|
+
// Layout width is the space the drawer occupies in the flex row; the visual
|
|
190
|
+
// width is how wide the drawer actually renders. When hovered (but not
|
|
191
|
+
// pinned open) the layout width stays collapsed while the visual width
|
|
192
|
+
// expands, so the drawer *floats* over the content instead of pushing it.
|
|
193
|
+
const layoutWidth = !props.displayed ? 0 : (props.open ? DRAWER_WIDTH : 72);
|
|
194
|
+
const visualWidth = !props.displayed ? 0 : ((props.open || props.hovered) ? DRAWER_WIDTH : 72);
|
|
195
|
+
const isFloating = props.hovered && !props.open;
|
|
196
|
+
|
|
197
|
+
const transition = "left 75ms cubic-bezier(0.4, 0, 0.6, 1) 0ms, opacity 75ms cubic-bezier(0.4, 0, 0.6, 1) 0ms, width 75ms cubic-bezier(0.4, 0, 0.6, 1) 0ms";
|
|
198
|
+
|
|
181
199
|
const innerDrawer = <div
|
|
182
|
-
className={"
|
|
200
|
+
className={cls("h-full overflow-hidden", defaultBorderMixin,
|
|
201
|
+
isFloating
|
|
202
|
+
? "absolute top-0 left-0 bottom-0 z-30 bg-surface-50 dark:bg-surface-900 shadow-lg border-r"
|
|
203
|
+
: "relative bg-surface-50 dark:bg-surface-900")}
|
|
183
204
|
style={{
|
|
184
|
-
width,
|
|
185
|
-
transition
|
|
205
|
+
width: visualWidth,
|
|
206
|
+
transition
|
|
186
207
|
}}
|
|
187
208
|
>
|
|
188
|
-
|
|
189
|
-
{!props.open && props.displayed && (
|
|
190
|
-
<Tooltip title={t("open_menu")}
|
|
191
|
-
side="right"
|
|
192
|
-
sideOffset={12}
|
|
193
|
-
asChild={true}>
|
|
194
|
-
<div
|
|
195
|
-
className="ml-2 fixed top-1 left-2 sm:top-2 sm:left-2 !bg-surface-50 dark:!bg-surface-900 rounded-full w-fit z-20">
|
|
196
|
-
<IconButton
|
|
197
|
-
color="inherit"
|
|
198
|
-
aria-label={t("open_menu")}
|
|
199
|
-
onClick={() => props.setDrawerOpen(true)}
|
|
200
|
-
size="large"
|
|
201
|
-
>
|
|
202
|
-
<MenuIcon size="small" />
|
|
203
|
-
</IconButton>
|
|
204
|
-
</div>
|
|
205
|
-
</Tooltip>
|
|
206
|
-
)}
|
|
207
|
-
|
|
208
|
-
<div
|
|
209
|
-
className={`z-20 absolute right-0 top-4 ${props.open ? "opacity-100" : "opacity-0 invisible"
|
|
210
|
-
} transition-opacity duration-200 ease-in-out`}>
|
|
211
|
-
<IconButton
|
|
212
|
-
aria-label={t("close_drawer")}
|
|
213
|
-
onClick={() => props.setDrawerOpen(false)}
|
|
214
|
-
>
|
|
215
|
-
<ChevronLeftIcon />
|
|
216
|
-
</IconButton>
|
|
217
|
-
</div>
|
|
218
|
-
|
|
219
209
|
<div className={"flex flex-col h-full"}>
|
|
220
210
|
{props.children}
|
|
221
211
|
</div>
|
|
222
|
-
|
|
223
212
|
</div>;
|
|
224
213
|
|
|
225
214
|
const largeLayout = useLargeLayout();
|
|
@@ -250,13 +239,14 @@ function DrawerWrapper(props: {
|
|
|
250
239
|
|
|
251
240
|
return (
|
|
252
241
|
<div
|
|
253
|
-
className="z-20 relative"
|
|
242
|
+
className="z-20 relative flex-shrink-0 overflow-visible"
|
|
254
243
|
onMouseEnter={props.onMouseEnter}
|
|
255
244
|
onMouseMove={props.onMouseMove}
|
|
256
245
|
onMouseLeave={props.onMouseLeave}
|
|
257
246
|
style={{
|
|
258
|
-
width,
|
|
259
|
-
|
|
247
|
+
width: layoutWidth,
|
|
248
|
+
minWidth: layoutWidth,
|
|
249
|
+
transition
|
|
260
250
|
}}>
|
|
261
251
|
|
|
262
252
|
{innerDrawer}
|
package/src/app/useApp.tsx
CHANGED
|
@@ -245,7 +245,8 @@ export const EntityCollectionTable = function EntityCollectionTable<M extends Re
|
|
|
245
245
|
const columnsResult: VirtualTableColumn[] = propertiesToColumns({
|
|
246
246
|
properties,
|
|
247
247
|
sortable,
|
|
248
|
-
|
|
248
|
+
forcedFilters: tableController.forcedFilters,
|
|
249
|
+
allowedFilters: tableController.allowedFilters,
|
|
249
250
|
AdditionalHeaderWidget
|
|
250
251
|
});
|
|
251
252
|
|
|
@@ -353,6 +354,7 @@ export const EntityCollectionTable = function EntityCollectionTable<M extends Re
|
|
|
353
354
|
onTextSearch={textSearchEnabled ? onTextSearch : undefined}
|
|
354
355
|
textSearchLoading={textSearchLoading}
|
|
355
356
|
onTextSearchClick={textSearchEnabled ? onTextSearchClick : undefined}
|
|
357
|
+
initialSearchString={tableController.searchString}
|
|
356
358
|
title={title}
|
|
357
359
|
actionsStart={actionsStart}
|
|
358
360
|
actions={actions}
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { getTableCellAlignment, getTablePropertyColumnWidth } from "./internal/common";
|
|
3
3
|
import { FilterValues, ResolvedProperties, ResolvedProperty } from "../../types";
|
|
4
4
|
import { VirtualTableColumn } from "../VirtualTable";
|
|
5
|
-
import { getIconForProperty, getResolvedPropertyInPath } from "../../util";
|
|
5
|
+
import { getIconForProperty, getResolvedPropertyInPath, isDataTypeFilterable } from "../../util";
|
|
6
6
|
import { getColumnKeysForProperty } from "../common/useColumnsIds";
|
|
7
7
|
|
|
8
8
|
export function buildIdColumn(largeLayout?: boolean): VirtualTableColumn {
|
|
@@ -20,7 +20,8 @@ export function buildIdColumn(largeLayout?: boolean): VirtualTableColumn {
|
|
|
20
20
|
export interface PropertiesToColumnsParams<M extends Record<string, any>> {
|
|
21
21
|
properties: ResolvedProperties<M>;
|
|
22
22
|
sortable?: boolean;
|
|
23
|
-
|
|
23
|
+
forcedFilters?: (keyof M)[];
|
|
24
|
+
allowedFilters?: (keyof M)[];
|
|
24
25
|
AdditionalHeaderWidget?: React.ComponentType<{
|
|
25
26
|
property: ResolvedProperty,
|
|
26
27
|
propertyKey: string,
|
|
@@ -28,8 +29,7 @@ export interface PropertiesToColumnsParams<M extends Record<string, any>> {
|
|
|
28
29
|
}>;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export function propertiesToColumns<M extends Record<string, any>>({ properties, sortable,
|
|
32
|
-
const disabledFilter = Boolean(forceFilter);
|
|
32
|
+
export function propertiesToColumns<M extends Record<string, any>>({ properties, sortable, forcedFilters, AdditionalHeaderWidget, allowedFilters }: PropertiesToColumnsParams<M>): VirtualTableColumn[] {
|
|
33
33
|
return Object.entries<ResolvedProperty>(properties)
|
|
34
34
|
.flatMap(([key, property]) => getColumnKeysForProperty(property, key))
|
|
35
35
|
.map(({
|
|
@@ -39,14 +39,19 @@ export function propertiesToColumns<M extends Record<string, any>>({ properties,
|
|
|
39
39
|
const property = getResolvedPropertyInPath(properties, key);
|
|
40
40
|
if (!property)
|
|
41
41
|
throw Error("Internal error: no property found in path " + key);
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
const filterable = property.dataType === 'array' ? isDataTypeFilterable(property.of?.dataType, true) : isDataTypeFilterable(property.dataType);
|
|
44
|
+
const isFilterForced = forcedFilters?.includes(key) ?? false;
|
|
45
|
+
const isFilterAllowed = allowedFilters ? allowedFilters.includes(key) : filterable;
|
|
46
|
+
|
|
47
|
+
const filterEnabled = filterable && isFilterAllowed && !isFilterForced;
|
|
43
48
|
return {
|
|
44
49
|
key: key as string,
|
|
45
50
|
align: getTableCellAlignment(property),
|
|
46
51
|
icon: getIconForProperty(property, "small"),
|
|
47
52
|
title: property.name ?? key as string,
|
|
48
53
|
sortable: sortable,
|
|
49
|
-
filter:
|
|
54
|
+
filter: filterEnabled,
|
|
50
55
|
width: getTablePropertyColumnWidth(property),
|
|
51
56
|
resizable: true,
|
|
52
57
|
custom: {
|
|
@@ -59,16 +64,3 @@ export function propertiesToColumns<M extends Record<string, any>>({ properties,
|
|
|
59
64
|
} satisfies VirtualTableColumn;
|
|
60
65
|
});
|
|
61
66
|
}
|
|
62
|
-
|
|
63
|
-
function filterableProperty(property: ResolvedProperty, partOfArray = false): boolean {
|
|
64
|
-
if (partOfArray) {
|
|
65
|
-
return ["string", "number", "date", "reference"].includes(property.dataType);
|
|
66
|
-
}
|
|
67
|
-
if (property.dataType === "array") {
|
|
68
|
-
if (property.of)
|
|
69
|
-
return filterableProperty(property.of, true);
|
|
70
|
-
else
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
return ["string", "number", "boolean", "date", "reference", "array"].includes(property.dataType);
|
|
74
|
-
}
|
|
@@ -70,7 +70,7 @@ export const TableReferenceFieldInternal = React.memo(
|
|
|
70
70
|
}, [updateValue]);
|
|
71
71
|
|
|
72
72
|
const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
|
|
73
|
-
updateValue(entities.map((e) => getReferenceFrom(e)));
|
|
73
|
+
updateValue(entities.filter(Boolean).map((e) => getReferenceFrom(e)));
|
|
74
74
|
}, [updateValue]);
|
|
75
75
|
|
|
76
76
|
const selectedEntityIds = internalValue
|
|
@@ -20,6 +20,11 @@ interface CollectionTableToolbarProps {
|
|
|
20
20
|
onTextSearchClick?: () => void;
|
|
21
21
|
onTextSearch?: (searchString?: string) => void;
|
|
22
22
|
textSearchLoading?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Search term the search bar should start with, typically the one restored from the
|
|
25
|
+
* URL query params by the table controller.
|
|
26
|
+
*/
|
|
27
|
+
initialSearchString?: string;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export function CollectionTableToolbar({
|
|
@@ -29,6 +34,7 @@ export function CollectionTableToolbar({
|
|
|
29
34
|
onTextSearch,
|
|
30
35
|
onTextSearchClick,
|
|
31
36
|
textSearchLoading,
|
|
37
|
+
initialSearchString,
|
|
32
38
|
title,
|
|
33
39
|
viewModeToggle
|
|
34
40
|
}: CollectionTableToolbarProps) {
|
|
@@ -79,6 +85,7 @@ export function CollectionTableToolbar({
|
|
|
79
85
|
disabled={Boolean(onTextSearchClick)}
|
|
80
86
|
onClick={onTextSearchClick}
|
|
81
87
|
onTextSearch={onTextSearchClick ? undefined : onTextSearch}
|
|
88
|
+
initialValue={initialSearchString}
|
|
82
89
|
placeholder={t("search")}
|
|
83
90
|
expandable={true} />}
|
|
84
91
|
|
|
@@ -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,
|
|
@@ -106,6 +106,13 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
|
|
|
106
106
|
* Full path using navigation ids.
|
|
107
107
|
*/
|
|
108
108
|
fullIdPath?: string;
|
|
109
|
+
/**
|
|
110
|
+
* `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
111
|
+
* Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
|
|
112
|
+
* Defaults to splitting `fullPath`, which is correct for any backend whose ids cannot
|
|
113
|
+
* contain "/".
|
|
114
|
+
*/
|
|
115
|
+
pathSegments?: string[];
|
|
109
116
|
/**
|
|
110
117
|
* If this is a subcollection, specify the parent collection ids.
|
|
111
118
|
*/
|
|
@@ -152,6 +159,7 @@ export const EntityCollectionView = React.memo(
|
|
|
152
159
|
function EntityCollectionView<M extends Record<string, any>>({
|
|
153
160
|
fullPath: fullPathProp,
|
|
154
161
|
fullIdPath,
|
|
162
|
+
pathSegments,
|
|
155
163
|
parentCollectionIds,
|
|
156
164
|
isSubCollection,
|
|
157
165
|
className,
|
|
@@ -317,6 +325,7 @@ export const EntityCollectionView = React.memo(
|
|
|
317
325
|
|
|
318
326
|
const tableController = useDataSourceTableController<M>({
|
|
319
327
|
fullPath,
|
|
328
|
+
pathSegments,
|
|
320
329
|
collection,
|
|
321
330
|
lastDeleteTimestamp,
|
|
322
331
|
scrollRestoration,
|
|
@@ -365,6 +374,7 @@ export const EntityCollectionView = React.memo(
|
|
|
365
374
|
collection,
|
|
366
375
|
entityId: undefined,
|
|
367
376
|
path: fullPath,
|
|
377
|
+
pathSegments,
|
|
368
378
|
fullIdPath,
|
|
369
379
|
sideEntityController,
|
|
370
380
|
navigation,
|
|
@@ -440,6 +450,7 @@ export const EntityCollectionView = React.memo(
|
|
|
440
450
|
const onViewModeChange = useCallback((mode: ViewMode) => {
|
|
441
451
|
analyticsController.onAnalyticsEvent?.("view_mode_changed", {
|
|
442
452
|
path: fullPath,
|
|
453
|
+
pathSegments,
|
|
443
454
|
from: viewMode,
|
|
444
455
|
to: mode
|
|
445
456
|
});
|
|
@@ -581,6 +592,7 @@ export const EntityCollectionView = React.memo(
|
|
|
581
592
|
const onKanbanPropertyChange = useCallback((property: string) => {
|
|
582
593
|
analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
|
|
583
594
|
path: fullPath,
|
|
595
|
+
pathSegments,
|
|
584
596
|
property
|
|
585
597
|
});
|
|
586
598
|
setSelectedKanbanProperty(property);
|
|
@@ -645,6 +657,7 @@ export const EntityCollectionView = React.memo(
|
|
|
645
657
|
entityId: entity.id,
|
|
646
658
|
selectedTab: subcollection.id ?? subcollection.path,
|
|
647
659
|
path: fullPath,
|
|
660
|
+
pathSegments,
|
|
648
661
|
fullIdPath,
|
|
649
662
|
navigation,
|
|
650
663
|
sideEntityController
|
|
@@ -776,6 +789,7 @@ export const EntityCollectionView = React.memo(
|
|
|
776
789
|
// EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
|
|
777
790
|
const countFetcher = <EntitiesCount
|
|
778
791
|
fullPath={fullPath}
|
|
792
|
+
pathSegments={pathSegments}
|
|
779
793
|
collection={collection}
|
|
780
794
|
filter={tableController.filterValues}
|
|
781
795
|
sortBy={tableController.sortBy}
|
|
@@ -831,7 +845,11 @@ export const EntityCollectionView = React.memo(
|
|
|
831
845
|
} = useTableSearchHelper({
|
|
832
846
|
collection,
|
|
833
847
|
fullPath: resolvedFullPath,
|
|
834
|
-
parentCollectionIds
|
|
848
|
+
parentCollectionIds,
|
|
849
|
+
// When the search term comes from the URL instead of the search bar, text search
|
|
850
|
+
// has never been initialised for this collection. Initialise it so the search bar
|
|
851
|
+
// is usable (editable and clearable) right away.
|
|
852
|
+
initialSearchString: tableController.searchString
|
|
835
853
|
});
|
|
836
854
|
|
|
837
855
|
// Popover open state managed at parent level to prevent closing when view changes
|
|
@@ -881,6 +899,7 @@ export const EntityCollectionView = React.memo(
|
|
|
881
899
|
loading={tableController.dataLoading}
|
|
882
900
|
onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
|
|
883
901
|
onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
|
|
902
|
+
initialSearchString={tableController.searchString}
|
|
884
903
|
textSearchLoading={textSearchLoading}
|
|
885
904
|
viewModeToggle={viewModeToggleElement}
|
|
886
905
|
actionsStart={<EntityCollectionViewStartActions
|
|
@@ -915,6 +934,7 @@ export const EntityCollectionView = React.memo(
|
|
|
915
934
|
collection={collection}
|
|
916
935
|
tableController={tableController}
|
|
917
936
|
fullPath={fullPath}
|
|
937
|
+
pathSegments={pathSegments}
|
|
918
938
|
parentCollectionIds={parentCollectionIds}
|
|
919
939
|
columnProperty={selectedKanbanProperty}
|
|
920
940
|
onEntityClick={onEntityClick}
|
|
@@ -1045,6 +1065,7 @@ export const EntityCollectionView = React.memo(
|
|
|
1045
1065
|
|
|
1046
1066
|
{popupCell && <PopupFormField
|
|
1047
1067
|
key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
|
|
1068
|
+
pathSegments={pathSegments}
|
|
1048
1069
|
open={Boolean(popupCell)}
|
|
1049
1070
|
onClose={onPopupClose}
|
|
1050
1071
|
cellRect={popupCell?.cellRect}
|
|
@@ -1100,13 +1121,16 @@ function EntitiesCount({
|
|
|
1100
1121
|
collection,
|
|
1101
1122
|
filter,
|
|
1102
1123
|
sortBy,
|
|
1103
|
-
onCountChange
|
|
1124
|
+
onCountChange,
|
|
1125
|
+
pathSegments
|
|
1104
1126
|
}: {
|
|
1105
1127
|
fullPath: string,
|
|
1106
1128
|
collection: EntityCollection,
|
|
1107
1129
|
filter?: FilterValues<any>,
|
|
1108
1130
|
sortBy?: [string, "asc" | "desc"],
|
|
1109
1131
|
onCountChange?: (count: number) => void,
|
|
1132
|
+
/** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
|
|
1133
|
+
pathSegments?: string[],
|
|
1110
1134
|
}) {
|
|
1111
1135
|
|
|
1112
1136
|
const dataSource = useDataSource(collection);
|
|
@@ -1117,17 +1141,19 @@ function EntitiesCount({
|
|
|
1117
1141
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
1118
1142
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
1119
1143
|
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
|
|
1144
|
+
const resolvedPathSegments = pathSegments;
|
|
1120
1145
|
|
|
1121
1146
|
useEffect(() => {
|
|
1122
1147
|
if (dataSource.countEntities)
|
|
1123
1148
|
dataSource.countEntities({
|
|
1124
1149
|
path: resolvedPath,
|
|
1150
|
+
pathSegments: resolvedPathSegments,
|
|
1125
1151
|
collection,
|
|
1126
1152
|
filter,
|
|
1127
1153
|
orderBy: sortByProperty,
|
|
1128
1154
|
order: currentSort
|
|
1129
1155
|
}).then(setCount).catch(setError);
|
|
1130
|
-
}, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
|
|
1156
|
+
}, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
|
|
1131
1157
|
|
|
1132
1158
|
useEffect(() => {
|
|
1133
1159
|
if (onCountChange && count !== undefined) {
|
|
@@ -66,8 +66,10 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
66
66
|
collectionEntitiesCount
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
+
const hasAnyAllowedFilters = !tableController.allowedFilters || tableController.allowedFilters.length > 0;
|
|
70
|
+
|
|
69
71
|
// Filters button
|
|
70
|
-
const filtersButton = resolvedProperties && tableController.setFilterValues && (
|
|
72
|
+
const filtersButton = resolvedProperties && tableController.setFilterValues && hasAnyAllowedFilters && (
|
|
71
73
|
<Tooltip title={t("filters")}
|
|
72
74
|
key={"filters_tooltip"}>
|
|
73
75
|
<Badge
|
|
@@ -131,6 +133,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
131
133
|
filterValues={tableController.filterValues}
|
|
132
134
|
setFilterValues={(filterValues) => tableController.setFilterValues?.(filterValues ?? {})}
|
|
133
135
|
forceFilter={collection.forceFilter}
|
|
136
|
+
allowedFilters={tableController.allowedFilters?.map(key => key.toString())}
|
|
134
137
|
/>
|
|
135
138
|
)}
|
|
136
139
|
</>
|
|
@@ -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
|
);
|