@firecms/core 3.1.0-canary.1df3b2c → 3.1.0-canary.8958c1b
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/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
- package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +5 -10
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -1
- package/dist/form/components/ErrorFocus.d.ts +1 -1
- package/dist/index.es.js +247 -210
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +246 -209
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useRestoreScroll.d.ts +1 -1
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +8 -0
- package/dist/types/plugins.d.ts +16 -0
- package/dist/util/entities.d.ts +1 -1
- package/dist/util/resolutions.d.ts +2 -2
- package/package.json +7 -7
- package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +1 -1
- package/src/components/EntityCollectionView/EntityCard.tsx +4 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +23 -3
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +50 -16
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +27 -30
- package/src/components/VirtualTable/VirtualTable.tsx +116 -113
- package/src/components/VirtualTable/VirtualTableHeader.tsx +42 -42
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +1 -1
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +3 -3
- package/src/core/DefaultAppBar.tsx +1 -1
- package/src/core/EntitySidePanel.tsx +28 -26
- package/src/core/field_configs.tsx +14 -9
- package/src/form/EntityForm.tsx +69 -60
- package/src/form/PropertyFieldBinding.tsx +3 -3
- package/src/form/components/ErrorFocus.tsx +3 -3
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +1 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
- package/src/hooks/useBuildNavigationController.tsx +4 -4
- package/src/hooks/useValidateAuthenticator.tsx +1 -1
- package/src/internal/useBuildDataSource.ts +1 -2
- package/src/preview/PropertyPreview.tsx +1 -0
- package/src/types/analytics.ts +10 -0
- package/src/types/collections.ts +9 -0
- package/src/types/plugins.tsx +18 -0
- package/src/util/entities.ts +1 -1
- package/src/util/previews.ts +2 -2
- package/src/util/property_utils.tsx +1 -1
- package/src/util/resolutions.ts +5 -3
|
@@ -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";
|
|
@@ -326,6 +326,14 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
326
326
|
* Defaults to "table".
|
|
327
327
|
*/
|
|
328
328
|
defaultViewMode?: ViewMode;
|
|
329
|
+
/**
|
|
330
|
+
* Which view modes are available for this collection.
|
|
331
|
+
* Possible values: "table", "cards", "kanban".
|
|
332
|
+
* Defaults to all three: ["table", "cards", "kanban"].
|
|
333
|
+
* Note: "kanban" will only be available if the collection has at least
|
|
334
|
+
* one string property with enumValues defined, regardless of this setting.
|
|
335
|
+
*/
|
|
336
|
+
enabledViews?: ViewMode[];
|
|
329
337
|
/**
|
|
330
338
|
* Configuration for Kanban board view mode.
|
|
331
339
|
* When set, the Kanban view mode becomes available.
|
package/dist/types/plugins.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/util/entities.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
4
|
+
"version": "3.1.0-canary.8958c1b",
|
|
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.
|
|
57
|
-
"@firecms/formex": "^3.1.0-canary.
|
|
58
|
-
"@firecms/ui": "^3.1.0-canary.
|
|
56
|
+
"@firecms/editor": "^3.1.0-canary.8958c1b",
|
|
57
|
+
"@firecms/formex": "^3.1.0-canary.8958c1b",
|
|
58
|
+
"@firecms/ui": "^3.1.0-canary.8958c1b",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.10",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
61
61
|
"compressorjs": "^1.2.1",
|
|
@@ -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": "^
|
|
93
|
-
"@types/react-dom": "^
|
|
92
|
+
"@types/react": "^19.1.0",
|
|
93
|
+
"@types/react-dom": "^19.1.0",
|
|
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": "
|
|
114
|
+
"gitHead": "c415ea47be324c74539718a2c108ea89fa78e3a1",
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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="
|
|
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
|
|
|
@@ -501,18 +506,24 @@ export const EntityCollectionView = React.memo(
|
|
|
501
506
|
authController,
|
|
502
507
|
}), [collection, fullPath]);
|
|
503
508
|
|
|
504
|
-
// Check if Kanban view is
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}, [
|
|
509
|
+
// Check if Kanban view is possible (collection has at least one string enum property)
|
|
510
|
+
const hasEnumProperty = useMemo(() => {
|
|
511
|
+
const properties = resolvedCollection.properties;
|
|
512
|
+
return Object.values(properties).some((prop: any) =>
|
|
513
|
+
prop && prop.dataType === "string" && prop.enumValues
|
|
514
|
+
);
|
|
515
|
+
}, [resolvedCollection.properties]);
|
|
511
516
|
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
517
|
+
// Compute the effective enabled views:
|
|
518
|
+
// - Start from collection.enabledViews (defaults to all three)
|
|
519
|
+
// - Filter out kanban if no enum properties exist
|
|
520
|
+
const enabledViews: ViewMode[] = useMemo(() => {
|
|
521
|
+
const configured = collection.enabledViews ?? ["table", "cards", "kanban"];
|
|
522
|
+
if (!hasEnumProperty) {
|
|
523
|
+
return configured.filter(v => v !== "kanban");
|
|
524
|
+
}
|
|
525
|
+
return configured;
|
|
526
|
+
}, [collection.enabledViews, hasEnumProperty]);
|
|
516
527
|
|
|
517
528
|
// Compute available enum properties for kanban column selection
|
|
518
529
|
const kanbanPropertyOptions: KanbanPropertyOption[] = useMemo(() => {
|
|
@@ -562,12 +573,16 @@ export const EntityCollectionView = React.memo(
|
|
|
562
573
|
|
|
563
574
|
// Handle kanban property change
|
|
564
575
|
const onKanbanPropertyChange = useCallback((property: string) => {
|
|
576
|
+
analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
|
|
577
|
+
path: fullPath,
|
|
578
|
+
property
|
|
579
|
+
});
|
|
565
580
|
setSelectedKanbanProperty(property);
|
|
566
581
|
// Save to local persistence
|
|
567
582
|
if (userConfigPersistence) {
|
|
568
583
|
onCollectionModifiedForUser(fullPath, { kanbanColumnProperty: property } as any);
|
|
569
584
|
}
|
|
570
|
-
}, [userConfigPersistence, onCollectionModifiedForUser, fullPath]);
|
|
585
|
+
}, [userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController]);
|
|
571
586
|
|
|
572
587
|
const getPropertyFor = useCallback(({
|
|
573
588
|
propertyKey,
|
|
@@ -819,8 +834,7 @@ export const EntityCollectionView = React.memo(
|
|
|
819
834
|
<ViewModeToggle
|
|
820
835
|
viewMode={viewMode}
|
|
821
836
|
onViewModeChange={onViewModeChange}
|
|
822
|
-
|
|
823
|
-
hasKanbanConfigPlugin={hasKanbanConfigPlugin}
|
|
837
|
+
enabledViews={enabledViews}
|
|
824
838
|
size={viewMode === "table" ? tableSize : viewMode === "cards" ? cardSize : undefined}
|
|
825
839
|
onSizeChanged={viewMode === "table" ? onTableSizeChanged : viewMode === "cards" ? setCardSize : undefined}
|
|
826
840
|
open={viewModePopoverOpen}
|
|
@@ -831,6 +845,24 @@ export const EntityCollectionView = React.memo(
|
|
|
831
845
|
/>
|
|
832
846
|
);
|
|
833
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
|
+
|
|
834
866
|
return (
|
|
835
867
|
<div className={cls("overflow-hidden h-full w-full rounded-md flex flex-col", className)}
|
|
836
868
|
ref={containerRef}>
|
|
@@ -867,7 +899,9 @@ export const EntityCollectionView = React.memo(
|
|
|
867
899
|
/>
|
|
868
900
|
|
|
869
901
|
{/* View content - only the view-specific content changes */}
|
|
870
|
-
{
|
|
902
|
+
{tableController.dataLoadingError && pluginErrorView
|
|
903
|
+
? pluginErrorView
|
|
904
|
+
: viewMode === "kanban" && enabledViews.includes("kanban") ? (
|
|
871
905
|
<EntityCollectionBoardView
|
|
872
906
|
key={`kanban-view-${fullPath}-${selectedKanbanProperty}`}
|
|
873
907
|
collection={collection}
|
|
@@ -22,16 +22,11 @@ export type ViewModeToggleProps = {
|
|
|
22
22
|
viewMode?: ViewMode;
|
|
23
23
|
onViewModeChange?: (mode: ViewMode) => void;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Which view modes are enabled for this collection.
|
|
26
|
+
* Only these modes will appear in the toggle.
|
|
27
|
+
* Defaults to all three: ["table", "cards", "kanban"].
|
|
27
28
|
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Whether a plugin exists that can configure Kanban (e.g., collection editor).
|
|
31
|
-
* When true, Kanban option is always shown (enabled or not based on kanbanEnabled).
|
|
32
|
-
* When false, Kanban option is shown but disabled.
|
|
33
|
-
*/
|
|
34
|
-
hasKanbanConfigPlugin?: boolean;
|
|
29
|
+
enabledViews?: ViewMode[];
|
|
35
30
|
/**
|
|
36
31
|
* Current size for card/table views
|
|
37
32
|
*/
|
|
@@ -62,11 +57,12 @@ export type ViewModeToggleProps = {
|
|
|
62
57
|
onKanbanPropertyChange?: (property: string) => void;
|
|
63
58
|
}
|
|
64
59
|
|
|
60
|
+
const ALL_VIEW_MODES: ViewMode[] = ["table", "cards", "kanban"];
|
|
61
|
+
|
|
65
62
|
export function ViewModeToggle({
|
|
66
63
|
viewMode = "table",
|
|
67
64
|
onViewModeChange,
|
|
68
|
-
|
|
69
|
-
hasKanbanConfigPlugin = false,
|
|
65
|
+
enabledViews = ALL_VIEW_MODES,
|
|
70
66
|
size,
|
|
71
67
|
onSizeChanged,
|
|
72
68
|
open,
|
|
@@ -93,16 +89,15 @@ export function ViewModeToggle({
|
|
|
93
89
|
return "List";
|
|
94
90
|
};
|
|
95
91
|
|
|
96
|
-
const showKanban = kanbanEnabled || hasKanbanConfigPlugin;
|
|
97
92
|
const showSizeSelector = size && onSizeChanged && (viewMode === "table" || viewMode === "cards");
|
|
98
93
|
const showKanbanPropertySelector = viewMode === "kanban" &&
|
|
99
94
|
kanbanPropertyOptions &&
|
|
100
95
|
kanbanPropertyOptions.length > 0 &&
|
|
101
96
|
onKanbanPropertyChange;
|
|
102
97
|
|
|
103
|
-
// Build toggle options
|
|
98
|
+
// Build toggle options based on enabledViews
|
|
104
99
|
const viewModeOptions: ToggleButtonOption<ViewMode>[] = useMemo(() => {
|
|
105
|
-
const
|
|
100
|
+
const allOptions: ToggleButtonOption<ViewMode>[] = [
|
|
106
101
|
{
|
|
107
102
|
value: "table",
|
|
108
103
|
label: "List",
|
|
@@ -112,20 +107,21 @@ export function ViewModeToggle({
|
|
|
112
107
|
value: "cards",
|
|
113
108
|
label: "Cards",
|
|
114
109
|
icon: <AppsIcon size="small" />
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
value: "kanban",
|
|
113
|
+
label: "Board",
|
|
114
|
+
icon: <ViewKanbanIcon size="small" />
|
|
115
115
|
}
|
|
116
116
|
];
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
value: "kanban",
|
|
121
|
-
label: "Board",
|
|
122
|
-
icon: <ViewKanbanIcon size="small" />,
|
|
123
|
-
disabled: !kanbanEnabled && !hasKanbanConfigPlugin
|
|
124
|
-
});
|
|
125
|
-
}
|
|
118
|
+
return allOptions.filter(option => enabledViews.includes(option.value));
|
|
119
|
+
}, [enabledViews]);
|
|
126
120
|
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
// Don't render if only one view is enabled
|
|
122
|
+
if (viewModeOptions.length <= 1 && !showSizeSelector) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
129
125
|
|
|
130
126
|
return (
|
|
131
127
|
<Popover
|
|
@@ -141,11 +137,13 @@ export function ViewModeToggle({
|
|
|
141
137
|
>
|
|
142
138
|
<div className="p-3 flex flex-col gap-3 min-w-[240px]">
|
|
143
139
|
{/* View mode toggle using ToggleButtonGroup */}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
{viewModeOptions.length > 1 && (
|
|
141
|
+
<ToggleButtonGroup
|
|
142
|
+
value={viewMode}
|
|
143
|
+
onValueChange={onViewModeChange}
|
|
144
|
+
options={viewModeOptions}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
149
147
|
|
|
150
148
|
{/* Size selector */}
|
|
151
149
|
{showSizeSelector && (
|
|
@@ -199,4 +197,3 @@ export function ViewModeToggle({
|
|
|
199
197
|
</Popover>
|
|
200
198
|
);
|
|
201
199
|
}
|
|
202
|
-
|