@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.269",
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.269",
57
- "@firecms/formex": "^3.0.0-canary.269",
58
- "@firecms/ui": "^3.0.0-canary.269",
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": "a6558e6ef9de00858888fb9b69d276921cc28da8",
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
- // console.warn("ErrorView", error)
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
- Hook for DnD
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 group={adminGroupData.name}>
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, // New prop to handle editing
11
- dndDisabled // New prop to disable editing when D&D is off
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; // Callback to open dialog
18
- dndDisabled?: boolean; // Added dndDisabled prop
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
- <div className={`flex items-center ${isPreview ? "px-1 py-0.5 m-0" : "ml-3.5 mt-6"} `}
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
- onMouseEnter={() => setIsHovered(true)}
33
- onMouseLeave={() => setIsHovered(false)}>
34
- <Typography
35
- variant={isPreview ? "body2" : "caption"}
36
- component={"h2"}
37
- color="secondary"
38
- // Minimal padding and no margin for preview title
39
- className={`${isPreview ? "px-1 py-0.5" : "ml-3.5"} font-medium uppercase text-sm text-surface-600 dark:text-surface-400`}
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
- {currentGroupName}
42
- </Typography>
43
- {!isPreview && onEditGroup && !dndDisabled && (
44
- <IconButton
45
- size="smallest"
46
- onClick={(e) => {
47
- e.stopPropagation(); // Prevent other click events
48
- onEditGroup(currentGroupName);
49
- }}
50
- className={cls("ml-2 ", isHovered ? "opacity-100" : "opacity-0", "transition-opacity duration-100")}
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
- <EditIcon size="smallest"/>
53
- </IconButton>
54
- )}
55
- </div>
135
+ {TitleContent}
136
+ </div>
56
137
 
57
- {isPreview ? (
58
- children
59
- ) : minimised ? (
60
- // For minimised view in the main list
61
- <div className={cls("mt-4 p-8 bg-surface-accent-200 dark:bg-surface-accent-800 rounded-lg")}
62
- style={{ minHeight: "50px" }}>
63
- </div>
64
- ) : (
65
- // If highlighted, the parent div already has padding, so children (NavigationGroupDroppable) don't need extra margin top as much.
66
- // The inner content of NavigationGroupDroppable will define its own padding if needed when active.
67
- <div className={cls("mt-4", !minimised ? "pt-0" : "")}>
68
- {children}
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
  );
@@ -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,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\n";
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
+ }