@firecms/core 3.0.1 → 3.1.0-canary.7d91b7c

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 (185) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AIIcon.d.ts +16 -0
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
  5. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
  6. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
  7. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
  8. package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
  9. package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  10. package/dist/components/EntityCollectionView/Board.d.ts +2 -0
  11. package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
  12. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
  13. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
  14. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
  15. package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
  16. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
  17. package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
  18. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
  19. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
  20. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
  21. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +44 -0
  22. package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
  23. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
  24. package/dist/components/ErrorBoundary.d.ts +1 -1
  25. package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
  26. package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
  27. package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
  28. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +3 -1
  29. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  30. package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
  31. package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
  32. package/dist/components/VirtualTable/types.d.ts +2 -0
  33. package/dist/components/index.d.ts +3 -0
  34. package/dist/contexts/index.d.ts +10 -0
  35. package/dist/core/DrawerNavigationGroup.d.ts +45 -0
  36. package/dist/core/index.d.ts +1 -0
  37. package/dist/form/components/ErrorFocus.d.ts +1 -1
  38. package/dist/form/validation.d.ts +3 -2
  39. package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
  40. package/dist/hooks/useCollapsedGroups.d.ts +4 -1
  41. package/dist/index.es.js +5266 -1578
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/index.umd.js +5260 -1573
  44. package/dist/index.umd.js.map +1 -1
  45. package/dist/internal/useRestoreScroll.d.ts +1 -1
  46. package/dist/preview/PropertyPreviewProps.d.ts +5 -0
  47. package/dist/preview/components/DatePreview.d.ts +13 -3
  48. package/dist/preview/components/ImagePreview.d.ts +5 -1
  49. package/dist/preview/components/StorageThumbnail.d.ts +2 -1
  50. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  51. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  52. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  53. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  54. package/dist/types/analytics.d.ts +1 -1
  55. package/dist/types/collections.d.ts +50 -2
  56. package/dist/types/datasource.d.ts +0 -1
  57. package/dist/types/plugins.d.ts +62 -1
  58. package/dist/types/properties.d.ts +259 -4
  59. package/dist/util/__tests__/conditions.test.d.ts +1 -0
  60. package/dist/util/__tests__/objects.test.d.ts +1 -0
  61. package/dist/util/conditions.d.ts +26 -0
  62. package/dist/util/entities.d.ts +2 -3
  63. package/dist/util/index.d.ts +2 -1
  64. package/dist/util/property_utils.d.ts +2 -1
  65. package/dist/util/resolutions.d.ts +3 -3
  66. package/package.json +14 -11
  67. package/src/app/Scaffold.tsx +14 -15
  68. package/src/components/AIIcon.tsx +39 -0
  69. package/src/components/ArrayContainer.tsx +1 -4
  70. package/src/components/ClearFilterSortButton.tsx +19 -16
  71. package/src/components/ConfirmationDialog.tsx +0 -2
  72. package/src/components/DeleteEntityDialog.tsx +2 -4
  73. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
  74. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
  75. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
  76. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
  77. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
  78. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
  79. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  80. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  81. package/src/components/EntityCollectionView/Board.tsx +324 -0
  82. package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
  83. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
  84. package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
  85. package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
  86. package/src/components/EntityCollectionView/EntityCard.tsx +235 -0
  87. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +733 -0
  88. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
  89. package/src/components/EntityCollectionView/EntityCollectionView.tsx +519 -203
  90. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
  91. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
  92. package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
  93. package/src/components/EntityCollectionView/ViewModeToggle.tsx +199 -0
  94. package/src/components/EntityCollectionView/board_types.ts +113 -0
  95. package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
  96. package/src/components/ErrorTooltip.tsx +2 -1
  97. package/src/components/HomePage/DefaultHomePage.tsx +47 -10
  98. package/src/components/HomePage/HomePageDnD.tsx +56 -41
  99. package/src/components/HomePage/NavigationCard.tsx +20 -18
  100. package/src/components/HomePage/NavigationGroup.tsx +17 -16
  101. package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
  102. package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
  103. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
  104. package/src/components/ReferenceWidget.tsx +2 -4
  105. package/src/components/SelectableTable/SelectableTable.tsx +75 -67
  106. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
  107. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
  108. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
  109. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
  110. package/src/components/UnsavedChangesDialog.tsx +0 -2
  111. package/src/components/UserDisplay.tsx +4 -4
  112. package/src/components/VirtualTable/VirtualTable.tsx +272 -118
  113. package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
  114. package/src/components/VirtualTable/VirtualTableHeader.tsx +59 -50
  115. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
  116. package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
  117. package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
  118. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
  119. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +19 -6
  120. package/src/components/VirtualTable/types.tsx +2 -0
  121. package/src/components/common/useColumnsIds.tsx +95 -3
  122. package/src/components/index.tsx +4 -0
  123. package/src/contexts/BreacrumbsContext.tsx +15 -8
  124. package/src/contexts/index.ts +10 -0
  125. package/src/core/DefaultAppBar.tsx +40 -27
  126. package/src/core/DefaultDrawer.tsx +42 -56
  127. package/src/core/DrawerNavigationGroup.tsx +118 -0
  128. package/src/core/DrawerNavigationItem.tsx +4 -3
  129. package/src/core/EntityEditView.tsx +41 -43
  130. package/src/core/EntitySidePanel.tsx +28 -26
  131. package/src/core/SideDialogs.tsx +4 -2
  132. package/src/core/field_configs.tsx +14 -9
  133. package/src/core/index.tsx +1 -0
  134. package/src/form/EntityForm.tsx +69 -60
  135. package/src/form/PropertyFieldBinding.tsx +61 -46
  136. package/src/form/components/ErrorFocus.tsx +3 -3
  137. package/src/form/components/StorageItemPreview.tsx +2 -1
  138. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
  139. package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
  140. package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
  141. package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
  142. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +22 -18
  143. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
  144. package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
  145. package/src/form/validation.ts +245 -160
  146. package/src/hooks/useBreadcrumbsController.tsx +18 -0
  147. package/src/hooks/useBuildNavigationController.tsx +46 -23
  148. package/src/hooks/useCollapsedGroups.ts +12 -4
  149. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  150. package/src/internal/useBuildDataSource.ts +68 -34
  151. package/src/internal/useBuildSideDialogsController.tsx +11 -8
  152. package/src/internal/useBuildSideEntityController.tsx +2 -4
  153. package/src/internal/useRestoreScroll.tsx +26 -14
  154. package/src/preview/PropertyPreview.tsx +41 -32
  155. package/src/preview/PropertyPreviewProps.tsx +6 -0
  156. package/src/preview/components/DatePreview.tsx +72 -4
  157. package/src/preview/components/EmptyValue.tsx +1 -1
  158. package/src/preview/components/ImagePreview.tsx +37 -21
  159. package/src/preview/components/StorageThumbnail.tsx +16 -12
  160. package/src/preview/components/UrlComponentPreview.tsx +28 -25
  161. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
  162. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
  163. package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
  164. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
  165. package/src/routes/CustomCMSRoute.tsx +1 -0
  166. package/src/routes/FireCMSRoute.tsx +26 -13
  167. package/src/types/analytics.ts +10 -0
  168. package/src/types/collections.ts +57 -3
  169. package/src/types/datasource.ts +54 -56
  170. package/src/types/plugins.tsx +69 -1
  171. package/src/types/properties.ts +347 -27
  172. package/src/util/__tests__/conditions.test.ts +506 -0
  173. package/src/util/__tests__/objects.test.ts +196 -0
  174. package/src/util/callbacks.ts +6 -3
  175. package/src/util/collections.ts +51 -6
  176. package/src/util/conditions.ts +339 -0
  177. package/src/util/entities.ts +29 -30
  178. package/src/util/entity_cache.ts +2 -1
  179. package/src/util/index.ts +2 -1
  180. package/src/util/join_collections.ts +10 -8
  181. package/src/util/objects.ts +31 -13
  182. package/src/util/{references.ts → previews.ts} +16 -2
  183. package/src/util/property_utils.tsx +37 -11
  184. package/src/util/resolutions.ts +62 -58
  185. /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
@@ -0,0 +1,172 @@
1
+ import React, { memo, useEffect, useMemo, useRef } from "react";
2
+ import { useDroppable } from "@dnd-kit/core";
3
+ import { useSortable } from "@dnd-kit/sortable";
4
+ import { CSS } from "@dnd-kit/utilities";
5
+ import { CircularProgress, cls } from "@firecms/ui";
6
+ import { BoardItem, BoardItemViewProps } from "./board_types";
7
+
8
+ interface BoardSortableListProps<M extends Record<string, any>> {
9
+ columnId: string;
10
+ items: BoardItem<M>[];
11
+ ItemComponent: React.ComponentType<BoardItemViewProps<M>>;
12
+ isDragging: boolean;
13
+ isDragOverColumn: boolean;
14
+ loading?: boolean;
15
+ hasMore?: boolean;
16
+ onLoadMore?: () => void;
17
+ }
18
+
19
+ export function BoardSortableList<M extends Record<string, any>>({
20
+ columnId,
21
+ items,
22
+ ItemComponent,
23
+ isDragging,
24
+ isDragOverColumn,
25
+ loading = false,
26
+ hasMore = false,
27
+ onLoadMore,
28
+ }: BoardSortableListProps<M>) {
29
+ const {
30
+ setNodeRef,
31
+ } = useDroppable({
32
+ id: columnId,
33
+ data: { type: "ITEM-LIST" }
34
+ });
35
+
36
+ // Infinite scroll sentinel ref - must be inside scrollable container
37
+ const sentinelRef = useRef<HTMLDivElement>(null);
38
+ const isLoadingRef = useRef(false);
39
+ isLoadingRef.current = loading;
40
+ const lastLoadTimeRef = useRef(0);
41
+
42
+ // Set up IntersectionObserver for infinite scroll
43
+ useEffect(() => {
44
+ if (!sentinelRef.current || !hasMore || !onLoadMore) return;
45
+
46
+ const sentinel = sentinelRef.current;
47
+
48
+ const observer = new IntersectionObserver(
49
+ (entries) => {
50
+ const now = Date.now();
51
+ if (
52
+ entries[0].isIntersecting &&
53
+ hasMore &&
54
+ !isLoadingRef.current &&
55
+ now - lastLoadTimeRef.current > 500
56
+ ) {
57
+ lastLoadTimeRef.current = now;
58
+ onLoadMore();
59
+ }
60
+ },
61
+ { threshold: 0.1 }
62
+ );
63
+
64
+ observer.observe(sentinel);
65
+
66
+ // Check if sentinel is already visible when effect runs
67
+ // This handles the case where the sentinel was visible before the observer was created
68
+ const rect = sentinel.getBoundingClientRect();
69
+ const containerRect = sentinel.parentElement?.getBoundingClientRect();
70
+ if (containerRect && rect.top < containerRect.bottom && rect.bottom > containerRect.top) {
71
+ const now = Date.now();
72
+ if (hasMore && !isLoadingRef.current && now - lastLoadTimeRef.current > 500) {
73
+ lastLoadTimeRef.current = now;
74
+ onLoadMore();
75
+ }
76
+ }
77
+
78
+ return () => observer.disconnect();
79
+ }, [hasMore, onLoadMore]);
80
+
81
+ // Memoize className to avoid recomputation on every render
82
+ const containerClassName = useMemo(() => cls(
83
+ "flex flex-col p-2 transition-opacity duration-100 transition-bg ease-linear w-full overflow-y-auto no-scrollbar flex-1 rounded-md",
84
+ isDragging && isDragOverColumn
85
+ ? "bg-surface-accent-200 dark:bg-surface-800"
86
+ : isDragging
87
+ ? "bg-surface-50 dark:bg-surface-950 hover:bg-surface-accent-100 dark:hover:bg-surface-800"
88
+ : "bg-surface-50 dark:bg-surface-950"
89
+ ), [isDragging, isDragOverColumn]);
90
+
91
+ return (
92
+ <div
93
+ ref={setNodeRef}
94
+ className={containerClassName}
95
+ style={{ minHeight: 80 }}
96
+ >
97
+ {items.length === 0 && !loading ? (
98
+ <div className="flex-1 flex items-center justify-center">
99
+ <span className="text-xs text-surface-400 dark:text-surface-500">
100
+ No items
101
+ </span>
102
+ </div>
103
+ ) : (
104
+ <>
105
+ {items.map((item, index) => (
106
+ <SortableItem
107
+ key={item.id}
108
+ item={item}
109
+ index={index}
110
+ columnId={columnId}
111
+ ItemComponent={ItemComponent}
112
+ />
113
+ ))}
114
+ {/* Infinite scroll sentinel - inside scrollable container */}
115
+ {(loading || hasMore) && (
116
+ <div ref={sentinelRef} className="flex items-center justify-center py-2 min-h-6">
117
+ {loading && <CircularProgress size="smallest" />}
118
+ </div>
119
+ )}
120
+ </>
121
+ )}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ interface SortableItemProps<M extends Record<string, any>> {
127
+ item: BoardItem<M>;
128
+ index: number;
129
+ columnId: string;
130
+ ItemComponent: React.ComponentType<BoardItemViewProps<M>>;
131
+ }
132
+
133
+ // Memoized to prevent unnecessary re-renders when other items in the list change
134
+ const SortableItem = memo(function SortableItem<M extends Record<string, any>>({
135
+ item,
136
+ index,
137
+ columnId,
138
+ ItemComponent,
139
+ }: SortableItemProps<M>) {
140
+ const {
141
+ setNodeRef,
142
+ attributes,
143
+ listeners,
144
+ isDragging: isItemBeingDragged,
145
+ transform,
146
+ transition,
147
+ } = useSortable({
148
+ id: item.id,
149
+ data: {
150
+ type: "ITEM",
151
+ columnId
152
+ }
153
+ });
154
+
155
+ // Memoize style object to prevent object recreation on each render
156
+ const sortableStyle = useMemo(() => ({
157
+ transform: CSS.Transform.toString(transform),
158
+ transition,
159
+ zIndex: isItemBeingDragged ? 2 : 1,
160
+ opacity: isItemBeingDragged ? 0 : 1,
161
+ }), [transform, transition, isItemBeingDragged]);
162
+
163
+ return (
164
+ <div ref={setNodeRef} style={sortableStyle} {...attributes} {...listeners}>
165
+ <ItemComponent
166
+ item={item}
167
+ isDragging={isItemBeingDragged}
168
+ index={index}
169
+ />
170
+ </div>
171
+ );
172
+ }) as <M extends Record<string, any>>(props: SortableItemProps<M>) => React.ReactElement;
@@ -0,0 +1,212 @@
1
+ import React, { memo, useCallback, useMemo } from "react";
2
+ import { Entity, EntityCollection, ResolvedProperty } from "../../types";
3
+ import {
4
+ getEntityImagePreviewPropertyKey,
5
+ getEntityTitlePropertyKey,
6
+ getValueInPath,
7
+ IconForView,
8
+ resolveCollection
9
+ } from "../../util";
10
+ import { Checkbox, cls, defaultBorderMixin } from "@firecms/ui";
11
+ import { PropertyPreview } from "../../preview";
12
+ import { useAuthController, useCustomizationController } from "../../hooks";
13
+ import { BoardItemViewProps } from "./board_types";
14
+
15
+ export type EntityBoardCardProps<M extends Record<string, any> = any> = BoardItemViewProps<M> & {
16
+ collection: EntityCollection<M>;
17
+ onClick?: (entity: Entity<M>) => void;
18
+ selected?: boolean;
19
+ onSelectionChange?: (entity: Entity<M>, selected: boolean) => void;
20
+ selectionEnabled?: boolean;
21
+ };
22
+
23
+ /**
24
+ * Compact card component for displaying an entity in a Kanban board.
25
+ * Shows thumbnail, title, and optional selection checkbox.
26
+ */
27
+ function EntityBoardCardInner<M extends Record<string, any> = any>({
28
+ item,
29
+ isDragging,
30
+ isGroupedOver,
31
+ style,
32
+ collection,
33
+ onClick,
34
+ selected,
35
+ onSelectionChange,
36
+ selectionEnabled = false
37
+ }: EntityBoardCardProps<M>) {
38
+ const entity = item.entity;
39
+ const authController = useAuthController();
40
+ const customizationController = useCustomizationController();
41
+
42
+ const resolvedCollection = useMemo(() => resolveCollection({
43
+ collection,
44
+ path: entity.path,
45
+ values: entity.values,
46
+ propertyConfigs: customizationController.propertyConfigs,
47
+ authController
48
+ }), [collection, entity.path, entity.values, customizationController.propertyConfigs, authController]);
49
+
50
+ const titlePropertyKey = useMemo(
51
+ () => getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs),
52
+ [resolvedCollection, customizationController.propertyConfigs]
53
+ );
54
+
55
+ const imagePropertyKey = useMemo(
56
+ () => getEntityImagePreviewPropertyKey(resolvedCollection),
57
+ [resolvedCollection]
58
+ );
59
+
60
+ const imageProperty = imagePropertyKey ? resolvedCollection.properties[imagePropertyKey] : undefined;
61
+ const usedImageProperty = imageProperty && "of" in imageProperty ? imageProperty.of : imageProperty;
62
+
63
+ const imageValue = imagePropertyKey ? getValueInPath(entity.values, imagePropertyKey) : undefined;
64
+ const usedImageValue = imageProperty !== undefined
65
+ ? ("of" in imageProperty
66
+ ? ((imageValue ?? []).length > 0 ? imageValue[0] : undefined)
67
+ : imageValue)
68
+ : undefined;
69
+
70
+ const titleValue = titlePropertyKey ? getValueInPath(entity.values, titlePropertyKey) : undefined;
71
+ const titleProperty = titlePropertyKey ? resolvedCollection.properties[titlePropertyKey] as ResolvedProperty : undefined;
72
+
73
+ const handleClick = useCallback((e: React.MouseEvent) => {
74
+ // Cmd+click (Mac) or Ctrl+click (Windows) toggles selection
75
+ if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+ onSelectionChange?.(entity, !selected);
79
+ return;
80
+ }
81
+ if (onClick) {
82
+ e.stopPropagation();
83
+ onClick(entity);
84
+ }
85
+ }, [entity, onClick, onSelectionChange, selected, selectionEnabled]);
86
+
87
+ const handleCheckboxClick = useCallback((e: React.MouseEvent) => {
88
+ e.stopPropagation();
89
+ }, []);
90
+
91
+ const handleSelectionChange = useCallback((checked: boolean) => {
92
+ onSelectionChange?.(entity, checked);
93
+ }, [entity, onSelectionChange]);
94
+
95
+ // Memoize className computations
96
+ const backgroundColor = useMemo((): string => {
97
+ if (isDragging) {
98
+ return "bg-surface-100 dark:bg-surface-800";
99
+ }
100
+ if (isGroupedOver) {
101
+ return "bg-surface-200";
102
+ }
103
+ return "bg-white dark:bg-surface-900 hover:bg-surface-100 dark:hover:bg-surface-800";
104
+ }, [isDragging, isGroupedOver]);
105
+
106
+ const borderColor = useMemo((): string =>
107
+ isDragging ? "ring-2 ring-primary" : "", [isDragging]);
108
+
109
+ // Memoize the card className
110
+ const cardClassName = useMemo(() => cls(
111
+ "p-2 flex items-start border rounded-lg cursor-pointer transition-colors",
112
+ defaultBorderMixin,
113
+ borderColor,
114
+ backgroundColor,
115
+ selected && "ring-2 ring-primary"
116
+ ), [borderColor, backgroundColor, selected]);
117
+
118
+ return (
119
+ <div
120
+ style={style}
121
+ className="py-1"
122
+ data-is-dragging={isDragging}
123
+ data-testid={item.id}
124
+ onClick={handleClick}
125
+ >
126
+ <div className={cardClassName}>
127
+ {/* Thumbnail */}
128
+ {usedImageProperty && usedImageValue ? (
129
+ <div className="w-10 h-10 rounded-md overflow-hidden shrink-0 mr-2">
130
+ <PropertyPreview
131
+ property={usedImageProperty}
132
+ propertyKey={imagePropertyKey as string}
133
+ size="small"
134
+ value={usedImageValue}
135
+ fill={true}
136
+ />
137
+ </div>
138
+ ) : (
139
+ <div
140
+ className="w-10 h-10 rounded-md bg-surface-100 dark:bg-surface-800 shrink-0 mr-2 flex items-center justify-center">
141
+ <IconForView
142
+ collectionOrView={collection}
143
+ color="disabled"
144
+ size="small"
145
+ />
146
+ </div>
147
+ )}
148
+
149
+ {/* Content */}
150
+ <div className="flex-1 min-w-0">
151
+ {/* Title */}
152
+ <div className="line-clamp-2 text-sm font-medium">
153
+ {titleProperty && titleValue ? (
154
+ <PropertyPreview
155
+ propertyKey={titlePropertyKey as string}
156
+ value={titleValue}
157
+ property={titleProperty}
158
+ size="small"
159
+ />
160
+ ) : (
161
+ <span className="text-surface-500">{entity.id}</span>
162
+ )}
163
+ </div>
164
+ {/* ID */}
165
+ <div className="text-xs text-surface-500 font-mono truncate">
166
+ {entity.id}
167
+ </div>
168
+ </div>
169
+
170
+ {/* Selection checkbox */}
171
+ {selectionEnabled && (
172
+ <div className="ml-2 shrink-0" onClick={handleCheckboxClick}>
173
+ <Checkbox
174
+ checked={selected ?? false}
175
+ onCheckedChange={handleSelectionChange}
176
+ size="smallest"
177
+ />
178
+ </div>
179
+ )}
180
+ </div>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ // Memoized to prevent unnecessary re-renders when other cards in the board change
186
+ export const EntityBoardCard = memo(EntityBoardCardInner) as typeof EntityBoardCardInner;
187
+
188
+ /**
189
+ * Wrapper component that adapts EntityBoardCard to BoardItemViewProps interface
190
+ */
191
+ export function createEntityBoardCardComponent<M extends Record<string, any>>(
192
+ collection: EntityCollection<M>,
193
+ options: {
194
+ onClick?: (entity: Entity<M>) => void;
195
+ isEntitySelected?: (entity: Entity<M>) => boolean;
196
+ onSelectionChange?: (entity: Entity<M>, selected: boolean) => void;
197
+ selectionEnabled?: boolean;
198
+ }
199
+ ): React.ComponentType<BoardItemViewProps<M>> {
200
+ return function EntityBoardCardWrapper(props: BoardItemViewProps<M>) {
201
+ return (
202
+ <EntityBoardCard
203
+ {...props}
204
+ collection={collection}
205
+ onClick={options.onClick}
206
+ selected={options.isEntitySelected?.(props.item.entity)}
207
+ onSelectionChange={options.onSelectionChange}
208
+ selectionEnabled={options.selectionEnabled}
209
+ />
210
+ );
211
+ };
212
+ }
@@ -0,0 +1,235 @@
1
+ import React, { useMemo } from "react";
2
+ import {
3
+ CollectionSize,
4
+ Entity,
5
+ EntityCollection,
6
+ ResolvedProperty
7
+ } from "../../types";
8
+ import {
9
+ getEntityImagePreviewPropertyKey,
10
+ getEntityPreviewKeys,
11
+ getEntityTitlePropertyKey,
12
+ getValueInPath,
13
+ IconForView,
14
+ resolveCollection
15
+ } from "../../util";
16
+ import {
17
+ Card,
18
+ Checkbox,
19
+ cls,
20
+ KeyboardTabIcon,
21
+ IconButton,
22
+ Skeleton,
23
+ Tooltip,
24
+ Typography
25
+ } from "@firecms/ui";
26
+ import { PropertyPreview, SkeletonPropertyComponent } from "../../preview";
27
+ import {
28
+ useAuthController,
29
+ useCustomizationController,
30
+ useNavigationController,
31
+ useSideEntityController
32
+ } from "../../hooks";
33
+ import { useAnalyticsController } from "../../hooks/useAnalyticsController";
34
+
35
+ export type EntityCardProps<M extends Record<string, any> = any> = {
36
+ entity: Entity<M>;
37
+ collection: EntityCollection<M>;
38
+ onClick?: (entity: Entity<M>) => void;
39
+ selected?: boolean;
40
+ highlighted?: boolean;
41
+ onSelectionChange?: (entity: Entity<M>, selected: boolean) => void;
42
+ selectionEnabled?: boolean;
43
+ /**
44
+ * Size of the card - affects checkbox styling
45
+ */
46
+ size?: CollectionSize;
47
+ };
48
+
49
+ /**
50
+ * Card component for displaying an entity in a grid view.
51
+ * Shows thumbnail, title, and preview properties.
52
+ */
53
+ export function EntityCard<M extends Record<string, any> = any>({
54
+ entity,
55
+ collection,
56
+ onClick,
57
+ selected,
58
+ highlighted,
59
+ onSelectionChange,
60
+ selectionEnabled,
61
+ size = "m"
62
+ }: EntityCardProps<M>) {
63
+ const authController = useAuthController();
64
+ const analyticsController = useAnalyticsController();
65
+ const sideEntityController = useSideEntityController();
66
+ const customizationController = useCustomizationController();
67
+ const navigationController = useNavigationController();
68
+
69
+ const resolvedCollection = useMemo(() => resolveCollection({
70
+ collection,
71
+ path: entity.path,
72
+ values: entity.values,
73
+ propertyConfigs: customizationController.propertyConfigs,
74
+ authController
75
+ }), [collection, entity.path, entity.values]);
76
+
77
+ const titlePropertyKey = useMemo(
78
+ () => getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs),
79
+ [resolvedCollection, customizationController.propertyConfigs]
80
+ );
81
+
82
+ const imagePropertyKey = useMemo(
83
+ () => getEntityImagePreviewPropertyKey(resolvedCollection),
84
+ [resolvedCollection]
85
+ );
86
+
87
+ const previewKeys = useMemo(
88
+ () => getEntityPreviewKeys(authController, resolvedCollection, customizationController.propertyConfigs, undefined, 2)
89
+ .filter(key => key !== titlePropertyKey && key !== imagePropertyKey),
90
+ [authController, resolvedCollection, customizationController.propertyConfigs, titlePropertyKey, imagePropertyKey]
91
+ );
92
+
93
+ const imageProperty = imagePropertyKey ? resolvedCollection.properties[imagePropertyKey] : undefined;
94
+ const usedImageProperty = imageProperty && "of" in imageProperty ? imageProperty.of : imageProperty;
95
+
96
+ const imageValue = imagePropertyKey ? getValueInPath(entity.values, imagePropertyKey) : undefined;
97
+ const usedImageValue = imageProperty !== undefined
98
+ ? ("of" in imageProperty
99
+ ? ((imageValue ?? []).length > 0 ? imageValue[0] : undefined)
100
+ : imageValue)
101
+ : undefined;
102
+
103
+ const titleValue = titlePropertyKey ? getValueInPath(entity.values, titlePropertyKey) : undefined;
104
+ const titleProperty = titlePropertyKey ? resolvedCollection.properties[titlePropertyKey] as ResolvedProperty : undefined;
105
+
106
+ const handleClick = (e?: React.MouseEvent) => {
107
+ // Cmd+click (Mac) or Ctrl+click (Windows) toggles selection
108
+ if (e && (e.metaKey || e.ctrlKey) && selectionEnabled) {
109
+ e.preventDefault();
110
+ onSelectionChange?.(entity, !selected);
111
+ return;
112
+ }
113
+ if (onClick) {
114
+ analyticsController.onAnalyticsEvent?.("card_view_entity_click", {
115
+ path: entity.path,
116
+ entityId: entity.id
117
+ });
118
+ onClick(entity);
119
+ }
120
+ };
121
+
122
+ const handleCheckboxClick = (e: React.MouseEvent) => {
123
+ e.stopPropagation();
124
+ };
125
+
126
+ const handleSelectionChange = (checked: boolean) => {
127
+ onSelectionChange?.(entity, checked);
128
+ };
129
+
130
+
131
+ return (
132
+ <Card
133
+ className={cls(
134
+ "cursor-pointer overflow-hidden group relative",
135
+ "transition-all duration-200",
136
+ "hover:shadow-lg hover:-translate-y-0.5",
137
+ selected && "ring-2 ring-primary",
138
+ highlighted && !selected && "ring-2 ring-primary ring-opacity-50"
139
+ )}
140
+ onClick={handleClick}
141
+ >
142
+ {/* Thumbnail area */}
143
+ <div className="aspect-[4/3] relative overflow-hidden bg-surface-100 dark:bg-surface-800">
144
+ {usedImageProperty && usedImageValue ? (
145
+ <div className="w-full h-full">
146
+ <PropertyPreview
147
+ property={usedImageProperty}
148
+ propertyKey={imagePropertyKey as string}
149
+ size="medium"
150
+ value={usedImageValue}
151
+ fill={true}
152
+ />
153
+ </div>
154
+ ) : (
155
+ <div className="w-full h-full flex items-center justify-center">
156
+ <IconForView
157
+ collectionOrView={collection}
158
+ color="disabled"
159
+ size="large"
160
+ />
161
+ </div>
162
+ )}
163
+
164
+ {/* Hover overlay */}
165
+ <div className={cls(
166
+ "absolute inset-0 bg-black/0 group-hover:bg-black/10",
167
+ "transition-colors duration-200"
168
+ )} />
169
+
170
+ {/* Selection checkbox */}
171
+ {selectionEnabled && (
172
+ <div
173
+ className={cls(
174
+ "absolute",
175
+ size === "xs" || size === "s" ? "top-1 left-1" : "top-2 left-2"
176
+ )}
177
+ onClick={handleCheckboxClick}
178
+ >
179
+ <Checkbox
180
+ checked={selected ?? false}
181
+ onCheckedChange={handleSelectionChange}
182
+ size={size === "xs" ? "smallest" : "small"}
183
+ />
184
+ </div>
185
+ )}
186
+
187
+ </div>
188
+
189
+ {/* Content area */}
190
+ <div className="p-3">
191
+ {/* Entity ID */}
192
+ <Typography
193
+ variant="caption"
194
+ color="disabled"
195
+ className="font-mono truncate block"
196
+ >
197
+ {entity.id}
198
+ </Typography>
199
+
200
+ {/* Title */}
201
+ <div className="truncate my-1 text-sm font-medium min-h-[20px]">
202
+ {titleProperty && titleValue ? (
203
+ <PropertyPreview
204
+ propertyKey={titlePropertyKey as string}
205
+ value={titleValue}
206
+ property={titleProperty}
207
+ size="small"
208
+ />
209
+ ) : (
210
+ <Typography variant="body2" className="text-surface-500">
211
+ {entity.id}
212
+ </Typography>
213
+ )}
214
+ </div>
215
+
216
+ {/* Preview properties */}
217
+ {previewKeys.slice(0, 2).map((key) => {
218
+ const property = resolvedCollection.properties[key] as ResolvedProperty;
219
+ if (!property) return null;
220
+ const value = getValueInPath(entity.values, key);
221
+ return (
222
+ <div key={key} className="truncate text-xs text-surface-600 dark:text-surface-400">
223
+ <PropertyPreview
224
+ propertyKey={key}
225
+ value={value}
226
+ property={property}
227
+ size="small"
228
+ />
229
+ </div>
230
+ );
231
+ })}
232
+ </div>
233
+ </Card>
234
+ );
235
+ }