@firecms/core 3.3.0-canary.040c21c → 3.3.0-canary.1e1cce9
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/components/EntityCollectionTable/column_utils.d.ts +4 -3
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityPreview.d.ts +3 -1
- package/dist/index.es.js +408 -297
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +408 -297
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +17 -0
- package/dist/types/translations.d.ts +11 -3
- package/dist/util/entities.d.ts +1 -0
- package/package.json +5 -5
- package/src/app/Scaffold.tsx +13 -1
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +2 -1
- package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +5 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityPreview.tsx +41 -40
- package/src/components/HomePage/NavigationCardBinding.tsx +6 -3
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
- package/src/components/common/useDataSourceTableController.tsx +41 -4
- package/src/core/DefaultDrawer.tsx +1 -1
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
- package/src/hooks/useBuildNavigationController.tsx +5 -1
- package/src/locales/de.ts +13 -5
- package/src/locales/en.ts +13 -5
- package/src/locales/es.ts +13 -5
- package/src/locales/fr.ts +13 -5
- package/src/locales/hi.ts +13 -5
- package/src/locales/it.ts +13 -5
- package/src/locales/pt.ts +13 -5
- package/src/types/collections.ts +18 -0
- package/src/types/translations.ts +11 -3
- package/src/util/entities.ts +11 -0
- package/src/util/resolutions.ts +3 -0
|
@@ -232,6 +232,21 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
232
232
|
* e.g. `initialFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
|
|
233
233
|
*/
|
|
234
234
|
initialFilter?: FilterValues<Extract<keyof M, string>>;
|
|
235
|
+
/**
|
|
236
|
+
* Array of property keys that are allowed for filtering. Allowed filters will be displayed in the collection view, table row headers
|
|
237
|
+
* and will be used to filter data.
|
|
238
|
+
*
|
|
239
|
+
* If not specified, all properties that are filterable will be allowed.
|
|
240
|
+
*
|
|
241
|
+
* If you specify this prop, the filters will be displayed in the collection view if they are filterable.
|
|
242
|
+
*
|
|
243
|
+
* If you set it as an empty array, no filters will be displayed.
|
|
244
|
+
*
|
|
245
|
+
* e.g. `allowedFilters: ["name", "category"]`
|
|
246
|
+
*
|
|
247
|
+
* e.g. `allowedFilters: []`
|
|
248
|
+
*/
|
|
249
|
+
allowedFilters?: (keyof M)[];
|
|
235
250
|
/**
|
|
236
251
|
* Default sort applied to this collection.
|
|
237
252
|
* When setting this prop, entities will have a default order
|
|
@@ -621,6 +636,8 @@ export type EntityTableController<M extends Record<string, any> = any> = {
|
|
|
621
636
|
dataLoadingError?: Error;
|
|
622
637
|
filterValues?: FilterValues<Extract<keyof M, string>>;
|
|
623
638
|
setFilterValues?: (filterValues: FilterValues<Extract<keyof M, string>>) => void;
|
|
639
|
+
allowedFilters?: (keyof M)[];
|
|
640
|
+
forcedFilters?: (keyof M)[];
|
|
624
641
|
sortBy?: [Extract<keyof M, string>, "asc" | "desc"];
|
|
625
642
|
setSortBy?: (sortBy?: [Extract<keyof M, string>, "asc" | "desc"]) => void;
|
|
626
643
|
searchString?: string;
|
|
@@ -412,8 +412,8 @@ export interface FireCMSTranslations {
|
|
|
412
412
|
cms_users: string;
|
|
413
413
|
roles_menu: string;
|
|
414
414
|
project_settings: string;
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
firestore_manager: string;
|
|
416
|
+
manage_your_firestore_data: string;
|
|
417
417
|
build_admin_panel_in_minutes: string;
|
|
418
418
|
go_live_instantly: string;
|
|
419
419
|
create_production_ready_back_offices: string;
|
|
@@ -481,10 +481,18 @@ export interface FireCMSTranslations {
|
|
|
481
481
|
auto_setup_collections_button: string;
|
|
482
482
|
auto_setup_collections_title: string;
|
|
483
483
|
auto_setup_collections_desc: string;
|
|
484
|
-
|
|
484
|
+
setting_up_collections: string;
|
|
485
|
+
setting_up_collection: string;
|
|
485
486
|
no_collections_found_to_setup: string;
|
|
486
487
|
collections_have_been_setup: string;
|
|
487
488
|
error_setting_up_collections: string;
|
|
489
|
+
setup_collections_title: string;
|
|
490
|
+
setup_collections_select_desc: string;
|
|
491
|
+
select_all: string;
|
|
492
|
+
deselect_all: string;
|
|
493
|
+
setup_collections_confirm: string;
|
|
494
|
+
collection_setup_success: string;
|
|
495
|
+
go_to_collection: string;
|
|
488
496
|
add_your: string;
|
|
489
497
|
database_collections: string;
|
|
490
498
|
to_firecms: string;
|
package/dist/util/entities.d.ts
CHANGED
|
@@ -25,3 +25,4 @@ export declare function sanitizeData<M extends Record<string, any>>(values: Enti
|
|
|
25
25
|
export declare function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M>): EntityReference;
|
|
26
26
|
export declare function traverseValuesProperties<M extends Record<string, any>>(inputValues: Partial<EntityValues<M>>, properties: ResolvedProperties<M>, operation: (value: any, property: Property) => any): EntityValues<M> | undefined;
|
|
27
27
|
export declare function traverseValueProperty(inputValue: any, property: Property, operation: (value: any, property: Property) => any): any;
|
|
28
|
+
export declare function isDataTypeFilterable(dataType: DataType, isPartOfArray?: boolean): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.3.0-canary.
|
|
4
|
+
"version": "3.3.0-canary.1e1cce9",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/firecmsco"
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@dnd-kit/core": "^6.3.1",
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
|
-
"@firecms/formex": "^3.3.0-canary.
|
|
57
|
-
"@firecms/ui": "^3.3.0-canary.
|
|
56
|
+
"@firecms/formex": "^3.3.0-canary.1e1cce9",
|
|
57
|
+
"@firecms/ui": "^3.3.0-canary.1e1cce9",
|
|
58
58
|
"@floating-ui/dom": "^1.7.4",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.10",
|
|
60
60
|
"@radix-ui/react-slot": "^1.2.4",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"history": "^5.3.0",
|
|
66
66
|
"i18next": "^23.16.4",
|
|
67
67
|
"json-logic-js": "^2.0.5",
|
|
68
|
-
"markdown-it": "^14.
|
|
68
|
+
"markdown-it": "^14.2.0",
|
|
69
69
|
"markdown-it-ins": "^4.0.0",
|
|
70
70
|
"markdown-it-mark": "^4.0.0",
|
|
71
71
|
"markdown-it-task-lists": "^2.1.1",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
|
124
124
|
"jest": "^29.7.0",
|
|
125
125
|
"jest-environment-jsdom": "^30.2.0",
|
|
126
|
-
"npm-run-
|
|
126
|
+
"npm-run-all2": "^6.2.6",
|
|
127
127
|
"react-router": "^6.30.2",
|
|
128
128
|
"react-router-dom": "^6.30.2",
|
|
129
129
|
"ts-jest": "^29.4.5",
|
package/src/app/Scaffold.tsx
CHANGED
|
@@ -70,7 +70,13 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
|
|
|
70
70
|
const includeDrawer = drawerChildren.length > 0;
|
|
71
71
|
const largeLayout = useLargeLayout();
|
|
72
72
|
|
|
73
|
-
const [drawerOpen, setDrawerOpen] = React.useState(
|
|
73
|
+
const [drawerOpen, setDrawerOpen] = React.useState(() => {
|
|
74
|
+
try {
|
|
75
|
+
return localStorage.getItem("firecms_drawer_open") === "true";
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
74
80
|
const [onHover, setOnHover] = React.useState(false);
|
|
75
81
|
|
|
76
82
|
const setOnHoverTrue = useCallback(() => setOnHover(true), []);
|
|
@@ -78,10 +84,16 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
|
|
|
78
84
|
|
|
79
85
|
const handleDrawerOpen = useCallback(() => {
|
|
80
86
|
setDrawerOpen(true);
|
|
87
|
+
try {
|
|
88
|
+
localStorage.setItem("firecms_drawer_open", "true");
|
|
89
|
+
} catch { /* ignore */ }
|
|
81
90
|
}, []);
|
|
82
91
|
|
|
83
92
|
const handleDrawerClose = useCallback(() => {
|
|
84
93
|
setDrawerOpen(false);
|
|
94
|
+
try {
|
|
95
|
+
localStorage.setItem("firecms_drawer_open", "false");
|
|
96
|
+
} catch { /* ignore */ }
|
|
85
97
|
}, []);
|
|
86
98
|
|
|
87
99
|
const computedDrawerOpen: boolean = drawerOpen || Boolean(largeLayout && autoOpenDrawer && onHover);
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -178,7 +178,9 @@ export const EntityCollectionView = React.memo(
|
|
|
178
178
|
|
|
179
179
|
const collection = useMemo(() => {
|
|
180
180
|
const userOverride = userConfigPersistence?.getCollectionConfig<M>(fullPath);
|
|
181
|
-
|
|
181
|
+
if (!userOverride) return collectionProp;
|
|
182
|
+
const { properties, ...rest } = userOverride;
|
|
183
|
+
return mergeDeep(collectionProp, rest) as EntityCollection<M>;
|
|
182
184
|
}, [collectionProp, fullPath, userConfigPersistence?.getCollectionConfig]);
|
|
183
185
|
|
|
184
186
|
const openEntityMode = collection?.openEntityMode ?? DEFAULT_ENTITY_OPEN_MODE;
|
|
@@ -507,7 +509,8 @@ export const EntityCollectionView = React.memo(
|
|
|
507
509
|
path: fullPath,
|
|
508
510
|
propertyConfigs: customizationController.propertyConfigs,
|
|
509
511
|
authController,
|
|
510
|
-
|
|
512
|
+
userConfigPersistence,
|
|
513
|
+
}), [collection, fullPath, userConfigPersistence]);
|
|
511
514
|
|
|
512
515
|
// Check if Kanban view is possible (collection has at least one string enum property)
|
|
513
516
|
const hasEnumProperty = useMemo(() => {
|
|
@@ -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
|
);
|
|
@@ -200,43 +200,44 @@ export type EntityPreviewContainerProps = {
|
|
|
200
200
|
onClick?: (e: React.SyntheticEvent) => void;
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
-
export
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
203
|
+
export function EntityPreviewContainer({
|
|
204
|
+
children,
|
|
205
|
+
hover,
|
|
206
|
+
onClick,
|
|
207
|
+
size = "medium",
|
|
208
|
+
style,
|
|
209
|
+
className,
|
|
210
|
+
fullwidth = true,
|
|
211
|
+
ref
|
|
212
|
+
}: EntityPreviewContainerProps & { ref?: React.Ref<HTMLDivElement> }) {
|
|
213
|
+
const divClassName = cls(
|
|
214
|
+
"bg-white dark:bg-surface-900",
|
|
215
|
+
"min-h-[44px]",
|
|
216
|
+
fullwidth ? "w-full" : "",
|
|
217
|
+
"items-center",
|
|
218
|
+
hover ? "hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800" : "",
|
|
219
|
+
size === "small" ? "p-1" : "px-2 py-1",
|
|
220
|
+
"flex border rounded-lg",
|
|
221
|
+
onClick ? "cursor-pointer" : "",
|
|
222
|
+
defaultBorderMixin,
|
|
223
|
+
className
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const handleClick = onClick
|
|
227
|
+
? (event: React.MouseEvent<HTMLDivElement>) => {
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
onClick(event);
|
|
230
|
+
}
|
|
231
|
+
: undefined;
|
|
232
|
+
|
|
233
|
+
const divProps: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> } = {
|
|
234
|
+
ref,
|
|
235
|
+
tabIndex: 0,
|
|
236
|
+
style,
|
|
237
|
+
className: divClassName,
|
|
238
|
+
onClick: handleClick,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return <div {...divProps}>{children}</div>;
|
|
242
|
+
}
|
|
243
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useNavigate } from "react-router-dom";
|
|
2
2
|
|
|
3
|
-
import { useCustomizationController, useFireCMSContext } from "../../hooks";
|
|
3
|
+
import { useCustomizationController, useFireCMSContext, useTranslation } from "../../hooks";
|
|
4
4
|
import { NavigationEntry, PluginHomePageActionsProps } from "../../types";
|
|
5
5
|
import { IconForView } from "../../util";
|
|
6
6
|
import { useUserConfigurationPersistence } from "../../hooks/useUserConfigurationPersistence";
|
|
@@ -38,6 +38,7 @@ export function NavigationCardBinding({
|
|
|
38
38
|
}) {
|
|
39
39
|
|
|
40
40
|
const userConfigurationPersistence = useUserConfigurationPersistence();
|
|
41
|
+
const { t } = useTranslation();
|
|
41
42
|
const collectionIcon = <IconForView collectionOrView={collection ?? view}/>;
|
|
42
43
|
|
|
43
44
|
const navigate = useNavigate();
|
|
@@ -92,15 +93,17 @@ export function NavigationCardBinding({
|
|
|
92
93
|
{actionsArray}
|
|
93
94
|
</>
|
|
94
95
|
|
|
96
|
+
const translatedName = t(name);
|
|
97
|
+
|
|
95
98
|
if (type === "admin") {
|
|
96
99
|
return <SmallNavigationCard icon={collectionIcon}
|
|
97
|
-
name={
|
|
100
|
+
name={translatedName}
|
|
98
101
|
url={url}/>;
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
return <NavigationCard
|
|
102
105
|
icon={collectionIcon}
|
|
103
|
-
name={
|
|
106
|
+
name={translatedName}
|
|
104
107
|
description={description}
|
|
105
108
|
actions={actions}
|
|
106
109
|
onClick={() => {
|
|
@@ -73,7 +73,7 @@ export function ReferenceWidget<M extends Record<string, any>>({
|
|
|
73
73
|
if (disabled)
|
|
74
74
|
return;
|
|
75
75
|
if (onMultipleReferenceSelected) {
|
|
76
|
-
const references = entities ? entities.map(e => getReferenceFrom(e)) : null;
|
|
76
|
+
const references = entities ? entities.filter(Boolean).map(e => getReferenceFrom(e)) : null;
|
|
77
77
|
onMultipleReferenceSelected({
|
|
78
78
|
references,
|
|
79
79
|
entities
|
|
@@ -113,11 +113,11 @@ export function ReferenceFilterField({
|
|
|
113
113
|
}, [path]);
|
|
114
114
|
|
|
115
115
|
const onSingleEntitySelected = (entity: Entity<any>) => {
|
|
116
|
-
updateFilter(operation, getReferenceFrom(entity));
|
|
116
|
+
if (entity) updateFilter(operation, getReferenceFrom(entity));
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
const onMultipleEntitiesSelected = (entities: Entity<any>[]) => {
|
|
120
|
-
updateFilter(operation, entities.map(e => getReferenceFrom(e)));
|
|
120
|
+
updateFilter(operation, entities.filter(Boolean).map(e => getReferenceFrom(e)));
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
const multiple = multipleSelectOperations.includes(operation);
|
|
@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
|
|
|
4
4
|
import { useDataSource, useFireCMSContext, useNavigationController } from "../../hooks";
|
|
5
5
|
import { useDataOrder } from "../../hooks/data/useDataOrder";
|
|
6
6
|
import {
|
|
7
|
+
DataType,
|
|
7
8
|
Entity,
|
|
8
9
|
EntityCollection,
|
|
9
10
|
EntityReference,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
} from "../../types";
|
|
17
18
|
import { useDebouncedData } from "./useDebouncedData";
|
|
18
19
|
import { ScrollRestorationController } from "./useScrollRestoration";
|
|
20
|
+
import { isDataTypeFilterable } from "../../util";
|
|
19
21
|
|
|
20
22
|
const DEFAULT_PAGE_SIZE = 50;
|
|
21
23
|
|
|
@@ -135,8 +137,41 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
135
137
|
filterValues: initialFilterUrl,
|
|
136
138
|
sortBy: initialSortUrl,
|
|
137
139
|
} = parseFilterAndSort(location.search);
|
|
140
|
+
|
|
141
|
+
const availableFilterKeys = collection.allowedFilters ?? Object.keys(collection.properties);
|
|
142
|
+
const forcedFilterKeys = collection.forceFilter ? Object.keys(collection.forceFilter) : [];
|
|
138
143
|
|
|
139
|
-
const
|
|
144
|
+
const allowedFilterKeys = useMemo(() => {
|
|
145
|
+
const availableKeys = availableFilterKeys.filter((key) => {
|
|
146
|
+
const property = collection.properties[key];
|
|
147
|
+
|
|
148
|
+
if (!property) return false;
|
|
149
|
+
|
|
150
|
+
if (typeof property === "function") return false;
|
|
151
|
+
|
|
152
|
+
const dataType = property.dataType;
|
|
153
|
+
const filterable = dataType === "array"
|
|
154
|
+
? isDataTypeFilterable((property as { of?: { dataType: DataType } }).of?.dataType as DataType, true)
|
|
155
|
+
: isDataTypeFilterable(dataType);
|
|
156
|
+
|
|
157
|
+
return filterable;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const forcedKeys = forcedFilterKeys.filter((key) => !availableKeys.includes(key));
|
|
161
|
+
|
|
162
|
+
return [...availableKeys, ...forcedKeys];
|
|
163
|
+
|
|
164
|
+
}, [collection.properties, availableFilterKeys, forcedFilterKeys]);
|
|
165
|
+
|
|
166
|
+
const removeUnallowedFilters = useCallback((filters?: FilterValues<Extract<keyof M, string>>) => {
|
|
167
|
+
if (!filters) return;
|
|
168
|
+
|
|
169
|
+
return Object.fromEntries(Object.entries(filters).filter(([key]) => allowedFilterKeys.includes(key as keyof M))) as FilterValues<Extract<keyof M, string>>;
|
|
170
|
+
}, [allowedFilterKeys]);
|
|
171
|
+
|
|
172
|
+
const initFilters = forceFilter ?? (updateUrl ? initialFilterUrl : undefined) ?? initialFilter ?? undefined
|
|
173
|
+
|
|
174
|
+
const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string>> | undefined>(removeUnallowedFilters(initFilters));
|
|
140
175
|
const [sortBy, setSortBy] = React.useState<[Extract<keyof M, string>, "asc" | "desc"] | undefined>((updateUrl ? initialSortUrl : undefined) ?? initialSortInternal);
|
|
141
176
|
|
|
142
177
|
useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
|
|
@@ -168,7 +203,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
168
203
|
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
169
204
|
const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
|
|
170
205
|
|
|
171
|
-
const clearFilter = useCallback(() => setFilterValues(forceFilter
|
|
206
|
+
const clearFilter = useCallback(() => setFilterValues(removeUnallowedFilters(forceFilter)), [forceFilter, removeUnallowedFilters]);
|
|
172
207
|
|
|
173
208
|
const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string>> | undefined) => {
|
|
174
209
|
if (forceFilter) {
|
|
@@ -178,9 +213,9 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
178
213
|
if (updatedFilter && Object.keys(updatedFilter).length === 0) {
|
|
179
214
|
setFilterValues(undefined);
|
|
180
215
|
} else {
|
|
181
|
-
setFilterValues(updatedFilter);
|
|
216
|
+
setFilterValues(removeUnallowedFilters(updatedFilter));
|
|
182
217
|
}
|
|
183
|
-
}, [forceFilter]);
|
|
218
|
+
}, [forceFilter, removeUnallowedFilters]);
|
|
184
219
|
|
|
185
220
|
useEffect(() => {
|
|
186
221
|
|
|
@@ -268,6 +303,8 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
268
303
|
dataLoadingError,
|
|
269
304
|
filterValues,
|
|
270
305
|
setFilterValues: updateFilterValues,
|
|
306
|
+
allowedFilters: allowedFilterKeys,
|
|
307
|
+
forcedFilters: forcedFilterKeys,
|
|
271
308
|
sortBy,
|
|
272
309
|
setSortBy,
|
|
273
310
|
searchString,
|
|
@@ -58,7 +58,7 @@ export function ArrayOfReferencesFieldBinding({
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
|
|
61
|
-
setValue(entities.map(e => getReferenceFrom(e)));
|
|
61
|
+
setValue(entities.filter(Boolean).map(e => getReferenceFrom(e)));
|
|
62
62
|
}, [setValue]);
|
|
63
63
|
|
|
64
64
|
const referenceDialogController = useReferenceDialog({
|