@firecms/core 3.1.0-canary.9e89e98 → 3.1.0

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 (43) hide show
  1. package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  2. package/dist/components/ErrorBoundary.d.ts +1 -1
  3. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -1
  4. package/dist/form/components/ErrorFocus.d.ts +1 -1
  5. package/dist/index.es.js +118 -54
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +118 -54
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/internal/useRestoreScroll.d.ts +1 -1
  10. package/dist/types/analytics.d.ts +1 -1
  11. package/dist/types/plugins.d.ts +16 -0
  12. package/dist/util/entities.d.ts +1 -1
  13. package/dist/util/resolutions.d.ts +2 -2
  14. package/package.json +9 -9
  15. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  16. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  17. package/src/components/EntityCollectionView/EntityBoardCard.tsx +1 -1
  18. package/src/components/EntityCollectionView/EntityCard.tsx +4 -0
  19. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +23 -3
  20. package/src/components/EntityCollectionView/EntityCollectionView.tsx +32 -3
  21. package/src/components/VirtualTable/VirtualTable.tsx +116 -113
  22. package/src/components/VirtualTable/VirtualTableHeader.tsx +42 -42
  23. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +1 -1
  24. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +3 -3
  25. package/src/core/DefaultAppBar.tsx +1 -1
  26. package/src/core/EntitySidePanel.tsx +28 -26
  27. package/src/core/field_configs.tsx +14 -9
  28. package/src/form/EntityForm.tsx +69 -60
  29. package/src/form/PropertyFieldBinding.tsx +3 -3
  30. package/src/form/components/ErrorFocus.tsx +3 -3
  31. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +1 -1
  32. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
  33. package/src/hooks/useBuildNavigationController.tsx +4 -4
  34. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  35. package/src/internal/useBuildDataSource.ts +1 -2
  36. package/src/preview/PropertyPreview.tsx +1 -0
  37. package/src/types/analytics.ts +10 -0
  38. package/src/types/plugins.tsx +18 -0
  39. package/src/util/entities.ts +1 -1
  40. package/src/util/join_collections.ts +10 -8
  41. package/src/util/previews.ts +2 -2
  42. package/src/util/property_utils.tsx +1 -1
  43. package/src/util/resolutions.ts +5 -3
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  export declare function useRestoreScroll(): {
3
- containerRef: React.RefObject<HTMLDivElement>;
3
+ containerRef: React.RefObject<HTMLDivElement | null>;
4
4
  scroll: number;
5
5
  direction: "up" | "down";
6
6
  };
@@ -1 +1 @@
1
- export type CMSAnalyticsEvent = "entity_click" | "entity_click_from_reference" | "reference_selection_clear" | "reference_selection_toggle" | "reference_selected_single" | "reference_selection_new_entity" | "edit_entity_clicked" | "entity_edited" | "new_entity_click" | "new_entity_saved" | "copy_entity_click" | "entity_copied" | "single_delete_dialog_open" | "multiple_delete_dialog_open" | "single_entity_deleted" | "multiple_entities_deleted" | "drawer_navigate_to_home" | "drawer_navigate_to_collection" | "drawer_navigate_to_view" | "home_navigate_to_collection" | "home_favorite_navigate_to_collection" | "home_navigate_to_view" | "home_navigate_to_admin_view" | "home_favorite_navigate_to_view" | "home_move_card" | "home_move_group" | "home_drop_new_group" | "collection_inline_editing" | "unmapped_event";
1
+ export type CMSAnalyticsEvent = "entity_click" | "entity_click_from_reference" | "reference_selection_clear" | "reference_selection_toggle" | "reference_selected_single" | "reference_selection_new_entity" | "edit_entity_clicked" | "entity_edited" | "new_entity_click" | "new_entity_saved" | "copy_entity_click" | "entity_copied" | "single_delete_dialog_open" | "multiple_delete_dialog_open" | "single_entity_deleted" | "multiple_entities_deleted" | "drawer_navigate_to_home" | "drawer_navigate_to_collection" | "drawer_navigate_to_view" | "home_navigate_to_collection" | "home_favorite_navigate_to_collection" | "home_navigate_to_view" | "home_navigate_to_admin_view" | "home_favorite_navigate_to_view" | "home_move_card" | "home_move_group" | "home_drop_new_group" | "collection_inline_editing" | "view_mode_changed" | "kanban_card_moved" | "kanban_column_reorder" | "kanban_property_changed" | "kanban_new_entity_in_column" | "kanban_backfill_order" | "card_view_entity_click" | "unmapped_event";
@@ -91,6 +91,18 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
91
91
  onNavigationEntriesUpdate?: (entries: NavigationGroupMapping[]) => void;
92
92
  };
93
93
  collectionView?: {
94
+ /**
95
+ * Custom component to render when a collection loading error occurs.
96
+ * If provided, this replaces the default error view in all collection view modes
97
+ * (table, card, kanban).
98
+ * Return `null` from the component to fall back to the default error view.
99
+ */
100
+ CollectionError?: React.ComponentType<{
101
+ path: string;
102
+ collection: EC;
103
+ parentCollectionIds?: string[];
104
+ error: Error;
105
+ }>;
94
106
  /**
95
107
  * Use this component to add custom actions to the entity collections
96
108
  * toolbar.
@@ -194,6 +206,10 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
194
206
  * Add custom actions to the top of the form
195
207
  */
196
208
  ActionsTop?: React.ComponentType<PluginFormActionProps<any, EC>>;
209
+ /**
210
+ * Add custom content above the entity title in the form view
211
+ */
212
+ BeforeTitle?: React.ComponentType<PluginFormActionProps<any, EC>>;
197
213
  fieldBuilder?: <T extends CMSType = CMSType>(props: PluginFieldBuilderParams<T, any, EC>) => React.ComponentType<FieldProps<T>> | null;
198
214
  fieldBuilderEnabled?: <T extends CMSType = CMSType>(props: PluginFieldBuilderParams<T>) => boolean;
199
215
  };
@@ -1,7 +1,7 @@
1
1
  import { CMSType, DataType, Entity, EntityReference, EntityStatus, EntityValues, PropertiesOrBuilders, Property, PropertyBuilder, PropertyOrBuilder, ResolvedProperties, ResolvedProperty } from "../types";
2
2
  export declare function isReadOnly(property: Property<any> | ResolvedProperty<any>): boolean;
3
3
  export declare function isHidden(property: Property | ResolvedProperty): boolean;
4
- export declare function isPropertyBuilder<T extends CMSType, M extends Record<string, any>>(propertyOrBuilder?: PropertyOrBuilder<T, M> | Property<T> | ResolvedProperty<T>): propertyOrBuilder is PropertyBuilder<T, M>;
4
+ export declare function isPropertyBuilder<T extends CMSType = CMSType, M extends Record<string, any> = any>(propertyOrBuilder?: PropertyOrBuilder<T, M> | Property | ResolvedProperty): propertyOrBuilder is PropertyBuilder<T, M>;
5
5
  export declare function getDefaultValuesFor<M extends Record<string, any>>(properties: PropertiesOrBuilders<M> | ResolvedProperties<M>): Partial<EntityValues<M>>;
6
6
  export declare function getDefaultValueFor(property?: PropertyOrBuilder): {} | null | undefined;
7
7
  export declare function getDefaultValueForDataType(dataType: DataType): {} | null;
@@ -1,4 +1,4 @@
1
- import { ArrayProperty, AuthController, CMSType, CustomizationController, EntityAction, EntityCollection, EntityCustomView, EntityValues, EnumValueConfig, EnumValues, NumberProperty, PropertiesOrBuilders, PropertyConfig, PropertyOrBuilder, ResolvedArrayProperty, ResolvedEntityCollection, ResolvedNumberProperty, ResolvedProperties, ResolvedProperty, ResolvedStringProperty, StringProperty, UserConfigurationPersistence } from "../types";
1
+ import { ArrayProperty, AuthController, CMSType, CustomizationController, EntityAction, EntityCollection, EntityCustomView, EntityValues, EnumValueConfig, EnumValues, NumberProperty, PropertiesOrBuilders, Property, PropertyConfig, PropertyOrBuilder, ResolvedArrayProperty, ResolvedEntityCollection, ResolvedNumberProperty, ResolvedProperties, ResolvedProperty, ResolvedStringProperty, StringProperty, UserConfigurationPersistence } from "../types";
2
2
  export declare const resolveCollection: <M extends Record<string, any>>({ collection, path, entityId, values, previousValues, userConfigPersistence, propertyConfigs, ignoreMissingFields, authController }: {
3
3
  collection: EntityCollection<M> | ResolvedEntityCollection<M>;
4
4
  path: string;
@@ -17,7 +17,7 @@ export declare const resolveCollection: <M extends Record<string, any>>({ collec
17
17
  */
18
18
  export declare function resolveProperty<T extends CMSType = CMSType, M extends Record<string, any> = any>({ propertyOrBuilder, fromBuilder, ignoreMissingFields, ...props }: {
19
19
  propertyKey?: string;
20
- propertyOrBuilder: PropertyOrBuilder<T, M> | ResolvedProperty<T>;
20
+ propertyOrBuilder: PropertyOrBuilder<T, M> | ResolvedProperty<T> | PropertyOrBuilder | Property | ResolvedProperty | undefined;
21
21
  values?: Partial<M>;
22
22
  previousValues?: Partial<M>;
23
23
  path?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.1.0-canary.9e89e98",
4
+ "version": "3.1.0",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -53,9 +53,9 @@
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/editor": "^3.1.0-canary.9e89e98",
57
- "@firecms/formex": "^3.1.0-canary.9e89e98",
58
- "@firecms/ui": "^3.1.0-canary.9e89e98",
56
+ "@firecms/editor": "^3.1.0",
57
+ "@firecms/formex": "^3.1.0",
58
+ "@firecms/ui": "^3.1.0",
59
59
  "@radix-ui/react-portal": "^1.1.10",
60
60
  "clsx": "^2.1.1",
61
61
  "compressorjs": "^1.2.1",
@@ -76,8 +76,8 @@
76
76
  "yup": "^1.7.1"
77
77
  },
78
78
  "peerDependencies": {
79
- "react": ">=18.0.0",
80
- "react-dom": ">=18.0.0",
79
+ "react": ">=18.3.1 || >=19.0.0",
80
+ "react-dom": ">=18.3.1 || >=19.0.0",
81
81
  "react-router": "^6.28.0",
82
82
  "react-router-dom": "^6.28.0"
83
83
  },
@@ -89,8 +89,8 @@
89
89
  "@types/json-logic-js": "^2.0.8",
90
90
  "@types/node": "^20.19.17",
91
91
  "@types/object-hash": "^3.0.6",
92
- "@types/react": "^18.3.24",
93
- "@types/react-dom": "^18.3.7",
92
+ "@types/react": "^19.2.3",
93
+ "@types/react-dom": "^19.2.3",
94
94
  "@types/react-measure": "^2.0.12",
95
95
  "@vitejs/plugin-react": "^4.7.0",
96
96
  "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417",
@@ -111,7 +111,7 @@
111
111
  "dist",
112
112
  "src"
113
113
  ],
114
- "gitHead": "7631fbfb6cde4415513e45c0557e18073ab359b8",
114
+ "gitHead": "40f8d9860cb2649c0a195ecebd1a92ccb37f33a6",
115
115
  "publishConfig": {
116
116
  "access": "public"
117
117
  },
@@ -28,7 +28,7 @@ export function EntityTableCellActions({
28
28
  }
29
29
  }, []);
30
30
 
31
- const iconRef = useRef<HTMLButtonElement>();
31
+ const iconRef = useRef<HTMLButtonElement>(undefined);
32
32
  useEffect(() => {
33
33
  if (iconRef.current && selected) {
34
34
  iconRef.current.focus({ preventScroll: true });
@@ -1,20 +1,20 @@
1
1
  import React, { useCallback, useEffect } from "react";
2
2
 
3
3
  interface DraggableProps {
4
- containerRef: React.RefObject<HTMLDivElement>,
5
- innerRef: React.RefObject<HTMLDivElement>,
4
+ containerRef: React.RefObject<HTMLDivElement | null>,
5
+ innerRef: React.RefObject<HTMLDivElement | null>,
6
6
  x?: number;
7
7
  y?: number;
8
8
  onMove: (params: { x: number, y: number }) => void,
9
9
  }
10
10
 
11
11
  export function useDraggable({
12
- containerRef,
13
- innerRef,
14
- x,
15
- y,
16
- onMove
17
- }: DraggableProps) {
12
+ containerRef,
13
+ innerRef,
14
+ x,
15
+ y,
16
+ onMove
17
+ }: DraggableProps) {
18
18
 
19
19
  let relX = 0;
20
20
  let relY = 0;
@@ -62,9 +62,9 @@ export function useDraggable({
62
62
  if (event.target.localName === "input" || !listeningRef.current)
63
63
  return;
64
64
  onMove({
65
- x: event.screenX - relX,
66
- y: event.screenY - relY
67
- }
65
+ x: event.screenX - relX,
66
+ y: event.screenY - relY
67
+ }
68
68
  );
69
69
  event.stopPropagation();
70
70
  };
@@ -149,7 +149,7 @@ function EntityBoardCardInner<M extends Record<string, any> = any>({
149
149
  {/* Content */}
150
150
  <div className="flex-1 min-w-0">
151
151
  {/* Title */}
152
- <div className="truncate text-sm font-medium">
152
+ <div className="line-clamp-2 text-sm font-medium">
153
153
  {titleProperty && titleValue ? (
154
154
  <PropertyPreview
155
155
  propertyKey={titlePropertyKey as string}
@@ -111,6 +111,10 @@ export function EntityCard<M extends Record<string, any> = any>({
111
111
  return;
112
112
  }
113
113
  if (onClick) {
114
+ analyticsController.onAnalyticsEvent?.("card_view_entity_click", {
115
+ path: entity.path,
116
+ entityId: entity.id
117
+ });
114
118
  onClick(entity);
115
119
  }
116
120
  };
@@ -34,6 +34,7 @@ import {
34
34
  useFireCMSContext,
35
35
  useSideEntityController
36
36
  } from "../../hooks";
37
+ import { useAnalyticsController } from "../../hooks/useAnalyticsController";
37
38
  import { SaveEntityProps } from "../../types/datasource";
38
39
  import { setIn } from "@firecms/formex";
39
40
  import { useBoardDataController } from "./useBoardDataController";
@@ -74,6 +75,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
74
75
  const context = useFireCMSContext();
75
76
  const dataSource = useDataSource(collection);
76
77
  const sideEntityController = useSideEntityController();
78
+ const analyticsController = useAnalyticsController();
77
79
  const plugins = customizationController.plugins ?? [];
78
80
 
79
81
  // State for backfill dialog
@@ -222,6 +224,10 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
222
224
  }, [plugins]);
223
225
 
224
226
  const handleColumnReorder = useCallback((newColumns: string[]) => {
227
+ analyticsController.onAnalyticsEvent?.("kanban_column_reorder", {
228
+ path: fullPath,
229
+ columnProperty
230
+ });
225
231
  setHasUserReordered(true);
226
232
  setLocalColumnsOrder(newColumns);
227
233
  plugins
@@ -235,7 +241,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
235
241
  newColumnsOrder: newColumns
236
242
  });
237
243
  });
238
- }, [plugins, fullPath, parentCollectionIds, collection, columnProperty]);
244
+ }, [plugins, fullPath, parentCollectionIds, collection, columnProperty, analyticsController]);
239
245
 
240
246
  // Collection-level count queries to detect missing order property
241
247
  // Just TWO counts: total and ordered (for the entire collection, not per column)
@@ -393,6 +399,13 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
393
399
  const entity = items.find(item => item.id === moveInfo?.itemId)?.entity;
394
400
  if (!entity) return;
395
401
 
402
+ analyticsController.onAnalyticsEvent?.("kanban_card_moved", {
403
+ path: fullPath,
404
+ entityId: entity.id,
405
+ sourceColumn: moveInfo?.sourceColumn,
406
+ targetColumn: moveInfo?.targetColumn
407
+ });
408
+
396
409
  const isColumnChange = moveInfo && moveInfo.sourceColumn !== moveInfo.targetColumn;
397
410
 
398
411
  // If no orderProperty and not a column change, nothing to do
@@ -440,7 +453,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
440
453
  } catch (e) {
441
454
  console.error("Error saving entity:", e);
442
455
  }
443
- }, [collection, columnProperty, orderProperty, context, dataSource, calculateNewOrder, boardDataController]);
456
+ }, [collection, columnProperty, orderProperty, context, dataSource, calculateNewOrder, boardDataController, analyticsController, fullPath]);
444
457
 
445
458
  // Backfill order values for all entities
446
459
  const handleBackfill = useCallback(async () => {
@@ -449,6 +462,9 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
449
462
  console.log("No orderProperty, returning");
450
463
  return;
451
464
  }
465
+ analyticsController.onAnalyticsEvent?.("kanban_backfill_order", {
466
+ path: fullPath
467
+ });
452
468
  setBackfillLoading(true);
453
469
 
454
470
  try {
@@ -514,7 +530,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
514
530
  } finally {
515
531
  setBackfillLoading(false);
516
532
  }
517
- }, [orderProperty, fullPath, collection, dataSource, context, boardDataController]);
533
+ }, [orderProperty, fullPath, collection, dataSource, context, boardDataController, analyticsController]);
518
534
 
519
535
  const handleEntityClick = useCallback((entity: Entity<M>) => {
520
536
  onEntityClick?.(entity);
@@ -667,6 +683,10 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
667
683
  columnLoadingState={columnLoadingState}
668
684
  onLoadMoreColumn={(column) => boardDataController.loadMoreColumn(column)}
669
685
  onAddItemToColumn={(column) => {
686
+ analyticsController.onAnalyticsEvent?.("kanban_new_entity_in_column", {
687
+ path: fullPath,
688
+ column
689
+ });
670
690
  sideEntityController.open({
671
691
  path: fullPath,
672
692
  collection,
@@ -433,12 +433,17 @@ export const EntityCollectionView = React.memo(
433
433
 
434
434
  // View mode change: update URL + save to local persistence
435
435
  const onViewModeChange = useCallback((mode: ViewMode) => {
436
+ analyticsController.onAnalyticsEvent?.("view_mode_changed", {
437
+ path: fullPath,
438
+ from: viewMode,
439
+ to: mode
440
+ });
436
441
  setViewMode(mode);
437
442
  // Save to local persistence for next visit
438
443
  if (userConfigPersistence) {
439
444
  onCollectionModifiedForUser(fullPath, { defaultViewMode: mode } as PartialEntityCollection<M>);
440
445
  }
441
- }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath]);
446
+ }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
442
447
 
443
448
  const createEnabled = canCreateEntity(collection, authController, fullPath, null);
444
449
 
@@ -568,12 +573,16 @@ export const EntityCollectionView = React.memo(
568
573
 
569
574
  // Handle kanban property change
570
575
  const onKanbanPropertyChange = useCallback((property: string) => {
576
+ analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
577
+ path: fullPath,
578
+ property
579
+ });
571
580
  setSelectedKanbanProperty(property);
572
581
  // Save to local persistence
573
582
  if (userConfigPersistence) {
574
583
  onCollectionModifiedForUser(fullPath, { kanbanColumnProperty: property } as any);
575
584
  }
576
- }, [userConfigPersistence, onCollectionModifiedForUser, fullPath]);
585
+ }, [userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController]);
577
586
 
578
587
  const getPropertyFor = useCallback(({
579
588
  propertyKey,
@@ -836,6 +845,24 @@ export const EntityCollectionView = React.memo(
836
845
  />
837
846
  );
838
847
 
848
+ // Compute plugin-provided error view for collection loading errors
849
+ const pluginErrorView = useMemo(() => {
850
+ const error = tableController.dataLoadingError;
851
+ if (!error || !customizationController.plugins) return null;
852
+ for (const plugin of customizationController.plugins) {
853
+ if (plugin.collectionView?.CollectionError) {
854
+ const CollectionError = plugin.collectionView.CollectionError;
855
+ return <CollectionError
856
+ path={fullPath}
857
+ collection={collection}
858
+ parentCollectionIds={parentCollectionIds}
859
+ error={error}
860
+ />;
861
+ }
862
+ }
863
+ return null;
864
+ }, [tableController.dataLoadingError, customizationController.plugins, fullPath, collection, parentCollectionIds]);
865
+
839
866
  return (
840
867
  <div className={cls("overflow-hidden h-full w-full rounded-md flex flex-col", className)}
841
868
  ref={containerRef}>
@@ -872,7 +899,9 @@ export const EntityCollectionView = React.memo(
872
899
  />
873
900
 
874
901
  {/* View content - only the view-specific content changes */}
875
- {viewMode === "kanban" && enabledViews.includes("kanban") ? (
902
+ {tableController.dataLoadingError && pluginErrorView
903
+ ? pluginErrorView
904
+ : viewMode === "kanban" && enabledViews.includes("kanban") ? (
876
905
  <EntityCollectionBoardView
877
906
  key={`kanban-view-${fullPath}-${selectedKanbanProperty}`}
878
907
  collection={collection}