@firecms/core 3.3.0-canary.d3242eb → 3.4.0-canary.0ef7442

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/app/useApp.d.ts +1 -0
  2. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  3. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  4. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  5. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +7 -0
  7. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  8. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  9. package/dist/components/EntityPreview.d.ts +3 -1
  10. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  11. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  12. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  13. package/dist/components/common/table_url_params.d.ts +24 -0
  14. package/dist/components/common/useDataSourceTableController.d.ts +7 -1
  15. package/dist/components/common/useTableSearchHelper.d.ts +7 -1
  16. package/dist/core/DefaultDrawer.d.ts +10 -3
  17. package/dist/core/EntityEditView.d.ts +7 -1
  18. package/dist/core/field_configs.d.ts +1 -1
  19. package/dist/form/EntityForm.d.ts +6 -1
  20. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  21. package/dist/form/index.d.ts +1 -0
  22. package/dist/hooks/data/save.d.ts +1 -1
  23. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  24. package/dist/hooks/data/useEntityFetch.d.ts +6 -1
  25. package/dist/index.es.js +3085 -1398
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/index.umd.js +3083 -1396
  28. package/dist/index.umd.js.map +1 -1
  29. package/dist/locales/pl.d.ts +2 -0
  30. package/dist/preview/index.d.ts +1 -0
  31. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  32. package/dist/types/collections.d.ts +24 -0
  33. package/dist/types/datasource.d.ts +59 -4
  34. package/dist/types/entities.d.ts +21 -1
  35. package/dist/types/properties.d.ts +17 -1
  36. package/dist/types/side_entity_controller.d.ts +6 -0
  37. package/dist/types/translations.d.ts +17 -3
  38. package/dist/util/__tests__/collections.test.d.ts +1 -0
  39. package/dist/util/__tests__/urls.test.d.ts +1 -0
  40. package/dist/util/collections.d.ts +1 -1
  41. package/dist/util/entities.d.ts +1 -0
  42. package/dist/util/geopoint.d.ts +22 -0
  43. package/dist/util/index.d.ts +2 -0
  44. package/dist/util/navigation_blocking.d.ts +22 -0
  45. package/dist/util/navigation_from_path.d.ts +17 -0
  46. package/dist/util/navigation_utils.d.ts +23 -1
  47. package/dist/util/parent_references_from_path.d.ts +1 -0
  48. package/dist/util/urls.d.ts +22 -0
  49. package/package.json +17 -10
  50. package/src/app/Scaffold.tsx +47 -45
  51. package/src/app/useApp.tsx +1 -0
  52. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  53. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  54. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  55. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  56. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  57. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +4 -0
  58. package/src/components/EntityCollectionView/EntityCollectionView.tsx +34 -5
  59. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
  60. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  61. package/src/components/EntityCollectionView/useBoardDataController.tsx +11 -0
  62. package/src/components/EntityPreview.tsx +41 -40
  63. package/src/components/HomePage/NavigationCardBinding.tsx +6 -3
  64. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +11 -0
  65. package/src/components/ReferenceWidget.tsx +1 -1
  66. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  67. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  68. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  69. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  70. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  71. package/src/components/common/table_url_params.ts +172 -0
  72. package/src/components/common/useDataSourceTableController.tsx +104 -159
  73. package/src/components/common/useTableSearchHelper.ts +21 -2
  74. package/src/contexts/SnackbarProvider.tsx +14 -1
  75. package/src/core/DefaultDrawer.tsx +151 -65
  76. package/src/core/DrawerNavigationGroup.tsx +27 -29
  77. package/src/core/DrawerNavigationItem.tsx +10 -12
  78. package/src/core/EntityEditView.tsx +78 -32
  79. package/src/core/EntitySidePanel.tsx +2 -0
  80. package/src/core/field_configs.tsx +15 -0
  81. package/src/form/EntityForm.tsx +9 -3
  82. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  83. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  84. package/src/form/index.tsx +1 -0
  85. package/src/hooks/data/delete.ts +2 -0
  86. package/src/hooks/data/save.ts +2 -0
  87. package/src/hooks/data/useCollectionFetch.tsx +13 -0
  88. package/src/hooks/data/useEntityFetch.tsx +18 -0
  89. package/src/hooks/useBuildNavigationController.tsx +5 -1
  90. package/src/hooks/useResolvedNavigationFrom.tsx +2 -0
  91. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  92. package/src/i18n/FireCMSi18nProvider.tsx +2 -0
  93. package/src/internal/useBuildDataSource.ts +25 -4
  94. package/src/internal/useBuildSideEntityController.tsx +8 -1
  95. package/src/locales/de.ts +18 -5
  96. package/src/locales/en.ts +18 -5
  97. package/src/locales/es.ts +18 -5
  98. package/src/locales/fr.ts +18 -5
  99. package/src/locales/hi.ts +18 -5
  100. package/src/locales/it.ts +18 -5
  101. package/src/locales/pl.ts +735 -0
  102. package/src/locales/pt.ts +18 -5
  103. package/src/preview/PropertyPreview.tsx +12 -0
  104. package/src/preview/components/StorageThumbnail.tsx +24 -2
  105. package/src/preview/index.ts +1 -0
  106. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  107. package/src/routes/FireCMSRoute.tsx +47 -25
  108. package/src/types/collections.ts +26 -0
  109. package/src/types/datasource.ts +59 -4
  110. package/src/types/entities.ts +24 -1
  111. package/src/types/properties.ts +17 -0
  112. package/src/types/side_entity_controller.tsx +6 -0
  113. package/src/types/translations.ts +17 -3
  114. package/src/util/__tests__/collections.test.ts +32 -0
  115. package/src/util/__tests__/urls.test.ts +131 -0
  116. package/src/util/collections.ts +1 -1
  117. package/src/util/entities.ts +16 -1
  118. package/src/util/geopoint.ts +81 -0
  119. package/src/util/index.ts +2 -0
  120. package/src/util/navigation_blocking.ts +45 -0
  121. package/src/util/navigation_from_path.ts +41 -6
  122. package/src/util/navigation_utils.ts +40 -2
  123. package/src/util/parent_references_from_path.ts +14 -4
  124. package/src/util/previews.ts +8 -1
  125. package/src/util/resolutions.ts +11 -0
  126. package/src/util/urls.ts +52 -0
@@ -1,5 +1,5 @@
1
1
  import React, { PropsWithChildren, useCallback } from "react";
2
- import { ChevronLeftIcon, cls, defaultBorderMixin, IconButton, MenuIcon, Sheet, Tooltip } from "@firecms/ui";
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";
@@ -70,31 +70,51 @@ 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(false);
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), []);
77
- const setOnHoverFalse = useCallback(() => setOnHover(false), []);
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
+ }, []);
78
88
 
79
89
  const handleDrawerOpen = useCallback(() => {
80
90
  setDrawerOpen(true);
91
+ try {
92
+ localStorage.setItem("firecms_drawer_open", "true");
93
+ } catch { /* ignore */ }
81
94
  }, []);
82
95
 
83
96
  const handleDrawerClose = useCallback(() => {
84
97
  setDrawerOpen(false);
98
+ try {
99
+ localStorage.setItem("firecms_drawer_open", "false");
100
+ } catch { /* ignore */ }
85
101
  }, []);
86
102
 
87
- const computedDrawerOpen: boolean = drawerOpen || Boolean(largeLayout && autoOpenDrawer && onHover);
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);
88
107
 
89
108
  const hasAppBar = Boolean(appBarChildren.length > 0);
90
109
  return (
91
110
  <AppContext.Provider value={{
92
111
  logo,
93
112
  hasDrawer: includeDrawer,
94
- drawerHovered: onHover,
113
+ drawerHovered: computedDrawerHovered,
95
114
  drawerOpen: computedDrawerOpen,
96
115
  closeDrawer: handleDrawerClose,
97
116
  openDrawer: handleDrawerOpen,
117
+ closeHover: setOnHoverFalse,
98
118
  autoOpenDrawer
99
119
  }}>
100
120
  <div
@@ -116,7 +136,7 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
116
136
  onMouseMove={setOnHoverTrue}
117
137
  onMouseLeave={setOnHoverFalse}
118
138
  open={computedDrawerOpen}
119
- hovered={onHover}
139
+ hovered={computedDrawerHovered}
120
140
  setDrawerOpen={setDrawerOpen}>
121
141
  {includeDrawer && drawerChildren}
122
142
  </DrawerWrapper>
@@ -165,49 +185,30 @@ function DrawerWrapper(props: {
165
185
  }) {
166
186
 
167
187
  const { t } = useTranslation();
168
- const width = !props.displayed ? 0 : (props.open ? DRAWER_WIDTH : 72);
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
+
169
199
  const innerDrawer = <div
170
- className={"relative h-full no-scrollbar overflow-y-auto overflow-x-hidden"}
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")}
171
204
  style={{
172
- width,
173
- transition: "left 100ms cubic-bezier(0.4, 0, 0.6, 1) 0ms, opacity 100ms cubic-bezier(0.4, 0, 0.6, 1) 0ms, width 100ms cubic-bezier(0.4, 0, 0.6, 1) 0ms"
205
+ width: visualWidth,
206
+ transition
174
207
  }}
175
208
  >
176
-
177
- {!props.open && props.displayed && (
178
- <Tooltip title={t("open_menu")}
179
- side="right"
180
- sideOffset={12}
181
- asChild={true}>
182
- <div
183
- 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">
184
- <IconButton
185
- color="inherit"
186
- aria-label={t("open_menu")}
187
- onClick={() => props.setDrawerOpen(true)}
188
- size="large"
189
- >
190
- <MenuIcon size="small" />
191
- </IconButton>
192
- </div>
193
- </Tooltip>
194
- )}
195
-
196
- <div
197
- className={`z-20 absolute right-0 top-4 ${props.open ? "opacity-100" : "opacity-0 invisible"
198
- } transition-opacity duration-200 ease-in-out`}>
199
- <IconButton
200
- aria-label={t("close_drawer")}
201
- onClick={() => props.setDrawerOpen(false)}
202
- >
203
- <ChevronLeftIcon />
204
- </IconButton>
205
- </div>
206
-
207
209
  <div className={"flex flex-col h-full"}>
208
210
  {props.children}
209
211
  </div>
210
-
211
212
  </div>;
212
213
 
213
214
  const largeLayout = useLargeLayout();
@@ -238,13 +239,14 @@ function DrawerWrapper(props: {
238
239
 
239
240
  return (
240
241
  <div
241
- className="z-20 relative"
242
+ className="z-20 relative flex-shrink-0 overflow-visible"
242
243
  onMouseEnter={props.onMouseEnter}
243
244
  onMouseMove={props.onMouseMove}
244
245
  onMouseLeave={props.onMouseLeave}
245
246
  style={{
246
- width,
247
- transition: "left 100ms cubic-bezier(0.4, 0, 0.6, 1) 0ms, opacity 100ms cubic-bezier(0.4, 0, 0.6, 1) 0ms, width 100ms cubic-bezier(0.4, 0, 0.6, 1) 0ms"
247
+ width: layoutWidth,
248
+ minWidth: layoutWidth,
249
+ transition
248
250
  }}>
249
251
 
250
252
  {innerDrawer}
@@ -10,6 +10,7 @@ export type AppState = {
10
10
  drawerOpen: boolean,
11
11
  openDrawer: () => void,
12
12
  closeDrawer: () => void,
13
+ closeHover?: () => void,
13
14
  autoOpenDrawer?: boolean,
14
15
  logo?: string
15
16
  }
@@ -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
- forceFilter,
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
- forceFilter?: FilterValues<keyof M extends string ? keyof M : never>;
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, forceFilter, AdditionalHeaderWidget }: PropertiesToColumnsParams<M>): VirtualTableColumn[] {
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
- const filterable = filterableProperty(property);
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: !disabledFilter && filterable,
54
+ filter: filterEnabled,
50
55
  width: getTablePropertyColumnWidth(property),
51
56
  resizable: true,
52
57
  custom: {
@@ -59,16 +64,3 @@ export function propertiesToColumns<M extends Record<string, any>>({ properties,
59
64
  } satisfies VirtualTableColumn;
60
65
  });
61
66
  }
62
-
63
- function filterableProperty(property: ResolvedProperty, partOfArray = false): boolean {
64
- if (partOfArray) {
65
- return ["string", "number", "date", "reference"].includes(property.dataType);
66
- }
67
- if (property.dataType === "array") {
68
- if (property.of)
69
- return filterableProperty(property.of, true);
70
- else
71
- return false;
72
- }
73
- return ["string", "number", "boolean", "date", "reference", "array"].includes(property.dataType);
74
- }
@@ -70,7 +70,7 @@ export const TableReferenceFieldInternal = React.memo(
70
70
  }, [updateValue]);
71
71
 
72
72
  const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
73
- updateValue(entities.map((e) => getReferenceFrom(e)));
73
+ updateValue(entities.filter(Boolean).map((e) => getReferenceFrom(e)));
74
74
  }, [updateValue]);
75
75
 
76
76
  const selectedEntityIds = internalValue
@@ -20,6 +20,11 @@ interface CollectionTableToolbarProps {
20
20
  onTextSearchClick?: () => void;
21
21
  onTextSearch?: (searchString?: string) => void;
22
22
  textSearchLoading?: boolean;
23
+ /**
24
+ * Search term the search bar should start with, typically the one restored from the
25
+ * URL query params by the table controller.
26
+ */
27
+ initialSearchString?: string;
23
28
  }
24
29
 
25
30
  export function CollectionTableToolbar({
@@ -29,6 +34,7 @@ export function CollectionTableToolbar({
29
34
  onTextSearch,
30
35
  onTextSearchClick,
31
36
  textSearchLoading,
37
+ initialSearchString,
32
38
  title,
33
39
  viewModeToggle
34
40
  }: CollectionTableToolbarProps) {
@@ -79,6 +85,7 @@ export function CollectionTableToolbar({
79
85
  disabled={Boolean(onTextSearchClick)}
80
86
  onClick={onTextSearchClick}
81
87
  onTextSearch={onTextSearchClick ? undefined : onTextSearch}
88
+ initialValue={initialSearchString}
82
89
  placeholder={t("search")}
83
90
  expandable={true} />}
84
91
 
@@ -26,6 +26,8 @@ import { OnCellValueChangeParams } from "../../../common";
26
26
  interface PopupFormFieldProps<M extends Record<string, any>> {
27
27
  customFieldValidator?: CustomFieldValidator;
28
28
  path: string;
29
+ /** `path` split at its real segment boundaries; forwarded to the datasource. */
30
+ pathSegments?: string[];
29
31
  entityId: string;
30
32
  tableKey: string;
31
33
  propertyKey?: Extract<keyof M, string>;
@@ -54,6 +56,7 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
54
56
  propertyKey,
55
57
  collection: inputCollection,
56
58
  path,
59
+ pathSegments,
57
60
  cellRect,
58
61
  open,
59
62
  onClose,
@@ -66,11 +69,12 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
66
69
  if (entityId && inputCollection) {
67
70
  dataSource.fetchEntity({
68
71
  path,
72
+ pathSegments,
69
73
  entityId,
70
74
  collection: inputCollection
71
75
  }).then(setEntity);
72
76
  }
73
- }, [entityId, inputCollection, dataSource, path]);
77
+ }, [entityId, inputCollection, dataSource, path, pathSegments]);
74
78
 
75
79
  if (!entity) return null;
76
80
  return <PopupFormFieldInternal {...{
@@ -42,6 +42,8 @@ export type EntityCollectionBoardViewProps<M extends Record<string, any> = any>
42
42
  collection: EntityCollection<M>;
43
43
  tableController: EntityTableController<M>;
44
44
  fullPath: string;
45
+ /** `fullPath` split at its real segment boundaries; forwarded to the datasource. */
46
+ pathSegments?: string[];
45
47
  parentCollectionIds?: string[];
46
48
  columnProperty: string;
47
49
  onEntityClick?: (entity: Entity<M>) => void;
@@ -60,6 +62,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
60
62
  collection,
61
63
  tableController,
62
64
  fullPath,
65
+ pathSegments,
63
66
  parentCollectionIds = [],
64
67
  columnProperty,
65
68
  onEntityClick,
@@ -169,6 +172,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
169
172
  // Use the new per-column data controller
170
173
  const boardDataController = useBoardDataController<M>({
171
174
  fullPath,
175
+ pathSegments,
172
176
  collection,
173
177
  columnProperty,
174
178
  columns,
@@ -106,6 +106,13 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
106
106
  * Full path using navigation ids.
107
107
  */
108
108
  fullIdPath?: string;
109
+ /**
110
+ * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
111
+ * Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
112
+ * Defaults to splitting `fullPath`, which is correct for any backend whose ids cannot
113
+ * contain "/".
114
+ */
115
+ pathSegments?: string[];
109
116
  /**
110
117
  * If this is a subcollection, specify the parent collection ids.
111
118
  */
@@ -152,6 +159,7 @@ export const EntityCollectionView = React.memo(
152
159
  function EntityCollectionView<M extends Record<string, any>>({
153
160
  fullPath: fullPathProp,
154
161
  fullIdPath,
162
+ pathSegments,
155
163
  parentCollectionIds,
156
164
  isSubCollection,
157
165
  className,
@@ -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
- return (userOverride ? mergeDeep(collectionProp, userOverride) : collectionProp) as EntityCollection<M>;
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
- }), [collection, fullPath]);
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
@@ -773,6 +789,7 @@ export const EntityCollectionView = React.memo(
773
789
  // EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
774
790
  const countFetcher = <EntitiesCount
775
791
  fullPath={fullPath}
792
+ pathSegments={pathSegments}
776
793
  collection={collection}
777
794
  filter={tableController.filterValues}
778
795
  sortBy={tableController.sortBy}
@@ -828,7 +845,11 @@ export const EntityCollectionView = React.memo(
828
845
  } = useTableSearchHelper({
829
846
  collection,
830
847
  fullPath: resolvedFullPath,
831
- parentCollectionIds
848
+ parentCollectionIds,
849
+ // When the search term comes from the URL instead of the search bar, text search
850
+ // has never been initialised for this collection. Initialise it so the search bar
851
+ // is usable (editable and clearable) right away.
852
+ initialSearchString: tableController.searchString
832
853
  });
833
854
 
834
855
  // Popover open state managed at parent level to prevent closing when view changes
@@ -878,6 +899,7 @@ export const EntityCollectionView = React.memo(
878
899
  loading={tableController.dataLoading}
879
900
  onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
880
901
  onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
902
+ initialSearchString={tableController.searchString}
881
903
  textSearchLoading={textSearchLoading}
882
904
  viewModeToggle={viewModeToggleElement}
883
905
  actionsStart={<EntityCollectionViewStartActions
@@ -912,6 +934,7 @@ export const EntityCollectionView = React.memo(
912
934
  collection={collection}
913
935
  tableController={tableController}
914
936
  fullPath={fullPath}
937
+ pathSegments={pathSegments}
915
938
  parentCollectionIds={parentCollectionIds}
916
939
  columnProperty={selectedKanbanProperty}
917
940
  onEntityClick={onEntityClick}
@@ -1042,6 +1065,7 @@ export const EntityCollectionView = React.memo(
1042
1065
 
1043
1066
  {popupCell && <PopupFormField
1044
1067
  key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
1068
+ pathSegments={pathSegments}
1045
1069
  open={Boolean(popupCell)}
1046
1070
  onClose={onPopupClose}
1047
1071
  cellRect={popupCell?.cellRect}
@@ -1097,13 +1121,16 @@ function EntitiesCount({
1097
1121
  collection,
1098
1122
  filter,
1099
1123
  sortBy,
1100
- onCountChange
1124
+ onCountChange,
1125
+ pathSegments
1101
1126
  }: {
1102
1127
  fullPath: string,
1103
1128
  collection: EntityCollection,
1104
1129
  filter?: FilterValues<any>,
1105
1130
  sortBy?: [string, "asc" | "desc"],
1106
1131
  onCountChange?: (count: number) => void,
1132
+ /** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
1133
+ pathSegments?: string[],
1107
1134
  }) {
1108
1135
 
1109
1136
  const dataSource = useDataSource(collection);
@@ -1114,17 +1141,19 @@ function EntitiesCount({
1114
1141
  const sortByProperty = sortBy ? sortBy[0] : undefined;
1115
1142
  const currentSort = sortBy ? sortBy[1] : undefined;
1116
1143
  const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
1144
+ const resolvedPathSegments = pathSegments;
1117
1145
 
1118
1146
  useEffect(() => {
1119
1147
  if (dataSource.countEntities)
1120
1148
  dataSource.countEntities({
1121
1149
  path: resolvedPath,
1150
+ pathSegments: resolvedPathSegments,
1122
1151
  collection,
1123
1152
  filter,
1124
1153
  orderBy: sortByProperty,
1125
1154
  order: currentSort
1126
1155
  }).then(setCount).catch(setError);
1127
- }, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
1156
+ }, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
1128
1157
 
1129
1158
  useEffect(() => {
1130
1159
  if (onCountChange && count !== undefined) {
@@ -66,8 +66,10 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
66
66
  collectionEntitiesCount
67
67
  };
68
68
 
69
+ const hasAnyAllowedFilters = !tableController.allowedFilters || tableController.allowedFilters.length > 0;
70
+
69
71
  // Filters button
70
- const filtersButton = resolvedProperties && tableController.setFilterValues && (
72
+ const filtersButton = resolvedProperties && tableController.setFilterValues && hasAnyAllowedFilters && (
71
73
  <Tooltip title={t("filters")}
72
74
  key={"filters_tooltip"}>
73
75
  <Badge
@@ -131,6 +133,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
131
133
  filterValues={tableController.filterValues}
132
134
  setFilterValues={(filterValues) => tableController.setFilterValues?.(filterValues ?? {})}
133
135
  forceFilter={collection.forceFilter}
136
+ allowedFilters={tableController.allowedFilters?.map(key => key.toString())}
134
137
  />
135
138
  )}
136
139
  </>
@@ -12,7 +12,6 @@ import {
12
12
  DialogActions,
13
13
  DialogContent,
14
14
  DialogTitle,
15
- FilterListIcon,
16
15
  Typography
17
16
  } from "@firecms/ui";
18
17
  import { useTranslation } from "../../hooks/useTranslation";
@@ -30,6 +29,7 @@ export interface FiltersDialogProps {
30
29
  filterValues: FilterValues<any> | undefined;
31
30
  setFilterValues: (filterValues?: FilterValues<any>) => void;
32
31
  forceFilter?: FilterValues<any>;
32
+ allowedFilters?: string[];
33
33
  }
34
34
 
35
35
  /**
@@ -42,7 +42,8 @@ export function FiltersDialog({
42
42
  properties,
43
43
  filterValues,
44
44
  setFilterValues,
45
- forceFilter
45
+ forceFilter,
46
+ allowedFilters
46
47
  }: FiltersDialogProps) {
47
48
  const { t } = useTranslation();
48
49
 
@@ -59,18 +60,18 @@ export function FiltersDialog({
59
60
  }
60
61
  }, [open, filterValues]);
61
62
 
62
- // Get list of filterable properties
63
- const filterableProperties = useMemo(() => {
64
- return Object.entries(properties).filter(([key, property]) => {
65
- if (!property) return false;
66
- // Force filter properties should not be editable
67
- if (forceFilter && key in forceFilter) return false;
68
- // Check if property type is filterable
69
- const baseProperty = property.dataType === "array" ? property.of : property;
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
- const activeFilterCount = Object.keys(localFilters).length;
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
- {filterableProperties.length === 0 ? (
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
- {filterableProperties.map(([propertyKey, property], index) => {
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
- <Button
238
- variant="text"
239
- onClick={() => onOpenChange(false)}
240
- >
241
- {t("cancel")}
242
- </Button>
243
- <Button
244
- variant="filled"
245
- onClick={handleApply}
246
- >
247
- {t("apply_filters")}
248
- </Button>
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
  );