diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index f4949580408..3e7b1fec01b 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -34,6 +34,7 @@ import { sanitizeAuthorPrefix, } from "../workspaces/utils/git"; import { getDefaultProjectColor } from "./utils/colors"; +import { discoverFavicon } from "./utils/favicon-discovery"; import { fetchGitHubOwner, getGitHubAvatarUrl } from "./utils/github"; type Project = SelectProject; @@ -53,10 +54,39 @@ export type OpenNewResult = | OpenNewNeedsGitInit | OpenNewError; +/** + * Fire-and-forget favicon discovery for a project. + * Finds a favicon in the repo directory and stores it as a base64 data URL. + */ +function triggerFaviconDiscovery({ + projectId, + repoPath, +}: { + projectId: string; + repoPath: string; +}): void { + discoverFavicon(repoPath) + .then((icon) => { + if (icon) { + localDb + .update(projects) + .set({ iconUrl: icon }) + .where(eq(projects.id, projectId)) + .run(); + console.log( + `[upsertProject] Discovered favicon for project: ${projectId}`, + ); + } + }) + .catch((err) => { + console.error("[upsertProject] Favicon discovery error:", err); + }); +} + /** * Creates or updates a project record in the database. * If a project with the same mainRepoPath exists, updates lastOpenedAt. - * Otherwise, creates a new project. + * Otherwise, creates a new project and triggers favicon discovery. */ function upsertProject(mainRepoPath: string, defaultBranch: string): Project { const name = basename(mainRepoPath); @@ -73,6 +103,14 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project { .set({ lastOpenedAt: Date.now(), defaultBranch }) .where(eq(projects.id, existing.id)) .run(); + + if (!existing.iconOverride && !existing.iconUrl) { + triggerFaviconDiscovery({ + projectId: existing.id, + repoPath: mainRepoPath, + }); + } + return { ...existing, lastOpenedAt: Date.now(), defaultBranch }; } @@ -87,6 +125,8 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project { .returning() .get(); + triggerFaviconDiscovery({ projectId: project.id, repoPath: mainRepoPath }); + return project; } @@ -184,6 +224,10 @@ async function ensureMainWorkspace(project: Project): Promise { // Allows most valid Git repo names while avoiding path traversal characters const SAFE_REPO_NAME_REGEX = /^[a-zA-Z0-9._\- ]+$/; +const ICON_DATA_URL_REGEX = + /^data:image\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+$/i; +const MAX_ICON_DATA_URL_LENGTH = 512 * 1024; + /** * Extracts and validates a repository name from a git URL. * Handles HTTP/HTTPS URLs, SSH-style URLs (git@host:user/repo), and edge cases. @@ -713,7 +757,6 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { const git = simpleGit(); await git.clone(input.url, clonePath); - // Create new project const name = basename(clonePath); const defaultBranch = await getDefaultBranch(clonePath); const project = localDb @@ -727,7 +770,11 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { .returning() .get(); - // Auto-create main workspace if it doesn't exist + triggerFaviconDiscovery({ + projectId: project.id, + repoPath: clonePath, + }); + await ensureMainWorkspace(project); track("project_opened", { @@ -1036,6 +1083,74 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { prefix: sanitizeAuthorPrefix(authorName), }; }), + + setProjectIcon: publicProcedure + .input( + z.object({ + id: z.string(), + icon: z + .string() + .trim() + .max(MAX_ICON_DATA_URL_LENGTH, "Icon data URL is too large") + .regex(ICON_DATA_URL_REGEX, "Icon must be a base64 image data URL") + .nullable(), // Base64 data URL or null to remove + }), + ) + .mutation(({ input }) => { + const project = localDb + .select() + .from(projects) + .where(eq(projects.id, input.id)) + .get(); + + if (!project) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Project ${input.id} not found`, + }); + } + + localDb + .update(projects) + .set({ iconOverride: input.icon }) + .where(eq(projects.id, input.id)) + .run(); + + return { success: true }; + }), + + discoverProjectIcon: publicProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input }) => { + const project = localDb + .select() + .from(projects) + .where(eq(projects.id, input.id)) + .get(); + + if (!project) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Project ${input.id} not found`, + }); + } + + if (project.iconOverride || project.iconUrl) { + return { success: true, found: false, skipped: true }; + } + + const icon = await discoverFavicon(project.mainRepoPath); + + if (icon) { + localDb + .update(projects) + .set({ iconUrl: icon }) + .where(eq(projects.id, input.id)) + .run(); + } + + return { success: true, found: !!icon }; + }), }); }; diff --git a/apps/desktop/src/lib/trpc/routers/projects/utils/favicon-discovery.ts b/apps/desktop/src/lib/trpc/routers/projects/utils/favicon-discovery.ts new file mode 100644 index 00000000000..62ac13fbb1b --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/projects/utils/favicon-discovery.ts @@ -0,0 +1,61 @@ +import { readFileSync, statSync } from "node:fs"; +import fg from "fast-glob"; + +const FAVICON_GLOB = "**/favicon.{ico,png,svg,jpg,jpeg,webp}"; + +const MAX_FILE_SIZE = 500 * 1024; // 500KB + +/** + * Discovers a favicon in the project directory and returns it as a base64 data URL. + * Picks the favicon closest to the project root if multiple are found. + */ +export async function discoverFavicon( + repoPath: string, +): Promise { + try { + const matches = await fg(FAVICON_GLOB, { + cwd: repoPath, + absolute: true, + onlyFiles: true, + dot: false, + }); + + if (matches.length === 0) return null; + + const shortest = matches.sort((a, b) => a.length - b.length)[0]; + + const stats = statSync(shortest); + if (stats.size > MAX_FILE_SIZE) { + console.log( + `[favicon-discovery] File too large: ${shortest} (${stats.size} bytes)`, + ); + return null; + } + + const buffer = readFileSync(shortest); + const base64 = buffer.toString("base64"); + const ext = shortest.split(".").pop()?.toLowerCase(); + const mime = getMimeType(ext); + + console.log( + `[favicon-discovery] Found favicon: ${shortest} (${stats.size} bytes)`, + ); + + return `data:${mime};base64,${base64}`; + } catch (error) { + console.error("[favicon-discovery] Error:", error); + return null; + } +} + +function getMimeType(ext: string | undefined): string { + const mimes: Record = { + ico: "image/x-icon", + png: "image/png", + svg: "image/svg+xml", + jpg: "image/jpeg", + jpeg: "image/jpeg", + webp: "image/webp", + }; + return mimes[ext ?? ""] ?? "image/png"; +} diff --git a/apps/desktop/src/lib/trpc/routers/window.ts b/apps/desktop/src/lib/trpc/routers/window.ts index e050e1847cd..aa84a27d347 100644 --- a/apps/desktop/src/lib/trpc/routers/window.ts +++ b/apps/desktop/src/lib/trpc/routers/window.ts @@ -59,7 +59,7 @@ export const createWindowRouter = (getWindow: () => BrowserWindow | null) => { const result = await dialog.showOpenDialog(window, { properties: ["openFile"], - title: "Select Organization Logo", + title: "Select Image", filters: [ { name: "Images", diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts index 3b3894b374a..f4f9fa2f15f 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts @@ -159,6 +159,8 @@ export const createQueryProcedures = () => { tabOrder: number; githubOwner: string | null; mainRepoPath: string; + iconUrl: string | null; + iconOverride: string | null; }; workspaces: Array<{ id: string; @@ -188,6 +190,8 @@ export const createQueryProcedures = () => { tabOrder: project.tabOrder!, githubOwner: project.githubOwner ?? null, mainRepoPath: project.mainRepoPath, + iconUrl: project.iconUrl ?? null, + iconOverride: project.iconOverride ?? null, }, workspaces: [], }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx index 39f4b72ab62..b210eae7cc8 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx @@ -8,12 +8,18 @@ import { SelectTrigger, SelectValue, } from "@superset/ui/select"; +import { toast } from "@superset/ui/sonner"; import { useEffect, useState } from "react"; -import { HiOutlineCog6Tooth, HiOutlineFolder } from "react-icons/hi2"; +import { + HiOutlineCog6Tooth, + HiOutlineFolder, + HiOutlinePhoto, +} from "react-icons/hi2"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { resolveBranchPrefix, sanitizeSegment } from "shared/utils/branch"; import { ClickablePath } from "../../../../components/ClickablePath"; import { BRANCH_PREFIX_MODE_LABELS_WITH_DEFAULT } from "../../../../utils/branch-prefix"; +import { IconUploader } from "./components/IconUploader"; import { ScriptsEditor } from "./components/ScriptsEditor"; interface ProjectSettingsProps { @@ -102,6 +108,17 @@ export function ProjectSettings({ projectId }: ProjectSettingsProps) { ); }; + const setIcon = electronTrpc.projects.setProjectIcon.useMutation({ + onSuccess: () => { + utils.projects.get.invalidate({ id: projectId }); + utils.workspaces.getAllGrouped.invalidate(); + }, + onError: (err) => { + console.error("[project-settings/setIcon] Failed to update icon:", err); + toast.error(`Failed to update icon: ${err.message}`); + }, + }); + if (!project) { return null; } @@ -116,6 +133,24 @@ export function ProjectSettings({ projectId }: ProjectSettingsProps) {
+
+

+ + Project Icon +

+

+ Upload a custom icon or use the auto-discovered favicon. +

+ setIcon.mutate({ id: projectId, icon })} + isPending={setIcon.isPending} + /> +
+

Name

{project.name}

diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/IconUploader/IconUploader.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/IconUploader/IconUploader.tsx new file mode 100644 index 00000000000..312510c7ce7 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/IconUploader/IconUploader.tsx @@ -0,0 +1,120 @@ +import { Button } from "@superset/ui/button"; +import { cn } from "@superset/ui/utils"; +import { HiXMark } from "react-icons/hi2"; +import { LuUpload } from "react-icons/lu"; +import { electronTrpc } from "renderer/lib/electron-trpc"; + +interface IconUploaderProps { + projectName: string; + iconUrl: string | null; + iconOverride: string | null; + githubOwner: string | null; + onIconChange: (icon: string | null) => void; + isPending?: boolean; +} + +function getGitHubAvatarUrl(owner: string): string { + return `https://github.com/${owner}.png?size=64`; +} + +export function IconUploader({ + projectName, + iconUrl, + iconOverride, + githubOwner, + onIconChange, + isPending = false, +}: IconUploaderProps) { + const selectImageMutation = electronTrpc.window.selectImageFile.useMutation(); + + const effectiveIcon = iconOverride || iconUrl; + const hasCustomIcon = !!iconOverride; + const hasDiscoveredIcon = !!iconUrl && !iconOverride; + const isLoading = isPending || selectImageMutation.isPending; + + async function handleUpload() { + try { + const result = await selectImageMutation.mutateAsync(); + if (result.canceled || !result.dataUrl) return; + onIconChange(result.dataUrl); + } catch (error) { + console.error("[icon-uploader] Failed to select image:", error); + } + } + + const firstLetter = projectName.charAt(0).toUpperCase(); + + return ( +
+
+ {effectiveIcon ? ( + {`${projectName} + ) : githubOwner ? ( + {`${projectName} + ) : ( + + {firstLetter} + + )} +
+ +
+
+ {hasCustomIcon && ( + Custom icon uploaded + )} + {hasDiscoveredIcon && ( + + Auto-discovered from project + + )} + {!effectiveIcon && githubOwner && ( + Using GitHub avatar + )} + {!effectiveIcon && !githubOwner && ( + Using default icon + )} +
+ +
+ + + {hasCustomIcon && ( + + )} +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/IconUploader/index.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/IconUploader/index.ts new file mode 100644 index 00000000000..1eddc2c5e5a --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/components/IconUploader/index.ts @@ -0,0 +1 @@ +export { IconUploader } from "./IconUploader"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx index 197e01342f8..94ce1a612ab 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx @@ -40,6 +40,8 @@ interface ProjectHeaderProps { projectColor: string; githubOwner: string | null; mainRepoPath: string; + iconUrl?: string | null; + iconOverride?: string | null; /** Whether the project section is collapsed (workspaces hidden) */ isCollapsed: boolean; /** Whether the sidebar is in collapsed mode (icon-only view) */ @@ -55,6 +57,8 @@ export function ProjectHeader({ projectColor, githubOwner, mainRepoPath, + iconUrl, + iconOverride, isCollapsed, isSidebarCollapsed = false, onToggleCollapse, @@ -197,6 +201,8 @@ export function ProjectHeader({ projectName={projectName} projectColor={projectColor} githubOwner={githubOwner} + iconUrl={iconUrl} + iconOverride={iconOverride} /> @@ -270,6 +276,8 @@ export function ProjectHeader({ projectName={projectName} projectColor={projectColor} githubOwner={githubOwner} + iconUrl={iconUrl} + iconOverride={iconOverride} /> {projectName} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx index 308d1625130..31ec3939d98 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx @@ -28,6 +28,8 @@ interface ProjectSectionProps { projectColor: string; githubOwner: string | null; mainRepoPath: string; + iconUrl?: string | null; + iconOverride?: string | null; workspaces: Workspace[]; /** Base index for keyboard shortcuts (0-based) */ shortcutBaseIndex: number; @@ -43,6 +45,8 @@ export function ProjectSection({ projectColor, githubOwner, mainRepoPath, + iconUrl, + iconOverride, workspaces, shortcutBaseIndex, index, @@ -140,6 +144,8 @@ export function ProjectSection({ projectColor={projectColor} githubOwner={githubOwner} mainRepoPath={mainRepoPath} + iconUrl={iconUrl} + iconOverride={iconOverride} isCollapsed={isCollapsed} isSidebarCollapsed={isSidebarCollapsed} onToggleCollapse={() => toggleProjectCollapsed(projectId)} @@ -196,6 +202,8 @@ export function ProjectSection({ projectColor={projectColor} githubOwner={githubOwner} mainRepoPath={mainRepoPath} + iconUrl={iconUrl} + iconOverride={iconOverride} isCollapsed={isCollapsed} isSidebarCollapsed={isSidebarCollapsed} onToggleCollapse={() => toggleProjectCollapsed(projectId)} 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 9dcc8f2b342..beb098eea52 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,5 +1,5 @@ import { cn } from "@superset/ui/utils"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors"; @@ -8,6 +8,8 @@ interface ProjectThumbnailProps { projectName: string; projectColor: string; githubOwner: string | null; + iconUrl?: string | null; + iconOverride?: string | null; className?: string; } @@ -37,15 +39,28 @@ export function ProjectThumbnail({ projectName, projectColor, githubOwner, + iconUrl, + iconOverride, className, }: ProjectThumbnailProps) { const [imageError, setImageError] = useState(false); + const [iconError, setIconError] = useState(false); + + const effectiveIcon = iconOverride || iconUrl; + + // Reset error state when icon source changes + const prevIconRef = useRef(effectiveIcon); + if (prevIconRef.current !== effectiveIcon) { + setIconError(false); + prevIconRef.current = effectiveIcon; + } const { data: avatarData } = electronTrpc.projects.getGitHubAvatar.useQuery( { id: projectId }, { staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false, + enabled: !effectiveIcon, }, ); @@ -53,7 +68,6 @@ export function ProjectThumbnail({ const firstLetter = projectName.charAt(0).toUpperCase(); const hasCustomColor = isCustomColor(projectColor); - // Border: gray by default, custom color with slight transparency when set const borderClasses = cn( "border-[1.5px]", hasCustomColor ? undefined : "border-border", @@ -62,12 +76,31 @@ export function ProjectThumbnail({ ? { borderColor: hexToRgba(projectColor, 0.6) } : undefined; - // Show GitHub avatar if available + if (effectiveIcon && !iconError) { + return ( +
+ {`${projectName} setIconError(true)} + /> +
+ ); + } + if (owner && !imageError) { return (
statement-breakpoint +ALTER TABLE `projects` ADD `icon_override` text; \ No newline at end of file diff --git a/packages/local-db/drizzle/meta/0018_snapshot.json b/packages/local-db/drizzle/meta/0018_snapshot.json new file mode 100644 index 00000000000..b8f4e7a681c --- /dev/null +++ b/packages/local-db/drizzle/meta/0018_snapshot.json @@ -0,0 +1,1078 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "c0e04d13-ff21-4de1-9fbb-4e1411285e9c", + "prevId": "2795f5b8-dabc-492f-9510-1d29005705a0", + "tables": { + "organization_members": { + "name": "organization_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_members_organization_id_idx": { + "name": "organization_members_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "organization_members_user_id_idx": { + "name": "organization_members_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clerk_org_id": { + "name": "clerk_org_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_clerk_org_id_unique": { + "name": "organizations_clerk_org_id_unique", + "columns": [ + "clerk_org_id" + ], + "isUnique": true + }, + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "organizations_clerk_org_id_idx": { + "name": "organizations_clerk_org_id_idx", + "columns": [ + "clerk_org_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "main_repo_path": { + "name": "main_repo_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_toast_dismissed": { + "name": "config_toast_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_owner": { + "name": "github_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_mode": { + "name": "branch_prefix_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_custom": { + "name": "branch_prefix_custom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_override": { + "name": "icon_override", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_main_repo_path_idx": { + "name": "projects_main_repo_path_idx", + "columns": [ + "main_repo_path" + ], + "isUnique": false + }, + "projects_last_opened_at_idx": { + "name": "projects_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_app": { + "name": "last_used_app", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_presets": { + "name": "terminal_presets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_presets_initialized": { + "name": "terminal_presets_initialized", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selected_ringtone_id": { + "name": "selected_ringtone_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_on_quit": { + "name": "confirm_on_quit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_link_behavior": { + "name": "terminal_link_behavior", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "persist_terminal": { + "name": "persist_terminal", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "auto_apply_default_preset": { + "name": "auto_apply_default_preset", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_mode": { + "name": "branch_prefix_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_custom": { + "name": "branch_prefix_custom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notification_sounds_muted": { + "name": "notification_sounds_muted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_color": { + "name": "status_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_type": { + "name": "status_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_position": { + "name": "status_position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "estimate": { + "name": "estimate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_provider": { + "name": "external_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tasks_slug_unique": { + "name": "tasks_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "tasks_slug_idx": { + "name": "tasks_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "tasks_organization_id_idx": { + "name": "tasks_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "tasks_assignee_id_idx": { + "name": "tasks_assignee_id_idx", + "columns": [ + "assignee_id" + ], + "isUnique": false + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "tasks_created_at_idx": { + "name": "tasks_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tasks_organization_id_organizations_id_fk": { + "name": "tasks_organization_id_organizations_id_fk", + "tableFrom": "tasks", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_creator_id_users_id_fk": { + "name": "tasks_creator_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clerk_id": { + "name": "clerk_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "columns": [ + "clerk_id" + ], + "isUnique": true + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "users_clerk_id_idx": { + "name": "users_clerk_id_idx", + "columns": [ + "clerk_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspaces": { + "name": "workspaces", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worktree_id": { + "name": "worktree_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_unread": { + "name": "is_unread", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "is_unnamed": { + "name": "is_unnamed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "deleting_at": { + "name": "deleting_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspaces_project_id_idx": { + "name": "workspaces_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "workspaces_worktree_id_idx": { + "name": "workspaces_worktree_id_idx", + "columns": [ + "worktree_id" + ], + "isUnique": false + }, + "workspaces_last_opened_at_idx": { + "name": "workspaces_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "workspaces_project_id_projects_id_fk": { + "name": "workspaces_project_id_projects_id_fk", + "tableFrom": "workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_worktree_id_worktrees_id_fk": { + "name": "workspaces_worktree_id_worktrees_id_fk", + "tableFrom": "workspaces", + "tableTo": "worktrees", + "columnsFrom": [ + "worktree_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "worktrees": { + "name": "worktrees", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "git_status": { + "name": "git_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_status": { + "name": "github_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "worktrees_project_id_idx": { + "name": "worktrees_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "worktrees_branch_idx": { + "name": "worktrees_branch_idx", + "columns": [ + "branch" + ], + "isUnique": false + } + }, + "foreignKeys": { + "worktrees_project_id_projects_id_fk": { + "name": "worktrees_project_id_projects_id_fk", + "tableFrom": "worktrees", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/local-db/drizzle/meta/_journal.json b/packages/local-db/drizzle/meta/_journal.json index ccb9d33bc4d..a99b772fc3a 100644 --- a/packages/local-db/drizzle/meta/_journal.json +++ b/packages/local-db/drizzle/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1770103994672, "tag": "0017_add_is_unnamed_to_workspaces", "breakpoints": true + }, + { + "idx": 18, + "version": "6", + "when": 1770117318144, + "tag": "0018_add_project_icons", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/local-db/src/schema/schema.ts b/packages/local-db/src/schema/schema.ts index 4abacb11cff..bc9425b37d6 100644 --- a/packages/local-db/src/schema/schema.ts +++ b/packages/local-db/src/schema/schema.ts @@ -37,6 +37,8 @@ export const projects = sqliteTable( githubOwner: text("github_owner"), branchPrefixMode: text("branch_prefix_mode").$type(), branchPrefixCustom: text("branch_prefix_custom"), + iconUrl: text("icon_url"), + iconOverride: text("icon_override"), }, (table) => [ index("projects_main_repo_path_idx").on(table.mainRepoPath),