Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { resolveProjectIconUrl } from "./resolveProjectIconUrl";
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test";
import { resolveProjectIconUrl } from "./resolveProjectIconUrl";

describe("resolveProjectIconUrl", () => {
test("custom icon wins over the GitHub avatar", () => {
expect(
resolveProjectIconUrl({
icon: "data:image/png;base64,AAAA",
repoOwner: "acme",
}),
).toBe("data:image/png;base64,AAAA");
});

test("falls back to the GitHub owner avatar when no custom icon", () => {
expect(resolveProjectIconUrl({ icon: null, repoOwner: "acme" })).toBe(
"https://github.com/acme.png?size=64",
);
});

test("returns null when there is neither a custom icon nor a repo owner", () => {
expect(resolveProjectIconUrl({ icon: null, repoOwner: null })).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Resolve the icon URL to display for a project: a custom local-first icon
* (data-URI, set in project settings) wins; otherwise fall back to the linked
* GitHub owner's avatar; otherwise none (callers render a placeholder).
*
* This is the single source of truth for project-icon display — every surface
* (sidebar, settings, pickers, workspace lists) must go through it so a custom
* icon shows everywhere, not just where it was set.
*/
export function resolveProjectIconUrl(project: {
icon: string | null;
repoOwner: string | null;
}): string | null {
if (project.icon) return project.icon;
if (project.repoOwner) {
return `https://github.com/${project.repoOwner}.png?size=64`;
}
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface HostProjectRow {
repoName: string | null;
repoUrl: string | null;
worktreeBaseDir: string | null;
/** Custom icon data-URI, or null to fall back to the GitHub avatar. */
icon: string | null;
createdAt: number;
updatedAt: number;
}
Expand All @@ -30,6 +32,8 @@ export interface HostProjectItem {
repoOwner: string | null;
repoName: string | null;
repoUrl: string | null;
/** Custom icon data-URI, or null to fall back to the GitHub avatar. */
icon: string | null;
/** Hosts that serve this project. */
hostIds: string[];
/** False when no serving host answered live (snapshot data only). */
Expand Down Expand Up @@ -131,6 +135,7 @@ export function normalizeHostProjectRow(
repoName: row.repoName ?? null,
repoUrl: row.repoUrl ?? null,
worktreeBaseDir: row.worktreeBaseDir ?? null,
icon: row.icon ?? null,
createdAt: row.createdAt ?? 0,
updatedAt: row.updatedAt ?? row.createdAt ?? 0,
};
Expand Down Expand Up @@ -234,6 +239,7 @@ export function applyProjectChangedEvent(
repoName: snapshot.repoName,
repoUrl: snapshot.repoUrl,
worktreeBaseDir: snapshot.worktreeBaseDir,
icon: snapshot.icon,
createdAt: snapshot.createdAt,
updatedAt: snapshot.updatedAt,
};
Expand Down Expand Up @@ -274,6 +280,7 @@ export function mergeHostProjects({
repoOwner: row.repoOwner,
repoName: row.repoName,
repoUrl: row.repoUrl,
icon: row.icon,
hostIds: [result.target.machineId],
hostReachable: result.reachable,
createdAt: row.createdAt,
Expand All @@ -297,6 +304,7 @@ export function mergeHostProjects({
existing.repoPath = row.repoPath;
existing.repoOwner = row.repoOwner;
existing.repoName = row.repoName;
existing.icon = row.icon;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { resolveProjectIconUrl } from "renderer/hooks/host-projects/resolveProjectIconUrl";
import { useHostProjects } from "renderer/hooks/host-projects/useHostProjects";
import type { ProjectOption } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/types";

Expand All @@ -14,9 +15,7 @@ export function useRecentProjects(): ProjectOption[] {
name: project.name,
githubOwner: project.repoOwner,
githubRepoName: project.repoName,
iconUrl: project.repoOwner
? `https://github.com/${project.repoOwner}.png?size=64`
: null,
iconUrl: resolveProjectIconUrl(project),
needsSetup: null,
})),
[hostProjects],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useLiveQuery } from "@tanstack/react-db";
import { useQueries, useQueryClient } from "@tanstack/react-query";
import { useCallback, useMemo, useRef } from "react";
import { resolveProjectIconUrl } from "renderer/hooks/host-projects/resolveProjectIconUrl";
import { useHostProjects } from "renderer/hooks/host-projects/useHostProjects";
import { useRelayUrl } from "renderer/hooks/useRelayUrl";
import { getHostServiceClientByUrl } from "renderer/lib/host-service-client";
Expand Down Expand Up @@ -222,9 +223,7 @@ export function useDashboardSidebarData() {
name: project.name,
githubOwner: project.repoOwner,
githubRepoName: project.repoName,
iconUrl: project.repoOwner
? `https://github.com/${project.repoOwner}.png?size=64`
: null,
iconUrl: resolveProjectIconUrl(project),
createdAt: new Date(project.createdAt),
updatedAt: new Date(project.updatedAt),
isCollapsed: row.isCollapsed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover";
import { useMemo, useState } from "react";
import { HiCheck, HiChevronDown, HiOutlineFolder } from "react-icons/hi2";
import { resolveProjectIconUrl } from "renderer/hooks/host-projects/resolveProjectIconUrl";
import { useHostProjects } from "renderer/hooks/host-projects/useHostProjects";
import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail";

Expand All @@ -29,9 +30,7 @@ export function ProjectFilter({ value, onChange }: ProjectFilterProps) {
hostProjects.map((project) => ({
id: project.projectKey,
name: project.name,
iconUrl: project.repoOwner
? `https://github.com/${project.repoOwner}.png?size=64`
: null,
iconUrl: resolveProjectIconUrl(project),
})),
[hostProjects],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useState } from "react";

interface V2WorkspaceProjectIconProps {
projectName: string;
githubOwner: string | null;
/** Resolved icon URL (custom icon or GitHub avatar); null = show initial. */
iconUrl: string | null;
size?: "sm" | "md";
className?: string;
}
Expand All @@ -16,20 +17,16 @@ const SIZE_CLASSES: Record<
md: "size-6 text-xs",
};

function githubAvatarUrl(owner: string): string {
return `https://github.com/${owner}.png?size=64`;
}

export function V2WorkspaceProjectIcon({
projectName,
githubOwner,
iconUrl,
size = "md",
className,
}: V2WorkspaceProjectIconProps) {
const [failedOwner, setFailedOwner] = useState<string | null>(null);
const imageFailed = githubOwner != null && failedOwner === githubOwner;
const [failedUrl, setFailedUrl] = useState<string | null>(null);
const imageFailed = iconUrl != null && failedUrl === iconUrl;
const dimensions = SIZE_CLASSES[size];
const showImage = githubOwner != null && !imageFailed;
const showImage = iconUrl != null && !imageFailed;

if (showImage) {
return (
Expand All @@ -41,11 +38,11 @@ export function V2WorkspaceProjectIcon({
)}
>
<img
src={githubAvatarUrl(githubOwner)}
src={iconUrl}
alt=""
aria-hidden
className="size-full object-cover"
onError={() => setFailedOwner(githubOwner)}
onError={() => setFailedUrl(iconUrl)}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ interface V2WorkspacesHeaderProps {
string,
{ hostName: string; isOnline: boolean; isLocal: boolean }
>;
projectsById: Map<
string,
{ projectName: string; githubOwner: string | null }
>;
projectsById: Map<string, { projectName: string; iconUrl: string | null }>;
}

export function V2WorkspacesHeader({
Expand Down Expand Up @@ -95,12 +92,12 @@ export function V2WorkspacesHeader({
const selectedProjectLabel = selectedProjectFromOptions
? {
projectName: selectedProjectFromOptions.projectName,
githubOwner: selectedProjectFromOptions.githubOwner,
iconUrl: selectedProjectFromOptions.iconUrl,
}
: selectedProjectFallback
? {
projectName: selectedProjectFallback.projectName,
githubOwner: selectedProjectFallback.githubOwner,
iconUrl: selectedProjectFallback.iconUrl,
}
: undefined;

Expand Down Expand Up @@ -169,7 +166,7 @@ export function V2WorkspacesHeader({
<span className="flex w-full min-w-0 items-center gap-2">
<V2WorkspaceProjectIcon
projectName={project.projectName}
githubOwner={project.githubOwner}
iconUrl={project.iconUrl}
size="sm"
/>
<span className="min-w-0 flex-1 truncate">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { PROJECT_FILTER_ALL } from "renderer/routes/_authenticated/_dashboard/v2

interface ProjectFilterTriggerLabelProps {
projectFilter: string;
selectedProject:
| { projectName: string; githubOwner: string | null }
| undefined;
selectedProject: { projectName: string; iconUrl: string | null } | undefined;
}

export function ProjectFilterTriggerLabel({
Expand All @@ -33,7 +31,7 @@ export function ProjectFilterTriggerLabel({
<span className="flex min-w-0 items-center gap-2">
<V2WorkspaceProjectIcon
projectName={selectedProject.projectName}
githubOwner={selectedProject.githubOwner}
iconUrl={selectedProject.iconUrl}
size="sm"
/>
<span className="min-w-0 truncate">{selectedProject.projectName}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface V2WorkspacesListProps {
interface ProjectGroup {
projectId: string;
projectName: string;
githubOwner: string | null;
iconUrl: string | null;
workspaces: AccessibleV2Workspace[];
latestCreatedAt: number;
}
Expand Down Expand Up @@ -97,7 +97,7 @@ function groupByProject(
project = {
projectId: workspace.projectId,
projectName: workspace.projectName,
githubOwner: workspace.projectGithubOwner,
iconUrl: workspace.projectIconUrl,
workspaces: [],
latestCreatedAt: 0,
};
Expand Down Expand Up @@ -329,7 +329,7 @@ function ProjectSection({ project, currentWorkspaceId }: ProjectSectionProps) {
<Chevron className="size-3 shrink-0 text-muted-foreground" />
<V2WorkspaceProjectIcon
projectName={project.projectName}
githubOwner={project.githubOwner}
iconUrl={project.iconUrl}
size="sm"
/>
<h3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useLiveQuery } from "@tanstack/react-db";
import { useQueries } from "@tanstack/react-query";
import { useMemo } from "react";
import { env } from "renderer/env.renderer";
import { resolveProjectIconUrl } from "renderer/hooks/host-projects/resolveProjectIconUrl";
import { useHostProjects } from "renderer/hooks/host-projects/useHostProjects";
import { useHostWorkspacesSource } from "renderer/hooks/host-workspaces/useHostWorkspaces";
import { useRelayUrl } from "renderer/hooks/useRelayUrl";
Expand Down Expand Up @@ -63,7 +64,7 @@ export interface AccessibleV2Workspace {
projectId: string;
projectName: string;
projectRepoId: string | null;
projectGithubOwner: string | null;
projectIconUrl: string | null;
hostId: string;
hostName: string;
hostIsOnline: boolean;
Expand All @@ -82,7 +83,7 @@ export interface V2WorkspaceHostOption {
export interface V2WorkspaceProjectOption {
projectId: string;
projectName: string;
githubOwner: string | null;
iconUrl: string | null;
count: number;
}

Expand All @@ -96,10 +97,7 @@ export interface UseAccessibleV2WorkspacesResult {
string,
{ hostName: string; isOnline: boolean; isLocal: boolean }
>;
projectsById: Map<
string,
{ projectName: string; githubOwner: string | null }
>;
projectsById: Map<string, { projectName: string; iconUrl: string | null }>;
}

interface UseAccessibleV2WorkspacesOptions {
Expand Down Expand Up @@ -370,7 +368,10 @@ export function useAccessibleV2Workspaces(
projectId: project.projectKey,
projectName: project.name,
projectRepoId: repo?.id ?? null,
projectGithubOwner: project.repoOwner ?? repo?.owner ?? null,
projectIconUrl: resolveProjectIconUrl({
icon: project.icon,
repoOwner: project.repoOwner ?? repo?.owner ?? null,
}),
hostId: workspace.hostId,
hostName:
host?.name ??
Expand Down Expand Up @@ -527,7 +528,7 @@ export function useAccessibleV2Workspaces(
projectId: row.projectId,
projectName: row.projectName,
projectRepoId: row.projectRepoId,
projectGithubOwner: row.projectGithubOwner ?? null,
projectIconUrl: row.projectIconUrl ?? null,
hostId: row.hostId,
hostName: row.hostName,
hostIsOnline: row.hostIsOnline,
Expand Down Expand Up @@ -593,7 +594,7 @@ export function useAccessibleV2Workspaces(
byProject.set(workspace.projectId, {
projectId: workspace.projectId,
projectName: workspace.projectName,
githubOwner: workspace.projectGithubOwner,
iconUrl: workspace.projectIconUrl,
count: 1,
});
}
Expand Down Expand Up @@ -625,13 +626,13 @@ export function useAccessibleV2Workspaces(
const projectsById = useMemo(() => {
const map = new Map<
string,
{ projectName: string; githubOwner: string | null }
{ projectName: string; iconUrl: string | null }
>();
for (const workspace of enriched) {
if (map.has(workspace.projectId)) continue;
map.set(workspace.projectId, {
projectName: workspace.projectName,
githubOwner: workspace.projectGithubOwner,
iconUrl: workspace.projectIconUrl,
});
}
return map;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef } from "react";
import { resolveProjectIconUrl } from "renderer/hooks/host-projects/resolveProjectIconUrl";
import { useHostProjects } from "renderer/hooks/host-projects/useHostProjects";
import { useV2WorkspaceCreateDefaultsStore } from "renderer/stores/v2-workspace-create-defaults";
import { useDashboardNewWorkspaceDraft } from "../../DashboardNewWorkspaceDraftContext";
Expand Down Expand Up @@ -37,9 +38,7 @@ export function DashboardNewWorkspaceModalContent({
name: project.name,
githubOwner: project.repoOwner,
githubRepoName: project.repoName,
iconUrl: project.repoOwner
? `https://github.com/${project.repoOwner}.png?size=64`
: null,
iconUrl: resolveProjectIconUrl(project),
needsSetup:
setUpProjectIds === null
? null
Expand Down
Loading
Loading