@firecms/core 3.3.0 → 3.4.0-canary.f6a889a
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/EntityCollectionView/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityPreview.d.ts +3 -1
- package/dist/core/DefaultDrawer.d.ts +10 -3
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/index.es.js +1979 -721
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1978 -720
- 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 +17 -0
- package/dist/util/entities.d.ts +1 -0
- package/dist/util/geopoint.d.ts +22 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/navigation_blocking.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +10 -0
- package/dist/util/navigation_utils.d.ts +20 -0
- package/package.json +16 -9
- package/src/app/Scaffold.tsx +34 -44
- package/src/app/useApp.tsx +1 -0
- 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/EntityCollectionViewStartActions.tsx +4 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityPreview.tsx +41 -40
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
- package/src/components/VirtualTable/VirtualTable.tsx +19 -8
- package/src/components/common/useDataSourceTableController.tsx +66 -10
- 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 +57 -32
- package/src/core/field_configs.tsx +15 -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/i18n/FireCMSi18nProvider.tsx +2 -0
- package/src/internal/useBuildSideEntityController.tsx +2 -1
- package/src/locales/de.ts +2 -2
- package/src/locales/en.ts +2 -2
- package/src/locales/es.ts +2 -2
- package/src/locales/fr.ts +2 -2
- package/src/locales/hi.ts +2 -2
- package/src/locales/it.ts +2 -2
- package/src/locales/pl.ts +730 -0
- package/src/locales/pt.ts +2 -2
- package/src/preview/PropertyPreview.tsx +12 -0
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
- package/src/routes/FireCMSRoute.tsx +44 -25
- package/src/types/collections.ts +18 -0
- package/src/util/entities.ts +13 -0
- package/src/util/geopoint.ts +81 -0
- package/src/util/index.ts +1 -0
- package/src/util/navigation_blocking.ts +45 -0
- package/src/util/navigation_from_path.ts +23 -6
- package/src/util/navigation_utils.ts +36 -2
- package/src/util/parent_references_from_path.ts +4 -2
package/dist/preview/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./property_previews/ArrayOfStringsPreview";
|
|
|
4
4
|
export * from "./property_previews/ArrayPropertyEnumPreview";
|
|
5
5
|
export * from "./property_previews/ArrayOfMapsPreview";
|
|
6
6
|
export * from "./property_previews/NumberPropertyPreview";
|
|
7
|
+
export * from "./property_previews/GeopointPropertyPreview";
|
|
7
8
|
export * from "./property_previews/StringPropertyPreview";
|
|
8
9
|
export * from "./property_previews/ArrayOfStorageComponentsPreview";
|
|
9
10
|
export * from "./property_previews/ArrayOfReferencesPreview";
|
|
@@ -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;
|
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;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { GeoPoint } from "../types";
|
|
2
|
+
export type GeoPointLike = GeoPoint | {
|
|
3
|
+
latitude?: number;
|
|
4
|
+
longitude?: number;
|
|
5
|
+
lat?: number;
|
|
6
|
+
lng?: number;
|
|
7
|
+
_lat?: number;
|
|
8
|
+
_long?: number;
|
|
9
|
+
} | null | undefined;
|
|
10
|
+
export interface GeoPointCoordinates {
|
|
11
|
+
latitude: number;
|
|
12
|
+
longitude: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function getGeoPointCoordinates(value: GeoPointLike): GeoPointCoordinates | undefined;
|
|
15
|
+
export declare function normalizeGeoPoint(value: GeoPointLike): GeoPoint | undefined;
|
|
16
|
+
export declare function formatGeoPoint(value: GeoPointLike, options?: {
|
|
17
|
+
maximumFractionDigits?: number;
|
|
18
|
+
}): string;
|
|
19
|
+
export declare function parseGeoPoint(input: string): {
|
|
20
|
+
point: GeoPoint | null;
|
|
21
|
+
error?: string;
|
|
22
|
+
};
|
package/dist/util/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./useDebouncedCallback";
|
|
|
12
12
|
export * from "./property_utils";
|
|
13
13
|
export * from "./resolutions";
|
|
14
14
|
export * from "./permissions";
|
|
15
|
+
export * from "./geopoint";
|
|
15
16
|
export * from "./icon_list";
|
|
16
17
|
export * from "./icon_synonyms";
|
|
17
18
|
export * from "./icons";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure predicate deciding whether an in-progress navigation away from an open
|
|
3
|
+
* entity form should be blocked (to warn about unsaved changes).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from FireCMSRoute so the branching can be unit-tested without
|
|
6
|
+
* mounting the router. This is an internal helper — it is intentionally not
|
|
7
|
+
* re-exported from the package index.
|
|
8
|
+
*/
|
|
9
|
+
export interface NavigationBlockLocation {
|
|
10
|
+
pathname: string;
|
|
11
|
+
hash: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function shouldBlockEntityNavigation(params: {
|
|
14
|
+
currentLocation: NavigationBlockLocation;
|
|
15
|
+
nextLocation: NavigationBlockLocation;
|
|
16
|
+
/** Path of the currently open entity (basePath + "/" + entityId). */
|
|
17
|
+
entityPath: string;
|
|
18
|
+
/** Collection path the entity form lives under. */
|
|
19
|
+
basePath: string;
|
|
20
|
+
/** Whether the form currently has unsaved modifications. */
|
|
21
|
+
blocked: boolean;
|
|
22
|
+
}): boolean;
|
|
@@ -24,10 +24,20 @@ export interface NavigationViewEntityCustomInternal<M extends Record<string, any
|
|
|
24
24
|
entityId: string;
|
|
25
25
|
view: EntityCustomView<M>;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Note on encoding: `path` arrives from the URL, so any entity id in it is escaped (see
|
|
29
|
+
* `encodeEntityId`). Two chains are threaded through the recursion:
|
|
30
|
+
*
|
|
31
|
+
* - `currentFullPath` carries RAW ids and feeds the `path` / `fullPath` used to address
|
|
32
|
+
* the datasource.
|
|
33
|
+
* - `currentFullUrlPath` carries ESCAPED ids and feeds `fullPath`, which is handed back
|
|
34
|
+
* to `buildUrlCollectionPath` for breadcrumbs and links.
|
|
35
|
+
*/
|
|
27
36
|
export declare function getNavigationEntriesFromPath(props: {
|
|
28
37
|
path: string;
|
|
29
38
|
collections: EntityCollection[] | undefined;
|
|
30
39
|
currentFullPath?: string;
|
|
31
40
|
currentFullIdPath?: string;
|
|
41
|
+
currentFullUrlPath?: string;
|
|
32
42
|
contextEntityViews?: EntityCustomView<any>[];
|
|
33
43
|
}): NavigationViewInternal[];
|
|
@@ -3,6 +3,26 @@ export declare function removeInitialAndTrailingSlashes(s: string): string;
|
|
|
3
3
|
export declare function removeInitialSlash(s: string): string;
|
|
4
4
|
export declare function removeTrailingSlash(s: string): string;
|
|
5
5
|
export declare function addInitialSlash(s: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Characters that would otherwise be read as structure once an entity id is joined into a
|
|
8
|
+
* path: "/" separates segments, "?" and "#" start the query and hash in a URL, and "%"
|
|
9
|
+
* has to be escaped first so that no escape sequence we introduce can be misread as one
|
|
10
|
+
* that was already part of the id.
|
|
11
|
+
*
|
|
12
|
+
* Entity ids are escaped ONLY inside URL-facing strings — anything handed to
|
|
13
|
+
* `buildUrlCollectionPath` or `navigate`. Every other path string (the `path` field of a
|
|
14
|
+
* navigation entry, datasource paths, `EntityReference.path`) carries raw ids.
|
|
15
|
+
*
|
|
16
|
+
* @group Hooks and utilities
|
|
17
|
+
*/
|
|
18
|
+
export declare function encodeEntityId(entityId: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Inverse of {@link encodeEntityId}. "%25" is undone last, mirroring the encoder, so an id
|
|
21
|
+
* that legitimately contains the text "%2F" survives the round trip.
|
|
22
|
+
*
|
|
23
|
+
* @group Hooks and utilities
|
|
24
|
+
*/
|
|
25
|
+
export declare function decodeEntityId(encodedEntityId: string): string;
|
|
6
26
|
export declare function getLastSegment(path: string): string;
|
|
7
27
|
export declare function resolveCollectionPathIds(path: string, allCollections: EntityCollection[]): string;
|
|
8
28
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.4.0-canary.f6a889a",
|
|
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": "
|
|
57
|
-
"@firecms/ui": "
|
|
56
|
+
"@firecms/formex": "3.4.0-canary.f6a889a",
|
|
57
|
+
"@firecms/ui": "3.4.0-canary.f6a889a",
|
|
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",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"date-fns": "^3.6.0",
|
|
64
64
|
"fuse.js": "^7.1.0",
|
|
65
65
|
"history": "^5.3.0",
|
|
66
|
-
"i18next": "^
|
|
66
|
+
"i18next": "^26.3.3",
|
|
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",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"react-transition-group": "^4.4.5",
|
|
97
97
|
"react-use-measure": "^2.1.7",
|
|
98
98
|
"react-window": "^1.8.11",
|
|
99
|
-
"vite-plugin-static-copy": "
|
|
99
|
+
"vite-plugin-static-copy": "4.1.1",
|
|
100
100
|
"yup": "^1.7.1"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"@testing-library/user-event": "^14.6.1",
|
|
112
112
|
"@types/jest": "^29.5.14",
|
|
113
113
|
"@types/json-logic-js": "^2.0.8",
|
|
114
|
-
"@types/node": "^
|
|
114
|
+
"@types/node": "^26.1.1",
|
|
115
115
|
"@types/object-hash": "^3.0.6",
|
|
116
116
|
"@types/react": "^19.2.3",
|
|
117
117
|
"@types/react-dom": "^19.2.3",
|
|
@@ -123,12 +123,12 @@
|
|
|
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",
|
|
130
130
|
"ts-node": "^10.9.2",
|
|
131
|
-
"tsd": "^0.
|
|
131
|
+
"tsd": "^0.33.0",
|
|
132
132
|
"typescript": "^5.9.3",
|
|
133
133
|
"vite": "^7.2.4"
|
|
134
134
|
},
|
|
@@ -163,6 +163,10 @@
|
|
|
163
163
|
"^.+\\.tsx?$": "ts-jest"
|
|
164
164
|
},
|
|
165
165
|
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
|
|
166
|
+
"testPathIgnorePatterns": [
|
|
167
|
+
"/node_modules/",
|
|
168
|
+
"/dist/"
|
|
169
|
+
],
|
|
166
170
|
"moduleFileExtensions": [
|
|
167
171
|
"ts",
|
|
168
172
|
"tsx",
|
|
@@ -172,6 +176,9 @@
|
|
|
172
176
|
"node"
|
|
173
177
|
],
|
|
174
178
|
"testEnvironment": "node",
|
|
179
|
+
"setupFiles": [
|
|
180
|
+
"<rootDir>/test/setup_jsdom.js"
|
|
181
|
+
],
|
|
175
182
|
"moduleNameMapper": {
|
|
176
183
|
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
|
|
177
184
|
}
|
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
|
|
|
@@ -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
|
|
@@ -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
|
);
|