feat: add host-local custom project icons (#2335)
* feat(projects): add local icons and colors * refactor(projects): simplify appearance handling * fix(projects): serialize appearance saves * test(app): update project appearance expectations * test: fix project appearance CI expectations * refactor(projects): organize appearance code * fix(server): adopt main's reconcile model in project appearance updates * fix(projects): preserve host-local appearance targets * fix(desktop): restore Linux development startup Ubuntu can block Electron's unprivileged user-namespace fallback while the npm-installed sandbox helper is not SUID. Disable the Chromium process sandbox only in the Linux development runner so packaged desktop policy remains unchanged. * feat(projects): add custom project icons Keep URL acquisition in the client so the host daemon only validates uploaded bytes. Project name and icon changes share one adaptive edit sheet. * refactor(settings): scope projects by host --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
706ae3d9bb
commit
800bd2bef1
@@ -193,13 +193,13 @@ describe("projects settings routes", () => {
|
||||
expect(buildSettingsAddHostRoute("retry 1")).toBe("/settings/general?addHost=retry%201");
|
||||
});
|
||||
|
||||
it("buildProjectsSettingsRoute returns /settings/projects", () => {
|
||||
expect(buildProjectsSettingsRoute()).toBe("/settings/projects");
|
||||
it("buildProjectsSettingsRoute scopes the list to a host", () => {
|
||||
expect(buildProjectsSettingsRoute("host a")).toBe("/settings/hosts/host%20a/projects");
|
||||
});
|
||||
|
||||
it("buildProjectSettingsRoute addresses a host-local project id", () => {
|
||||
expect(buildProjectSettingsRoute("host a", "project/1")).toBe(
|
||||
"/settings/projects/host%20a/project%2F1",
|
||||
"/settings/hosts/host%20a/projects/project%2F1",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -248,6 +248,7 @@ describe("host settings section slugs", () => {
|
||||
expect(normalizeHostSectionSlug("connections")).toBe("connections");
|
||||
expect(normalizeHostSectionSlug("agents")).toBe("agents");
|
||||
expect(normalizeHostSectionSlug("workspaces")).toBe("workspaces");
|
||||
expect(normalizeHostSectionSlug("projects")).toBe("projects");
|
||||
expect(normalizeHostSectionSlug("providers")).toBe("providers");
|
||||
expect(normalizeHostSectionSlug("usage")).toBe("usage");
|
||||
expect(normalizeHostSectionSlug("host")).toBe("host");
|
||||
|
||||
@@ -507,6 +507,7 @@ export function isSettingsSectionSlug(value: string): value is SettingsSectionSl
|
||||
}
|
||||
|
||||
export const HOST_SECTION_SLUGS = [
|
||||
"projects",
|
||||
"connections",
|
||||
"agents",
|
||||
"workspaces",
|
||||
@@ -562,15 +563,19 @@ export function buildSettingsHostSectionRoute(serverId: string, section: HostSec
|
||||
return `/settings/hosts/${encodeSegment(normalized)}/${section}` as const;
|
||||
}
|
||||
|
||||
export function buildProjectsSettingsRoute() {
|
||||
return "/settings/projects" as const;
|
||||
export function buildProjectsSettingsRoute(serverId: string) {
|
||||
const normalized = trimNonEmpty(serverId);
|
||||
if (!normalized) {
|
||||
throw new Error("buildProjectsSettingsRoute requires a non-empty serverId");
|
||||
}
|
||||
return `/settings/hosts/${encodeSegment(normalized)}/projects` as const;
|
||||
}
|
||||
|
||||
export function buildProjectSettingsRoute(serverId: string, projectId: string) {
|
||||
if (!serverId.trim() || !projectId.trim()) {
|
||||
throw new Error("buildProjectSettingsRoute requires a serverId and projectId");
|
||||
}
|
||||
return `/settings/projects/${encodeSegment(serverId)}/${encodeSegment(projectId)}` as const;
|
||||
return `/settings/hosts/${encodeSegment(serverId)}/projects/${encodeSegment(projectId)}` as const;
|
||||
}
|
||||
|
||||
export function normalizeProjectSettingsRouteId(value: string | string[] | undefined): string {
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ProjectHostEntry {
|
||||
workspaces: WorkspaceSummary[];
|
||||
gitRuntime?: WorkspaceDescriptor["gitRuntime"];
|
||||
githubRuntime?: WorkspaceDescriptor["githubRuntime"];
|
||||
customIconRevision?: string | null;
|
||||
}
|
||||
|
||||
export interface ProjectSummary {
|
||||
@@ -85,6 +86,7 @@ interface HostGroup {
|
||||
serverName: string;
|
||||
isOnline: boolean;
|
||||
workspaces: WorkspaceDescriptor[];
|
||||
customIconRevision?: string | null;
|
||||
// Repo root for a project parent that has no workspaces yet. Without it the
|
||||
// host's repoRoot resolves to "" and the project reads as non-editable.
|
||||
fallbackRepoRoot: string;
|
||||
@@ -156,6 +158,7 @@ function toHostEntry(group: HostGroup): ProjectHostEntry {
|
||||
workspaces: group.workspaces.map(toWorkspaceSummary),
|
||||
gitRuntime: canonical?.gitRuntime,
|
||||
githubRuntime: canonical?.githubRuntime,
|
||||
customIconRevision: canonical?.projectCustomIconRevision ?? group.customIconRevision,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,6 +222,7 @@ function addHostProjects(
|
||||
serverName: host.serverName,
|
||||
isOnline: host.isOnline,
|
||||
workspaces: [],
|
||||
customIconRevision: placement.customIconRevision,
|
||||
fallbackRepoRoot: repoRootByProjectId.get(projectId) ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface SidebarProjectHostTarget {
|
||||
serverId: string;
|
||||
projectId: string;
|
||||
iconWorkingDir: string;
|
||||
customIconRevision?: string | null;
|
||||
}
|
||||
|
||||
export type SidebarProjectTrailingAction =
|
||||
@@ -23,12 +24,18 @@ function hostTarget(input: {
|
||||
serverId: string;
|
||||
projectId: string;
|
||||
iconWorkingDir: string;
|
||||
customIconRevision?: string | null;
|
||||
}): SidebarProjectHostTarget | null {
|
||||
const iconWorkingDir = input.iconWorkingDir.trim();
|
||||
if (!input.serverId || !iconWorkingDir) {
|
||||
return null;
|
||||
}
|
||||
return { serverId: input.serverId, projectId: input.projectId, iconWorkingDir };
|
||||
return {
|
||||
serverId: input.serverId,
|
||||
projectId: input.projectId,
|
||||
iconWorkingDir,
|
||||
customIconRevision: input.customIconRevision,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSidebarProjectIconTarget(
|
||||
|
||||
Reference in New Issue
Block a user