@firecms/core 3.3.0 → 3.4.0-canary.2575a08

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 (155) 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 +8 -0
  7. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  8. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  9. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  10. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  11. package/dist/components/EntityPreview.d.ts +3 -1
  12. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  13. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  14. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  15. package/dist/components/common/table_url_params.d.ts +24 -0
  16. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  17. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  18. package/dist/core/DefaultDrawer.d.ts +10 -3
  19. package/dist/core/EntityEditView.d.ts +9 -1
  20. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  21. package/dist/core/field_configs.d.ts +1 -1
  22. package/dist/form/EntityForm.d.ts +8 -1
  23. package/dist/form/EntityFormActions.d.ts +2 -0
  24. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  25. package/dist/form/index.d.ts +1 -0
  26. package/dist/hooks/data/delete.d.ts +1 -1
  27. package/dist/hooks/data/save.d.ts +1 -1
  28. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  29. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  30. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  31. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  32. package/dist/index.es.js +3658 -1820
  33. package/dist/index.es.js.map +1 -1
  34. package/dist/index.umd.js +3655 -1817
  35. package/dist/index.umd.js.map +1 -1
  36. package/dist/locales/pl.d.ts +2 -0
  37. package/dist/preview/index.d.ts +1 -0
  38. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  39. package/dist/types/collections.d.ts +31 -0
  40. package/dist/types/datasource.d.ts +67 -4
  41. package/dist/types/entities.d.ts +21 -1
  42. package/dist/types/entity_actions.d.ts +6 -0
  43. package/dist/types/entity_callbacks.d.ts +21 -0
  44. package/dist/types/navigation.d.ts +34 -5
  45. package/dist/types/plugins.d.ts +13 -0
  46. package/dist/types/properties.d.ts +17 -1
  47. package/dist/types/side_entity_controller.d.ts +7 -0
  48. package/dist/types/translations.d.ts +6 -0
  49. package/dist/util/__tests__/collections.test.d.ts +1 -0
  50. package/dist/util/__tests__/urls.test.d.ts +1 -0
  51. package/dist/util/collections.d.ts +1 -1
  52. package/dist/util/entities.d.ts +1 -0
  53. package/dist/util/entity_cache.d.ts +11 -0
  54. package/dist/util/geopoint.d.ts +22 -0
  55. package/dist/util/index.d.ts +2 -0
  56. package/dist/util/navigation_blocking.d.ts +22 -0
  57. package/dist/util/navigation_from_path.d.ts +17 -0
  58. package/dist/util/navigation_utils.d.ts +107 -3
  59. package/dist/util/parent_references_from_path.d.ts +16 -0
  60. package/dist/util/paths.d.ts +28 -0
  61. package/dist/util/permissions.d.ts +10 -4
  62. package/dist/util/urls.d.ts +22 -0
  63. package/package.json +17 -10
  64. package/src/app/Scaffold.tsx +34 -44
  65. package/src/app/useApp.tsx +1 -0
  66. package/src/components/DeleteEntityDialog.tsx +2 -0
  67. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  68. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  69. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  70. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  71. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  72. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  73. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  74. package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
  75. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  76. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
  77. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  78. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  79. package/src/components/EntityPreview.tsx +45 -41
  80. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  81. package/src/components/ReferenceWidget.tsx +1 -1
  82. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  83. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  84. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  85. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  86. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  87. package/src/components/common/default_entity_actions.tsx +22 -2
  88. package/src/components/common/table_url_params.ts +172 -0
  89. package/src/components/common/useDataSourceTableController.tsx +126 -163
  90. package/src/components/common/useTableSearchHelper.ts +26 -2
  91. package/src/contexts/SnackbarProvider.tsx +14 -1
  92. package/src/core/DefaultDrawer.tsx +150 -64
  93. package/src/core/DrawerNavigationGroup.tsx +27 -29
  94. package/src/core/DrawerNavigationItem.tsx +10 -12
  95. package/src/core/EntityEditView.tsx +77 -35
  96. package/src/core/EntityEditViewFormActions.tsx +3 -2
  97. package/src/core/EntitySidePanel.tsx +8 -4
  98. package/src/core/field_configs.tsx +15 -0
  99. package/src/form/EntityForm.tsx +31 -10
  100. package/src/form/EntityFormActions.tsx +2 -0
  101. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  102. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  103. package/src/form/index.tsx +1 -0
  104. package/src/hooks/data/delete.ts +8 -0
  105. package/src/hooks/data/save.ts +6 -1
  106. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  107. package/src/hooks/data/useEntityFetch.tsx +35 -4
  108. package/src/hooks/useBuildNavigationController.tsx +23 -8
  109. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  110. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  111. package/src/i18n/FireCMSi18nProvider.tsx +50 -4
  112. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  113. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  114. package/src/internal/useBuildDataSource.ts +37 -15
  115. package/src/internal/useBuildSideEntityController.tsx +20 -5
  116. package/src/locales/de.ts +7 -2
  117. package/src/locales/en.ts +7 -2
  118. package/src/locales/es.ts +7 -2
  119. package/src/locales/fr.ts +7 -2
  120. package/src/locales/hi.ts +7 -2
  121. package/src/locales/it.ts +7 -2
  122. package/src/locales/pl.ts +735 -0
  123. package/src/locales/pt.ts +7 -2
  124. package/src/preview/PropertyPreview.tsx +12 -0
  125. package/src/preview/components/ReferencePreview.tsx +7 -3
  126. package/src/preview/components/StorageThumbnail.tsx +24 -2
  127. package/src/preview/index.ts +1 -0
  128. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  129. package/src/routes/FireCMSRoute.tsx +54 -27
  130. package/src/types/collections.ts +34 -0
  131. package/src/types/datasource.ts +67 -4
  132. package/src/types/entities.ts +24 -1
  133. package/src/types/entity_actions.tsx +6 -0
  134. package/src/types/entity_callbacks.ts +24 -0
  135. package/src/types/navigation.ts +35 -5
  136. package/src/types/plugins.tsx +13 -0
  137. package/src/types/properties.ts +17 -0
  138. package/src/types/side_entity_controller.tsx +7 -0
  139. package/src/types/translations.ts +6 -0
  140. package/src/util/__tests__/collections.test.ts +32 -0
  141. package/src/util/__tests__/urls.test.ts +131 -0
  142. package/src/util/collections.ts +1 -1
  143. package/src/util/entities.ts +16 -1
  144. package/src/util/entity_cache.ts +18 -0
  145. package/src/util/geopoint.ts +81 -0
  146. package/src/util/index.ts +2 -0
  147. package/src/util/navigation_blocking.ts +45 -0
  148. package/src/util/navigation_from_path.ts +47 -7
  149. package/src/util/navigation_utils.ts +244 -4
  150. package/src/util/parent_references_from_path.ts +45 -4
  151. package/src/util/paths.ts +40 -3
  152. package/src/util/permissions.ts +22 -10
  153. package/src/util/previews.ts +8 -1
  154. package/src/util/resolutions.ts +8 -0
  155. package/src/util/urls.ts +52 -0
@@ -3,22 +3,124 @@ export declare function removeInitialAndTrailingSlashes(s: string): string;
3
3
  export declare function removeInitialSlash(s: string): string;
4
4
  export declare function removeTrailingSlash(s: string): string;
5
5
  export declare function addInitialSlash(s: string): string;
6
+ /**
7
+ * Characters that would otherwise be read as structure once an entity id is joined into a
8
+ * path: "/" separates segments, "?" and "#" start the query and hash in a URL, and "%"
9
+ * has to be escaped first so that no escape sequence we introduce can be misread as one
10
+ * that was already part of the id.
11
+ *
12
+ * Entity ids are escaped ONLY inside URL-facing strings — anything handed to
13
+ * `buildUrlCollectionPath` or `navigate`. Every other path string (the `path` field of a
14
+ * navigation entry, datasource paths, `EntityReference.path`) carries raw ids.
15
+ *
16
+ * @group Hooks and utilities
17
+ */
18
+ export declare function encodeEntityId(entityId: string): string;
19
+ /**
20
+ * Inverse of {@link encodeEntityId}. "%25" is undone last, mirroring the encoder, so an id
21
+ * that legitimately contains the text "%2F" survives the round trip.
22
+ *
23
+ * @group Hooks and utilities
24
+ */
25
+ export declare function decodeEntityId(encodedEntityId: string): string;
26
+ /**
27
+ * Segments of a subcollection sitting under the entity `entityId` of `parentPathSegments`.
28
+ *
29
+ * The parent entity id is kept whole however many slashes it contains, while the
30
+ * subcollection's own configured path is spread — a collection path never contains an
31
+ * entity id, so splitting that one is unambiguous.
32
+ *
33
+ * So `["test"] + "test/test" + "accommodation"` yields three segments,
34
+ * `["test", "test/test", "accommodation"]`, where splitting the flattened path
35
+ * `"test/test/test/accommodation"` would wrongly yield four.
36
+ *
37
+ * Returns undefined when the parent segments are unknown: the parent chain cannot be
38
+ * recovered from a flattened path, so there is nothing honest to build on.
39
+ *
40
+ * @group Hooks and utilities
41
+ */
42
+ export declare function buildSubcollectionPathSegments(parentPathSegments: string[] | undefined, entityId: string | undefined, subcollectionPath: string): string[] | undefined;
6
43
  export declare function getLastSegment(path: string): string;
7
- export declare function resolveCollectionPathIds(path: string, allCollections: EntityCollection[]): string;
44
+ /**
45
+ * The outcome of walking a segment chain against a collection tree.
46
+ * @group Hooks and utilities
47
+ */
48
+ export type PathSegmentsWalkStep = {
49
+ collection: EntityCollection;
50
+ /** Resolved path of this collection, e.g. `"products/pid/locales"`. */
51
+ collectionPath: string;
52
+ /** `collectionPath` split at its real segment boundaries. */
53
+ collectionSegments: string[];
54
+ /** The entity addressed inside it, when the chain continues past the collection. */
55
+ entityId?: string;
56
+ };
57
+ export type PathSegmentsWalk = {
58
+ /** Segments with every collection id replaced by the collection's real `path`. */
59
+ resolved: string[];
60
+ /** The collection the chain ends in, if the whole chain matched. */
61
+ collection?: EntityCollection;
62
+ /** One entry per collection level traversed, outermost first. */
63
+ steps: PathSegmentsWalkStep[];
64
+ /** The first segment that could not be matched, if the walk stopped early. */
65
+ unmatched?: string;
66
+ };
67
+ /**
68
+ * Walk a segment chain against a collection tree, matching a collection by either its
69
+ * `path` or its `id` and treating exactly one segment as each entity id.
70
+ *
71
+ * This is the primitive the string-based helpers cannot have: working on the flattened
72
+ * path they must *guess* where an entity id ends by reading up to the next "/", which is
73
+ * wrong for any id that contains one. Here the boundaries are given.
74
+ *
75
+ * Stops at the first segment it cannot match and reports it, rather than throwing.
76
+ *
77
+ * @group Hooks and utilities
78
+ */
79
+ export declare function walkPathSegments(pathSegments: string[], allCollections: EntityCollection[]): PathSegmentsWalk;
80
+ /**
81
+ * Resolve collection aliases in a segment array: every collection id is replaced by the
82
+ * collection's real `path`, and every entity id is carried through untouched.
83
+ *
84
+ * The segment-wise counterpart of {@link resolveCollectionPathIds}, and the reason it
85
+ * exists: that one works on the flattened string, so an entity id containing "/" shifts
86
+ * the rest of the chain by a segment and resolution fails with
87
+ * "Collection definition not found for segment starting with …".
88
+ *
89
+ * Matching accepts either a collection's `path` or its `id`, so already-resolved segments
90
+ * pass through unchanged — the function is idempotent.
91
+ *
92
+ * Mirrors the string version when a chain cannot be matched: it warns and carries the
93
+ * remainder through unresolved, rather than throwing.
94
+ *
95
+ * @group Hooks and utilities
96
+ */
97
+ export declare function resolveCollectionPathSegments(pathSegments: string[], allCollections: EntityCollection[]): string[];
98
+ /**
99
+ * Find the collection a segment chain addresses, whether it ends on the collection itself
100
+ * or on one of its entities.
101
+ *
102
+ * The segment-wise counterpart of {@link getCollectionByPathOrId}, which asserts an odd
103
+ * number of "/"-separated parts — an assertion a slash-bearing entity id fails, throwing
104
+ * where it should simply have resolved.
105
+ *
106
+ * @group Hooks and utilities
107
+ */
108
+ export declare function getCollectionByPathSegments(pathSegments: string[], collections: EntityCollection[]): EntityCollection | undefined;
109
+ export declare function resolveCollectionPathIds(path: string, allCollections: EntityCollection[], pathSegments?: string[]): string;
8
110
  /**
9
111
  * Find the corresponding view at any depth for a given path.
10
112
  * Note that path or segments of the paths can be collection aliases.
11
113
  * @param pathOrId
12
114
  * @param collections
13
115
  */
14
- export declare function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[]): EntityCollection | undefined;
116
+ export declare function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[], pathSegments?: string[]): EntityCollection | undefined;
15
117
  /**
16
118
  * Get the subcollection combinations from a path:
17
119
  * "sites/es/locales" => ["sites/es/locales", "sites"]
18
120
  * @param subpaths
19
121
  */
20
122
  export declare function getCollectionPathsCombinations(subpaths: string[]): string[];
21
- export declare function navigateToEntity({ openEntityMode, collection, entityId, copy, path, fullIdPath, selectedTab, sideEntityController, onClose, navigation }: {
123
+ export declare function navigateToEntity({ openEntityMode, collection, entityId, copy, path, fullIdPath, pathSegments, selectedTab, sideEntityController, onClose, navigation }: {
22
124
  openEntityMode: "side_panel" | "full_screen";
23
125
  collection?: EntityCollection;
24
126
  entityId?: string;
@@ -26,6 +128,8 @@ export declare function navigateToEntity({ openEntityMode, collection, entityId,
26
128
  copy?: boolean;
27
129
  path: string;
28
130
  fullIdPath?: string;
131
+ /** `path` split at its real segment boundaries. */
132
+ pathSegments?: string[];
29
133
  sideEntityController: SideEntityController;
30
134
  onClose?: () => void;
31
135
  navigation: NavigationController;
@@ -1,6 +1,22 @@
1
1
  import { EntityCollection, EntityReference } from "../types";
2
+ /**
3
+ * References to every parent entity in a path.
4
+ *
5
+ * NOTE on encoding: `path` is expected ESCAPED, because it comes from the URL — the entity
6
+ * ids in it are decoded on the way out. Pass `pathSegments` instead whenever the path at
7
+ * hand is a raw datasource path (`entity.path`), which is not escaped and whose ids may
8
+ * therefore contain a bare "/": splitting that would silently produce the wrong references.
9
+ */
2
10
  export declare function getParentReferencesFromPath(props: {
3
11
  path: string;
4
12
  collections: EntityCollection[] | undefined;
5
13
  currentFullPath?: string;
14
+ currentPathSegments?: string[];
15
+ /** `path` split at its real segment boundaries, with RAW ids. Takes precedence. */
16
+ pathSegments?: string[];
6
17
  }): EntityReference[];
18
+ /**
19
+ * The segment-wise counterpart: the boundaries are given, so an entity id keeps its
20
+ * slashes and nothing has to be decoded.
21
+ */
22
+ export declare function getParentReferencesFromPathSegments(pathSegments: string[], collections: EntityCollection[]): EntityReference[];
@@ -9,6 +9,34 @@ export declare function segmentsToStrippedPath(paths: string[]): string;
9
9
  /**
10
10
  * Extract the collection path routes
11
11
  * `products/B44RG6APH/locales` => [`products`, `locales`]
12
+ *
13
+ * Finds the entity ids by position, which only works while no id contains "/" — with one,
14
+ * every following segment shifts and the wrong elements are kept. Use
15
+ * {@link collectionSegmentsFrom} when the real boundaries are known.
16
+ *
12
17
  * @param path
13
18
  */
14
19
  export declare function fullPathToCollectionSegments(path: string): string[];
20
+ /**
21
+ * The collection chain of an already-segmented path:
22
+ * `['products', 'B44RG6APH', 'locales']` => [`products`, `locales`]
23
+ *
24
+ * The counterpart of {@link fullPathToCollectionSegments} for callers that know the real
25
+ * segment boundaries. Kept as a separate function rather than an extra parameter on that
26
+ * one: these helpers are small enough to be passed point-free, and `paths.map(fn)` would
27
+ * then hand the array index in as the new argument.
28
+ */
29
+ export declare function collectionSegmentsFrom(pathSegments: string[]): string[];
30
+ /**
31
+ * The historical, positional reading of a flattened path:
32
+ * `products/B44RG6APH/locales` => [`products`, `locales`]
33
+ *
34
+ * This produces a **collection chain**, not a datasource `pathSegments` value, and must
35
+ * never be used as one — see `path_segments_no_fabrication.test.ts`. It is only correct
36
+ * while no entity id contains "/", which is why every caller prefers real segments when
37
+ * they are available, and why it is named for what it assumes.
38
+ *
39
+ * It is still the right reading for a path that came from a URL, where entity ids arrive
40
+ * escaped and so cannot contain a bare "/".
41
+ */
42
+ export declare function positionalCollectionSegments(path: string): string[];
@@ -1,5 +1,11 @@
1
1
  import { AuthController, Entity, EntityCollection, Permissions, User } from "../types";
2
- export declare function resolvePermissions<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): Permissions | undefined;
3
- export declare function canEditEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
4
- export declare function canCreateEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
5
- export declare function canDeleteEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
2
+ /**
3
+ * `pathSegments` is `path` split at its real segment boundaries. It is forwarded so the
4
+ * collection chain handed to a permissions builder stays correct when an entity id contains
5
+ * "/" splitting `path` positionally would keep the wrong elements. Optional everywhere:
6
+ * omitted, behaviour is exactly as before.
7
+ */
8
+ export declare function resolvePermissions<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null, pathSegments?: string[]): Permissions | undefined;
9
+ export declare function canEditEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null, pathSegments?: string[]): boolean;
10
+ export declare function canCreateEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null, pathSegments?: string[]): boolean;
11
+ export declare function canDeleteEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null, pathSegments?: string[]): boolean;
@@ -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.3.0",
4
+ "version": "3.4.0-canary.2575a08",
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.3.0",
57
- "@firecms/ui": "^3.3.0",
56
+ "@firecms/formex": "3.4.0-canary.2575a08",
57
+ "@firecms/ui": "3.4.0-canary.2575a08",
58
58
  "@floating-ui/dom": "^1.7.4",
59
59
  "@radix-ui/react-portal": "^1.1.10",
60
60
  "@radix-ui/react-slot": "^1.2.4",
@@ -63,9 +63,9 @@
63
63
  "date-fns": "^3.6.0",
64
64
  "fuse.js": "^7.1.0",
65
65
  "history": "^5.3.0",
66
- "i18next": "^23.16.4",
66
+ "i18next": "^26.3.3",
67
67
  "json-logic-js": "^2.0.5",
68
- "markdown-it": "^14.1.0",
68
+ "markdown-it": "^14.2.0",
69
69
  "markdown-it-ins": "^4.0.0",
70
70
  "markdown-it-mark": "^4.0.0",
71
71
  "markdown-it-task-lists": "^2.1.1",
@@ -89,14 +89,14 @@
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",
96
96
  "react-transition-group": "^4.4.5",
97
97
  "react-use-measure": "^2.1.7",
98
98
  "react-window": "^1.8.11",
99
- "vite-plugin-static-copy": "3.1.4",
99
+ "vite-plugin-static-copy": "4.1.1",
100
100
  "yup": "^1.7.1"
101
101
  },
102
102
  "peerDependencies": {
@@ -111,7 +111,7 @@
111
111
  "@testing-library/user-event": "^14.6.1",
112
112
  "@types/jest": "^29.5.14",
113
113
  "@types/json-logic-js": "^2.0.8",
114
- "@types/node": "^20.19.17",
114
+ "@types/node": "^26.1.1",
115
115
  "@types/object-hash": "^3.0.6",
116
116
  "@types/react": "^19.2.3",
117
117
  "@types/react-dom": "^19.2.3",
@@ -123,12 +123,12 @@
123
123
  "eslint-plugin-react-compiler": "^19.1.0-rc.2",
124
124
  "jest": "^29.7.0",
125
125
  "jest-environment-jsdom": "^30.2.0",
126
- "npm-run-all": "^4.1.5",
126
+ "npm-run-all2": "^6.2.6",
127
127
  "react-router": "^6.30.2",
128
128
  "react-router-dom": "^6.30.2",
129
129
  "ts-jest": "^29.4.5",
130
130
  "ts-node": "^10.9.2",
131
- "tsd": "^0.31.2",
131
+ "tsd": "^0.33.0",
132
132
  "typescript": "^5.9.3",
133
133
  "vite": "^7.2.4"
134
134
  },
@@ -163,6 +163,10 @@
163
163
  "^.+\\.tsx?$": "ts-jest"
164
164
  },
165
165
  "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
166
+ "testPathIgnorePatterns": [
167
+ "/node_modules/",
168
+ "/dist/"
169
+ ],
166
170
  "moduleFileExtensions": [
167
171
  "ts",
168
172
  "tsx",
@@ -172,6 +176,9 @@
172
176
  "node"
173
177
  ],
174
178
  "testEnvironment": "node",
179
+ "setupFiles": [
180
+ "<rootDir>/test/setup_jsdom.js"
181
+ ],
175
182
  "moduleNameMapper": {
176
183
  "\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
177
184
  }
@@ -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
  }
@@ -96,6 +96,8 @@ export function DeleteEntityDialog<M extends Record<string, any>>({
96
96
  deleteEntityWithCallbacks({
97
97
  dataSource,
98
98
  entity,
99
+ // The entity is the authority on where it lives; stated rather than left implicit.
100
+ pathSegments: entity.pathSegments,
99
101
  collection: resolvedCollection,
100
102
  callbacks,
101
103
  onDeleteSuccess,
@@ -3,7 +3,7 @@ import React, { MouseEvent, useCallback } from "react";
3
3
  import { CollectionSize, Entity, EntityAction, EntityCollection, SelectionController } from "../../types";
4
4
  import { Badge, Checkbox, cls, IconButton, Menu, MenuItem, MoreVertIcon, Skeleton, Tooltip } from "@firecms/ui";
5
5
  import { useFireCMSContext, useLargeLayout } from "../../hooks";
6
- import { getEntityFromCache } from "../../util/entity_cache";
6
+ import { entityCacheKey, getEntityFromCache } from "../../util/entity_cache";
7
7
  import { getLocalChangesBackup } from "../../util";
8
8
  import { getChanges } from "../../form/EntityForm";
9
9
 
@@ -82,7 +82,7 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
82
82
  const collapsedActions = actions.filter(a => a.collapsed || a.collapsed === undefined);
83
83
  const uncollapsedActions = actions.filter(a => a.collapsed === false);
84
84
  const enableLocalChangesBackup = collection ? getLocalChangesBackup(collection) : false;
85
- const cachedData = enableLocalChangesBackup ? getEntityFromCache(fullPath + "/" + entity.id) : undefined;
85
+ const cachedData = enableLocalChangesBackup ? getEntityFromCache(entityCacheKey(fullPath, entity.id)) : undefined;
86
86
  const hasDraft = (() => {
87
87
  if (!cachedData || typeof cachedData !== "object" || Object.keys(cachedData).length === 0) return false;
88
88
  const realChanges = getChanges(cachedData as any, (entity?.values ?? {}) as any);
@@ -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