@firecms/core 3.0.1 → 3.1.0-canary.1df3b2c
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.
- package/README.md +1 -1
- package/dist/components/AIIcon.d.ts +16 -0
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
- package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
- package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
- package/dist/components/EntityCollectionView/Board.d.ts +2 -0
- package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
- package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
- package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
- package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
- package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
- package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
- package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +49 -0
- package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
- package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
- package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
- package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
- package/dist/components/VirtualTable/VirtualTableHeader.d.ts +2 -0
- package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
- package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
- package/dist/components/VirtualTable/types.d.ts +2 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/contexts/index.d.ts +10 -0
- package/dist/core/DrawerNavigationGroup.d.ts +45 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/form/validation.d.ts +3 -2
- package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
- package/dist/hooks/useCollapsedGroups.d.ts +4 -1
- package/dist/index.es.js +5239 -1590
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5233 -1585
- package/dist/index.umd.js.map +1 -1
- package/dist/preview/PropertyPreviewProps.d.ts +5 -0
- package/dist/preview/components/DatePreview.d.ts +13 -3
- package/dist/preview/components/ImagePreview.d.ts +5 -1
- package/dist/preview/components/StorageThumbnail.d.ts +2 -1
- package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
- package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
- package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
- package/dist/types/collections.d.ts +42 -2
- package/dist/types/datasource.d.ts +0 -1
- package/dist/types/plugins.d.ts +46 -1
- package/dist/types/properties.d.ts +259 -4
- package/dist/util/__tests__/conditions.test.d.ts +1 -0
- package/dist/util/__tests__/objects.test.d.ts +1 -0
- package/dist/util/conditions.d.ts +26 -0
- package/dist/util/entities.d.ts +1 -2
- package/dist/util/index.d.ts +2 -1
- package/dist/util/property_utils.d.ts +2 -1
- package/dist/util/resolutions.d.ts +1 -1
- package/package.json +10 -7
- package/src/app/Scaffold.tsx +14 -15
- package/src/components/AIIcon.tsx +39 -0
- package/src/components/ArrayContainer.tsx +1 -4
- package/src/components/ClearFilterSortButton.tsx +19 -16
- package/src/components/ConfirmationDialog.tsx +0 -2
- package/src/components/DeleteEntityDialog.tsx +2 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
- package/src/components/EntityCollectionView/Board.tsx +324 -0
- package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
- package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
- package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
- package/src/components/EntityCollectionView/EntityCard.tsx +231 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +713 -0
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +485 -203
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
- package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +202 -0
- package/src/components/EntityCollectionView/board_types.ts +113 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
- package/src/components/ErrorTooltip.tsx +2 -1
- package/src/components/HomePage/DefaultHomePage.tsx +47 -10
- package/src/components/HomePage/HomePageDnD.tsx +56 -41
- package/src/components/HomePage/NavigationCard.tsx +20 -18
- package/src/components/HomePage/NavigationGroup.tsx +17 -16
- package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
- package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
- package/src/components/ReferenceWidget.tsx +2 -4
- package/src/components/SelectableTable/SelectableTable.tsx +75 -67
- package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
- package/src/components/UnsavedChangesDialog.tsx +0 -2
- package/src/components/UserDisplay.tsx +4 -4
- package/src/components/VirtualTable/VirtualTable.tsx +170 -19
- package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
- package/src/components/VirtualTable/VirtualTableHeader.tsx +20 -11
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
- package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
- package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +17 -4
- package/src/components/VirtualTable/types.tsx +2 -0
- package/src/components/common/useColumnsIds.tsx +95 -3
- package/src/components/index.tsx +4 -0
- package/src/contexts/BreacrumbsContext.tsx +15 -8
- package/src/contexts/index.ts +10 -0
- package/src/core/DefaultAppBar.tsx +39 -26
- package/src/core/DefaultDrawer.tsx +42 -56
- package/src/core/DrawerNavigationGroup.tsx +118 -0
- package/src/core/DrawerNavigationItem.tsx +4 -3
- package/src/core/EntityEditView.tsx +41 -43
- package/src/core/SideDialogs.tsx +4 -2
- package/src/core/index.tsx +1 -0
- package/src/form/PropertyFieldBinding.tsx +58 -43
- package/src/form/components/StorageItemPreview.tsx +2 -1
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +21 -17
- package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
- package/src/form/validation.ts +245 -160
- package/src/hooks/useBreadcrumbsController.tsx +18 -0
- package/src/hooks/useBuildNavigationController.tsx +42 -19
- package/src/hooks/useCollapsedGroups.ts +12 -4
- package/src/internal/useBuildDataSource.ts +69 -34
- package/src/internal/useBuildSideDialogsController.tsx +11 -8
- package/src/internal/useBuildSideEntityController.tsx +2 -4
- package/src/internal/useRestoreScroll.tsx +26 -14
- package/src/preview/PropertyPreview.tsx +40 -32
- package/src/preview/PropertyPreviewProps.tsx +6 -0
- package/src/preview/components/DatePreview.tsx +72 -4
- package/src/preview/components/EmptyValue.tsx +1 -1
- package/src/preview/components/ImagePreview.tsx +37 -21
- package/src/preview/components/StorageThumbnail.tsx +16 -12
- package/src/preview/components/UrlComponentPreview.tsx +28 -25
- package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
- package/src/routes/CustomCMSRoute.tsx +1 -0
- package/src/routes/FireCMSRoute.tsx +26 -13
- package/src/types/collections.ts +48 -3
- package/src/types/datasource.ts +54 -56
- package/src/types/plugins.tsx +51 -1
- package/src/types/properties.ts +347 -27
- package/src/util/__tests__/conditions.test.ts +506 -0
- package/src/util/__tests__/objects.test.ts +196 -0
- package/src/util/callbacks.ts +6 -3
- package/src/util/collections.ts +51 -6
- package/src/util/conditions.ts +339 -0
- package/src/util/entities.ts +28 -29
- package/src/util/entity_cache.ts +2 -1
- package/src/util/index.ts +2 -1
- package/src/util/objects.ts +31 -13
- package/src/util/{references.ts → previews.ts} +14 -0
- package/src/util/property_utils.tsx +36 -10
- package/src/util/resolutions.ts +57 -55
- /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
|
@@ -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="truncate 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,231 @@
|
|
|
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
|
+
onClick(entity);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleCheckboxClick = (e: React.MouseEvent) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleSelectionChange = (checked: boolean) => {
|
|
123
|
+
onSelectionChange?.(entity, checked);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Card
|
|
129
|
+
className={cls(
|
|
130
|
+
"cursor-pointer overflow-hidden group relative",
|
|
131
|
+
"transition-all duration-200",
|
|
132
|
+
"hover:shadow-lg hover:-translate-y-0.5",
|
|
133
|
+
selected && "ring-2 ring-primary",
|
|
134
|
+
highlighted && !selected && "ring-2 ring-primary ring-opacity-50"
|
|
135
|
+
)}
|
|
136
|
+
onClick={handleClick}
|
|
137
|
+
>
|
|
138
|
+
{/* Thumbnail area */}
|
|
139
|
+
<div className="aspect-[4/3] relative overflow-hidden bg-surface-100 dark:bg-surface-800">
|
|
140
|
+
{usedImageProperty && usedImageValue ? (
|
|
141
|
+
<div className="w-full h-full">
|
|
142
|
+
<PropertyPreview
|
|
143
|
+
property={usedImageProperty}
|
|
144
|
+
propertyKey={imagePropertyKey as string}
|
|
145
|
+
size="medium"
|
|
146
|
+
value={usedImageValue}
|
|
147
|
+
fill={true}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
) : (
|
|
151
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
152
|
+
<IconForView
|
|
153
|
+
collectionOrView={collection}
|
|
154
|
+
color="disabled"
|
|
155
|
+
size="large"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* Hover overlay */}
|
|
161
|
+
<div className={cls(
|
|
162
|
+
"absolute inset-0 bg-black/0 group-hover:bg-black/10",
|
|
163
|
+
"transition-colors duration-200"
|
|
164
|
+
)} />
|
|
165
|
+
|
|
166
|
+
{/* Selection checkbox */}
|
|
167
|
+
{selectionEnabled && (
|
|
168
|
+
<div
|
|
169
|
+
className={cls(
|
|
170
|
+
"absolute",
|
|
171
|
+
size === "xs" || size === "s" ? "top-1 left-1" : "top-2 left-2"
|
|
172
|
+
)}
|
|
173
|
+
onClick={handleCheckboxClick}
|
|
174
|
+
>
|
|
175
|
+
<Checkbox
|
|
176
|
+
checked={selected ?? false}
|
|
177
|
+
onCheckedChange={handleSelectionChange}
|
|
178
|
+
size={size === "xs" ? "smallest" : "small"}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Content area */}
|
|
186
|
+
<div className="p-3">
|
|
187
|
+
{/* Entity ID */}
|
|
188
|
+
<Typography
|
|
189
|
+
variant="caption"
|
|
190
|
+
color="disabled"
|
|
191
|
+
className="font-mono truncate block"
|
|
192
|
+
>
|
|
193
|
+
{entity.id}
|
|
194
|
+
</Typography>
|
|
195
|
+
|
|
196
|
+
{/* Title */}
|
|
197
|
+
<div className="truncate my-1 text-sm font-medium min-h-[20px]">
|
|
198
|
+
{titleProperty && titleValue ? (
|
|
199
|
+
<PropertyPreview
|
|
200
|
+
propertyKey={titlePropertyKey as string}
|
|
201
|
+
value={titleValue}
|
|
202
|
+
property={titleProperty}
|
|
203
|
+
size="small"
|
|
204
|
+
/>
|
|
205
|
+
) : (
|
|
206
|
+
<Typography variant="body2" className="text-surface-500">
|
|
207
|
+
{entity.id}
|
|
208
|
+
</Typography>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Preview properties */}
|
|
213
|
+
{previewKeys.slice(0, 2).map((key) => {
|
|
214
|
+
const property = resolvedCollection.properties[key] as ResolvedProperty;
|
|
215
|
+
if (!property) return null;
|
|
216
|
+
const value = getValueInPath(entity.values, key);
|
|
217
|
+
return (
|
|
218
|
+
<div key={key} className="truncate text-xs text-surface-600 dark:text-surface-400">
|
|
219
|
+
<PropertyPreview
|
|
220
|
+
propertyKey={key}
|
|
221
|
+
value={value}
|
|
222
|
+
property={property}
|
|
223
|
+
size="small"
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
</Card>
|
|
230
|
+
);
|
|
231
|
+
}
|