diff --git a/apps/desktop/src/renderer/components/ColorSelector/ColorSelector.tsx b/apps/desktop/src/renderer/components/ColorSelector/ColorSelector.tsx index 970dcd033ea..b1ae20a3319 100644 --- a/apps/desktop/src/renderer/components/ColorSelector/ColorSelector.tsx +++ b/apps/desktop/src/renderer/components/ColorSelector/ColorSelector.tsx @@ -12,6 +12,7 @@ interface ColorSelectorProps { selectedColor?: string | null; onSelectColor: (color: string) => void; variant?: ColorSelectorVariant; + includeDefault?: boolean; className?: string; } @@ -43,14 +44,18 @@ export function ColorSelector({ selectedColor, onSelectColor, variant = "inline", + includeDefault = false, className, }: ColorSelectorProps) { const selectedValue = selectedColor ?? PROJECT_COLOR_DEFAULT; + const colorOptions = includeDefault + ? [{ name: "Default", value: PROJECT_COLOR_DEFAULT }, ...PROJECT_COLORS] + : PROJECT_COLORS; if (variant === "menu") { return ( <> - {PROJECT_COLORS.map((color) => { + {colorOptions.map((color) => { const isSelected = selectedValue === color.value; return ( @@ -73,7 +78,7 @@ export function ColorSelector({ return (
- {PROJECT_COLORS.map((color) => { + {colorOptions.map((color) => { const isSelected = selectedValue === color.value; return ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx index df2d8c24766..82ea29f7eb7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx @@ -47,7 +47,9 @@ export function DashboardSidebarProjectSection({ handleNewWorkspace, handleOpenInFinder, handleOpenSettings, + handleSetColor, isRenaming, + projectColor, renameSection, renameValue, setRenameValue, @@ -63,16 +65,19 @@ export function DashboardSidebarProjectSection({ if (isSidebarCollapsed) { return (
{ projectName: string; iconUrl: string | null; + projectColor: string; isCollapsed: boolean; totalWorkspaceCount: number; workspaces: DashboardSidebarWorkspace[]; @@ -26,6 +27,7 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef< { projectName, iconUrl, + projectColor, isCollapsed, totalWorkspaceCount, workspaces, @@ -56,7 +58,11 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef< "hover:bg-muted/50 transition-colors", )} > - + 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 c7f93cdd38a..95019539049 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 @@ -3,31 +3,40 @@ import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, ContextMenuTrigger, } from "@superset/ui/context-menu"; import { LuFolderOpen, LuFolderPlus, + LuPalette, LuPencil, LuSettings, LuX, } from "react-icons/lu"; +import { ColorSelector } from "renderer/components/ColorSelector"; interface DashboardSidebarProjectContextMenuProps { + projectColor: string; onCreateSection: () => void; onOpenInFinder: () => void; onOpenSettings: () => void; onRemoveFromSidebar: () => void; onRename: () => void; + onSetColor: (color: string) => void; children: React.ReactNode; } export function DashboardSidebarProjectContextMenu({ + projectColor, onCreateSection, onOpenInFinder, onOpenSettings, onRemoveFromSidebar, onRename, + onSetColor, children, }: DashboardSidebarProjectContextMenuProps) { return ( @@ -47,6 +56,20 @@ export function DashboardSidebarProjectContextMenu({ Project Settings + + + + Set Color + + + + + New group diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx index 6a3339da429..70344023841 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx @@ -9,6 +9,7 @@ interface DashboardSidebarProjectRowProps extends ComponentPropsWithoutRef<"div"> { projectName: string; iconUrl: string | null; + projectColor: string; totalWorkspaceCount: number; isCollapsed: boolean; isRenaming: boolean; @@ -29,6 +30,7 @@ export const DashboardSidebarProjectRow = forwardRef< { projectName, iconUrl, + projectColor, totalWorkspaceCount, isCollapsed, isRenaming, @@ -74,6 +76,7 @@ export const DashboardSidebarProjectRow = forwardRef< state.projects[project.id]?.color ?? PROJECT_COLOR_DEFAULT, + ); + const setProjectColor = useV2ProjectLocalMetaStore( + (state) => state.setProjectColor, + ); const { createSection, deleteSection, @@ -48,6 +56,10 @@ export function useDashboardSidebarProjectSectionActions({ projectActions.renameProject(project.id, trimmed); }; + const handleSetColor = (color: string) => { + setProjectColor(project.id, color); + }; + const handleOpenInFinder = () => { toast.info("Open in Finder is coming soon"); }; @@ -95,7 +107,9 @@ export function useDashboardSidebarProjectSectionActions({ handleNewWorkspace, handleOpenInFinder, handleOpenSettings, + handleSetColor, isRenaming, + projectColor, renameSection, renameValue, setRenameValue, diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx index 9330aa1e74b..f7b1d7eb0ef 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx @@ -1,28 +1,40 @@ import { cn } from "@superset/ui/utils"; import { useState } from "react"; +import { + hexToRgba, + isCustomProjectColor, +} from "shared/constants/project-colors"; interface ProjectThumbnailProps { projectName: string; iconUrl?: string | null; + color?: string | null; className?: string; } export function ProjectThumbnail({ projectName, iconUrl, + color, className, }: ProjectThumbnailProps) { const [failedUrl, setFailedUrl] = useState(null); const firstLetter = projectName.charAt(0).toUpperCase(); + const hasCustomColor = isCustomProjectColor(color); + const customBorderStyle = hasCustomColor + ? { borderColor: hexToRgba(color, 0.6) } + : undefined; if (iconUrl && failedUrl !== iconUrl) { return (
{firstLetter}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx index 916dabc8e1b..fb00dcd45af 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx @@ -1,7 +1,11 @@ import { cn } from "@superset/ui/utils"; import { useState } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors"; +import { + hexToRgba, + isCustomProjectColor, + PROJECT_COLOR_DEFAULT, +} from "shared/constants/project-colors"; interface ProjectThumbnailProps { projectId: string; @@ -17,23 +21,6 @@ function getGitHubAvatarUrl(owner: string): string { return `https://github.com/${owner}.png?size=64`; } -/** - * Converts a hex color to rgba with the specified alpha. - */ -function hexToRgba(hex: string, alpha: number): string { - const r = Number.parseInt(hex.slice(1, 3), 16); - const g = Number.parseInt(hex.slice(3, 5), 16); - const b = Number.parseInt(hex.slice(5, 7), 16); - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -} - -/** - * Checks if a color value is a custom hex color (not the "default" value). - */ -function isCustomColor(color: string): boolean { - return color !== PROJECT_COLOR_DEFAULT && color.startsWith("#"); -} - /** * Determines whether the GitHub avatar should be displayed. * Exported for unit testing. @@ -72,7 +59,7 @@ export function ProjectThumbnail({ const owner = avatarData?.owner ?? githubOwner; const firstLetter = projectName.charAt(0).toUpperCase(); - const hasCustomColor = isCustomColor(projectColor); + const hasCustomColor = isCustomProjectColor(projectColor); const shouldUseTransparentIconFrame = projectColor === PROJECT_COLOR_DEFAULT; // Border: gray by default, custom color with slight transparency when set diff --git a/apps/desktop/src/renderer/stores/v2-project-local-meta.ts b/apps/desktop/src/renderer/stores/v2-project-local-meta.ts index 96951432579..e2bbccd7bfe 100644 --- a/apps/desktop/src/renderer/stores/v2-project-local-meta.ts +++ b/apps/desktop/src/renderer/stores/v2-project-local-meta.ts @@ -1,9 +1,11 @@ +import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors"; import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; interface ProjectMeta { isCollapsed: boolean; tabOrder: number; + color: string; } interface V2ProjectLocalMetaState { @@ -12,11 +14,13 @@ interface V2ProjectLocalMetaState { getProjectMeta: (id: string) => ProjectMeta; toggleProjectCollapsed: (id: string) => void; setProjectTabOrder: (id: string, order: number) => void; + setProjectColor: (id: string, color: string) => void; } const DEFAULT_PROJECT_META: ProjectMeta = { isCollapsed: false, tabOrder: 0, + color: PROJECT_COLOR_DEFAULT, }; export const useV2ProjectLocalMetaStore = create()( @@ -52,6 +56,18 @@ export const useV2ProjectLocalMetaStore = create()( }; }); }, + + setProjectColor: (id, color) => { + set((state) => { + const current = state.projects[id] ?? DEFAULT_PROJECT_META; + return { + projects: { + ...state.projects, + [id]: { ...current, color }, + }, + }; + }); + }, }), { name: "v2-project-local-meta", diff --git a/apps/desktop/src/shared/constants/project-colors.ts b/apps/desktop/src/shared/constants/project-colors.ts index 0341c438e22..c554aab8411 100644 --- a/apps/desktop/src/shared/constants/project-colors.ts +++ b/apps/desktop/src/shared/constants/project-colors.ts @@ -21,3 +21,18 @@ export const PROJECT_CUSTOM_COLORS = PROJECT_COLORS; export const PROJECT_COLOR_VALUES: string[] = PROJECT_COLORS.map( (color) => color.value, ); + +/** Checks if a color value is a custom hex color (not the "default" value). */ +export function isCustomProjectColor( + color: string | null | undefined, +): color is string { + return !!color && color !== PROJECT_COLOR_DEFAULT && color.startsWith("#"); +} + +/** Converts a hex color to an rgba string with the specified alpha. */ +export function hexToRgba(hex: string, alpha: number): string { + const r = Number.parseInt(hex.slice(1, 3), 16); + const g = Number.parseInt(hex.slice(3, 5), 16); + const b = Number.parseInt(hex.slice(5, 7), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +}