@firecms/core 3.3.0 → 3.4.0-canary.00f9be5

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 (220) hide show
  1. package/README.md +4 -0
  2. package/dist/app/useApp.d.ts +1 -0
  3. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  4. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  5. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  8. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  9. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  10. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  11. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  12. package/dist/components/EntityPreview.d.ts +3 -1
  13. package/dist/components/ErrorBoundary.d.ts +1 -3
  14. package/dist/components/LicenseBanner.d.ts +23 -0
  15. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  16. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  17. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  18. package/dist/components/common/table_url_params.d.ts +24 -0
  19. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  20. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  21. package/dist/components/index.d.ts +1 -0
  22. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  23. package/dist/core/DefaultDrawer.d.ts +10 -3
  24. package/dist/core/EntityEditView.d.ts +9 -1
  25. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  26. package/dist/core/field_configs.d.ts +1 -1
  27. package/dist/core/index.d.ts +1 -0
  28. package/dist/core/plugin_translations.d.ts +38 -0
  29. package/dist/core/pro_plugins.d.ts +40 -0
  30. package/dist/de-BF5vcQPY.js +703 -0
  31. package/dist/de-BF5vcQPY.js.map +1 -0
  32. package/dist/editor/extensions/Image.d.ts +11 -0
  33. package/dist/editor/index.d.ts +2 -1
  34. package/dist/editor/lazy_editor.d.ts +6 -0
  35. package/dist/editor-BSo_aS1I.js +4563 -0
  36. package/dist/editor-BSo_aS1I.js.map +1 -0
  37. package/dist/form/EntityForm.d.ts +8 -1
  38. package/dist/form/EntityFormActions.d.ts +2 -0
  39. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  40. package/dist/form/index.d.ts +1 -0
  41. package/dist/fr-DH9_wyFI.js +703 -0
  42. package/dist/fr-DH9_wyFI.js.map +1 -0
  43. package/dist/hi-BM8dm1Lu.js +703 -0
  44. package/dist/hi-BM8dm1Lu.js.map +1 -0
  45. package/dist/hooks/data/delete.d.ts +1 -1
  46. package/dist/hooks/data/save.d.ts +1 -1
  47. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  48. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  49. package/dist/hooks/index.d.ts +2 -0
  50. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  51. package/dist/hooks/useProjectLog.d.ts +11 -6
  52. package/dist/hooks/useTranslation.d.ts +1 -1
  53. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  54. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  55. package/dist/index.es.js +4516 -10668
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/index.umd.js +9403 -6774
  58. package/dist/index.umd.js.map +1 -1
  59. package/dist/it-D3K1gfe4.js +703 -0
  60. package/dist/it-D3K1gfe4.js.map +1 -0
  61. package/dist/locales/pl.d.ts +2 -0
  62. package/dist/pl-CP1zeLzn.js +705 -0
  63. package/dist/pl-CP1zeLzn.js.map +1 -0
  64. package/dist/preview/index.d.ts +1 -0
  65. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  66. package/dist/pt-Cus7eKQR.js +705 -0
  67. package/dist/pt-Cus7eKQR.js.map +1 -0
  68. package/dist/types/analytics.d.ts +1 -1
  69. package/dist/types/collections.d.ts +31 -0
  70. package/dist/types/datasource.d.ts +67 -4
  71. package/dist/types/entities.d.ts +21 -1
  72. package/dist/types/entity_actions.d.ts +6 -0
  73. package/dist/types/entity_callbacks.d.ts +21 -0
  74. package/dist/types/firecms.d.ts +18 -1
  75. package/dist/types/firecms_context.d.ts +8 -0
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/license.d.ts +55 -0
  78. package/dist/types/navigation.d.ts +34 -5
  79. package/dist/types/plugins.d.ts +22 -1
  80. package/dist/types/properties.d.ts +17 -1
  81. package/dist/types/side_entity_controller.d.ts +7 -0
  82. package/dist/types/translations.d.ts +43 -2
  83. package/dist/util/__tests__/collections.test.d.ts +1 -0
  84. package/dist/util/__tests__/urls.test.d.ts +1 -0
  85. package/dist/util/collections.d.ts +1 -1
  86. package/dist/util/date_fns_locales.d.ts +18 -0
  87. package/dist/util/entities.d.ts +1 -0
  88. package/dist/util/entity_cache.d.ts +11 -0
  89. package/dist/util/geopoint.d.ts +22 -0
  90. package/dist/util/index.d.ts +3 -0
  91. package/dist/util/lazy_eager.d.ts +3 -3
  92. package/dist/util/navigation_blocking.d.ts +22 -0
  93. package/dist/util/navigation_from_path.d.ts +17 -0
  94. package/dist/util/navigation_utils.d.ts +107 -3
  95. package/dist/util/parent_references_from_path.d.ts +16 -0
  96. package/dist/util/paths.d.ts +28 -0
  97. package/dist/util/permissions.d.ts +10 -4
  98. package/dist/util/urls.d.ts +22 -0
  99. package/package.json +17 -11
  100. package/src/app/Scaffold.tsx +41 -44
  101. package/src/app/useApp.tsx +1 -0
  102. package/src/components/DeleteEntityDialog.tsx +2 -0
  103. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  104. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  105. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  106. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  107. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  108. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  109. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  110. package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
  111. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  112. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
  113. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  114. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  115. package/src/components/EntityPreview.tsx +45 -41
  116. package/src/components/ErrorBoundary.tsx +12 -18
  117. package/src/components/LicenseBanner.tsx +207 -0
  118. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  119. package/src/components/ReferenceWidget.tsx +1 -1
  120. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  121. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  122. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  123. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  124. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  125. package/src/components/common/default_entity_actions.tsx +22 -2
  126. package/src/components/common/table_url_params.ts +172 -0
  127. package/src/components/common/useDataSourceTableController.tsx +126 -163
  128. package/src/components/common/useTableSearchHelper.ts +26 -2
  129. package/src/components/index.tsx +1 -0
  130. package/src/contexts/LicenseStatusContext.tsx +50 -0
  131. package/src/contexts/SnackbarProvider.tsx +14 -1
  132. package/src/core/DefaultAppBar.tsx +28 -16
  133. package/src/core/DefaultDrawer.tsx +150 -64
  134. package/src/core/DrawerNavigationGroup.tsx +27 -29
  135. package/src/core/DrawerNavigationItem.tsx +10 -12
  136. package/src/core/EntityEditView.tsx +77 -35
  137. package/src/core/EntityEditViewFormActions.tsx +3 -2
  138. package/src/core/EntitySidePanel.tsx +8 -4
  139. package/src/core/FireCMS.tsx +73 -47
  140. package/src/core/SideDialogs.tsx +44 -1
  141. package/src/core/field_configs.tsx +15 -0
  142. package/src/core/index.tsx +1 -0
  143. package/src/core/plugin_translations.ts +87 -0
  144. package/src/core/pro_plugins.ts +79 -0
  145. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  146. package/src/editor/extensions/Image.ts +17 -1
  147. package/src/editor/hooks/useProseMirror.ts +5 -0
  148. package/src/editor/index.ts +11 -1
  149. package/src/editor/lazy_editor.tsx +33 -0
  150. package/src/form/EntityForm.tsx +31 -10
  151. package/src/form/EntityFormActions.tsx +2 -0
  152. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  153. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  154. package/src/form/index.tsx +1 -0
  155. package/src/hooks/data/delete.ts +8 -0
  156. package/src/hooks/data/save.ts +6 -1
  157. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  158. package/src/hooks/data/useEntityFetch.tsx +35 -4
  159. package/src/hooks/index.tsx +2 -0
  160. package/src/hooks/useBuildNavigationController.tsx +70 -15
  161. package/src/hooks/useFireCMSContext.tsx +7 -3
  162. package/src/hooks/useLicenseStatus.tsx +32 -0
  163. package/src/hooks/useProjectLog.tsx +27 -9
  164. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  165. package/src/hooks/useTranslation.ts +3 -1
  166. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  167. package/src/i18n/FireCMSi18nProvider.tsx +199 -23
  168. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  169. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  170. package/src/internal/useBuildDataSource.ts +37 -15
  171. package/src/internal/useBuildSideEntityController.tsx +20 -5
  172. package/src/locales/de.ts +26 -4
  173. package/src/locales/en.ts +30 -6
  174. package/src/locales/es.ts +26 -4
  175. package/src/locales/fr.ts +26 -4
  176. package/src/locales/hi.ts +26 -4
  177. package/src/locales/it.ts +26 -4
  178. package/src/locales/pl.ts +752 -0
  179. package/src/locales/pt.ts +26 -4
  180. package/src/preview/PropertyPreview.tsx +12 -0
  181. package/src/preview/components/DatePreview.tsx +2 -4
  182. package/src/preview/components/ReferencePreview.tsx +7 -3
  183. package/src/preview/components/StorageThumbnail.tsx +24 -2
  184. package/src/preview/index.ts +1 -0
  185. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  186. package/src/routes/FireCMSRoute.tsx +54 -27
  187. package/src/types/analytics.ts +3 -0
  188. package/src/types/collections.ts +34 -0
  189. package/src/types/datasource.ts +67 -4
  190. package/src/types/entities.ts +24 -1
  191. package/src/types/entity_actions.tsx +6 -0
  192. package/src/types/entity_callbacks.ts +24 -0
  193. package/src/types/firecms.tsx +19 -1
  194. package/src/types/firecms_context.tsx +9 -0
  195. package/src/types/index.ts +1 -0
  196. package/src/types/license.ts +63 -0
  197. package/src/types/navigation.ts +35 -5
  198. package/src/types/plugins.tsx +23 -1
  199. package/src/types/properties.ts +17 -0
  200. package/src/types/side_entity_controller.tsx +7 -0
  201. package/src/types/translations.ts +43 -2
  202. package/src/util/__tests__/collections.test.ts +32 -0
  203. package/src/util/__tests__/urls.test.ts +131 -0
  204. package/src/util/collections.ts +1 -1
  205. package/src/util/date_fns_locales.ts +168 -0
  206. package/src/util/entities.ts +16 -1
  207. package/src/util/entity_cache.ts +18 -0
  208. package/src/util/geopoint.ts +81 -0
  209. package/src/util/index.ts +3 -0
  210. package/src/util/lazy_eager.tsx +30 -8
  211. package/src/util/navigation_blocking.ts +45 -0
  212. package/src/util/navigation_from_path.ts +47 -7
  213. package/src/util/navigation_utils.ts +250 -5
  214. package/src/util/parent_references_from_path.ts +45 -4
  215. package/src/util/paths.ts +40 -3
  216. package/src/util/permissions.ts +22 -10
  217. package/src/util/previews.ts +8 -1
  218. package/src/util/resolutions.ts +8 -0
  219. package/src/util/urls.ts +52 -0
  220. package/src/util/useStorageUploadController.tsx +6 -1
@@ -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.00f9be5",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -10,7 +10,7 @@
10
10
  "license": "MIT",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/FireCMSco/firecms.git",
13
+ "url": "git+https://github.com/firecmsco/firecms.git",
14
14
  "directory": "packages/firecms_core"
15
15
  },
16
16
  "main": "./dist/index.umd.js",
@@ -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.00f9be5",
57
+ "@firecms/ui": "3.4.0-canary.00f9be5",
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,13 @@
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",
100
99
  "yup": "^1.7.1"
101
100
  },
102
101
  "peerDependencies": {
@@ -111,7 +110,7 @@
111
110
  "@testing-library/user-event": "^14.6.1",
112
111
  "@types/jest": "^29.5.14",
113
112
  "@types/json-logic-js": "^2.0.8",
114
- "@types/node": "^20.19.17",
113
+ "@types/node": "^26.1.1",
115
114
  "@types/object-hash": "^3.0.6",
116
115
  "@types/react": "^19.2.3",
117
116
  "@types/react-dom": "^19.2.3",
@@ -123,12 +122,12 @@
123
122
  "eslint-plugin-react-compiler": "^19.1.0-rc.2",
124
123
  "jest": "^29.7.0",
125
124
  "jest-environment-jsdom": "^30.2.0",
126
- "npm-run-all": "^4.1.5",
125
+ "npm-run-all2": "^6.2.6",
127
126
  "react-router": "^6.30.2",
128
127
  "react-router-dom": "^6.30.2",
129
128
  "ts-jest": "^29.4.5",
130
129
  "ts-node": "^10.9.2",
131
- "tsd": "^0.31.2",
130
+ "tsd": "^0.33.0",
132
131
  "typescript": "^5.9.3",
133
132
  "vite": "^7.2.4"
134
133
  },
@@ -163,6 +162,10 @@
163
162
  "^.+\\.tsx?$": "ts-jest"
164
163
  },
165
164
  "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
165
+ "testPathIgnorePatterns": [
166
+ "/node_modules/",
167
+ "/dist/"
168
+ ],
166
169
  "moduleFileExtensions": [
167
170
  "ts",
168
171
  "tsx",
@@ -172,6 +175,9 @@
172
175
  "node"
173
176
  ],
174
177
  "testEnvironment": "node",
178
+ "setupFiles": [
179
+ "<rootDir>/test/setup_jsdom.js"
180
+ ],
175
181
  "moduleNameMapper": {
176
182
  "\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
177
183
  }
@@ -1,8 +1,9 @@
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";
6
+ import { LicenseBanner } from "../components/LicenseBanner";
6
7
  import { AppContext } from "./useApp";
7
8
  import { useTranslation } from "../hooks/useTranslation";
8
9
 
@@ -80,7 +81,11 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
80
81
  const [onHover, setOnHover] = React.useState(false);
81
82
 
82
83
  const setOnHoverTrue = useCallback(() => setOnHover(true), []);
83
- const setOnHoverFalse = useCallback(() => setOnHover(false), []);
84
+ const setOnHoverFalse = useCallback(() => {
85
+ // Don't collapse the drawer while a popover/dropdown is open
86
+ if (typeof document !== "undefined" && document.querySelector("[data-radix-popper-content-wrapper]")) return;
87
+ setOnHover(false);
88
+ }, []);
84
89
 
85
90
  const handleDrawerOpen = useCallback(() => {
86
91
  setDrawerOpen(true);
@@ -96,17 +101,21 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
96
101
  } catch { /* ignore */ }
97
102
  }, []);
98
103
 
99
- const computedDrawerOpen: boolean = drawerOpen || Boolean(largeLayout && autoOpenDrawer && onHover);
104
+ // Pinned-open state only; hover is tracked separately so it can *float*
105
+ // over the content instead of pushing it.
106
+ const computedDrawerOpen: boolean = drawerOpen;
107
+ const computedDrawerHovered: boolean = Boolean(largeLayout && onHover);
100
108
 
101
109
  const hasAppBar = Boolean(appBarChildren.length > 0);
102
110
  return (
103
111
  <AppContext.Provider value={{
104
112
  logo,
105
113
  hasDrawer: includeDrawer,
106
- drawerHovered: onHover,
114
+ drawerHovered: computedDrawerHovered,
107
115
  drawerOpen: computedDrawerOpen,
108
116
  closeDrawer: handleDrawerClose,
109
117
  openDrawer: handleDrawerOpen,
118
+ closeHover: setOnHoverFalse,
110
119
  autoOpenDrawer
111
120
  }}>
112
121
  <div
@@ -128,7 +137,7 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
128
137
  onMouseMove={setOnHoverTrue}
129
138
  onMouseLeave={setOnHoverFalse}
130
139
  open={computedDrawerOpen}
131
- hovered={onHover}
140
+ hovered={computedDrawerHovered}
132
141
  setDrawerOpen={setDrawerOpen}>
133
142
  {includeDrawer && drawerChildren}
134
143
  </DrawerWrapper>
@@ -138,6 +147,12 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
138
147
 
139
148
  {hasAppBar && <DrawerHeader />}
140
149
 
150
+ <LicenseBanner
151
+ className={cls("flex-shrink-0", {
152
+ "px-2 pb-1 lg:px-4 lg:pb-2": padding,
153
+ "pt-2 lg:pt-4": !hasAppBar
154
+ })}/>
155
+
141
156
  <div
142
157
  className={cls(defaultBorderMixin, "flex-grow overflow-auto m-0 ", {
143
158
  "lg:mt-4": !hasAppBar,
@@ -177,49 +192,30 @@ function DrawerWrapper(props: {
177
192
  }) {
178
193
 
179
194
  const { t } = useTranslation();
180
- const width = !props.displayed ? 0 : (props.open ? DRAWER_WIDTH : 72);
195
+
196
+ // Layout width is the space the drawer occupies in the flex row; the visual
197
+ // width is how wide the drawer actually renders. When hovered (but not
198
+ // pinned open) the layout width stays collapsed while the visual width
199
+ // expands, so the drawer *floats* over the content instead of pushing it.
200
+ const layoutWidth = !props.displayed ? 0 : (props.open ? DRAWER_WIDTH : 72);
201
+ const visualWidth = !props.displayed ? 0 : ((props.open || props.hovered) ? DRAWER_WIDTH : 72);
202
+ const isFloating = props.hovered && !props.open;
203
+
204
+ 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";
205
+
181
206
  const innerDrawer = <div
182
- className={"relative h-full no-scrollbar overflow-y-auto overflow-x-hidden"}
207
+ className={cls("h-full overflow-hidden", defaultBorderMixin,
208
+ isFloating
209
+ ? "absolute top-0 left-0 bottom-0 z-30 bg-surface-50 dark:bg-surface-900 shadow-lg border-r"
210
+ : "relative bg-surface-50 dark:bg-surface-900")}
183
211
  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"
212
+ width: visualWidth,
213
+ transition
186
214
  }}
187
215
  >
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
216
  <div className={"flex flex-col h-full"}>
220
217
  {props.children}
221
218
  </div>
222
-
223
219
  </div>;
224
220
 
225
221
  const largeLayout = useLargeLayout();
@@ -250,13 +246,14 @@ function DrawerWrapper(props: {
250
246
 
251
247
  return (
252
248
  <div
253
- className="z-20 relative"
249
+ className="z-20 relative flex-shrink-0 overflow-visible"
254
250
  onMouseEnter={props.onMouseEnter}
255
251
  onMouseMove={props.onMouseMove}
256
252
  onMouseLeave={props.onMouseLeave}
257
253
  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"
254
+ width: layoutWidth,
255
+ minWidth: layoutWidth,
256
+ transition
260
257
  }}>
261
258
 
262
259
  {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
 
@@ -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,
@@ -270,6 +274,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
270
274
 
271
275
  currentDataSource.countEntities({
272
276
  path: fullPath,
277
+ pathSegments,
273
278
  collection: currentCollection
274
279
  }).then(count => {
275
280
  totalCount = count;
@@ -281,6 +286,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
281
286
 
282
287
  currentDataSource.countEntities({
283
288
  path: fullPath,
289
+ pathSegments,
284
290
  collection: currentCollection,
285
291
  filter: { [orderProperty]: ["!=", null] } as FilterValues<string>
286
292
  }).then(count => {
@@ -432,6 +438,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
432
438
 
433
439
  const saveProps: SaveEntityProps = {
434
440
  path: entity.path,
441
+ // Paired with `entity.path`: the segments the entity was loaded with.
442
+ pathSegments: entity.pathSegments,
435
443
  entityId: entity.id,
436
444
  values: updatedValues as M,
437
445
  previousValues: entity.values,
@@ -472,6 +480,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
472
480
  console.log("Fetching all documents from collection...");
473
481
  const allDocs = await dataSource.fetchCollection<M>({
474
482
  path: fullPath,
483
+ pathSegments,
475
484
  collection,
476
485
  limit: 10000 // Fetch all
477
486
  });
@@ -492,6 +501,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
492
501
 
493
502
  const saveProps: SaveEntityProps = {
494
503
  path: entity.path,
504
+ // Paired with `entity.path`: the segments the entity was loaded with.
505
+ pathSegments: entity.pathSegments,
495
506
  entityId: entity.id,
496
507
  values: updatedValues as M,
497
508
  previousValues: entity.values,
@@ -663,6 +674,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
663
674
  });
664
675
  sideEntityController.open({
665
676
  path: fullPath,
677
+ pathSegments,
666
678
  collection,
667
679
  entityId: undefined,
668
680
  updateUrl: true,