diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx index bb8cd3977ad..e48f89e7a02 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx @@ -24,6 +24,7 @@ import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/u import { DashboardSidebarHeader } from "./components/DashboardSidebarHeader"; import { DashboardSidebarPortsList } from "./components/DashboardSidebarPortsList"; import { DashboardSidebarProjectSection } from "./components/DashboardSidebarProjectSection"; +import { DashboardSidebarSectionRenameProvider } from "./components/DashboardSidebarSectionRenameContext"; import { useDashboardSidebarData } from "./hooks/useDashboardSidebarData"; import { useDashboardSidebarShortcuts } from "./hooks/useDashboardSidebarShortcuts"; import type { DashboardSidebarProject } from "./types"; @@ -132,60 +133,62 @@ export function DashboardSidebar({ ); return ( -
- + +
+ -
- { - const project = groups.find((p) => p.id === active.id); - setActiveProject(project ?? null); - }} - onDragEnd={handleDragEnd} - onDragCancel={() => setActiveProject(null)} - > - + { + const project = groups.find((p) => p.id === active.id); + setActiveProject(project ?? null); + }} + onDragEnd={handleDragEnd} + onDragCancel={() => setActiveProject(null)} > - {orderedGroups.map((project) => ( - - ))} - + + {orderedGroups.map((project) => ( + + ))} + - {createPortal( - - {activeProject && ( -
- {}} - onToggleCollapse={() => {}} - /> -
- )} -
, - document.body, - )} -
+ {createPortal( + + {activeProject && ( +
+ {}} + onToggleCollapse={() => {}} + /> +
+ )} +
, + document.body, + )} + +
+ {!isCollapsed && }
- {!isCollapsed && } -
+ ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx index 51f356e337d..c7f93cdd38a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx @@ -49,7 +49,7 @@ export function DashboardSidebarProjectContextMenu({ - New Section + New group { - createSection(project.id); + const sectionId = createSection(project.id); + requestSectionRename(sectionId); + if (project.isCollapsed) { + toggleProjectCollapsed(project.id); + } }; return { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx deleted file mode 100644 index 5efaa456a2f..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useState } from "react"; -import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; -import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors"; -import type { DashboardSidebarSection as DashboardSidebarSectionRecord } from "../../types"; -import { DashboardSidebarSectionContent } from "./components/DashboardSidebarSectionContent"; -import { DashboardSidebarSectionContextMenu } from "./components/DashboardSidebarSectionContextMenu"; -import { DashboardSidebarSectionHeader } from "./components/DashboardSidebarSectionHeader"; - -interface DashboardSidebarSectionProps { - projectId: string; - section: DashboardSidebarSectionRecord; - allSections: Array<{ id: string; name: string }>; - workspaceShortcutLabels: Map; - onWorkspaceHover: (workspaceId: string) => void | Promise; - onDelete: (sectionId: string) => void; - onRename: (sectionId: string, name: string) => void; - onToggleCollapse: (sectionId: string) => void; -} - -export function DashboardSidebarSection({ - section, - workspaceShortcutLabels, - onWorkspaceHover, - onDelete, - onRename, - onToggleCollapse, -}: DashboardSidebarSectionProps) { - const { setSectionColor } = useDashboardSidebarState(); - const [isRenaming, setIsRenaming] = useState(false); - const [renameValue, setRenameValue] = useState(section.name); - const hasColor = - section.color != null && section.color !== PROJECT_COLOR_DEFAULT; - const sectionBorderStyle = { - borderLeft: hasColor - ? `2px solid ${section.color}` - : "2px solid var(--color-border)", - }; - - const handleSubmitRename = () => { - const trimmed = renameValue.trim(); - if (trimmed) { - onRename(section.id, trimmed); - } - setIsRenaming(false); - }; - - const handleCancelRename = () => { - setRenameValue(section.name); - setIsRenaming(false); - }; - - return ( -
- setIsRenaming(true)} - onSetColor={(color) => setSectionColor(section.id, color)} - onDelete={() => onDelete(section.id)} - > - { - setRenameValue(section.name); - setIsRenaming(true); - }} - onToggleCollapse={() => onToggleCollapse(section.id)} - /> - - - -
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx deleted file mode 100644 index fbc6e4202e8..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { AnimatePresence, motion } from "framer-motion"; -import type { DashboardSidebarSection } from "../../../../types"; -import { DashboardSidebarWorkspaceItem } from "../../../DashboardSidebarWorkspaceItem"; - -interface DashboardSidebarSectionContentProps { - section: DashboardSidebarSection; - workspaceShortcutLabels: Map; - onWorkspaceHover: (workspaceId: string) => void | Promise; -} - -export function DashboardSidebarSectionContent({ - section, - workspaceShortcutLabels, - onWorkspaceHover, -}: DashboardSidebarSectionContentProps) { - return ( - - {!section.isCollapsed && ( - -
- {section.workspaces.map((workspace) => ( - onWorkspaceHover(workspace.id)} - shortcutLabel={workspaceShortcutLabels.get(workspace.id)} - /> - ))} -
-
- )} -
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/index.ts deleted file mode 100644 index db341d136c1..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DashboardSidebarSectionContent } from "./DashboardSidebarSectionContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/DashboardSidebarSectionContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/DashboardSidebarSectionContextMenu.tsx index 74beb713ff3..ebff7132ef6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/DashboardSidebarSectionContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/DashboardSidebarSectionContextMenu.tsx @@ -1,22 +1,13 @@ import { ContextMenu, ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, ContextMenuTrigger, } from "@superset/ui/context-menu"; -import { LuPalette, LuPencil, LuTrash2 } from "react-icons/lu"; -import { ColorSelector } from "renderer/components/ColorSelector"; -import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors"; +import { SectionActionsMenuItems } from "./components/SectionActionsMenuItems"; +import type { DashboardSidebarSectionActionsProps } from "./types"; -interface DashboardSidebarSectionContextMenuProps { - color: string | null; - onRename: () => void; - onSetColor: (color: string | null) => void; - onDelete: () => void; +interface DashboardSidebarSectionContextMenuProps + extends DashboardSidebarSectionActionsProps { children: React.ReactNode; } @@ -30,38 +21,18 @@ export function DashboardSidebarSectionContextMenu({ return ( {children} - - - - Rename - - - - - Set Color - - - - onSetColor( - selectedColor === PROJECT_COLOR_DEFAULT - ? null - : selectedColor, - ) - } - /> - - - - - - Delete Section - + event.preventDefault()} + onClick={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + > + ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/DashboardSidebarSectionActionsDropdown/DashboardSidebarSectionActionsDropdown.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/DashboardSidebarSectionActionsDropdown/DashboardSidebarSectionActionsDropdown.tsx new file mode 100644 index 00000000000..2fca177b1aa --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/DashboardSidebarSectionActionsDropdown/DashboardSidebarSectionActionsDropdown.tsx @@ -0,0 +1,47 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; +import { LuEllipsis } from "react-icons/lu"; +import type { DashboardSidebarSectionActionsProps } from "../../types"; +import { SectionActionsMenuItems } from "../SectionActionsMenuItems"; + +export function DashboardSidebarSectionActionsDropdown({ + color, + onRename, + onSetColor, + onDelete, +}: DashboardSidebarSectionActionsProps) { + return ( + + + + + event.preventDefault()} + onClick={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + > + + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/DashboardSidebarSectionActionsDropdown/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/DashboardSidebarSectionActionsDropdown/index.ts new file mode 100644 index 00000000000..18ba11aca46 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/DashboardSidebarSectionActionsDropdown/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarSectionActionsDropdown } from "./DashboardSidebarSectionActionsDropdown"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/SectionActionsMenuItems/SectionActionsMenuItems.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/SectionActionsMenuItems/SectionActionsMenuItems.tsx new file mode 100644 index 00000000000..2923e5419b8 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/SectionActionsMenuItems/SectionActionsMenuItems.tsx @@ -0,0 +1,170 @@ +import { + ContextMenuItem, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, +} from "@superset/ui/context-menu"; +import { + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, +} from "@superset/ui/dropdown-menu"; +import { HiCheck } from "react-icons/hi2"; +import { LuPalette, LuPencil, LuTrash2 } from "react-icons/lu"; +import { + PROJECT_COLOR_DEFAULT, + PROJECT_COLORS, +} from "shared/constants/project-colors"; +import type { + DashboardSidebarSectionActionsProps, + SectionActionsMenuKind, +} from "../../types"; + +interface SectionActionsMenuItemsProps + extends DashboardSidebarSectionActionsProps { + kind: SectionActionsMenuKind; +} + +export function SectionActionsMenuItems({ + color, + kind, + onRename, + onSetColor, + onDelete, +}: SectionActionsMenuItemsProps) { + const selectedValue = color ?? PROJECT_COLOR_DEFAULT; + const colorOptions = [ + { name: "Default", value: PROJECT_COLOR_DEFAULT }, + ...PROJECT_COLORS, + ]; + const iconClassName = kind === "context" ? "size-4 mr-2" : "size-4"; + + const renderItem = ({ + children, + destructive = false, + key, + onSelect, + }: { + children: React.ReactNode; + destructive?: boolean; + key?: string; + onSelect?: () => void; + }) => { + if (kind === "context") { + return ( + { + event.stopPropagation(); + onSelect?.(); + }} + className={ + destructive ? "text-destructive focus:text-destructive" : undefined + } + > + {children} + + ); + } + + return ( + { + event.stopPropagation(); + onSelect?.(); + }} + variant={destructive ? "destructive" : "default"} + > + {children} + + ); + }; + + const colorItems = colorOptions.map((projectColor) => { + const isDefault = projectColor.value === PROJECT_COLOR_DEFAULT; + const isSelected = selectedValue === projectColor.value; + + return renderItem({ + key: projectColor.value, + onSelect: () => onSetColor(isDefault ? null : projectColor.value), + children: ( + <> + + {isDefault ? ( + + ) : null} + + {projectColor.name} + {isSelected ? ( + + ) : null} + + ), + }); + }); + const colorTrigger = ( + <> + + Set group color + + ); + + return ( + <> + {renderItem({ + onSelect: onRename, + children: ( + <> + + Rename group + + ), + })} + {kind === "context" ? ( + + {colorTrigger} + + {colorItems} + + + ) : ( + + {colorTrigger} + + {colorItems} + + + )} + {kind === "context" ? ( + + ) : ( + + )} + {renderItem({ + destructive: true, + onSelect: onDelete, + children: ( + <> + + Delete group + + ), + })} + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/SectionActionsMenuItems/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/SectionActionsMenuItems/index.ts new file mode 100644 index 00000000000..dd0c6ebf935 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/components/SectionActionsMenuItems/index.ts @@ -0,0 +1 @@ +export { SectionActionsMenuItems } from "./SectionActionsMenuItems"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/index.ts index eec632d0652..1a080acb50f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/index.ts @@ -1 +1,2 @@ +export { DashboardSidebarSectionActionsDropdown } from "./components/DashboardSidebarSectionActionsDropdown"; export { DashboardSidebarSectionContextMenu } from "./DashboardSidebarSectionContextMenu"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/types.ts new file mode 100644 index 00000000000..ced8619368d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/types.ts @@ -0,0 +1,8 @@ +export interface DashboardSidebarSectionActionsProps { + color: string | null; + onRename: () => void; + onSetColor: (color: string | null) => void; + onDelete: () => void; +} + +export type SectionActionsMenuKind = "context" | "dropdown"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/DashboardSidebarSectionHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/DashboardSidebarSectionHeader.tsx index 91584d872f5..ae08f7c245a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/DashboardSidebarSectionHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/DashboardSidebarSectionHeader.tsx @@ -1,7 +1,11 @@ import { cn } from "@superset/ui/utils"; -import { type ComponentPropsWithoutRef, forwardRef } from "react"; +import { + type ComponentPropsWithoutRef, + forwardRef, + type ReactNode, +} from "react"; import { HiChevronRight } from "react-icons/hi2"; -import { LuGripVertical, LuPencil } from "react-icons/lu"; +import { LuGripVertical } from "react-icons/lu"; import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; import type { DashboardSidebarSection } from "../../../../types"; @@ -13,8 +17,8 @@ interface DashboardSidebarSectionHeaderProps onRenameValueChange: (value: string) => void; onSubmitRename: () => void; onCancelRename: () => void; - onStartRename: () => void; onToggleCollapse: () => void; + actions?: ReactNode; } export const DashboardSidebarSectionHeader = forwardRef< @@ -29,8 +33,8 @@ export const DashboardSidebarSectionHeader = forwardRef< onRenameValueChange, onSubmitRename, onCancelRename, - onStartRename, onToggleCollapse, + actions, className, ...props }, @@ -54,14 +58,20 @@ export const DashboardSidebarSectionHeader = forwardRef< } } className={cn( - "group flex min-h-8 w-full items-center pl-0.5 pr-2 py-1.5 text-[11px] font-medium", + "group flex min-h-8 w-full items-center pl-0.5 pr-2 py-1.5 text-[13px] font-medium", "text-muted-foreground hover:bg-muted/50 transition-colors", className, )} {...props} > -
- +
+ +
@@ -71,49 +81,29 @@ export const DashboardSidebarSectionHeader = forwardRef< onChange={onRenameValueChange} onSubmit={onSubmitRename} onCancel={onCancelRename} - className="-ml-1 h-5 w-full min-w-0 px-1 py-0 text-[11px] font-medium bg-transparent border-none outline-none text-muted-foreground" + className="-ml-1 h-5 w-full min-w-0 px-1 py-0 text-[13px] font-medium bg-transparent border-none outline-none text-muted-foreground" /> ) : ( {section.name} )} {!isRenaming && ( -
- - ({section.workspaces.length}) - - -
+ + ({section.workspaces.length}) + )}
- + {!isRenaming && actions ? ( + // biome-ignore lint/a11y/noStaticElementInteractions: Nested action controls handle their own semantics; this wrapper only isolates events from the header toggle. +
event.stopPropagation()} + onKeyDown={(event) => event.stopPropagation()} + > + {actions} +
+ ) : null}
); }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/index.ts deleted file mode 100644 index 5823f140adf..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DashboardSidebarSection } from "./DashboardSidebarSection"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSectionRenameContext/DashboardSidebarSectionRenameContext.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSectionRenameContext/DashboardSidebarSectionRenameContext.tsx new file mode 100644 index 00000000000..775984b8ea7 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSectionRenameContext/DashboardSidebarSectionRenameContext.tsx @@ -0,0 +1,64 @@ +import { + createContext, + type ReactNode, + useCallback, + useContext, + useMemo, + useState, +} from "react"; + +interface DashboardSidebarSectionRenameContextValue { + pendingRenameSectionId: string | null; + requestSectionRename: (sectionId: string) => void; + clearPendingSectionRename: (sectionId: string) => void; +} + +const DashboardSidebarSectionRenameContext = + createContext(null); + +interface DashboardSidebarSectionRenameProviderProps { + children: ReactNode; +} + +export function DashboardSidebarSectionRenameProvider({ + children, +}: DashboardSidebarSectionRenameProviderProps) { + const [pendingRenameSectionId, setPendingRenameSectionId] = useState< + string | null + >(null); + + const requestSectionRename = useCallback((sectionId: string) => { + setPendingRenameSectionId(sectionId); + }, []); + + const clearPendingSectionRename = useCallback((sectionId: string) => { + setPendingRenameSectionId((currentSectionId) => + currentSectionId === sectionId ? null : currentSectionId, + ); + }, []); + + const value = useMemo( + () => ({ + clearPendingSectionRename, + pendingRenameSectionId, + requestSectionRename, + }), + [clearPendingSectionRename, pendingRenameSectionId, requestSectionRename], + ); + + return ( + + {children} + + ); +} + +export function useDashboardSidebarSectionRename() { + const context = useContext(DashboardSidebarSectionRenameContext); + if (!context) { + throw new Error( + "useDashboardSidebarSectionRename must be used within DashboardSidebarSectionRenameProvider", + ); + } + return context; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSectionRenameContext/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSectionRenameContext/index.ts new file mode 100644 index 00000000000..62352741d29 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSectionRenameContext/index.ts @@ -0,0 +1,4 @@ +export { + DashboardSidebarSectionRenameProvider, + useDashboardSidebarSectionRename, +} from "./DashboardSidebarSectionRenameContext"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx index 550488fc0af..c4b799efa0c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx @@ -107,14 +107,14 @@ export function DashboardSidebarWorkspaceContextMenu({ - Create Section Below + New group from workspace {(sections.length > 0 || isInSection) && } {sections.length > 0 && ( - Move to Section + Move to group {sections.map((section) => ( @@ -137,7 +137,7 @@ export function DashboardSidebarWorkspaceContextMenu({ {isInSection && ( onMoveToSection(null)}> - Remove from Group + Ungroup )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx index 91f64d61b4e..e2ab16510bc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx @@ -1,13 +1,13 @@ import { cn } from "@superset/ui/utils"; import { HiExclamationTriangle } from "react-icons/hi2"; import { - LuCloud, - LuCloudOff, LuGitMerge, LuGitPullRequest, LuGitPullRequestClosed, LuGitPullRequestDraft, } from "react-icons/lu"; +import { RxDot } from "react-icons/rx"; +import { TbCloud, TbCloudOff } from "react-icons/tb"; import { AsciiSpinner } from "renderer/screens/main/components/AsciiSpinner"; import { StatusIndicator } from "renderer/screens/main/components/StatusIndicator"; import type { ActivePaneStatus } from "shared/tabs-types"; @@ -71,19 +71,12 @@ export function DashboardSidebarWorkspaceIcon({ } if (hostType === "local-device") { - return ( - - ); + return ; } if (isRemoteDeviceOffline) { return ( - @@ -91,7 +84,7 @@ export function DashboardSidebarWorkspaceIcon({ } return ( - diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index f761c648ed0..5957d04a5ef 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -4,6 +4,7 @@ import { useState } from "react"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { electronTrpcClient } from "renderer/lib/trpc-client"; +import { useDashboardSidebarSectionRename } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSectionRenameContext"; import { getDeleteFocusTargetWorkspaceId } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId"; import { getFlattenedV2WorkspaceIds } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; @@ -31,6 +32,7 @@ export function useDashboardSidebarWorkspaceItemActions({ const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); const { v2Workspaces: workspaceActions } = useOptimisticCollectionActions(); + const { requestSectionRename } = useDashboardSidebarSectionRename(); const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = useDashboardSidebarState(); @@ -93,9 +95,9 @@ export function useDashboardSidebarWorkspaceItemActions({ }; const handleCreateSection = () => { - createSection(projectId, { - insertAfterWorkspaceId: workspaceId, - }); + const sectionId = createSection(projectId); + moveWorkspaceToSection(workspaceId, projectId, sectionId); + requestSectionRename(sectionId); }; const resolveWorktreePath = async (): Promise => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/SortableSectionHeader/SortableSectionHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/SortableSectionHeader/SortableSectionHeader.tsx index ffaf10713cb..e090834eb9e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/SortableSectionHeader/SortableSectionHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/SortableSectionHeader/SortableSectionHeader.tsx @@ -1,10 +1,14 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { useDashboardSidebarSectionRename } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSectionRenameContext"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors"; import type { DashboardSidebarSection } from "../../types"; -import { DashboardSidebarSectionContextMenu } from "../DashboardSidebarSection/components/DashboardSidebarSectionContextMenu"; +import { + DashboardSidebarSectionActionsDropdown, + DashboardSidebarSectionContextMenu, +} from "../DashboardSidebarSection/components/DashboardSidebarSectionContextMenu"; import { DashboardSidebarSectionHeader } from "../DashboardSidebarSection/components/DashboardSidebarSectionHeader"; interface SortableSectionHeaderProps { @@ -23,6 +27,8 @@ export function SortableSectionHeader({ onToggleCollapse, }: SortableSectionHeaderProps) { const { setSectionColor } = useDashboardSidebarState(); + const { clearPendingSectionRename, pendingRenameSectionId } = + useDashboardSidebarSectionRename(); const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(section.name); @@ -43,6 +49,21 @@ export function SortableSectionHeader({ if (trimmed) onRename(section.id, trimmed); setIsRenaming(false); }; + const startRename = useCallback(() => { + setRenameValue(section.name); + setIsRenaming(true); + }, [section.name]); + + useEffect(() => { + if (pendingRenameSectionId !== section.id) return; + startRename(); + clearPendingSectionRename(section.id); + }, [ + clearPendingSectionRename, + pendingRenameSectionId, + section.id, + startRename, + ]); return (
setIsRenaming(true)} + onRename={startRename} onSetColor={(color) => setSectionColor(section.id, color)} onDelete={() => onDelete(section.id)} > @@ -72,11 +93,15 @@ export function SortableSectionHeader({ setRenameValue(section.name); setIsRenaming(false); }} - onStartRename={() => { - setRenameValue(section.name); - setIsRenaming(true); - }} onToggleCollapse={() => onToggleCollapse(section.id)} + actions={ + setSectionColor(section.id, color)} + onDelete={() => onDelete(section.id)} + /> + } {...attributes} {...listeners} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts index 2cbb3417b05..6460b5366af 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts @@ -27,6 +27,92 @@ function getPrependTabOrder(items: Array<{ tabOrder: number }>): number { return minTabOrder - 1; } +type ProjectTopLevelItem = { + type: "workspace" | "section"; + id: string; + tabOrder: number; +}; + +type ProjectTopLevelCollections = Pick< + AppCollections, + "v2SidebarSections" | "v2WorkspaceLocalState" +>; + +function compareProjectTopLevelItems( + left: ProjectTopLevelItem, + right: ProjectTopLevelItem, +): number { + const orderDelta = left.tabOrder - right.tabOrder; + if (orderDelta !== 0) return orderDelta; + if (left.type === right.type) return 0; + return left.type === "section" ? -1 : 1; +} + +function getProjectTopLevelItems( + collections: ProjectTopLevelCollections, + projectId: string, + options: { excludeWorkspaceId?: string; excludeSectionId?: string } = {}, +): ProjectTopLevelItem[] { + return [ + ...Array.from(collections.v2WorkspaceLocalState.state.values()) + .filter( + (item) => + item.sidebarState.projectId === projectId && + item.sidebarState.sectionId === null && + item.workspaceId !== options.excludeWorkspaceId, + ) + .map((item) => ({ + type: "workspace" as const, + id: item.workspaceId, + tabOrder: item.sidebarState.tabOrder, + })), + ...Array.from(collections.v2SidebarSections.state.values()) + .filter( + (item) => + item.projectId === projectId && + item.sectionId !== options.excludeSectionId, + ) + .map((item) => ({ + type: "section" as const, + id: item.sectionId, + tabOrder: item.tabOrder, + })), + ].sort(compareProjectTopLevelItems); +} + +function getFirstSectionIndex(items: ProjectTopLevelItem[]): number { + const firstSectionIndex = items.findIndex((item) => item.type === "section"); + return firstSectionIndex === -1 ? items.length : firstSectionIndex; +} + +/** + * Rewrites the flat top-level project lane. Workspace items are explicitly + * ungrouped by setting sidebarState.projectId and clearing sidebarState.sectionId. + */ +function writeProjectTopLevelOrder( + collections: ProjectTopLevelCollections, + projectId: string, + items: ProjectTopLevelItem[], +): void { + items.forEach((item, index) => { + const tabOrder = index + 1; + if (item.type === "workspace") { + if (!collections.v2WorkspaceLocalState.get(item.id)) return; + collections.v2WorkspaceLocalState.update(item.id, (draft) => { + draft.sidebarState.projectId = projectId; + draft.sidebarState.sectionId = null; + draft.sidebarState.tabOrder = tabOrder; + }); + return; + } + + if (!collections.v2SidebarSections.get(item.id)) return; + collections.v2SidebarSections.update(item.id, (draft) => { + draft.tabOrder = tabOrder; + }); + }); +} + function ensureSidebarProjectRecord( collections: Pick, projectId: string, @@ -57,25 +143,14 @@ function ensureSidebarWorkspaceRecord( return; } - const topLevelOrders = [ - ...Array.from(collections.v2WorkspaceLocalState.state.values()) - .filter( - (item) => - item.sidebarState.projectId === projectId && - item.sidebarState.sectionId === null, - ) - .map((item) => ({ tabOrder: item.sidebarState.tabOrder })), - ...Array.from(collections.v2SidebarSections.state.values()).filter( - (item) => item.projectId === projectId, - ), - ]; + const topLevelItems = getProjectTopLevelItems(collections, projectId); collections.v2WorkspaceLocalState.insert({ workspaceId, createdAt: new Date(), sidebarState: { projectId, - tabOrder: getPrependTabOrder(topLevelOrders), + tabOrder: getPrependTabOrder(topLevelItems), sectionId: null, }, paneLayout: { @@ -217,11 +292,8 @@ export function useDashboardSidebarState() { ); const createSection = useCallback( - ( - projectId: string, - options: { name?: string; insertAfterWorkspaceId?: string } = {}, - ) => { - const { name = "New Section", insertAfterWorkspaceId } = options; + (projectId: string, options: { name?: string } = {}) => { + const { name = "New group" } = options; ensureSidebarProjectRecord(collections, projectId); const sectionId = crypto.randomUUID(); @@ -230,51 +302,9 @@ export function useDashboardSidebarState() { Math.floor(Math.random() * PROJECT_CUSTOM_COLORS.length) ].value; - let tabOrder: number; - if (insertAfterWorkspaceId) { - const anchorWorkspace = collections.v2WorkspaceLocalState.get( - insertAfterWorkspaceId, - ); - const anchorTabOrder = anchorWorkspace?.sidebarState.sectionId - ? (collections.v2SidebarSections.get( - anchorWorkspace.sidebarState.sectionId, - )?.tabOrder ?? 0) - : (anchorWorkspace?.sidebarState.tabOrder ?? 0); - - for (const workspace of collections.v2WorkspaceLocalState.state.values()) { - if ( - workspace.sidebarState.projectId === projectId && - workspace.sidebarState.sectionId === null && - workspace.sidebarState.tabOrder > anchorTabOrder - ) { - const nextOrder = workspace.sidebarState.tabOrder + 1; - collections.v2WorkspaceLocalState.update( - workspace.workspaceId, - (draft) => { - draft.sidebarState.tabOrder = nextOrder; - }, - ); - } - } - for (const section of collections.v2SidebarSections.state.values()) { - if ( - section.projectId === projectId && - section.tabOrder > anchorTabOrder - ) { - const nextOrder = section.tabOrder + 1; - collections.v2SidebarSections.update(section.sectionId, (draft) => { - draft.tabOrder = nextOrder; - }); - } - } - - tabOrder = anchorTabOrder + 1; - } else { - const sectionOrders = Array.from( - collections.v2SidebarSections.state.values(), - ).filter((item) => item.projectId === projectId); - tabOrder = getNextTabOrder(sectionOrders); - } + const tabOrder = getNextTabOrder( + getProjectTopLevelItems(collections, projectId), + ); collections.v2SidebarSections.insert({ sectionId, @@ -327,51 +357,16 @@ export function useDashboardSidebarState() { if (!existing) return; if (sectionId === null) { - // "Remove from group" — place right above the first section. - // Find the lowest section tabOrder, then use tabOrder - 1. - // If no sections exist, append to end of ungrouped workspaces. - const sectionOrders = Array.from( - collections.v2SidebarSections.state.values(), - ) - .filter((s) => s.projectId === projectId) - .map((s) => s.tabOrder); - - const firstSectionOrder = - sectionOrders.length > 0 ? Math.min(...sectionOrders) : null; - - let newTabOrder: number; - if (firstSectionOrder != null) { - // Place right before the first section, after existing ungrouped - const ungroupedOrders = Array.from( - collections.v2WorkspaceLocalState.state.values(), - ) - .filter( - (item) => - item.sidebarState.projectId === projectId && - item.workspaceId !== workspaceId && - item.sidebarState.sectionId === null, - ) - .map((item) => ({ tabOrder: item.sidebarState.tabOrder })); - newTabOrder = getNextTabOrder(ungroupedOrders); - } else { - // No sections — append to end - const ungroupedOrders = Array.from( - collections.v2WorkspaceLocalState.state.values(), - ) - .filter( - (item) => - item.sidebarState.projectId === projectId && - item.workspaceId !== workspaceId && - item.sidebarState.sectionId === null, - ) - .map((item) => ({ tabOrder: item.sidebarState.tabOrder })); - newTabOrder = getNextTabOrder(ungroupedOrders); - } - - collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { - draft.sidebarState.sectionId = null; - draft.sidebarState.tabOrder = newTabOrder; + const topLevelItems = getProjectTopLevelItems(collections, projectId, { + excludeWorkspaceId: workspaceId, + }); + const insertIndex = getFirstSectionIndex(topLevelItems); + topLevelItems.splice(insertIndex, 0, { + type: "workspace", + id: workspaceId, + tabOrder: 0, }); + writeProjectTopLevelOrder(collections, projectId, topLevelItems); return; } @@ -399,28 +394,35 @@ export function useDashboardSidebarState() { const section = collections.v2SidebarSections.get(sectionId); if (!section) return; - const siblingTopLevelRows = Array.from( + const topLevelItems = getProjectTopLevelItems( + collections, + section.projectId, + { excludeSectionId: sectionId }, + ); + const sectionWorkspaces = Array.from( collections.v2WorkspaceLocalState.state.values(), ) .filter( (item) => item.sidebarState.projectId === section.projectId && - item.sidebarState.sectionId === null, + item.sidebarState.sectionId === sectionId, ) - .map((item) => ({ tabOrder: item.sidebarState.tabOrder })); - - let nextOrder = getNextTabOrder(siblingTopLevelRows); - for (const workspace of collections.v2WorkspaceLocalState.state.values()) { - if (workspace.sidebarState.sectionId !== sectionId) continue; - collections.v2WorkspaceLocalState.update( - workspace.workspaceId, - (draft) => { - draft.sidebarState.sectionId = null; - draft.sidebarState.tabOrder = nextOrder; - }, + .sort( + (left, right) => + left.sidebarState.tabOrder - right.sidebarState.tabOrder, ); - nextOrder += 1; - } + + const insertIndex = getFirstSectionIndex(topLevelItems); + topLevelItems.splice( + insertIndex, + 0, + ...sectionWorkspaces.map((workspace) => ({ + type: "workspace" as const, + id: workspace.workspaceId, + tabOrder: 0, + })), + ); + writeProjectTopLevelOrder(collections, section.projectId, topLevelItems); collections.v2SidebarSections.delete(sectionId); }, diff --git a/apps/web/src/app/(dashboard-legacy)/components/Footer/Footer.tsx b/apps/web/src/app/(dashboard-legacy)/components/Footer/Footer.tsx index 8f16b8db631..107296310ed 100644 --- a/apps/web/src/app/(dashboard-legacy)/components/Footer/Footer.tsx +++ b/apps/web/src/app/(dashboard-legacy)/components/Footer/Footer.tsx @@ -1,10 +1,14 @@ import { env } from "@/env"; export function Footer() { + const currentYear = new Date().getFullYear(); + return (