@firecms/core 3.4.0-canary.8635253 → 3.4.0-canary.d20cc85

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 (87) hide show
  1. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  2. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  4. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +7 -0
  5. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  6. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  7. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  8. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  9. package/dist/components/common/table_url_params.d.ts +24 -0
  10. package/dist/components/common/useDataSourceTableController.d.ts +7 -1
  11. package/dist/components/common/useTableSearchHelper.d.ts +7 -1
  12. package/dist/core/EntityEditView.d.ts +7 -1
  13. package/dist/form/EntityForm.d.ts +6 -1
  14. package/dist/hooks/data/save.d.ts +1 -1
  15. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  16. package/dist/hooks/data/useEntityFetch.d.ts +6 -1
  17. package/dist/index.es.js +949 -622
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/index.umd.js +948 -621
  20. package/dist/index.umd.js.map +1 -1
  21. package/dist/types/collections.d.ts +7 -0
  22. package/dist/types/datasource.d.ts +59 -4
  23. package/dist/types/entities.d.ts +21 -1
  24. package/dist/types/properties.d.ts +17 -1
  25. package/dist/types/side_entity_controller.d.ts +6 -0
  26. package/dist/types/translations.d.ts +6 -0
  27. package/dist/util/__tests__/collections.test.d.ts +1 -0
  28. package/dist/util/__tests__/urls.test.d.ts +1 -0
  29. package/dist/util/collections.d.ts +1 -1
  30. package/dist/util/index.d.ts +1 -0
  31. package/dist/util/navigation_from_path.d.ts +7 -0
  32. package/dist/util/navigation_utils.d.ts +3 -1
  33. package/dist/util/parent_references_from_path.d.ts +1 -0
  34. package/dist/util/urls.d.ts +22 -0
  35. package/package.json +4 -4
  36. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -0
  37. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  38. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  39. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +4 -0
  40. package/src/components/EntityCollectionView/EntityCollectionView.tsx +29 -3
  41. package/src/components/EntityCollectionView/useBoardDataController.tsx +11 -0
  42. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +11 -0
  43. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  44. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  45. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  46. package/src/components/common/table_url_params.ts +172 -0
  47. package/src/components/common/useDataSourceTableController.tsx +64 -175
  48. package/src/components/common/useTableSearchHelper.ts +21 -2
  49. package/src/contexts/SnackbarProvider.tsx +14 -1
  50. package/src/core/EntityEditView.tsx +21 -0
  51. package/src/core/EntitySidePanel.tsx +2 -0
  52. package/src/form/EntityForm.tsx +9 -3
  53. package/src/hooks/data/delete.ts +2 -0
  54. package/src/hooks/data/save.ts +2 -0
  55. package/src/hooks/data/useCollectionFetch.tsx +13 -0
  56. package/src/hooks/data/useEntityFetch.tsx +18 -0
  57. package/src/hooks/useResolvedNavigationFrom.tsx +2 -0
  58. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  59. package/src/internal/useBuildDataSource.ts +25 -4
  60. package/src/internal/useBuildSideEntityController.tsx +6 -0
  61. package/src/locales/de.ts +5 -0
  62. package/src/locales/en.ts +5 -0
  63. package/src/locales/es.ts +5 -0
  64. package/src/locales/fr.ts +5 -0
  65. package/src/locales/hi.ts +5 -0
  66. package/src/locales/it.ts +5 -0
  67. package/src/locales/pl.ts +5 -0
  68. package/src/locales/pt.ts +5 -0
  69. package/src/preview/components/StorageThumbnail.tsx +24 -2
  70. package/src/routes/FireCMSRoute.tsx +3 -0
  71. package/src/types/collections.ts +8 -0
  72. package/src/types/datasource.ts +59 -4
  73. package/src/types/entities.ts +24 -1
  74. package/src/types/properties.ts +17 -0
  75. package/src/types/side_entity_controller.tsx +6 -0
  76. package/src/types/translations.ts +6 -0
  77. package/src/util/__tests__/collections.test.ts +32 -0
  78. package/src/util/__tests__/urls.test.ts +131 -0
  79. package/src/util/collections.ts +1 -1
  80. package/src/util/entities.ts +3 -1
  81. package/src/util/index.ts +1 -0
  82. package/src/util/navigation_from_path.ts +19 -1
  83. package/src/util/navigation_utils.ts +4 -0
  84. package/src/util/parent_references_from_path.ts +10 -2
  85. package/src/util/previews.ts +8 -1
  86. package/src/util/resolutions.ts +8 -0
  87. package/src/util/urls.ts +52 -0
@@ -87,6 +87,13 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
87
87
  * If not specified, the first property simple text property will be used.
88
88
  */
89
89
  titleProperty?: keyof M;
90
+ /**
91
+ * Property used as the image thumbnail in Cards and Kanban view modes.
92
+ * Must reference a storage/image property (or an array of them), or a
93
+ * string property with `url: "image"`.
94
+ * If not specified, the first matching image property will be used.
95
+ */
96
+ imageProperty?: keyof M;
90
97
  /**
91
98
  * When editing an entity, you can choose to open the entity in a side dialog
92
99
  * or in a full screen dialog. Defaults to `full_screen`.
@@ -7,6 +7,20 @@ import { FireCMSContext } from "./firecms_context";
7
7
  */
8
8
  export interface FetchEntityProps<M extends Record<string, any> = any> {
9
9
  path: string;
10
+ /**
11
+ * The path split at its real segment boundaries, e.g.
12
+ * `["nodes", "node/42", "edges"]`.
13
+ *
14
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
15
+ * recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
16
+ * nesting. This is the unambiguous form: one array element per segment, entity ids
17
+ * kept whole however many slashes they contain.
18
+ *
19
+ * For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
20
+ * delegates that do not care can keep using `path`. Delegates that do should prefer
21
+ * `pathSegments ?? path.split("/")`.
22
+ */
23
+ pathSegments?: string[];
10
24
  entityId: string;
11
25
  databaseId?: string;
12
26
  collection?: EntityCollection<M, any>;
@@ -23,6 +37,20 @@ export type ListenEntityProps<M extends Record<string, any> = any> = FetchEntity
23
37
  */
24
38
  export interface FetchCollectionProps<M extends Record<string, any> = any> {
25
39
  path: string;
40
+ /**
41
+ * The path split at its real segment boundaries, e.g.
42
+ * `["nodes", "node/42", "edges"]`.
43
+ *
44
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
45
+ * recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
46
+ * nesting. This is the unambiguous form: one array element per segment, entity ids
47
+ * kept whole however many slashes they contain.
48
+ *
49
+ * For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
50
+ * delegates that do not care can keep using `path`. Delegates that do should prefer
51
+ * `pathSegments ?? path.split("/")`.
52
+ */
53
+ pathSegments?: string[];
26
54
  collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
27
55
  filter?: FilterValues<Extract<keyof M, string>>;
28
56
  limit?: number;
@@ -43,6 +71,20 @@ export type ListenCollectionProps<M extends Record<string, any> = any> = FetchCo
43
71
  */
44
72
  export interface SaveEntityProps<M extends Record<string, any> = any> {
45
73
  path: string;
74
+ /**
75
+ * The path split at its real segment boundaries, e.g.
76
+ * `["nodes", "node/42", "edges"]`.
77
+ *
78
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
79
+ * recovered from it — `"nodes/node/42/edges"` is indistinguishable from a three-level
80
+ * nesting. This is the unambiguous form: one array element per segment, entity ids
81
+ * kept whole however many slashes they contain.
82
+ *
83
+ * For any backend whose ids cannot contain "/" this is exactly `path.split("/")`, so
84
+ * delegates that do not care can keep using `path`. Delegates that do should prefer
85
+ * `pathSegments ?? path.split("/")`.
86
+ */
87
+ pathSegments?: string[];
46
88
  values: Partial<EntityValues<M>>;
47
89
  entityId?: string;
48
90
  previousValues?: Partial<EntityValues<M>>;
@@ -54,6 +96,11 @@ export interface SaveEntityProps<M extends Record<string, any> = any> {
54
96
  */
55
97
  export interface DeleteEntityProps<M extends Record<string, any> = any> {
56
98
  entity: Entity<M>;
99
+ /**
100
+ * `entity.path` split at its real segment boundaries, when the caller knows them.
101
+ * Never derived from `entity.path` — see `pathSegments` on FetchCollectionProps.
102
+ */
103
+ pathSegments?: string[];
57
104
  collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
58
105
  }
59
106
  /**
@@ -133,11 +180,17 @@ export interface DataSource {
133
180
  * @param entityId
134
181
  * @return `true` if there are no other fields besides the given entity
135
182
  */
136
- checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
183
+ checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection,
184
+ /**
185
+ * Appended last, and optional, so existing implementations and callers keep
186
+ * working unchanged — a function declaring fewer parameters stays assignable.
187
+ * See `pathSegments` on FetchCollectionProps.
188
+ */
189
+ pathSegments?: string[]): Promise<boolean>;
137
190
  /**
138
191
  * Generate an id for a new entity
139
192
  */
140
- generateEntityId(path: string, collection: EntityCollection): string;
193
+ generateEntityId(path: string, collection: EntityCollection, pathSegments?: string[]): string;
141
194
  /**
142
195
  * Count the number of entities in a collection
143
196
  */
@@ -160,6 +213,8 @@ export interface DataSource {
160
213
  }) => Promise<boolean>;
161
214
  }
162
215
  export type FilterCombinationValidProps = {
216
+ /** `path` split at its real segment boundaries — see `pathSegments` on FetchCollectionProps. */
217
+ pathSegments?: string[];
163
218
  path: string;
164
219
  collection: EntityCollection<any>;
165
220
  filterValues: FilterValues<any>;
@@ -245,11 +300,11 @@ export interface DataSourceDelegate {
245
300
  * @param collection
246
301
  * @return `true` if there are no other fields besides the given entity
247
302
  */
248
- checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
303
+ checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection, pathSegments?: string[]): Promise<boolean>;
249
304
  /**
250
305
  * Generate an id for a new entity
251
306
  */
252
- generateEntityId(path: string, collection?: EntityCollection): string;
307
+ generateEntityId(path: string, collection?: EntityCollection, pathSegments?: string[]): string;
253
308
  /**
254
309
  * Count the number of entities in a collection
255
310
  */
@@ -17,6 +17,16 @@ export interface Entity<M extends object = any> {
17
17
  * to the root of the database).
18
18
  */
19
19
  path: string;
20
+ /**
21
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
22
+ *
23
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
24
+ * recovered from it. This keeps each segment whole.
25
+ *
26
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
27
+ * the case the field exists for. Absent means "not known here", not "no slashes".
28
+ */
29
+ pathSegments?: string[];
20
30
  /**
21
31
  * Current values
22
32
  */
@@ -46,7 +56,17 @@ export declare class EntityReference {
46
56
  * Optional database ID where the entity is stored (if multiple databases are used)
47
57
  */
48
58
  readonly databaseId?: string;
49
- constructor(id: string, path: string, databaseId?: string);
59
+ /**
60
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
61
+ *
62
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
63
+ * recovered from it. This keeps each segment whole.
64
+ *
65
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
66
+ * the case the field exists for. Absent means "not known here", not "no slashes".
67
+ */
68
+ readonly pathSegments?: string[];
69
+ constructor(id: string, path: string, databaseId?: string, pathSegments?: string[]);
50
70
  get pathWithId(): string;
51
71
  get pathWithIdAndDatabase(): string;
52
72
  isEntityReference(): boolean;
@@ -433,6 +433,22 @@ export type PropertyBuilderProps<M extends Record<string, any> = any> = {
433
433
  * Current value of this property
434
434
  */
435
435
  propertyValue?: any;
436
+ /**
437
+ * Path of this property within the entity, in dot notation, e.g.
438
+ * `my_map.my_field` or `my_array.0.my_field`.
439
+ * Useful to look up sibling values in `values` with `getValueInPath`.
440
+ *
441
+ * When a property builder is used as the `of` prop of an array property,
442
+ * it is resolved twice: once per existing element, with an index
443
+ * qualified key (`my_array.0`), and once as the template for the element
444
+ * shape, with the array's own key (`my_array`). In the latter case
445
+ * `propertyValue` is the whole array, since the template is not bound to
446
+ * any element.
447
+ *
448
+ * It may be undefined if the property is resolved outside the context of
449
+ * an entity.
450
+ */
451
+ propertyKey?: string;
436
452
  /**
437
453
  * Index of this property (only for arrays)
438
454
  */
@@ -456,7 +472,7 @@ export type PropertyBuilderProps<M extends Record<string, any> = any> = {
456
472
  * current value of the property, as well as the path and entity ID.
457
473
  * @group Entity properties
458
474
  */
459
- export type PropertyBuilder<T extends CMSType = any, M extends Record<string, any> = any> = ({ values, previousValues, propertyValue, index, path, entityId, authController }: PropertyBuilderProps<M>) => Property<T> | null;
475
+ export type PropertyBuilder<T extends CMSType = any, M extends Record<string, any> = any> = ({ values, previousValues, propertyValue, propertyKey, index, path, entityId, authController }: PropertyBuilderProps<M>) => Property<T> | null;
460
476
  /**
461
477
  * @group Entity properties
462
478
  */
@@ -15,6 +15,12 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
15
15
  * Full CMS path of the entity, including the collection and sub-collections.
16
16
  */
17
17
  fullIdPath?: string;
18
+ /**
19
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
20
+ * Optional: when omitted the panel falls back to splitting `path`, which is correct for
21
+ * any backend whose entity ids cannot contain "/".
22
+ */
23
+ pathSegments?: string[];
18
24
  /**
19
25
  * ID of the entity, if not set, it means we are creating a new entity
20
26
  */
@@ -368,6 +368,8 @@ export interface FireCMSTranslations {
368
368
  download: string;
369
369
  large_number_of_documents: string;
370
370
  include_undefined_values: string;
371
+ /** Toggle in the export dialog, only shown when a filter or sort is active */
372
+ export_apply_filter_sort: string;
371
373
  submit: string;
372
374
  no_filterable_properties: string;
373
375
  apply_filters: string;
@@ -547,6 +549,10 @@ export interface FireCMSTranslations {
547
549
  order_property_not_found: string;
548
550
  no_number_properties: string;
549
551
  order_property_description: string;
552
+ image_property: string;
553
+ image_property_not_found: string;
554
+ no_image_properties: string;
555
+ image_property_description: string;
550
556
  display_settings: string;
551
557
  default_row_size: string;
552
558
  side_dialog_width: string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -9,4 +9,4 @@ export declare function resolveDefaultSelectedView(defaultSelectedView: string |
9
9
  * @param permissionsBuilder
10
10
  */
11
11
  export declare const applyPermissionsFunctionIfEmpty: (collections: EntityCollection[], permissionsBuilder?: PermissionsBuilder<any, any>) => EntityCollection[];
12
- export declare function getLocalChangesBackup(collection: EntityCollection): "manual_apply" | "auto_apply";
12
+ export declare function getLocalChangesBackup(collection: EntityCollection): false | "manual_apply" | "auto_apply";
@@ -6,6 +6,7 @@ export * from "./enums";
6
6
  export * from "./objects";
7
7
  export * from "./paths";
8
8
  export * from "./regexp";
9
+ export * from "./urls";
9
10
  export * from "./navigation_utils";
10
11
  export * from "./entity_actions";
11
12
  export * from "./useDebouncedCallback";
@@ -6,6 +6,8 @@ export interface NavigationViewEntityInternal<M extends Record<string, any>> {
6
6
  path: string;
7
7
  fullIdPath: string;
8
8
  fullPath: string;
9
+ /** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
10
+ pathSegments: string[];
9
11
  parentCollection: EntityCollection<M>;
10
12
  }
11
13
  export interface NavigationViewCollectionInternal<M extends Record<string, any>> {
@@ -14,6 +16,8 @@ export interface NavigationViewCollectionInternal<M extends Record<string, any>>
14
16
  path: string;
15
17
  fullIdPath: string;
16
18
  fullPath: string;
19
+ /** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
20
+ pathSegments: string[];
17
21
  collection: EntityCollection<M>;
18
22
  }
19
23
  export interface NavigationViewEntityCustomInternal<M extends Record<string, any>> {
@@ -21,6 +25,8 @@ export interface NavigationViewEntityCustomInternal<M extends Record<string, any
21
25
  path: string;
22
26
  fullIdPath: string;
23
27
  fullPath: string;
28
+ /** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
29
+ pathSegments: string[];
24
30
  entityId: string;
25
31
  view: EntityCustomView<M>;
26
32
  }
@@ -39,5 +45,6 @@ export declare function getNavigationEntriesFromPath(props: {
39
45
  currentFullPath?: string;
40
46
  currentFullIdPath?: string;
41
47
  currentFullUrlPath?: string;
48
+ currentPathSegments?: string[];
42
49
  contextEntityViews?: EntityCustomView<any>[];
43
50
  }): NavigationViewInternal[];
@@ -38,7 +38,7 @@ export declare function getCollectionByPathOrId(pathOrId: string, collections: E
38
38
  * @param subpaths
39
39
  */
40
40
  export declare function getCollectionPathsCombinations(subpaths: string[]): string[];
41
- export declare function navigateToEntity({ openEntityMode, collection, entityId, copy, path, fullIdPath, selectedTab, sideEntityController, onClose, navigation }: {
41
+ export declare function navigateToEntity({ openEntityMode, collection, entityId, copy, path, fullIdPath, pathSegments, selectedTab, sideEntityController, onClose, navigation }: {
42
42
  openEntityMode: "side_panel" | "full_screen";
43
43
  collection?: EntityCollection;
44
44
  entityId?: string;
@@ -46,6 +46,8 @@ export declare function navigateToEntity({ openEntityMode, collection, entityId,
46
46
  copy?: boolean;
47
47
  path: string;
48
48
  fullIdPath?: string;
49
+ /** `path` split at its real segment boundaries. */
50
+ pathSegments?: string[];
49
51
  sideEntityController: SideEntityController;
50
52
  onClose?: () => void;
51
53
  navigation: NavigationController;
@@ -3,4 +3,5 @@ export declare function getParentReferencesFromPath(props: {
3
3
  path: string;
4
4
  collections: EntityCollection[] | undefined;
5
5
  currentFullPath?: string;
6
+ currentPathSegments?: string[];
6
7
  }): EntityReference[];
@@ -0,0 +1,22 @@
1
+ import type { PreviewType } from "../types";
2
+ /**
3
+ * Is the given value an absolute `http(s)` URL, as opposed to a relative
4
+ * storage path such as `images/my_image.png`?
5
+ *
6
+ * Values saved in the datasource for storage properties can be either, e.g.
7
+ * when the property is configured with `storeUrl: true`, or when the value was
8
+ * written directly pointing to an externally hosted file.
9
+ */
10
+ export declare function isAbsoluteHttpUrl(value?: string | null): boolean;
11
+ /**
12
+ * Extract the lowercase file extension of a URL or path, ignoring any query
13
+ * string or hash fragment. Returns `undefined` when there is no extension.
14
+ */
15
+ export declare function getFileExtensionFromUrl(urlOrPath?: string | null): string | undefined;
16
+ /**
17
+ * Best effort guess of the preview type of a URL or path, based on its file
18
+ * extension. Used when no storage metadata (and therefore no content type) is
19
+ * available, e.g. for externally hosted files.
20
+ * Returns `undefined` when the extension is missing or not recognised.
21
+ */
22
+ export declare function getPreviewTypeFromUrl(urlOrPath?: string | null): PreviewType | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.4.0-canary.8635253",
4
+ "version": "3.4.0-canary.d20cc85",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -53,8 +53,8 @@
53
53
  "@dnd-kit/core": "^6.3.1",
54
54
  "@dnd-kit/modifiers": "^9.0.0",
55
55
  "@dnd-kit/sortable": "^10.0.0",
56
- "@firecms/formex": "3.4.0-canary.8635253",
57
- "@firecms/ui": "3.4.0-canary.8635253",
56
+ "@firecms/formex": "3.4.0-canary.d20cc85",
57
+ "@firecms/ui": "3.4.0-canary.d20cc85",
58
58
  "@floating-ui/dom": "^1.7.4",
59
59
  "@radix-ui/react-portal": "^1.1.10",
60
60
  "@radix-ui/react-slot": "^1.2.4",
@@ -89,7 +89,7 @@
89
89
  "prosemirror-view": "^1.33.8",
90
90
  "react-dropzone": "^14.3.8",
91
91
  "react-fast-compare": "^3.2.2",
92
- "react-i18next": "^14.1.3",
92
+ "react-i18next": "^17.0.11",
93
93
  "react-image-crop": "^11.0.10",
94
94
  "react-markdown": "^9.1.0",
95
95
  "react-moveable": "^0.56.0",
@@ -354,6 +354,7 @@ export const EntityCollectionTable = function EntityCollectionTable<M extends Re
354
354
  onTextSearch={textSearchEnabled ? onTextSearch : undefined}
355
355
  textSearchLoading={textSearchLoading}
356
356
  onTextSearchClick={textSearchEnabled ? onTextSearchClick : undefined}
357
+ initialSearchString={tableController.searchString}
357
358
  title={title}
358
359
  actionsStart={actionsStart}
359
360
  actions={actions}
@@ -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) {