@firecms/core 3.3.0 → 3.4.0-canary.63041d0

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 (108) 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/EntityCollectionView/EntityCollectionView.d.ts +7 -0
  5. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  6. package/dist/components/EntityPreview.d.ts +3 -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/DefaultDrawer.d.ts +10 -3
  13. package/dist/core/EntityEditView.d.ts +7 -1
  14. package/dist/core/field_configs.d.ts +1 -1
  15. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  16. package/dist/form/index.d.ts +1 -0
  17. package/dist/hooks/data/save.d.ts +1 -1
  18. package/dist/hooks/data/useEntityFetch.d.ts +6 -1
  19. package/dist/index.es.js +2790 -1270
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/index.umd.js +2788 -1268
  22. package/dist/index.umd.js.map +1 -1
  23. package/dist/locales/pl.d.ts +2 -0
  24. package/dist/preview/index.d.ts +1 -0
  25. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  26. package/dist/types/collections.d.ts +24 -0
  27. package/dist/types/datasource.d.ts +42 -0
  28. package/dist/types/properties.d.ts +17 -1
  29. package/dist/types/side_entity_controller.d.ts +6 -0
  30. package/dist/types/translations.d.ts +6 -0
  31. package/dist/util/__tests__/collections.test.d.ts +1 -0
  32. package/dist/util/__tests__/urls.test.d.ts +1 -0
  33. package/dist/util/collections.d.ts +1 -1
  34. package/dist/util/entities.d.ts +1 -0
  35. package/dist/util/geopoint.d.ts +22 -0
  36. package/dist/util/index.d.ts +2 -0
  37. package/dist/util/navigation_blocking.d.ts +22 -0
  38. package/dist/util/navigation_from_path.d.ts +17 -0
  39. package/dist/util/navigation_utils.d.ts +23 -1
  40. package/dist/util/urls.d.ts +22 -0
  41. package/package.json +16 -9
  42. package/src/app/Scaffold.tsx +34 -44
  43. package/src/app/useApp.tsx +1 -0
  44. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  45. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  46. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  47. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  48. package/src/components/EntityCollectionView/EntityCollectionView.tsx +19 -1
  49. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
  50. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  51. package/src/components/EntityPreview.tsx +41 -40
  52. package/src/components/ReferenceWidget.tsx +1 -1
  53. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  54. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  55. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  56. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  57. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  58. package/src/components/common/table_url_params.ts +172 -0
  59. package/src/components/common/useDataSourceTableController.tsx +98 -159
  60. package/src/components/common/useTableSearchHelper.ts +21 -2
  61. package/src/contexts/SnackbarProvider.tsx +14 -1
  62. package/src/core/DefaultDrawer.tsx +150 -64
  63. package/src/core/DrawerNavigationGroup.tsx +27 -29
  64. package/src/core/DrawerNavigationItem.tsx +10 -12
  65. package/src/core/EntityEditView.tsx +75 -32
  66. package/src/core/EntitySidePanel.tsx +2 -0
  67. package/src/core/field_configs.tsx +15 -0
  68. package/src/form/EntityForm.tsx +1 -1
  69. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  70. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  71. package/src/form/index.tsx +1 -0
  72. package/src/hooks/data/save.ts +2 -0
  73. package/src/hooks/data/useEntityFetch.tsx +9 -0
  74. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  75. package/src/i18n/FireCMSi18nProvider.tsx +2 -0
  76. package/src/internal/useBuildDataSource.ts +11 -0
  77. package/src/internal/useBuildSideEntityController.tsx +8 -1
  78. package/src/locales/de.ts +7 -2
  79. package/src/locales/en.ts +7 -2
  80. package/src/locales/es.ts +7 -2
  81. package/src/locales/fr.ts +7 -2
  82. package/src/locales/hi.ts +7 -2
  83. package/src/locales/it.ts +7 -2
  84. package/src/locales/pl.ts +735 -0
  85. package/src/locales/pt.ts +7 -2
  86. package/src/preview/PropertyPreview.tsx +12 -0
  87. package/src/preview/components/StorageThumbnail.tsx +24 -2
  88. package/src/preview/index.ts +1 -0
  89. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  90. package/src/routes/FireCMSRoute.tsx +47 -25
  91. package/src/types/collections.ts +26 -0
  92. package/src/types/datasource.ts +42 -0
  93. package/src/types/properties.ts +17 -0
  94. package/src/types/side_entity_controller.tsx +6 -0
  95. package/src/types/translations.ts +6 -0
  96. package/src/util/__tests__/collections.test.ts +32 -0
  97. package/src/util/__tests__/urls.test.ts +131 -0
  98. package/src/util/collections.ts +1 -1
  99. package/src/util/entities.ts +13 -0
  100. package/src/util/geopoint.ts +81 -0
  101. package/src/util/index.ts +2 -0
  102. package/src/util/navigation_blocking.ts +45 -0
  103. package/src/util/navigation_from_path.ts +41 -6
  104. package/src/util/navigation_utils.ts +40 -2
  105. package/src/util/parent_references_from_path.ts +4 -2
  106. package/src/util/previews.ts +8 -1
  107. package/src/util/resolutions.ts +8 -0
  108. package/src/util/urls.ts +52 -0
@@ -0,0 +1,2 @@
1
+ import { FireCMSTranslations } from "../types/translations";
2
+ export declare const pl: FireCMSTranslations;
@@ -4,6 +4,7 @@ export * from "./property_previews/ArrayOfStringsPreview";
4
4
  export * from "./property_previews/ArrayPropertyEnumPreview";
5
5
  export * from "./property_previews/ArrayOfMapsPreview";
6
6
  export * from "./property_previews/NumberPropertyPreview";
7
+ export * from "./property_previews/GeopointPropertyPreview";
7
8
  export * from "./property_previews/StringPropertyPreview";
8
9
  export * from "./property_previews/ArrayOfStorageComponentsPreview";
9
10
  export * from "./property_previews/ArrayOfReferencesPreview";
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import { GeoPoint } from "../../types";
3
+ import { PropertyPreviewProps } from "../PropertyPreviewProps";
4
+ export declare function GeopointPropertyPreview({ value, size }: PropertyPreviewProps<GeoPoint>): React.ReactElement;
@@ -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`.
@@ -232,6 +239,21 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
232
239
  * e.g. `initialFilter: { related_user: ["==", new EntityReference("sdc43dsw2", "users")] }`
233
240
  */
234
241
  initialFilter?: FilterValues<Extract<keyof M, string>>;
242
+ /**
243
+ * Array of property keys that are allowed for filtering. Allowed filters will be displayed in the collection view, table row headers
244
+ * and will be used to filter data.
245
+ *
246
+ * If not specified, all properties that are filterable will be allowed.
247
+ *
248
+ * If you specify this prop, the filters will be displayed in the collection view if they are filterable.
249
+ *
250
+ * If you set it as an empty array, no filters will be displayed.
251
+ *
252
+ * e.g. `allowedFilters: ["name", "category"]`
253
+ *
254
+ * e.g. `allowedFilters: []`
255
+ */
256
+ allowedFilters?: (keyof M)[];
235
257
  /**
236
258
  * Default sort applied to this collection.
237
259
  * When setting this prop, entities will have a default order
@@ -621,6 +643,8 @@ export type EntityTableController<M extends Record<string, any> = any> = {
621
643
  dataLoadingError?: Error;
622
644
  filterValues?: FilterValues<Extract<keyof M, string>>;
623
645
  setFilterValues?: (filterValues: FilterValues<Extract<keyof M, string>>) => void;
646
+ allowedFilters?: (keyof M)[];
647
+ forcedFilters?: (keyof M)[];
624
648
  sortBy?: [Extract<keyof M, string>, "asc" | "desc"];
625
649
  setSortBy?: (sortBy?: [Extract<keyof M, string>, "asc" | "desc"]) => void;
626
650
  searchString?: string;
@@ -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>>;
@@ -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";
@@ -25,3 +25,4 @@ export declare function sanitizeData<M extends Record<string, any>>(values: Enti
25
25
  export declare function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M>): EntityReference;
26
26
  export declare function traverseValuesProperties<M extends Record<string, any>>(inputValues: Partial<EntityValues<M>>, properties: ResolvedProperties<M>, operation: (value: any, property: Property) => any): EntityValues<M> | undefined;
27
27
  export declare function traverseValueProperty(inputValue: any, property: Property, operation: (value: any, property: Property) => any): any;
28
+ export declare function isDataTypeFilterable(dataType: DataType, isPartOfArray?: boolean): boolean;
@@ -0,0 +1,22 @@
1
+ import { GeoPoint } from "../types";
2
+ export type GeoPointLike = GeoPoint | {
3
+ latitude?: number;
4
+ longitude?: number;
5
+ lat?: number;
6
+ lng?: number;
7
+ _lat?: number;
8
+ _long?: number;
9
+ } | null | undefined;
10
+ export interface GeoPointCoordinates {
11
+ latitude: number;
12
+ longitude: number;
13
+ }
14
+ export declare function getGeoPointCoordinates(value: GeoPointLike): GeoPointCoordinates | undefined;
15
+ export declare function normalizeGeoPoint(value: GeoPointLike): GeoPoint | undefined;
16
+ export declare function formatGeoPoint(value: GeoPointLike, options?: {
17
+ maximumFractionDigits?: number;
18
+ }): string;
19
+ export declare function parseGeoPoint(input: string): {
20
+ point: GeoPoint | null;
21
+ error?: string;
22
+ };
@@ -6,12 +6,14 @@ 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";
12
13
  export * from "./property_utils";
13
14
  export * from "./resolutions";
14
15
  export * from "./permissions";
16
+ export * from "./geopoint";
15
17
  export * from "./icon_list";
16
18
  export * from "./icon_synonyms";
17
19
  export * from "./icons";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pure predicate deciding whether an in-progress navigation away from an open
3
+ * entity form should be blocked (to warn about unsaved changes).
4
+ *
5
+ * Extracted from FireCMSRoute so the branching can be unit-tested without
6
+ * mounting the router. This is an internal helper — it is intentionally not
7
+ * re-exported from the package index.
8
+ */
9
+ export interface NavigationBlockLocation {
10
+ pathname: string;
11
+ hash: string;
12
+ }
13
+ export declare function shouldBlockEntityNavigation(params: {
14
+ currentLocation: NavigationBlockLocation;
15
+ nextLocation: NavigationBlockLocation;
16
+ /** Path of the currently open entity (basePath + "/" + entityId). */
17
+ entityPath: string;
18
+ /** Collection path the entity form lives under. */
19
+ basePath: string;
20
+ /** Whether the form currently has unsaved modifications. */
21
+ blocked: boolean;
22
+ }): boolean;
@@ -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,13 +25,26 @@ 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
  }
33
+ /**
34
+ * Note on encoding: `path` arrives from the URL, so any entity id in it is escaped (see
35
+ * `encodeEntityId`). Two chains are threaded through the recursion:
36
+ *
37
+ * - `currentFullPath` carries RAW ids and feeds the `path` / `fullPath` used to address
38
+ * the datasource.
39
+ * - `currentFullUrlPath` carries ESCAPED ids and feeds `fullPath`, which is handed back
40
+ * to `buildUrlCollectionPath` for breadcrumbs and links.
41
+ */
27
42
  export declare function getNavigationEntriesFromPath(props: {
28
43
  path: string;
29
44
  collections: EntityCollection[] | undefined;
30
45
  currentFullPath?: string;
31
46
  currentFullIdPath?: string;
47
+ currentFullUrlPath?: string;
48
+ currentPathSegments?: string[];
32
49
  contextEntityViews?: EntityCustomView<any>[];
33
50
  }): NavigationViewInternal[];
@@ -3,6 +3,26 @@ 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;
6
26
  export declare function getLastSegment(path: string): string;
7
27
  export declare function resolveCollectionPathIds(path: string, allCollections: EntityCollection[]): string;
8
28
  /**
@@ -18,7 +38,7 @@ export declare function getCollectionByPathOrId(pathOrId: string, collections: E
18
38
  * @param subpaths
19
39
  */
20
40
  export declare function getCollectionPathsCombinations(subpaths: string[]): string[];
21
- 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 }: {
22
42
  openEntityMode: "side_panel" | "full_screen";
23
43
  collection?: EntityCollection;
24
44
  entityId?: string;
@@ -26,6 +46,8 @@ export declare function navigateToEntity({ openEntityMode, collection, entityId,
26
46
  copy?: boolean;
27
47
  path: string;
28
48
  fullIdPath?: string;
49
+ /** `path` split at its real segment boundaries. */
50
+ pathSegments?: string[];
29
51
  sideEntityController: SideEntityController;
30
52
  onClose?: () => void;
31
53
  navigation: NavigationController;
@@ -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.63041d0",
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.63041d0",
57
+ "@firecms/ui": "3.4.0-canary.63041d0",
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",
@@ -96,7 +96,7 @@
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
  }
@@ -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}