@firecms/core 3.3.0-canary.d3242eb → 3.4.0-canary.63041d0
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/EntityCollectionView/EntityCollectionView.d.ts +7 -0
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityPreview.d.ts +3 -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/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/useEntityFetch.d.ts +6 -1
- package/dist/index.es.js +2960 -1338
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2958 -1336
- 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 +42 -0
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +6 -0
- package/dist/types/translations.d.ts +17 -3
- 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/urls.d.ts +22 -0
- package/package.json +16 -9
- package/src/app/Scaffold.tsx +47 -45
- 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/EntityCollectionView/EntityCollectionView.tsx +24 -3
- 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/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 +98 -159
- package/src/components/common/useTableSearchHelper.ts +21 -2
- package/src/contexts/SnackbarProvider.tsx +14 -1
- package/src/core/DefaultDrawer.tsx +151 -65
- package/src/core/DrawerNavigationGroup.tsx +27 -29
- package/src/core/DrawerNavigationItem.tsx +10 -12
- package/src/core/EntityEditView.tsx +75 -32
- package/src/core/EntitySidePanel.tsx +2 -0
- package/src/core/field_configs.tsx +15 -0
- package/src/form/EntityForm.tsx +1 -1
- 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/save.ts +2 -0
- package/src/hooks/data/useEntityFetch.tsx +9 -0
- package/src/hooks/useBuildNavigationController.tsx +5 -1
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +2 -0
- package/src/internal/useBuildDataSource.ts +11 -0
- package/src/internal/useBuildSideEntityController.tsx +8 -1
- package/src/locales/de.ts +18 -5
- package/src/locales/en.ts +18 -5
- package/src/locales/es.ts +18 -5
- package/src/locales/fr.ts +18 -5
- package/src/locales/hi.ts +18 -5
- package/src/locales/it.ts +18 -5
- package/src/locales/pl.ts +735 -0
- package/src/locales/pt.ts +18 -5
- 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 +42 -0
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +6 -0
- package/src/types/translations.ts +17 -3
- 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 +13 -0
- package/src/util/geopoint.ts +81 -0
- package/src/util/index.ts +2 -0
- package/src/util/navigation_blocking.ts +45 -0
- package/src/util/navigation_from_path.ts +41 -6
- package/src/util/navigation_utils.ts +40 -2
- package/src/util/parent_references_from_path.ts +4 -2
- package/src/util/previews.ts +8 -1
- package/src/util/resolutions.ts +11 -0
- package/src/util/urls.ts +52 -0
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
|
|
|
@@ -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,
|
|
@@ -178,7 +186,9 @@ export const EntityCollectionView = React.memo(
|
|
|
178
186
|
|
|
179
187
|
const collection = useMemo(() => {
|
|
180
188
|
const userOverride = userConfigPersistence?.getCollectionConfig<M>(fullPath);
|
|
181
|
-
|
|
189
|
+
if (!userOverride) return collectionProp;
|
|
190
|
+
const { properties, ...rest } = userOverride;
|
|
191
|
+
return mergeDeep(collectionProp, rest) as EntityCollection<M>;
|
|
182
192
|
}, [collectionProp, fullPath, userConfigPersistence?.getCollectionConfig]);
|
|
183
193
|
|
|
184
194
|
const openEntityMode = collection?.openEntityMode ?? DEFAULT_ENTITY_OPEN_MODE;
|
|
@@ -315,6 +325,7 @@ export const EntityCollectionView = React.memo(
|
|
|
315
325
|
|
|
316
326
|
const tableController = useDataSourceTableController<M>({
|
|
317
327
|
fullPath,
|
|
328
|
+
pathSegments,
|
|
318
329
|
collection,
|
|
319
330
|
lastDeleteTimestamp,
|
|
320
331
|
scrollRestoration,
|
|
@@ -363,6 +374,7 @@ export const EntityCollectionView = React.memo(
|
|
|
363
374
|
collection,
|
|
364
375
|
entityId: undefined,
|
|
365
376
|
path: fullPath,
|
|
377
|
+
pathSegments,
|
|
366
378
|
fullIdPath,
|
|
367
379
|
sideEntityController,
|
|
368
380
|
navigation,
|
|
@@ -438,6 +450,7 @@ export const EntityCollectionView = React.memo(
|
|
|
438
450
|
const onViewModeChange = useCallback((mode: ViewMode) => {
|
|
439
451
|
analyticsController.onAnalyticsEvent?.("view_mode_changed", {
|
|
440
452
|
path: fullPath,
|
|
453
|
+
pathSegments,
|
|
441
454
|
from: viewMode,
|
|
442
455
|
to: mode
|
|
443
456
|
});
|
|
@@ -507,7 +520,8 @@ export const EntityCollectionView = React.memo(
|
|
|
507
520
|
path: fullPath,
|
|
508
521
|
propertyConfigs: customizationController.propertyConfigs,
|
|
509
522
|
authController,
|
|
510
|
-
|
|
523
|
+
userConfigPersistence,
|
|
524
|
+
}), [collection, fullPath, userConfigPersistence]);
|
|
511
525
|
|
|
512
526
|
// Check if Kanban view is possible (collection has at least one string enum property)
|
|
513
527
|
const hasEnumProperty = useMemo(() => {
|
|
@@ -578,6 +592,7 @@ export const EntityCollectionView = React.memo(
|
|
|
578
592
|
const onKanbanPropertyChange = useCallback((property: string) => {
|
|
579
593
|
analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
|
|
580
594
|
path: fullPath,
|
|
595
|
+
pathSegments,
|
|
581
596
|
property
|
|
582
597
|
});
|
|
583
598
|
setSelectedKanbanProperty(property);
|
|
@@ -642,6 +657,7 @@ export const EntityCollectionView = React.memo(
|
|
|
642
657
|
entityId: entity.id,
|
|
643
658
|
selectedTab: subcollection.id ?? subcollection.path,
|
|
644
659
|
path: fullPath,
|
|
660
|
+
pathSegments,
|
|
645
661
|
fullIdPath,
|
|
646
662
|
navigation,
|
|
647
663
|
sideEntityController
|
|
@@ -828,7 +844,11 @@ export const EntityCollectionView = React.memo(
|
|
|
828
844
|
} = useTableSearchHelper({
|
|
829
845
|
collection,
|
|
830
846
|
fullPath: resolvedFullPath,
|
|
831
|
-
parentCollectionIds
|
|
847
|
+
parentCollectionIds,
|
|
848
|
+
// When the search term comes from the URL instead of the search bar, text search
|
|
849
|
+
// has never been initialised for this collection. Initialise it so the search bar
|
|
850
|
+
// is usable (editable and clearable) right away.
|
|
851
|
+
initialSearchString: tableController.searchString
|
|
832
852
|
});
|
|
833
853
|
|
|
834
854
|
// Popover open state managed at parent level to prevent closing when view changes
|
|
@@ -878,6 +898,7 @@ export const EntityCollectionView = React.memo(
|
|
|
878
898
|
loading={tableController.dataLoading}
|
|
879
899
|
onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
|
|
880
900
|
onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
|
|
901
|
+
initialSearchString={tableController.searchString}
|
|
881
902
|
textSearchLoading={textSearchLoading}
|
|
882
903
|
viewModeToggle={viewModeToggleElement}
|
|
883
904
|
actionsStart={<EntityCollectionViewStartActions
|
|
@@ -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);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TextField
|
|
12
12
|
} from "@firecms/ui";
|
|
13
13
|
import { EnumValueConfig } from "../../../types";
|
|
14
|
+
import { filterValueToText, parseFilterTextInput } from "./filter_text_input";
|
|
14
15
|
|
|
15
16
|
interface StringNumberFilterFieldProps {
|
|
16
17
|
name: string,
|
|
@@ -60,8 +61,15 @@ export function StringNumberFilterField({
|
|
|
60
61
|
const [fieldOperation, fieldValue] = value || [possibleOperations[0], undefined];
|
|
61
62
|
const [operation, setOperation] = useState<VirtualTableWhereFilterOp | "is-null">(fieldOperation === "==" && fieldValue === null ? "is-null" : fieldOperation);
|
|
62
63
|
const [internalValue, setInternalValue] = useState<string | number | string[] | number[] | null | undefined>(fieldValue);
|
|
64
|
+
// Raw text of the text input, kept apart from `internalValue` so that intermediate
|
|
65
|
+
// inputs like "-", "0." or ".0" are not destroyed while the user is typing.
|
|
66
|
+
const [inputText, setInputText] = useState<string>(filterValueToText(fieldValue));
|
|
67
|
+
// Value the current `inputText` was built from, used to tell updates coming from
|
|
68
|
+
// outside apart from the ones triggered by the user typing.
|
|
69
|
+
const syncedValueRef = React.useRef(fieldValue);
|
|
63
70
|
|
|
64
71
|
React.useEffect(() => {
|
|
72
|
+
const newValue = value ? value[1] : undefined;
|
|
65
73
|
if (value) {
|
|
66
74
|
const [op, val] = value;
|
|
67
75
|
setOperation(op === "==" && val === null ? "is-null" : op);
|
|
@@ -70,15 +78,25 @@ export function StringNumberFilterField({
|
|
|
70
78
|
setOperation(possibleOperations[0]);
|
|
71
79
|
setInternalValue(undefined);
|
|
72
80
|
}
|
|
81
|
+
// only reset the text when the value was changed somewhere else, so we don't
|
|
82
|
+
// interfere with what the user is currently typing
|
|
83
|
+
if (syncedValueRef.current !== newValue) {
|
|
84
|
+
syncedValueRef.current = newValue;
|
|
85
|
+
setInputText(filterValueToText(newValue));
|
|
86
|
+
}
|
|
73
87
|
}, [value, possibleOperations[0]]);
|
|
74
88
|
|
|
75
89
|
const isNullOperation = operation === "is-null";
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
// `keepInputText` is used when the update originates from the text input, where the
|
|
92
|
+
// text typed by the user is authoritative and must not be overwritten with `val`
|
|
93
|
+
function updateFilter(op: VirtualTableWhereFilterOp | "is-null", val: string | number | string[] | number[] | null | undefined, keepInputText = false) {
|
|
78
94
|
// Handle "is null" operation
|
|
79
95
|
if (op === "is-null") {
|
|
80
96
|
setOperation(op);
|
|
81
97
|
setInternalValue(null);
|
|
98
|
+
syncedValueRef.current = null;
|
|
99
|
+
setInputText("");
|
|
82
100
|
setValue(["==", null]);
|
|
83
101
|
return;
|
|
84
102
|
}
|
|
@@ -96,6 +114,9 @@ export function StringNumberFilterField({
|
|
|
96
114
|
|
|
97
115
|
setOperation(op);
|
|
98
116
|
setInternalValue(newValue);
|
|
117
|
+
syncedValueRef.current = newValue;
|
|
118
|
+
if (!keepInputText)
|
|
119
|
+
setInputText(filterValueToText(newValue));
|
|
99
120
|
|
|
100
121
|
const hasNewValue = newValue !== null && Array.isArray(newValue)
|
|
101
122
|
? newValue.length > 0
|
|
@@ -111,6 +132,14 @@ export function StringNumberFilterField({
|
|
|
111
132
|
}
|
|
112
133
|
}
|
|
113
134
|
|
|
135
|
+
function updateFilterFromText(text: string) {
|
|
136
|
+
setInputText(text);
|
|
137
|
+
const { updateValue, value: parsedValue } = parseFilterTextInput(text, dataType);
|
|
138
|
+
// intermediate inputs like "-" or "0." keep the filter as it is
|
|
139
|
+
if (updateValue)
|
|
140
|
+
updateFilter(operation, parsedValue, true);
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
const multiple = multipleSelectOperations.includes(operation);
|
|
115
144
|
|
|
116
145
|
return (
|
|
@@ -138,16 +167,13 @@ export function StringNumberFilterField({
|
|
|
138
167
|
{!enumValues && <TextField
|
|
139
168
|
size={"medium"}
|
|
140
169
|
type={dataType === "number" ? "number" : undefined}
|
|
141
|
-
value={
|
|
170
|
+
value={inputText}
|
|
142
171
|
disabled={isNullOperation}
|
|
143
172
|
placeholder={isNullOperation ? "null" : undefined}
|
|
144
173
|
onChange={(evt) => {
|
|
145
|
-
|
|
146
|
-
? parseFloat(evt.target.value)
|
|
147
|
-
: evt.target.value;
|
|
148
|
-
updateFilter(operation, val);
|
|
174
|
+
updateFilterFromText(evt.target.value);
|
|
149
175
|
}}
|
|
150
|
-
endAdornment={
|
|
176
|
+
endAdornment={inputText !== "" && <IconButton
|
|
151
177
|
onClick={(e) => updateFilter(operation, undefined)}>
|
|
152
178
|
<CloseIcon />
|
|
153
179
|
</IconButton>}
|