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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) 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 +2915 -1330
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/index.umd.js +2913 -1328
  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 +6 -0
  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 +34 -44
  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 +29 -3
  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/ReferenceTable/ReferenceSelectionTable.tsx +11 -0
  64. package/src/components/ReferenceWidget.tsx +1 -1
  65. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  66. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  67. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  68. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  69. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  70. package/src/components/common/table_url_params.ts +172 -0
  71. package/src/components/common/useDataSourceTableController.tsx +104 -159
  72. package/src/components/common/useTableSearchHelper.ts +21 -2
  73. package/src/contexts/SnackbarProvider.tsx +14 -1
  74. package/src/core/DefaultDrawer.tsx +150 -64
  75. package/src/core/DrawerNavigationGroup.tsx +27 -29
  76. package/src/core/DrawerNavigationItem.tsx +10 -12
  77. package/src/core/EntityEditView.tsx +78 -32
  78. package/src/core/EntitySidePanel.tsx +2 -0
  79. package/src/core/field_configs.tsx +15 -0
  80. package/src/form/EntityForm.tsx +9 -3
  81. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  82. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  83. package/src/form/index.tsx +1 -0
  84. package/src/hooks/data/delete.ts +2 -0
  85. package/src/hooks/data/save.ts +2 -0
  86. package/src/hooks/data/useCollectionFetch.tsx +13 -0
  87. package/src/hooks/data/useEntityFetch.tsx +18 -0
  88. package/src/hooks/useResolvedNavigationFrom.tsx +2 -0
  89. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  90. package/src/i18n/FireCMSi18nProvider.tsx +2 -0
  91. package/src/internal/useBuildDataSource.ts +25 -4
  92. package/src/internal/useBuildSideEntityController.tsx +8 -1
  93. package/src/locales/de.ts +7 -2
  94. package/src/locales/en.ts +7 -2
  95. package/src/locales/es.ts +7 -2
  96. package/src/locales/fr.ts +7 -2
  97. package/src/locales/hi.ts +7 -2
  98. package/src/locales/it.ts +7 -2
  99. package/src/locales/pl.ts +735 -0
  100. package/src/locales/pt.ts +7 -2
  101. package/src/preview/PropertyPreview.tsx +12 -0
  102. package/src/preview/components/StorageThumbnail.tsx +24 -2
  103. package/src/preview/index.ts +1 -0
  104. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  105. package/src/routes/FireCMSRoute.tsx +47 -25
  106. package/src/types/collections.ts +26 -0
  107. package/src/types/datasource.ts +59 -4
  108. package/src/types/entities.ts +24 -1
  109. package/src/types/properties.ts +17 -0
  110. package/src/types/side_entity_controller.tsx +6 -0
  111. package/src/types/translations.ts +6 -0
  112. package/src/util/__tests__/collections.test.ts +32 -0
  113. package/src/util/__tests__/urls.test.ts +131 -0
  114. package/src/util/collections.ts +1 -1
  115. package/src/util/entities.ts +16 -1
  116. package/src/util/geopoint.ts +81 -0
  117. package/src/util/index.ts +2 -0
  118. package/src/util/navigation_blocking.ts +45 -0
  119. package/src/util/navigation_from_path.ts +41 -6
  120. package/src/util/navigation_utils.ts +40 -2
  121. package/src/util/parent_references_from_path.ts +14 -4
  122. package/src/util/previews.ts +8 -1
  123. package/src/util/resolutions.ts +8 -0
  124. 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";
@@ -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(() => 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
+ }, []);
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
- 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);
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: onHover,
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={onHover}
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
- 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
+
181
199
  const innerDrawer = <div
182
- 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")}
183
204
  style={{
184
- width,
185
- 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
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
- 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
260
250
  }}>
261
251
 
262
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,
@@ -317,6 +325,7 @@ export const EntityCollectionView = React.memo(
317
325
 
318
326
  const tableController = useDataSourceTableController<M>({
319
327
  fullPath,
328
+ pathSegments,
320
329
  collection,
321
330
  lastDeleteTimestamp,
322
331
  scrollRestoration,
@@ -365,6 +374,7 @@ export const EntityCollectionView = React.memo(
365
374
  collection,
366
375
  entityId: undefined,
367
376
  path: fullPath,
377
+ pathSegments,
368
378
  fullIdPath,
369
379
  sideEntityController,
370
380
  navigation,
@@ -440,6 +450,7 @@ export const EntityCollectionView = React.memo(
440
450
  const onViewModeChange = useCallback((mode: ViewMode) => {
441
451
  analyticsController.onAnalyticsEvent?.("view_mode_changed", {
442
452
  path: fullPath,
453
+ pathSegments,
443
454
  from: viewMode,
444
455
  to: mode
445
456
  });
@@ -581,6 +592,7 @@ export const EntityCollectionView = React.memo(
581
592
  const onKanbanPropertyChange = useCallback((property: string) => {
582
593
  analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
583
594
  path: fullPath,
595
+ pathSegments,
584
596
  property
585
597
  });
586
598
  setSelectedKanbanProperty(property);
@@ -645,6 +657,7 @@ export const EntityCollectionView = React.memo(
645
657
  entityId: entity.id,
646
658
  selectedTab: subcollection.id ?? subcollection.path,
647
659
  path: fullPath,
660
+ pathSegments,
648
661
  fullIdPath,
649
662
  navigation,
650
663
  sideEntityController
@@ -776,6 +789,7 @@ export const EntityCollectionView = React.memo(
776
789
  // EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
777
790
  const countFetcher = <EntitiesCount
778
791
  fullPath={fullPath}
792
+ pathSegments={pathSegments}
779
793
  collection={collection}
780
794
  filter={tableController.filterValues}
781
795
  sortBy={tableController.sortBy}
@@ -831,7 +845,11 @@ export const EntityCollectionView = React.memo(
831
845
  } = useTableSearchHelper({
832
846
  collection,
833
847
  fullPath: resolvedFullPath,
834
- parentCollectionIds
848
+ parentCollectionIds,
849
+ // When the search term comes from the URL instead of the search bar, text search
850
+ // has never been initialised for this collection. Initialise it so the search bar
851
+ // is usable (editable and clearable) right away.
852
+ initialSearchString: tableController.searchString
835
853
  });
836
854
 
837
855
  // Popover open state managed at parent level to prevent closing when view changes
@@ -881,6 +899,7 @@ export const EntityCollectionView = React.memo(
881
899
  loading={tableController.dataLoading}
882
900
  onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
883
901
  onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
902
+ initialSearchString={tableController.searchString}
884
903
  textSearchLoading={textSearchLoading}
885
904
  viewModeToggle={viewModeToggleElement}
886
905
  actionsStart={<EntityCollectionViewStartActions
@@ -915,6 +934,7 @@ export const EntityCollectionView = React.memo(
915
934
  collection={collection}
916
935
  tableController={tableController}
917
936
  fullPath={fullPath}
937
+ pathSegments={pathSegments}
918
938
  parentCollectionIds={parentCollectionIds}
919
939
  columnProperty={selectedKanbanProperty}
920
940
  onEntityClick={onEntityClick}
@@ -1045,6 +1065,7 @@ export const EntityCollectionView = React.memo(
1045
1065
 
1046
1066
  {popupCell && <PopupFormField
1047
1067
  key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
1068
+ pathSegments={pathSegments}
1048
1069
  open={Boolean(popupCell)}
1049
1070
  onClose={onPopupClose}
1050
1071
  cellRect={popupCell?.cellRect}
@@ -1100,13 +1121,16 @@ function EntitiesCount({
1100
1121
  collection,
1101
1122
  filter,
1102
1123
  sortBy,
1103
- onCountChange
1124
+ onCountChange,
1125
+ pathSegments
1104
1126
  }: {
1105
1127
  fullPath: string,
1106
1128
  collection: EntityCollection,
1107
1129
  filter?: FilterValues<any>,
1108
1130
  sortBy?: [string, "asc" | "desc"],
1109
1131
  onCountChange?: (count: number) => void,
1132
+ /** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
1133
+ pathSegments?: string[],
1110
1134
  }) {
1111
1135
 
1112
1136
  const dataSource = useDataSource(collection);
@@ -1117,17 +1141,19 @@ function EntitiesCount({
1117
1141
  const sortByProperty = sortBy ? sortBy[0] : undefined;
1118
1142
  const currentSort = sortBy ? sortBy[1] : undefined;
1119
1143
  const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
1144
+ const resolvedPathSegments = pathSegments;
1120
1145
 
1121
1146
  useEffect(() => {
1122
1147
  if (dataSource.countEntities)
1123
1148
  dataSource.countEntities({
1124
1149
  path: resolvedPath,
1150
+ pathSegments: resolvedPathSegments,
1125
1151
  collection,
1126
1152
  filter,
1127
1153
  orderBy: sortByProperty,
1128
1154
  order: currentSort
1129
1155
  }).then(setCount).catch(setError);
1130
- }, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
1156
+ }, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
1131
1157
 
1132
1158
  useEffect(() => {
1133
1159
  if (onCountChange && count !== undefined) {
@@ -66,8 +66,10 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
66
66
  collectionEntitiesCount
67
67
  };
68
68
 
69
+ const hasAnyAllowedFilters = !tableController.allowedFilters || tableController.allowedFilters.length > 0;
70
+
69
71
  // Filters button
70
- const filtersButton = resolvedProperties && tableController.setFilterValues && (
72
+ const filtersButton = resolvedProperties && tableController.setFilterValues && hasAnyAllowedFilters && (
71
73
  <Tooltip title={t("filters")}
72
74
  key={"filters_tooltip"}>
73
75
  <Badge
@@ -131,6 +133,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
131
133
  filterValues={tableController.filterValues}
132
134
  setFilterValues={(filterValues) => tableController.setFilterValues?.(filterValues ?? {})}
133
135
  forceFilter={collection.forceFilter}
136
+ allowedFilters={tableController.allowedFilters?.map(key => key.toString())}
134
137
  />
135
138
  )}
136
139
  </>
@@ -12,7 +12,6 @@ import {
12
12
  DialogActions,
13
13
  DialogContent,
14
14
  DialogTitle,
15
- FilterListIcon,
16
15
  Typography
17
16
  } from "@firecms/ui";
18
17
  import { useTranslation } from "../../hooks/useTranslation";
@@ -30,6 +29,7 @@ export interface FiltersDialogProps {
30
29
  filterValues: FilterValues<any> | undefined;
31
30
  setFilterValues: (filterValues?: FilterValues<any>) => void;
32
31
  forceFilter?: FilterValues<any>;
32
+ allowedFilters?: string[];
33
33
  }
34
34
 
35
35
  /**
@@ -42,7 +42,8 @@ export function FiltersDialog({
42
42
  properties,
43
43
  filterValues,
44
44
  setFilterValues,
45
- forceFilter
45
+ forceFilter,
46
+ allowedFilters
46
47
  }: FiltersDialogProps) {
47
48
  const { t } = useTranslation();
48
49
 
@@ -59,18 +60,18 @@ export function FiltersDialog({
59
60
  }
60
61
  }, [open, filterValues]);
61
62
 
62
- // Get list of 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
  );