@firecms/core 3.0.0-canary.269 → 3.0.0-canary.270
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/HomePage/NavigationGroup.d.ts +3 -2
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useCollapsedGroups.d.ts +9 -0
- package/dist/index.es.js +156 -60
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +155 -59
- package/dist/index.umd.js.map +1 -1
- package/package.json +5 -5
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/HomePage/DefaultHomePage.tsx +18 -5
- package/src/components/HomePage/NavigationGroup.tsx +121 -41
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
- package/src/hooks/useCollapsedGroups.ts +64 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.0-canary.
|
|
4
|
+
"version": "3.0.0-canary.270",
|
|
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.0.0-canary.
|
|
57
|
-
"@firecms/formex": "^3.0.0-canary.
|
|
58
|
-
"@firecms/ui": "^3.0.0-canary.
|
|
56
|
+
"@firecms/editor": "^3.0.0-canary.270",
|
|
57
|
+
"@firecms/formex": "^3.0.0-canary.270",
|
|
58
|
+
"@firecms/ui": "^3.0.0-canary.270",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.9",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
61
61
|
"date-fns": "^3.6.0",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"dist",
|
|
109
109
|
"src"
|
|
110
110
|
],
|
|
111
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "ce08b2d307b70de5897d32f14a340f1601ea22a4",
|
|
112
112
|
"publishConfig": {
|
|
113
113
|
"access": "public"
|
|
114
114
|
},
|
|
@@ -26,7 +26,7 @@ export function ErrorView({
|
|
|
26
26
|
tooltip
|
|
27
27
|
}: ErrorViewProps): React.ReactElement {
|
|
28
28
|
const component = error instanceof Error ? error.message : error;
|
|
29
|
-
|
|
29
|
+
console.warn("ErrorView", JSON.stringify(error))
|
|
30
30
|
|
|
31
31
|
const body = (
|
|
32
32
|
<div
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
|
2
2
|
import Fuse from "fuse.js";
|
|
3
3
|
import { Container, SearchBar } from "@firecms/ui";
|
|
4
4
|
import { useCustomizationController, useFireCMSContext, useNavigationController } from "../../hooks";
|
|
5
|
+
import { useCollapsedGroups } from "../../hooks/useCollapsedGroups";
|
|
5
6
|
import {
|
|
6
7
|
CMSAnalyticsEvent,
|
|
7
8
|
NavigationEntry,
|
|
@@ -194,9 +195,15 @@ export function DefaultHomePage({
|
|
|
194
195
|
onNavigationEntriesUpdate(all);
|
|
195
196
|
};
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// Use custom hook for collapsed groups with localStorage persistence
|
|
199
|
+
const groupNames = useMemo(() => [
|
|
200
|
+
...items.map(item => item.name),
|
|
201
|
+
...(adminGroupData ? [adminGroupData.name] : [])
|
|
202
|
+
], [items, adminGroupData]);
|
|
203
|
+
|
|
204
|
+
const { isGroupCollapsed, toggleGroupCollapsed } = useCollapsedGroups(groupNames);
|
|
205
|
+
|
|
206
|
+
|
|
200
207
|
const {
|
|
201
208
|
sensors,
|
|
202
209
|
collisionDetection,
|
|
@@ -303,7 +310,7 @@ export function DefaultHomePage({
|
|
|
303
310
|
|
|
304
311
|
/* ───────────────────────────────────────────────────────────────
|
|
305
312
|
Render
|
|
306
|
-
|
|
313
|
+
─────────────────────────────────────────────────────────────── */
|
|
307
314
|
return (
|
|
308
315
|
<div ref={containerRef} className="py-2 overflow-auto h-full w-full">
|
|
309
316
|
<Container maxWidth="6xl">
|
|
@@ -400,6 +407,8 @@ export function DefaultHomePage({
|
|
|
400
407
|
if (dndDisabled) return;
|
|
401
408
|
setDialogOpenForGroup(groupKey);
|
|
402
409
|
}}
|
|
410
|
+
collapsed={isGroupCollapsed(groupKey)}
|
|
411
|
+
onToggleCollapsed={() => toggleGroupCollapsed(groupKey)}
|
|
403
412
|
>
|
|
404
413
|
<NavigationGroupDroppable
|
|
405
414
|
id={groupKey}
|
|
@@ -503,7 +512,11 @@ export function DefaultHomePage({
|
|
|
503
512
|
</DndContext>
|
|
504
513
|
|
|
505
514
|
{!performingSearch && adminGroupData && (
|
|
506
|
-
<NavigationGroup
|
|
515
|
+
<NavigationGroup
|
|
516
|
+
group={adminGroupData.name}
|
|
517
|
+
collapsed={isGroupCollapsed(adminGroupData.name)}
|
|
518
|
+
onToggleCollapsed={() => toggleGroupCollapsed(adminGroupData.name)}
|
|
519
|
+
>
|
|
507
520
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 ">
|
|
508
521
|
{adminGroupData.entries.map((entry) => (
|
|
509
522
|
<NavigationCardBinding
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { PropsWithChildren, useState } from "react";
|
|
2
|
-
import { cls, EditIcon, IconButton, Typography } from "@firecms/ui";
|
|
2
|
+
import { cls, EditIcon, IconButton, Typography, ExpandablePanel } from "@firecms/ui";
|
|
3
3
|
|
|
4
4
|
export function NavigationGroup({
|
|
5
5
|
children,
|
|
@@ -7,66 +7,146 @@ export function NavigationGroup({
|
|
|
7
7
|
minimised,
|
|
8
8
|
isPreview,
|
|
9
9
|
isPotentialCardDropTarget,
|
|
10
|
-
onEditGroup,
|
|
11
|
-
dndDisabled
|
|
10
|
+
onEditGroup,
|
|
11
|
+
dndDisabled,
|
|
12
|
+
collapsed,
|
|
13
|
+
onToggleCollapsed
|
|
12
14
|
}: PropsWithChildren<{
|
|
13
15
|
group: string | undefined,
|
|
14
16
|
minimised?: boolean,
|
|
15
17
|
isPreview?: boolean,
|
|
16
18
|
isPotentialCardDropTarget?: boolean,
|
|
17
|
-
onEditGroup?: (groupName: string) => void;
|
|
18
|
-
dndDisabled?: boolean;
|
|
19
|
+
onEditGroup?: (groupName: string) => void;
|
|
20
|
+
dndDisabled?: boolean;
|
|
21
|
+
collapsed?: boolean;
|
|
22
|
+
onToggleCollapsed?: () => void;
|
|
19
23
|
}>) {
|
|
20
24
|
|
|
21
25
|
const [isHovered, setIsHovered] = useState(false);
|
|
22
26
|
const currentGroupName = group ?? "Views";
|
|
23
27
|
|
|
28
|
+
// Show caret only when not in preview and there is a toggle handler
|
|
29
|
+
const showCaret = !isPreview && !!onToggleCollapsed;
|
|
30
|
+
|
|
31
|
+
// Helper for the title content (left side)
|
|
32
|
+
const TitleContent = (
|
|
33
|
+
<div className={cls("flex items-center", isPreview ? "px-1 py-0.5" : "")}
|
|
34
|
+
>
|
|
35
|
+
<Typography
|
|
36
|
+
variant={isPreview ? "body2" : "caption"}
|
|
37
|
+
component={"h2"}
|
|
38
|
+
color="secondary"
|
|
39
|
+
className={cls(
|
|
40
|
+
"p-4 py-2 rounded",
|
|
41
|
+
"font-medium uppercase text-sm text-surface-600 dark:text-surface-400"
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{currentGroupName}
|
|
45
|
+
</Typography>
|
|
46
|
+
{!isPreview && onEditGroup && !dndDisabled && (
|
|
47
|
+
<IconButton
|
|
48
|
+
size="smallest"
|
|
49
|
+
onClick={(e) => {
|
|
50
|
+
e.stopPropagation(); // Prevent toggle on click
|
|
51
|
+
onEditGroup(currentGroupName);
|
|
52
|
+
}}
|
|
53
|
+
className={cls("ml-2 ", isHovered ? "opacity-100" : "opacity-0", "transition-opacity duration-100")}
|
|
54
|
+
>
|
|
55
|
+
<EditIcon size="smallest"/>
|
|
56
|
+
</IconButton>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
|
|
24
61
|
return (
|
|
25
62
|
<div className={cls(
|
|
26
63
|
!isPotentialCardDropTarget ? "my-10" : "my-6",
|
|
27
64
|
"transition-all duration-200 ease-in-out"
|
|
28
65
|
)}
|
|
29
66
|
>
|
|
30
|
-
|
|
67
|
+
{/* Preview: static header + content (no caret / no collapse) */}
|
|
68
|
+
{isPreview && (
|
|
69
|
+
<>
|
|
70
|
+
<div
|
|
71
|
+
className={cls(
|
|
72
|
+
"flex items-center justify-between w-full",
|
|
73
|
+
"p-4 py-2"
|
|
74
|
+
)}
|
|
75
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
76
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
77
|
+
>
|
|
78
|
+
{TitleContent}
|
|
79
|
+
</div>
|
|
80
|
+
{children}
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
31
83
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
84
|
+
{/* Interactive collapsible version when a toggle handler is provided */}
|
|
85
|
+
{!isPreview && showCaret && (
|
|
86
|
+
<ExpandablePanel
|
|
87
|
+
invisible
|
|
88
|
+
expanded={!collapsed}
|
|
89
|
+
onExpandedChange={(open) => {
|
|
90
|
+
if (open !== !collapsed) {
|
|
91
|
+
onToggleCollapsed?.();
|
|
92
|
+
}
|
|
93
|
+
}}
|
|
94
|
+
className={cls("mt-6")}
|
|
95
|
+
titleClassName={cls(
|
|
96
|
+
"min-h-0 p-0 border-none",
|
|
97
|
+
"rounded-t flex items-center justify-between w-full",
|
|
98
|
+
"hover:bg-transparent",
|
|
99
|
+
"cursor-pointer select-none"
|
|
100
|
+
)}
|
|
101
|
+
innerClassName={cls("mt-4", !minimised ? "pt-0" : "")}
|
|
102
|
+
title={
|
|
103
|
+
<div
|
|
104
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
105
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
106
|
+
className="flex items-center"
|
|
107
|
+
>
|
|
108
|
+
{TitleContent}
|
|
109
|
+
</div>
|
|
110
|
+
}
|
|
40
111
|
>
|
|
41
|
-
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
112
|
+
{minimised ? (
|
|
113
|
+
<div className={cls("mt-4 p-8 bg-surface-accent-200 dark:bg-surface-accent-800 rounded-lg")}
|
|
114
|
+
style={{ minHeight: "50px" }}>
|
|
115
|
+
</div>
|
|
116
|
+
) : (
|
|
117
|
+
<div className={cls("mt-4", !minimised ? "pt-0" : "")}>
|
|
118
|
+
{children}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</ExpandablePanel>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{/* Non-collapsible (no caret) runtime, keep old behavior */}
|
|
125
|
+
{!isPreview && !showCaret && (
|
|
126
|
+
<>
|
|
127
|
+
<div
|
|
128
|
+
className={cls(
|
|
129
|
+
"flex items-center justify-between w-full",
|
|
130
|
+
"mt-6"
|
|
131
|
+
)}
|
|
132
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
133
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
51
134
|
>
|
|
52
|
-
|
|
53
|
-
</
|
|
54
|
-
)}
|
|
55
|
-
</div>
|
|
135
|
+
{TitleContent}
|
|
136
|
+
</div>
|
|
56
137
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
</div>
|
|
138
|
+
{!collapsed && (
|
|
139
|
+
minimised ? (
|
|
140
|
+
<div className={cls("mt-4 p-8 bg-surface-accent-200 dark:bg-surface-accent-800 rounded-lg")}
|
|
141
|
+
style={{ minHeight: "50px" }}>
|
|
142
|
+
</div>
|
|
143
|
+
) : (
|
|
144
|
+
<div className={cls("mt-4", !minimised ? "pt-0" : "")}>
|
|
145
|
+
{children}
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
)}
|
|
149
|
+
</>
|
|
70
150
|
)}
|
|
71
151
|
</div>
|
|
72
152
|
);
|
package/src/hooks/index.tsx
CHANGED
|
@@ -18,6 +18,7 @@ export * from "./useSnackbarController";
|
|
|
18
18
|
export * from "./useModeController";
|
|
19
19
|
export * from "./useClipboard";
|
|
20
20
|
export * from "./useLargeLayout";
|
|
21
|
+
export * from "./useCollapsedGroups";
|
|
21
22
|
|
|
22
23
|
export * from "./useReferenceDialog";
|
|
23
24
|
export * from "./useBrowserTitleAndIcon";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
|
|
3
|
-
const fireCMSLogo = "data:image/png;base64,
|
|
3
|
+
const fireCMSLogo = "data:image/png;base64,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";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Internal hook to handle the browser title and icon
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook for managing collapsed/expanded state of navigation groups
|
|
5
|
+
* with localStorage persistence. Automatically cleans up stale group entries
|
|
6
|
+
* when groups are removed from the navigation.
|
|
7
|
+
*/
|
|
8
|
+
export function useCollapsedGroups(groupNames: string[]) {
|
|
9
|
+
// Load collapsed groups from localStorage on mount
|
|
10
|
+
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>(() => {
|
|
11
|
+
try {
|
|
12
|
+
const stored = localStorage.getItem('firecms-collapsed-groups');
|
|
13
|
+
return stored ? JSON.parse(stored) : {};
|
|
14
|
+
} catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Save to localStorage whenever collapsedGroups changes
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
try {
|
|
22
|
+
localStorage.setItem('firecms-collapsed-groups', JSON.stringify(collapsedGroups));
|
|
23
|
+
} catch {
|
|
24
|
+
// Silently fail if localStorage is not available
|
|
25
|
+
}
|
|
26
|
+
}, [collapsedGroups]);
|
|
27
|
+
|
|
28
|
+
// Clean up collapsed groups state when groups change - remove entries for groups that no longer exist
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// Only clean up if we have actual groups loaded (avoid cleaning up during initial load)
|
|
31
|
+
if (groupNames.length === 0) return;
|
|
32
|
+
|
|
33
|
+
const currentGroupNames = new Set(groupNames);
|
|
34
|
+
|
|
35
|
+
setCollapsedGroups(prev => {
|
|
36
|
+
const cleaned = Object.fromEntries(
|
|
37
|
+
Object.entries(prev).filter(([groupName]) => currentGroupNames.has(groupName))
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Only update if something actually changed
|
|
41
|
+
const prevKeys = Object.keys(prev);
|
|
42
|
+
const cleanedKeys = Object.keys(cleaned);
|
|
43
|
+
|
|
44
|
+
if (prevKeys.length === cleanedKeys.length && prevKeys.every(key => cleanedKeys.includes(key))) {
|
|
45
|
+
return prev;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return cleaned;
|
|
49
|
+
});
|
|
50
|
+
}, [groupNames]);
|
|
51
|
+
|
|
52
|
+
const isGroupCollapsed = useCallback((name: string) => {
|
|
53
|
+
return !!collapsedGroups[name];
|
|
54
|
+
}, [collapsedGroups]);
|
|
55
|
+
|
|
56
|
+
const toggleGroupCollapsed = useCallback((name: string) => {
|
|
57
|
+
setCollapsedGroups(prev => ({ ...prev, [name]: !prev[name] }));
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
isGroupCollapsed,
|
|
62
|
+
toggleGroupCollapsed
|
|
63
|
+
};
|
|
64
|
+
}
|