diff --git a/.env.example b/.env.example index dda41bee262..792ab3dbd95 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,3 @@ # Database connection URL for local PostgreSQL # Connect to the 'superset' database -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/superset - -STUB_API_KEY= - -# Electron Desktop App -# Vite dev server port for Electron renderer process -# Default: 4927. Auto-increments when creating new worktrees to avoid port conflicts -VITE_DEV_SERVER_PORT=4927 - -# Enable new UI layout (workspace tabs at top, worktree sidebar) -# Set to 'true' to enable the new mock UI for visual iteration -ENABLE_NEW_UI=false +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/superset \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index a7d1c9434e7..042b0e14118 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -179,28 +179,6 @@ const result = await window.ipcRenderer.invoke("my-channel", { - `types.ts` - Data models - `ipc-channels.ts` - IPC type definitions -### Running Multiple Instances - -You can run multiple Electron instances simultaneously for parallel development. See `apps/desktop/MULTIPLE_INSTANCES.md` for full documentation. - -**Quick start:** -```bash -# Method 1: Auto-increment port when creating worktrees -# The update-port.sh script runs automatically during worktree setup -# and increments VITE_DEV_SERVER_PORT in the root .env - -# Method 2: Manual port update -./update-port.sh # Increments port in root .env -cd apps/desktop && bun dev - -# Method 3: Helper scripts (override .env) -./dev-instance.sh instance2 4928 -``` - -Each instance needs: -- **Separate dev server port** - Set via `VITE_DEV_SERVER_PORT` in root `.env` -- **Separate user data directory** - Pass via `--user-data-dir` flag - ### Environment Variable Loading The desktop app loads environment variables from the monorepo root `.env` file: diff --git a/apps/blog/src/components/Header.tsx b/apps/blog/src/components/Header.tsx index 928d47bb928..bbdffdbe5e8 100644 --- a/apps/blog/src/components/Header.tsx +++ b/apps/blog/src/components/Header.tsx @@ -40,7 +40,7 @@ export function Header() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1 }} > - {NAV_LINKS.map((link, idx) => ( + {NAV_LINKS.map((link) => ( { + ( + _event, + message: { id: string; cols: number; rows: number; seq: number }, + ) => { tmuxManager.resize(message.id, message.cols, message.rows, message.seq); }, ); diff --git a/apps/desktop/src/main/lib/terminal.ts b/apps/desktop/src/main/lib/terminal.ts index 2aeeb69dd4d..e6a511bd569 100644 --- a/apps/desktop/src/main/lib/terminal.ts +++ b/apps/desktop/src/main/lib/terminal.ts @@ -87,7 +87,11 @@ class TerminalManager { ptyProcess.onExit(({ exitCode }) => { console.log(`Terminal ${id} exited with code ${exitCode}`); // Notify renderer that terminal has exited (only if window still exists) - if (this.mainWindow && !this.mainWindow.isDestroyed() && !this.mainWindow.webContents.isDestroyed()) { + if ( + this.mainWindow && + !this.mainWindow.isDestroyed() && + !this.mainWindow.webContents.isDestroyed() + ) { this.mainWindow.webContents.send("terminal-exited", { id, exitCode, @@ -114,7 +118,11 @@ class TerminalManager { emitMessage(id: string, data: string): void { // Check if window exists and webContents is not destroyed before sending - if (this.mainWindow && !this.mainWindow.isDestroyed() && !this.mainWindow.webContents.isDestroyed()) { + if ( + this.mainWindow && + !this.mainWindow.isDestroyed() && + !this.mainWindow.webContents.isDestroyed() + ) { this.mainWindow.webContents.send("terminal-on-data", { id, data, diff --git a/apps/desktop/src/main/lib/tmux-manager.ts b/apps/desktop/src/main/lib/tmux-manager.ts index 8e2aaf2a3e2..e6b82cb3927 100644 --- a/apps/desktop/src/main/lib/tmux-manager.ts +++ b/apps/desktop/src/main/lib/tmux-manager.ts @@ -1,9 +1,9 @@ +import { spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { mkdir } from "node:fs/promises"; import os from "node:os"; import { dirname, join } from "node:path"; -import { spawnSync } from "node:child_process"; import type { BrowserWindow } from "electron"; import { app } from "electron"; import * as pty from "node-pty"; @@ -231,7 +231,9 @@ class TmuxManager { // Resize the tmux window BEFORE attaching to ensure proper dimensions // This is crucial for reconnection after restart - console.log(`[TmuxManager] Resizing tmux window ${sid} to ${cols}x${rows} before attach`); + console.log( + `[TmuxManager] Resizing tmux window ${sid} to ${cols}x${rows} before attach`, + ); const resizeResult = spawnSync("tmux", [ "-L", this.TMUX_SOCKET, @@ -253,13 +255,7 @@ class TmuxManager { // Force tmux to refresh and reflow content at new size // This ensures the pane content is properly wrapped for the new dimensions - spawnSync("tmux", [ - "-L", - this.TMUX_SOCKET, - "refresh-client", - "-t", - sid, - ]); + spawnSync("tmux", ["-L", this.TMUX_SOCKET, "refresh-client", "-t", sid]); // Attach to the session via node-pty console.log(`[TmuxManager] Attaching to session: ${sid}`); @@ -289,10 +285,6 @@ class TmuxManager { // Set up data listener ptyProcess.onData((data: string) => { - // Debug: log what's coming from PTY - if (data.includes("1;2c") || data.includes("0;276")) { - console.log(`[TmuxManager] PTY output from ${sid}:`, JSON.stringify(data), `(length: ${data.length})`); - } this.addTerminalMessage(sid, data); }); @@ -445,7 +437,6 @@ class TmuxManager { const session = this.sessions.get(sid); if (session?.pty) { // Debug: log what's being written - console.log(`[TmuxManager] Writing to ${sid}:`, JSON.stringify(data), `(length: ${data.length})`); session.pty.write(data); return true; } @@ -561,11 +552,8 @@ class TmuxManager { // Return in-memory history if available if (session.outputHistory.length > 0) { - console.log(`[TmuxManager] Returning ${session.outputHistory.length} bytes of cached history for ${sid}`); return session.outputHistory; } - - console.log(`[TmuxManager] No cached history for ${sid}, tmux will send content on attach`); return undefined; } @@ -631,9 +619,7 @@ class TmuxManager { "utf-8", ); - console.log( - `[TmuxManager] Saved ${metadata.length} sessions to disk`, - ); + console.log(`[TmuxManager] Saved ${metadata.length} sessions to disk`); } catch (error) { console.error("[TmuxManager] Failed to save sessions to disk:", error); } diff --git a/apps/desktop/src/main/lib/workspace-ipcs.ts b/apps/desktop/src/main/lib/workspace-ipcs.ts index 3ad812ab8ba..6c17467778e 100644 --- a/apps/desktop/src/main/lib/workspace-ipcs.ts +++ b/apps/desktop/src/main/lib/workspace-ipcs.ts @@ -525,6 +525,88 @@ export function registerWorkspaceIPCs() { }, ); + // Get git diff file list for a worktree (without detailed changes) + ipcMain.handle( + "worktree-get-git-diff-files", + async (_event, input: { workspaceId: string; worktreeId: string }) => { + try { + const workspace = await workspaceManager.getWorkspace( + input.workspaceId, + ); + if (!workspace) { + return { + success: false, + error: "Workspace not found", + }; + } + + const worktree = workspace.worktrees.find( + (wt) => wt.id === input.worktreeId, + ); + if (!worktree) { + return { + success: false, + error: "Worktree not found", + }; + } + + return await worktreeManager.getGitDiffFiles( + worktree.path, + workspace.branch, + ); + } catch (error) { + console.error("Failed to get git diff files:", error); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }, + ); + + // Get git diff for a single file + ipcMain.handle( + "worktree-get-git-file-diff", + async ( + _event, + input: { workspaceId: string; worktreeId: string; filePath: string }, + ) => { + try { + const workspace = await workspaceManager.getWorkspace( + input.workspaceId, + ); + if (!workspace) { + return { + success: false, + error: "Workspace not found", + }; + } + + const worktree = workspace.worktrees.find( + (wt) => wt.id === input.worktreeId, + ); + if (!worktree) { + return { + success: false, + error: "Worktree not found", + }; + } + + return await worktreeManager.getGitFileDiff( + worktree.path, + workspace.branch, + input.filePath, + ); + } catch (error) { + console.error("Failed to get git file diff:", error); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }, + ); + // Open app settings in Cursor ipcMain.handle("open-app-settings", async () => { try { diff --git a/apps/desktop/src/main/lib/worktree-manager.ts b/apps/desktop/src/main/lib/worktree-manager.ts index 3bcc1be7c9f..9bb2e4c0844 100644 --- a/apps/desktop/src/main/lib/worktree-manager.ts +++ b/apps/desktop/src/main/lib/worktree-manager.ts @@ -528,6 +528,249 @@ class WorktreeManager { } } + /** + * Get list of changed files without detailed line-by-line changes + * This is faster than getGitDiff for just listing files + */ + async getGitDiffFiles( + worktreePath: string, + mainBranch: string, + ): Promise<{ + success: boolean; + files?: Array<{ + id: string; + fileName: string; + filePath: string; + status: "added" | "deleted" | "modified" | "renamed"; + oldPath?: string; + additions: number; + deletions: number; + }>; + error?: string; + }> { + try { + // Get list of changed files with status + const diffFilesResult = await execAsync( + `git diff ${mainBranch} --name-status`, + { cwd: worktreePath }, + ); + + const fileLines = diffFilesResult.stdout + .trim() + .split("\n") + .filter(Boolean); + const files = []; + + for (const fileLine of fileLines) { + const parts = fileLine.split("\t"); + const statusCode = parts[0]; + const filePath = parts[1]; + const oldPath = parts[2]; // For renamed files + + // Determine status + let status: "added" | "deleted" | "modified" | "renamed" = "modified"; + if (statusCode.startsWith("A")) status = "added"; + else if (statusCode.startsWith("D")) status = "deleted"; + else if (statusCode.startsWith("R")) status = "renamed"; + else if (statusCode.startsWith("M")) status = "modified"; + + // Get numstat for additions/deletions count + const numstatCommand = `git diff ${mainBranch} --numstat -- "${oldPath || filePath}"`; + const numstatResult = await execAsync(numstatCommand, { + cwd: worktreePath, + }); + + let additions = 0; + let deletions = 0; + const numstatLine = numstatResult.stdout.trim(); + if (numstatLine) { + const [add, del] = numstatLine.split(/\s+/); + additions = add === "-" ? 0 : parseInt(add, 10) || 0; + deletions = del === "-" ? 0 : parseInt(del, 10) || 0; + } + + const fileName = path.basename(oldPath || filePath); + files.push({ + id: `file-${files.length}`, + fileName, + filePath: oldPath || filePath, + status, + ...(oldPath && { oldPath: filePath }), + additions, + deletions, + }); + } + + return { + success: true, + files, + }; + } catch (error) { + console.error("Failed to get git diff files:", error); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** + * Get detailed git diff for a single file + */ + async getGitFileDiff( + worktreePath: string, + mainBranch: string, + filePath: string, + ): Promise<{ + success: boolean; + diff?: { + id: string; + fileName: string; + filePath: string; + status: "added" | "deleted" | "modified" | "renamed"; + oldPath?: string; + additions: number; + deletions: number; + changes: Array<{ + type: "added" | "removed" | "modified" | "unchanged"; + oldLineNumber: number | null; + newLineNumber: number | null; + content: string; + }>; + }; + error?: string; + }> { + try { + // First get the file status + const statusResult = await execAsync( + `git diff ${mainBranch} --name-status -- "${filePath}"`, + { cwd: worktreePath }, + ); + + const statusLine = statusResult.stdout.trim(); + if (!statusLine) { + return { + success: false, + error: "File not found in diff", + }; + } + + const parts = statusLine.split("\t"); + const statusCode = parts[0]; + const currentPath = parts[1]; + const oldPath = parts[2]; // For renamed files + + // Determine status + let status: "added" | "deleted" | "modified" | "renamed" = "modified"; + if (statusCode.startsWith("A")) status = "added"; + else if (statusCode.startsWith("D")) status = "deleted"; + else if (statusCode.startsWith("R")) status = "renamed"; + else if (statusCode.startsWith("M")) status = "modified"; + + // Get detailed diff for this file + const diffCommand = + status === "deleted" + ? `git diff ${mainBranch} -- "${currentPath}"` + : `git diff ${mainBranch} -- "${oldPath || currentPath}"`; + + const fileDiffResult = await execAsync(diffCommand, { + cwd: worktreePath, + maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large diffs + }); + + const diffOutput = fileDiffResult.stdout; + + // Parse the diff output + const changes: Array<{ + type: "added" | "removed" | "modified" | "unchanged"; + oldLineNumber: number | null; + newLineNumber: number | null; + content: string; + }> = []; + + let oldLineNum = 0; + let newLineNum = 0; + let additions = 0; + let deletions = 0; + + const diffLines = diffOutput.split("\n"); + for (let i = 0; i < diffLines.length; i++) { + const line = diffLines[i]; + + // Parse hunk headers (e.g., @@ -1,4 +1,5 @@) + if (line.startsWith("@@")) { + const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/); + if (match) { + oldLineNum = parseInt(match[1], 10); + newLineNum = parseInt(match[2], 10); + } + continue; + } + + // Skip file headers + if ( + line.startsWith("diff --git") || + line.startsWith("index ") || + line.startsWith("---") || + line.startsWith("+++") + ) { + continue; + } + + // Parse actual changes + if (line.startsWith("+")) { + changes.push({ + type: "added", + oldLineNumber: null, + newLineNumber: newLineNum, + content: line.substring(1), + }); + newLineNum++; + additions++; + } else if (line.startsWith("-")) { + changes.push({ + type: "removed", + oldLineNumber: oldLineNum, + newLineNumber: null, + content: line.substring(1), + }); + oldLineNum++; + deletions++; + } else if (line.startsWith(" ")) { + changes.push({ + type: "unchanged", + oldLineNumber: oldLineNum, + newLineNumber: newLineNum, + content: line.substring(1), + }); + oldLineNum++; + newLineNum++; + } + } + + const fileName = path.basename(oldPath || currentPath); + return { + success: true, + diff: { + id: `file-${filePath}`, + fileName, + filePath: oldPath || currentPath, + status, + ...(oldPath && { oldPath: currentPath }), + additions, + deletions, + changes, + }, + }; + } catch (error) { + console.error("Failed to get git file diff:", error); + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + /** * Get detailed git diff for a worktree (line-by-line changes) */ diff --git a/apps/desktop/src/renderer/screens/main/MainScreen.tsx b/apps/desktop/src/renderer/screens/main/MainScreen.tsx index 9e60ff27fb4..9d79767aa66 100644 --- a/apps/desktop/src/renderer/screens/main/MainScreen.tsx +++ b/apps/desktop/src/renderer/screens/main/MainScreen.tsx @@ -70,14 +70,11 @@ function DroppableMainContent({ } export function MainScreen() { - // Check if new UI is enabled - const enableNewUI = import.meta.env.ENABLE_NEW_UI === "true"; - - // If new UI is enabled, render the new layout - if (enableNewUI) { - return ; - } + // Use the new layout by default + return ; +} +export function OldMainScreen() { // Otherwise, render the original layout const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [showSidebarOverlay, setShowSidebarOverlay] = useState(false); @@ -1687,18 +1684,16 @@ export function MainScreen() { )} {/* Sidebar overlay when hidden and hovering */} - {!isSidebarOpen && showSidebarOverlay && workspaces && ( + {!isSidebarOpen && showSidebarOverlay && currentWorkspace && (
setShowSidebarOverlay(false)} >
{ @@ -1723,13 +1718,11 @@ export function MainScreen() { onCollapse={() => setIsSidebarOpen(false)} onExpand={() => setIsSidebarOpen(true)} > - {isSidebarOpen && workspaces && ( + {isSidebarOpen && currentWorkspace && ( { @@ -1756,8 +1749,6 @@ export function MainScreen() { panel.expand(); } }} - workspaceName={currentWorkspace?.name} - currentBranch={currentWorkspace?.branch} /> {/* Content Area */} diff --git a/apps/desktop/src/renderer/screens/main/components/DiffView/DiffContent.tsx b/apps/desktop/src/renderer/screens/main/components/DiffView/DiffContent.tsx index 4f6de605209..1a25373bd94 100644 --- a/apps/desktop/src/renderer/screens/main/components/DiffView/DiffContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/DiffView/DiffContent.tsx @@ -6,12 +6,19 @@ import type { DiffLine, FileDiff } from "./types"; interface DiffContentProps { file: FileDiff; + isLoading?: boolean; } export const DiffContent = memo(function DiffContent({ file, + isLoading = false, }: DiffContentProps) { const language = detectLanguage(file.fileName); + + // Show loading skeleton if file content isn't loaded yet + const hasContent = file.changes.length > 0; + const showLoading = isLoading || !hasContent; + const renderDiffLine = (line: DiffLine, index: number) => { const getBgColor = () => { switch (line.type) { @@ -142,14 +149,15 @@ export const DiffContent = memo(function DiffContent({
)} {file.status} @@ -158,9 +166,22 @@ export const DiffContent = memo(function DiffContent({
{/* Diff content area */}
-
- {file.changes.map((line, index) => renderDiffLine(line, index))} -
+ {showLoading ? ( +
+
+
+
+

+ {isLoading ? "Loading file diff..." : "No changes to display"} +

+
+
+
+ ) : ( +
+ {file.changes.map((line, index) => renderDiffLine(line, index))} +
+ )}
); diff --git a/apps/desktop/src/renderer/screens/main/components/DiffView/DiffView.tsx b/apps/desktop/src/renderer/screens/main/components/DiffView/DiffView.tsx index dcdf1c9b4b2..4f148c3a474 100644 --- a/apps/desktop/src/renderer/screens/main/components/DiffView/DiffView.tsx +++ b/apps/desktop/src/renderer/screens/main/components/DiffView/DiffView.tsx @@ -28,6 +28,10 @@ interface DiffViewProps { onRefresh?: () => void; isRefreshing?: boolean; onClose?: () => void; + hideFileTree?: boolean; + externalSelectedFile?: string | null; + onFileSelect?: (fileId: string) => void; + loadingFiles?: Set; } export function DiffView({ @@ -35,15 +39,24 @@ export function DiffView({ onRefresh, isRefreshing = false, onClose, + hideFileTree = false, + externalSelectedFile = null, + onFileSelect: externalOnFileSelect, + loadingFiles = new Set(), }: DiffViewProps) { const [viewMode, setViewMode] = useState("files"); - const [selectedFile, setSelectedFile] = useState( - data.files[0]?.id || null, - ); - const [showFileTree, setShowFileTree] = useState(true); + const [internalSelectedFile, setInternalSelectedFile] = useState< + string | null + >(data.files[0]?.id || null); + const [showFileTree, setShowFileTree] = useState(!hideFileTree); const scrollContainerRef = useRef(null); const isScrollingProgrammatically = useRef(false); + // Use external selected file if provided, otherwise use internal state + const selectedFile = + externalSelectedFile !== null ? externalSelectedFile : internalSelectedFile; + const setSelectedFile = externalOnFileSelect || setInternalSelectedFile; + const getFileIcon = (status: FileDiff["status"]) => { switch (status) { case "added": @@ -338,44 +351,46 @@ export function DiffView({ ) : ( // Files changed view - scrollable list of all files (GitHub style) <> - {/* File tree sidebar - kept mounted, hidden with display:none for instant toggle */} -
-
-
-

Files

-
- - - - - -

Hide file tree

-
-
+ {/* File tree sidebar - hidden when hideFileTree prop is true */} + {!hideFileTree && ( +
+
+
+

Files

+
+ + + + + +

Hide file tree

+
+
+
+
+
+
-
-
-
-
+ )} {/* All files diff content - scrollable */}
- {!showFileTree && ( + {!hideFileTree && !showFileTree && (
))}
diff --git a/apps/desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx b/apps/desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx index 0b45377a395..69eea11d852 100644 --- a/apps/desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx @@ -17,6 +17,7 @@ interface TabContentProps { onTabFocus: (tabId: string) => void; workspaceName?: string; mainBranch?: string; + isVisibleInMosaic?: boolean; // Whether this tab is visible in a mosaic layout } /** @@ -39,6 +40,7 @@ export default function TabContent({ onTabFocus, workspaceName, mainBranch, + isVisibleInMosaic = false, }: TabContentProps) { const handleFocus = () => { onTabFocus(tab.id); @@ -71,6 +73,7 @@ export default function TabContent({ groupTabId={groupTabId} selectedTabId={selectedTabId} onFocus={handleFocus} + isVisibleInMosaic={isVisibleInMosaic} /> ); @@ -165,6 +168,7 @@ interface TerminalTabContentProps { groupTabId: string; // ID of the parent group tab selectedTabId?: string; // Currently selected tab ID onFocus: () => void; + isVisibleInMosaic?: boolean; // Whether this tab is visible in a mosaic layout } function TerminalTabContent({ @@ -175,17 +179,24 @@ function TerminalTabContent({ groupTabId, selectedTabId, onFocus, + isVisibleInMosaic = false, }: TerminalTabContentProps) { const terminalId = tab.id; const terminalCreatedRef = useRef(false); const isSelected = selectedTabId === tab.id; + // Terminal should be visible if it's either selected OR visible in a mosaic layout + const isVisible = isSelected || isVisibleInMosaic; // Terminal creation and lifecycle // NOTE: Actual terminal-create is now deferred to the Terminal component // so it can pass the correct dimensions when ready useEffect(() => { // Execute startup command if specified (only after terminal is created) - if (tab.command && tab.command.trim() !== "" && !terminalCreatedRef.current) { + if ( + tab.command && + tab.command.trim() !== "" && + !terminalCreatedRef.current + ) { terminalCreatedRef.current = true; const commandToExecute = tab.command; // Wait for terminal to be created and attached @@ -234,7 +245,7 @@ function TerminalTabContent({
@@ -259,29 +260,41 @@ export default function TabGroup({ .mosaic-theme-dark .mosaic-window { background: #1a1a1a; border: 1px solid #333; + outline: none; + transition: outline 0.15s ease; } .mosaic-theme-dark .mosaic-window .mosaic-window-toolbar { background: #262626; border-bottom: 1px solid #333; height: 32px; padding: 0 8px; + transition: background-color 0.15s ease; } .mosaic-theme-dark .mosaic-window .mosaic-window-title { color: #e5e5e5; font-size: 12px; + transition: color 0.15s ease; } .mosaic-theme-dark .mosaic-window-body { background: #1a1a1a; } .mosaic-theme-dark .mosaic-split { background: #333; + opacity: 0; + border-radius: 25px; + transition: opacity 0.2s ease, background-color 0.2s ease; } .mosaic-theme-dark .mosaic-split:hover { background: #444; + opacity: 1; } - .active-mosaic-window .mosaic-window { - border: 1px solid #3b82f6 !important; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3); + .mosaic-theme-dark .mosaic-split:active, + .mosaic-theme-dark .mosaic-split.mosaic-split-dragging { + background: #555; + opacity: 1; + } + .active-mosaic-window .mosaic-window-toolbar { + background: #3a3a3a !important; } `}
diff --git a/apps/desktop/src/renderer/screens/main/components/MainContent/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/MainContent/Terminal.tsx index bc26a27cd9e..a38b851b94b 100644 --- a/apps/desktop/src/renderer/screens/main/components/MainContent/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/MainContent/Terminal.tsx @@ -84,9 +84,6 @@ export default function TerminalComponent({ const fitFunctionRef = useRef<(() => void) | null>(null); const hasBeenVisibleRef = useRef(false); - // Log visibility changes for debugging - console.log(`[Terminal] Render: ${terminalId?.slice(0, 8)} hidden=${hidden} terminal=${!!terminal}`); - // Update the ref when onFocus changes useEffect(() => { onFocusRef.current = onFocus; @@ -101,8 +98,6 @@ export default function TerminalComponent({ // Handle visibility changes - fit terminal when it becomes visible useEffect(() => { - console.log(`[Terminal] Visibility effect fired: ${terminalIdRef.current?.slice(0, 8)} hidden=${hidden} terminal=${!!terminal} hasBeenVisible=${hasBeenVisibleRef.current}`); - if (!hidden && terminal && fitFunctionRef.current && terminalRef.current) { const isFirstTimeVisible = !hasBeenVisibleRef.current; hasBeenVisibleRef.current = true; @@ -116,14 +111,10 @@ export default function TerminalComponent({ const tryFit = () => { const rect = terminalRef.current?.getBoundingClientRect(); if (rect && rect.width > 0 && rect.height > 0) { - console.log(`[Terminal] Fitting terminal ${terminalIdRef.current?.slice(0, 8)} after becoming visible (attempt ${attempts + 1}, firstTime=${isFirstTimeVisible})`); fitFunctionRef.current?.(); } else if (attempts < maxAttempts) { attempts++; - console.log(`[Terminal] Container not ready for ${terminalIdRef.current?.slice(0, 8)}, retrying... (${rect?.width}x${rect?.height})`); setTimeout(tryFit, retryDelay); - } else { - console.warn(`[Terminal] Failed to fit ${terminalIdRef.current} after ${maxAttempts} attempts`); } }; @@ -145,16 +136,12 @@ export default function TerminalComponent({ return; } - console.log(`[Terminal] Initializing terminal: ${terminalId.slice(0, 8)}`); - // Set terminalIdRef immediately to prevent race conditions terminalIdRef.current = terminalId; const { term } = initTerminal(terminalRef.current, theme, onFocusRef); setTerminal(term); - console.log(`[Terminal] Initialized terminal: ${terminalId.slice(0, 8)}`); - return () => { // Don't dispose XTerm or cleanup on unmount // XTerm instances should persist through reordering @@ -232,7 +219,6 @@ export default function TerminalComponent({ const height = rect.height; if (width <= 0 || height <= 0) { - console.log(`[Terminal] Skipping fit for ${terminalIdRef.current} - container has no dimensions (${width}x${height})`); return; // Skip if container has no dimensions yet } @@ -240,7 +226,6 @@ export default function TerminalComponent({ // Then manually resize to ensure PTY gets the correct dimensions const dimensions = fitAddon.proposeDimensions(); if (dimensions) { - console.log(`[Terminal] Fitting ${terminalIdRef.current} to ${dimensions.cols}x${dimensions.rows}`); term.resize(dimensions.cols, dimensions.rows); } } catch (e) { @@ -276,15 +261,16 @@ export default function TerminalComponent({ const cols = dimensions?.cols || 80; const rows = dimensions?.rows || 30; - console.log(`[Terminal] Creating terminal ${terminalId.slice(0, 8)} with dimensions ${cols}x${rows}`); - window.ipcRenderer.invoke("terminal-create", { - id: terminalId, - cwd, - cols, - rows, - }).catch((error: Error) => { - console.error("Failed to create terminal:", error); - }); + window.ipcRenderer + .invoke("terminal-create", { + id: terminalId, + cwd, + cols, + rows, + }) + .catch((error: Error) => { + console.error("Failed to create terminal:", error); + }); } // Listen for container resize to auto-fit terminal @@ -365,20 +351,17 @@ export default function TerminalComponent({ // Filter out terminal response sequences that shouldn't be sent as input // These are generated by xterm.js in response to queries from the shell/tmux const isTerminalResponse = - data.startsWith('\x1b[?1;') || // Primary DA response (e.g., \x1b[?1;2c) - data.startsWith('\x1b[>') || // Secondary DA response (e.g., \x1b[>0;276;0c) - data.startsWith('\x1b]10;') || // Foreground color query response - data.startsWith('\x1b]11;') || // Background color query response - data === '\x1b[I' || // Focus in event - data === '\x1b[O'; // Focus out event + data.startsWith("\x1b[?1;") || // Primary DA response (e.g., \x1b[?1;2c) + data.startsWith("\x1b[>") || // Secondary DA response (e.g., \x1b[>0;276;0c) + data.startsWith("\x1b]10;") || // Foreground color query response + data.startsWith("\x1b]11;") || // Background color query response + data === "\x1b[I" || // Focus in event + data === "\x1b[O"; // Focus out event if (isTerminalResponse) { - console.log(`[Terminal] Filtered terminal response for ${terminalIdRef.current}:`, JSON.stringify(data)); return; } - // Debug: log user input - console.log(`[Terminal] User input for ${terminalIdRef.current}:`, JSON.stringify(data), `(length: ${data.length})`); window.ipcRenderer.send("terminal-input", { id: terminalIdRef.current, data, @@ -421,10 +404,6 @@ export default function TerminalComponent({ const terminalDataListener = (message: TerminalMessage) => { if (message?.id === terminalIdRef.current) { - // Debug: log data being written to xterm - if (message.data.includes("1;2c") || message.data.includes("0;276")) { - console.log(`[Terminal] Received data for ${terminalIdRef.current}:`, JSON.stringify(message.data), `(length: ${message.data.length})`); - } // If we're in the middle of a resize, queue the write if (isResizing) { writeQueue.push(message.data); diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/AddTaskModal.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/AddTaskModal.tsx index d745da73ee0..bde43cccc54 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/AddTaskModal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/AddTaskModal.tsx @@ -88,7 +88,7 @@ export const AddTaskModal: React.FC = ({ task.slug.toLowerCase().includes(query) || task.name.toLowerCase().includes(query) || task.description.toLowerCase().includes(query) || - task.assignee.toLowerCase().includes(query) + task.assignee.toLowerCase().includes(query), ); }, [tasks, searchQuery]); @@ -102,13 +102,14 @@ export const AddTaskModal: React.FC = ({ // Get currently selected task (from all tasks, not just filtered) const selectedTask = useMemo( () => tasks.find((task) => task.id === selectedTaskId) || null, - [tasks, selectedTaskId] + [tasks, selectedTaskId], ); // Check if selected task is already open const isSelectedTaskOpen = useMemo( - () => selectedTask ? openTasks.some((t) => t.id === selectedTask.id) : false, - [selectedTask, openTasks] + () => + selectedTask ? openTasks.some((t) => t.id === selectedTask.id) : false, + [selectedTask, openTasks], ); // Auto-generate branch name from task name @@ -140,7 +141,7 @@ export const AddTaskModal: React.FC = ({ if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); const currentIndex = filteredTasks.findIndex( - (task) => task.id === selectedTaskId + (task) => task.id === selectedTaskId, ); if (e.key === "ArrowDown" && currentIndex < filteredTasks.length - 1) { @@ -239,7 +240,12 @@ export const AddTaskModal: React.FC = ({
{mode === "list" && ( - @@ -252,10 +258,7 @@ export const AddTaskModal: React.FC = ({ <> {/* Two-column layout */}
- + {/* Left panel: Search + Task list */}
@@ -337,7 +340,10 @@ export const AddTaskModal: React.FC = ({ ) : ( <> {/* New task form - Description-focused layout */} -
+ {/* Title section */}
= ({
{/* Status */} - + setNewTaskStatus(value as TaskStatus) + } + > Planning - Needs Feedback - Ready to Merge + + Needs Feedback + + + Ready to Merge + {/* Assignee */} -
- + You
@@ -393,7 +415,11 @@ export const AddTaskModal: React.FC = ({ Agents
- + Claude
@@ -409,7 +435,12 @@ export const AddTaskModal: React.FC = ({
{/* Footer for new task form */}
- diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx index 7ee9236e4d4..97e1f63f81c 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx @@ -12,10 +12,10 @@ import { Background } from "../Background"; import TabContent from "../MainContent/TabContent"; import TabGroup from "../MainContent/TabGroup"; import { PlaceholderState } from "../PlaceholderState"; -import { Sidebar } from "../Sidebar"; import { DiffTab } from "../TabContent/components/DiffTab"; import { AddTaskModal } from "./AddTaskModal"; import { TaskTabs } from "./TaskTabs"; +import { WorktreeTabsSidebar } from "./WorktreeTabsSidebar"; import { WorktreeTabView } from "./WorktreeTabView"; // Mock tasks data - TODO: Replace with actual task data from backend @@ -29,7 +29,7 @@ const MOCK_TASKS = [ description: "Redesigning the homepage with new branding and improved UX", assignee: "Alice", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=1", - lastUpdated: "2 hours ago" + lastUpdated: "2 hours ago", }, { id: "2", @@ -40,7 +40,7 @@ const MOCK_TASKS = [ description: "Integrate new REST API endpoints for user management", assignee: "Bob", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=12", - lastUpdated: "1 day ago" + lastUpdated: "1 day ago", }, { id: "3", @@ -51,7 +51,7 @@ const MOCK_TASKS = [ description: "Collection of bug fixes reported by users", assignee: "Charlie", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=33", - lastUpdated: "3 days ago" + lastUpdated: "3 days ago", }, { id: "4", @@ -62,7 +62,7 @@ const MOCK_TASKS = [ description: "Optimize database queries for faster page loads", assignee: "Diana", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=9", - lastUpdated: "5 minutes ago" + lastUpdated: "5 minutes ago", }, { id: "5", @@ -70,10 +70,11 @@ const MOCK_TASKS = [ name: "User Authentication System", status: "working" as const, branch: "feature/auth-system", - description: "Implement OAuth2 and JWT-based authentication system with refresh tokens", + description: + "Implement OAuth2 and JWT-based authentication system with refresh tokens", assignee: "Eve", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=5", - lastUpdated: "3 hours ago" + lastUpdated: "3 hours ago", }, { id: "6", @@ -84,7 +85,7 @@ const MOCK_TASKS = [ description: "Add dark mode theme support across the entire application", assignee: "Frank", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=13", - lastUpdated: "2 days ago" + lastUpdated: "2 days ago", }, { id: "7", @@ -92,10 +93,11 @@ const MOCK_TASKS = [ name: "Database Migration Scripts", status: "ready-to-merge" as const, branch: "db/migration-scripts", - description: "Create automated migration scripts for production database updates", + description: + "Create automated migration scripts for production database updates", assignee: "Grace", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=20", - lastUpdated: "1 hour ago" + lastUpdated: "1 hour ago", }, { id: "8", @@ -103,10 +105,11 @@ const MOCK_TASKS = [ name: "Email Notification Service", status: "needs-feedback" as const, branch: "feature/email-notifications", - description: "Build email notification service using SendGrid for transactional emails", + description: + "Build email notification service using SendGrid for transactional emails", assignee: "Henry", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=8", - lastUpdated: "4 hours ago" + lastUpdated: "4 hours ago", }, { id: "9", @@ -114,10 +117,11 @@ const MOCK_TASKS = [ name: "Mobile Responsive Design", status: "working" as const, branch: "feature/mobile-responsive", - description: "Make the application fully responsive for mobile and tablet devices", + description: + "Make the application fully responsive for mobile and tablet devices", assignee: "Iris", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=16", - lastUpdated: "6 hours ago" + lastUpdated: "6 hours ago", }, { id: "10", @@ -125,10 +129,11 @@ const MOCK_TASKS = [ name: "Analytics Dashboard", status: "planning" as const, branch: "feature/analytics-dashboard", - description: "Create admin dashboard with charts and metrics for user analytics", + description: + "Create admin dashboard with charts and metrics for user analytics", assignee: "Jack", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=11", - lastUpdated: "1 week ago" + lastUpdated: "1 week ago", }, { id: "11", @@ -136,10 +141,11 @@ const MOCK_TASKS = [ name: "CI/CD Pipeline", status: "ready-to-merge" as const, branch: "devops/ci-cd-pipeline", - description: "Set up automated CI/CD pipeline with GitHub Actions and Docker", + description: + "Set up automated CI/CD pipeline with GitHub Actions and Docker", assignee: "Kate", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=25", - lastUpdated: "30 minutes ago" + lastUpdated: "30 minutes ago", }, { id: "12", @@ -150,7 +156,7 @@ const MOCK_TASKS = [ description: "Implement full-text search with Elasticsearch integration", assignee: "Liam", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=14", - lastUpdated: "5 hours ago" + lastUpdated: "5 hours ago", }, { id: "13", @@ -158,10 +164,11 @@ const MOCK_TASKS = [ name: "File Upload System", status: "needs-feedback" as const, branch: "feature/file-uploads", - description: "Build secure file upload system with S3 storage and virus scanning", + description: + "Build secure file upload system with S3 storage and virus scanning", assignee: "Mia", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=27", - lastUpdated: "2 hours ago" + lastUpdated: "2 hours ago", }, { id: "14", @@ -172,7 +179,7 @@ const MOCK_TASKS = [ description: "Implement rate limiting and throttling for API endpoints", assignee: "Noah", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=17", - lastUpdated: "4 days ago" + lastUpdated: "4 days ago", }, { id: "15", @@ -180,10 +187,11 @@ const MOCK_TASKS = [ name: "Internationalization", status: "working" as const, branch: "feature/i18n", - description: "Add multi-language support with i18next for English, Spanish, and French", + description: + "Add multi-language support with i18next for English, Spanish, and French", assignee: "Olivia", assigneeAvatarUrl: "https://i.pravatar.cc/150?img=32", - lastUpdated: "8 hours ago" + lastUpdated: "8 hours ago", }, ]; @@ -194,7 +202,7 @@ export const NewLayoutMain: React.FC = () => { const [isAddTaskModalOpen, setIsAddTaskModalOpen] = useState(false); // Initialize with first 4 tasks to match the tabs currently displayed const [openTasks, setOpenTasks] = useState( - MOCK_TASKS.slice(0, 4) + MOCK_TASKS.slice(0, 4), ); const [activeTaskId, setActiveTaskId] = useState(MOCK_TASKS[0].id); const [allTasks, setAllTasks] = useState(MOCK_TASKS); @@ -210,6 +218,8 @@ export const NewLayoutMain: React.FC = () => { const [selectedTabId, setSelectedTabId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [selectedDiffFile, setSelectedDiffFile] = useState(null); + const [sidebarMode, setSidebarMode] = useState<"tabs" | "changes">("tabs"); const handleCollapseSidebar = () => { const panel = sidebarPanelRef.current; @@ -473,7 +483,7 @@ export const NewLayoutMain: React.FC = () => { setIsAddTaskModalOpen(false); }; - const handleSelectTask = (task: typeof MOCK_TASKS[0]) => { + const handleSelectTask = (task: (typeof MOCK_TASKS)[0]) => { // Check if task is already open const isAlreadyOpen = openTasks.some((t) => t.id === task.id); if (!isAlreadyOpen) { @@ -551,6 +561,13 @@ export const NewLayoutMain: React.FC = () => { if (activeSelection?.worktreeId && activeSelection?.tabId) { setSelectedWorktreeId(activeSelection.worktreeId); setSelectedTabId(activeSelection.tabId); + } else if (workspace.worktrees && workspace.worktrees.length > 0) { + // Auto-select first worktree and its first tab if no selection exists + const firstWorktree = workspace.worktrees[0]; + setSelectedWorktreeId(firstWorktree.id); + if (firstWorktree.tabs && firstWorktree.tabs.length > 0) { + setSelectedTabId(firstWorktree.tabs[0].id); + } } } } @@ -585,6 +602,18 @@ export const NewLayoutMain: React.FC = () => { ); if (refreshedWorkspace) { setCurrentWorkspace(refreshedWorkspace); + + // Auto-select first worktree and tab if available + if ( + refreshedWorkspace.worktrees && + refreshedWorkspace.worktrees.length > 0 + ) { + const firstWorktree = refreshedWorkspace.worktrees[0]; + setSelectedWorktreeId(firstWorktree.id); + if (firstWorktree.tabs && firstWorktree.tabs.length > 0) { + setSelectedTabId(firstWorktree.tabs[0].id); + } + } } }; @@ -612,18 +641,65 @@ export const NewLayoutMain: React.FC = () => { onMouseLeave={() => setShowSidebarOverlay(false)} >
- { + { + if (selectedWorktreeId) { + handleTabSelect(selectedWorktreeId, tabId); + } + setShowSidebarOverlay(false); + }} + onTabClose={async (tabId) => { + if (!currentWorkspace || !selectedWorktreeId) return; + + const result = await window.ipcRenderer.invoke("tab-delete", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + tabId, + }); + + if (result.success) { + await handleWorktreeCreated(); + } + }} + onCreateTerminal={async () => { + if (!currentWorkspace || !selectedWorktreeId) return; + + const result = await window.ipcRenderer.invoke("tab-create", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + name: "Terminal", + type: "terminal", + }); + + if (result.success && result.tab) { + handleTabSelect(selectedWorktreeId, result.tab.id); + await handleWorktreeCreated(); + } + setShowSidebarOverlay(false); + }} + onCreatePreview={async () => { + if (!currentWorkspace || !selectedWorktreeId) return; + + const result = await window.ipcRenderer.invoke("tab-create", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + name: "Preview", + type: "preview", + }); + + if (result.success && result.tab) { + handleTabSelect(selectedWorktreeId, result.tab.id); + await handleWorktreeCreated(); + } setShowSidebarOverlay(false); }} - onShowDiff={handleShowDiff} + workspaceId={currentWorkspace?.id || null} + workspaceName={currentWorkspace?.name} + mainBranch={currentWorkspace?.branch} + onDiffFileSelect={setSelectedDiffFile} + onModeChange={setSidebarMode} />
@@ -631,19 +707,27 @@ export const NewLayoutMain: React.FC = () => {
- {/* Task tabs at the top */} + {/* Worktree tabs at the top */} { + setSelectedWorktreeId(worktreeId); + // Select first tab in the worktree + const worktree = currentWorkspace?.worktrees?.find( + (wt) => wt.id === worktreeId, + ); + if (worktree && worktree.tabs && worktree.tabs.length > 0) { + handleTabSelect(worktreeId, worktree.tabs[0].id); + } + }} /> {/* Main content area with resizable sidebar */} -
+
{ onCollapse={() => setIsSidebarOpen(false)} onExpand={() => setIsSidebarOpen(true)} > - {isSidebarOpen && workspaces && ( - { - const panel = sidebarPanelRef.current; - if (panel && !panel.isCollapsed()) { - panel.collapse(); + {isSidebarOpen && ( + { + if (selectedWorktreeId) { + handleTabSelect(selectedWorktreeId, tabId); + } + }} + onTabClose={async (tabId) => { + if (!currentWorkspace || !selectedWorktreeId) return; + + const result = await window.ipcRenderer.invoke( + "tab-delete", + { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + tabId, + }, + ); + + if (result.success) { + await handleWorktreeCreated(); + } + }} + onCreateTerminal={async () => { + if (!currentWorkspace || !selectedWorktreeId) return; + + const result = await window.ipcRenderer.invoke( + "tab-create", + { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + name: "Terminal", + type: "terminal", + }, + ); + + if (result.success && result.tab) { + handleTabSelect(selectedWorktreeId, result.tab.id); + await handleWorktreeCreated(); + } + }} + onCreatePreview={async () => { + if (!currentWorkspace || !selectedWorktreeId) return; + + const result = await window.ipcRenderer.invoke( + "tab-create", + { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + name: "Preview", + type: "preview", + }, + ); + + if (result.success && result.tab) { + handleTabSelect(selectedWorktreeId, result.tab.id); + await handleWorktreeCreated(); } }} - onShowDiff={handleShowDiff} + workspaceId={currentWorkspace?.id || null} + workspaceName={currentWorkspace?.name} + mainBranch={currentWorkspace?.branch} + onDiffFileSelect={setSelectedDiffFile} + onModeChange={setSidebarMode} /> )} @@ -682,16 +816,38 @@ export const NewLayoutMain: React.FC = () => { {/* Main content panel */} - {loading || - error || - !currentWorkspace || - !selectedTab || - !selectedWorktree ? ( + {loading || error || !currentWorkspace || !selectedWorktree ? ( + ) : sidebarMode === "changes" ? ( + // Changes mode - always show diff view +
+ +
+ ) : !selectedTab ? ( + ) : parentGroupTab ? ( // Selected tab is a sub-tab of a group → display the parent group's mosaic { worktree={selectedWorktree} workspaceName={currentWorkspace.name} mainBranch={currentWorkspace.branch} + selectedDiffFile={selectedDiffFile} + onDiffFileSelect={setSelectedDiffFile} />
) : ( diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/StatusIndicator.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/StatusIndicator.tsx index 2cdf994e6cd..9359f4e9734 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/StatusIndicator.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/StatusIndicator.tsx @@ -1,6 +1,10 @@ import type React from "react"; -export type TaskStatus = "planning" | "working" | "needs-feedback" | "ready-to-merge"; +export type TaskStatus = + | "planning" + | "working" + | "needs-feedback" + | "ready-to-merge"; interface StatusIndicatorProps { status: TaskStatus; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskListItem.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskListItem.tsx index c8c40c228f4..4ff1dfe5426 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskListItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskListItem.tsx @@ -56,7 +56,11 @@ export const TaskListItem: React.FC = ({ {/* Second line: Assignee + Time */}
- + {task.assignee}
· diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskPreview.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskPreview.tsx index e39f5e01146..44ff5944b3b 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskPreview.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskPreview.tsx @@ -26,7 +26,10 @@ const STATUS_LABELS: Record = { "ready-to-merge": "Ready to Merge", }; -export const TaskPreview: React.FC = ({ task, onOpenTask }) => { +export const TaskPreview: React.FC = ({ + task, + onOpenTask, +}) => { if (!task) { return (
@@ -54,7 +57,9 @@ export const TaskPreview: React.FC = ({ task, onOpenTask }) => {/* Description */}
-

{task.description}

+

+ {task.description} +

{/* Metadata grid */} @@ -63,7 +68,9 @@ export const TaskPreview: React.FC = ({ task, onOpenTask }) =>
Status
- {STATUS_LABELS[task.status]} + + {STATUS_LABELS[task.status]} +
@@ -74,7 +81,9 @@ export const TaskPreview: React.FC = ({ task, onOpenTask }) =>
Branch
-
{task.branch}
+
+ {task.branch} +
diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs.tsx index aa3bcb8d2d7..7079622da25 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs.tsx @@ -5,104 +5,38 @@ import { HoverCardTrigger, } from "@superset/ui/hover-card"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { PanelLeftClose, PanelLeftOpen, Plus } from "lucide-react"; +import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; import type React from "react"; -import { useState } from "react"; -import { TaskAssignee } from "./TaskAssignee"; -import { StatusIndicator, type TaskStatus } from "./StatusIndicator"; +import type { Worktree } from "shared/types"; -interface MockTask { - id: string; - slug: string; - name: string; - status: TaskStatus; - branch: string; - description: string; - assignee: string; - assigneeAvatarUrl: string; - lastUpdated: string; -} - -const MOCK_TASKS: MockTask[] = [ - { - id: "1", - slug: "SSET-1", - name: "Homepage Redesign", - status: "working", - branch: "feature/homepage-redesign", - description: "Redesigning the homepage with new branding and improved UX", - assignee: "Alice", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=1", - lastUpdated: "2 hours ago", - }, - { - id: "2", - slug: "SSET-2", - name: "API Integration", - status: "needs-feedback", - branch: "feature/api-integration", - description: "Integrate new REST API endpoints for user management", - assignee: "Bob", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=12", - lastUpdated: "1 day ago", - }, - { - id: "3", - slug: "SSET-3", - name: "Bug Fixes", - status: "planning", - branch: "fix/various-bugs", - description: "Collection of bug fixes reported by users", - assignee: "Charlie", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=33", - lastUpdated: "3 days ago", - }, - { - id: "4", - slug: "SSET-4", - name: "Performance Optimization", - status: "ready-to-merge", - branch: "perf/optimize-queries", - description: "Optimize database queries for faster page loads", - assignee: "Diana", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=9", - lastUpdated: "5 minutes ago", - }, -]; - -interface TaskTabsProps { +interface WorktreeTabsProps { onCollapseSidebar: () => void; onExpandSidebar: () => void; isSidebarOpen: boolean; - onAddTask: () => void; - activeTaskId: string; - onActiveTaskChange: (taskId: string) => void; - openTasks: MockTask[]; + worktrees: Worktree[]; + selectedWorktreeId: string | null; + onWorktreeSelect: (worktreeId: string) => void; } -export const TaskTabs: React.FC = ({ +export const TaskTabs: React.FC = ({ onCollapseSidebar, onExpandSidebar, isSidebarOpen, - onAddTask, - activeTaskId, - onActiveTaskChange, - openTasks, + worktrees, + selectedWorktreeId, + onWorktreeSelect, }) => { - return (
{/* Sidebar collapse/expand toggle */} @@ -140,99 +74,149 @@ export const TaskTabs: React.FC = ({ )}
- {/* Task tabs */} - {openTasks.map((task) => { - const statusLabel = task.status === "planning" ? "Planning" : - task.status === "working" ? "Working" : - task.status === "needs-feedback" ? "Needs Feedback" : - "Ready to Merge"; +
+ {/* Worktree tabs */} + {worktrees.map((worktree) => { + // Use description as title if available, otherwise use branch name + const displayTitle = worktree.description || worktree.branch; - return ( - - - - - -
- {/* Header with task slug/name and assignee */} -
-
-

- [{task.slug}] {task.name} -

-

- {task.description} -

+ > + {/* Status indicator dot */} +
+ + + + {hasPorts && ( + <> + + + + )}
- - {/* Assignee in top-right */} -
- + + {displayTitle} + + + + +
+ {/* Header with title */} +
+
+ {worktree.description ? ( + <> +

+ {worktree.description} +

+

+ Branch: {worktree.branch} +

+ + ) : ( +

+ {worktree.branch} +

+ )} +
-
- {/* Metadata grid */} -
-
- Status -
- - {statusLabel} + {/* Metadata grid */} +
+
+ Status +
+
+ + + +
+ + {hasPorts + ? "Running" + : hasActivity + ? "Active" + : "Inactive"} + +
-
-
- Updated - - {task.lastUpdated} - -
+ {worktree.tabs && worktree.tabs.length > 0 && ( +
+ Tabs + + {worktree.tabs.length} + +
+ )} -
- Branch - - {task.branch} - + {!worktree.description && ( +
+ Branch + + {worktree.branch} + +
+ )} + +
+ Path + + {worktree.path} + +
-
- - - ); - })} - - - - - -

Open task

-
-
+ + + ); + })} +
); diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/WorktreeTabsSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/WorktreeTabsSidebar.tsx new file mode 100644 index 00000000000..a81ba732b32 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/WorktreeTabsSidebar.tsx @@ -0,0 +1,553 @@ +import { Button } from "@superset/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { motion, useMotionValue, useTransform } from "framer-motion"; +import { + File, + FileEdit, + FilePlus, + FileText, + FileX, + GitBranch, + Monitor, + Plus, + RefreshCw, + Terminal as TerminalIcon, + X, +} from "lucide-react"; +import type React from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import type { Tab, Worktree } from "shared/types"; +import { FileTree } from "../DiffView/FileTree"; +import type { DiffViewData, FileDiff } from "../DiffView/types"; + +// Define sidebar modes +type SidebarMode = "tabs" | "changes"; + +interface Mode { + id: SidebarMode; + name: string; + icon: React.ComponentType<{ size?: number }>; +} + +const SIDEBAR_MODES: Mode[] = [ + { id: "tabs", name: "Tabs", icon: FileText }, + { id: "changes", name: "Changes", icon: GitBranch }, +]; + +interface WorktreeTabsSidebarProps { + worktree: Worktree | null; + selectedTabId: string | null; + onTabSelect: (tabId: string) => void; + onTabClose: (tabId: string) => void; + onCreateTerminal: () => void; + onCreatePreview: () => void; + workspaceId: string | null; + workspaceName?: string; + mainBranch?: string; + onDiffFileSelect?: (fileId: string | null) => void; + onModeChange?: (mode: SidebarMode) => void; +} + +export const WorktreeTabsSidebar: React.FC = ({ + worktree, + selectedTabId, + onTabSelect, + onTabClose, + onCreateTerminal, + onCreatePreview, + workspaceId, + workspaceName, + mainBranch, + onDiffFileSelect, + onModeChange, +}) => { + const [currentMode, setCurrentMode] = useState("tabs"); + const scrollProgress = useMotionValue(0); + const scrollContainerRef = useRef(null); + const scrollTimeoutRef = useRef(undefined); + const isInitialMount = useRef(true); + const backgroundX = useMotionValue(0); + + // Diff/changes state + const [diffData, setDiffData] = useState(null); + const [loadingDiff, setLoadingDiff] = useState(false); + const [diffError, setDiffError] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + + const tabs = worktree?.tabs || []; + const currentModeIndex = SIDEBAR_MODES.findIndex((m) => m.id === currentMode); + + // Track scroll position and update motion value + useEffect(() => { + const scrollContainer = scrollContainerRef.current; + if (!scrollContainer) return; + + let rafId: number | undefined; + + const updateProgress = () => { + const scrollLeft = scrollContainer.scrollLeft; + const containerWidth = scrollContainer.offsetWidth; + const progress = scrollLeft / containerWidth; + scrollProgress.set(progress); + }; + + const handleScroll = () => { + if (rafId !== undefined) { + cancelAnimationFrame(rafId); + } + rafId = requestAnimationFrame(updateProgress); + }; + + scrollContainer.addEventListener("scroll", handleScroll, { passive: true }); + updateProgress(); + + return () => { + scrollContainer.removeEventListener("scroll", handleScroll); + if (rafId !== undefined) { + cancelAnimationFrame(rafId); + } + }; + }, [scrollProgress]); + + // Scroll to current mode when it changes + useEffect(() => { + const scrollContainer = scrollContainerRef.current; + if (!scrollContainer) return; + + const targetScrollX = currentModeIndex * scrollContainer.offsetWidth; + + if (Math.abs(scrollContainer.scrollLeft - targetScrollX) > 10) { + scrollContainer.scrollTo({ + left: targetScrollX, + behavior: isInitialMount.current ? "auto" : "smooth", + }); + } + + isInitialMount.current = false; + }, [currentModeIndex]); + + // Detect when user finishes scrolling and update current mode + useEffect(() => { + const scrollContainer = scrollContainerRef.current; + if (!scrollContainer) return; + + const handleScroll = () => { + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + } + + scrollTimeoutRef.current = setTimeout(() => { + const scrollLeft = scrollContainer.scrollLeft; + const containerWidth = scrollContainer.offsetWidth; + const newIndex = Math.round(scrollLeft / containerWidth); + + if ( + newIndex >= 0 && + newIndex < SIDEBAR_MODES.length && + SIDEBAR_MODES[newIndex].id !== currentMode + ) { + const newMode = SIDEBAR_MODES[newIndex].id; + setCurrentMode(newMode); + onModeChange?.(newMode); + } + }, 150); + }; + + scrollContainer.addEventListener("scroll", handleScroll); + + return () => { + scrollContainer.removeEventListener("scroll", handleScroll); + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + } + }; + }, [currentMode, onModeChange]); + + // Calculate sliding background position (each button is 40px wide including gap) + useEffect(() => { + const unsubscribe = scrollProgress.on("change", (latest) => { + backgroundX.set(latest * 40); + }); + return unsubscribe; + }, [scrollProgress, backgroundX]); + + // Create opacity transforms for each mode (must be outside render loop) + const mode0Opacity = useTransform( + scrollProgress, + [-0.5, 0, 0.5], + [0.4, 1, 0.4], + ); + const mode1Opacity = useTransform( + scrollProgress, + [0.5, 1, 1.5], + [0.4, 1, 0.4], + ); + + const modeOpacities = [mode0Opacity, mode1Opacity]; + + // Load diff data when switching to Changes mode (only file list, not content) + const loadDiff = useCallback(async () => { + if (!workspaceId || !worktree?.id) return; + + setLoadingDiff(true); + setDiffError(null); + + try { + // Use the new lazy loading endpoint - only loads file list + const result = await window.ipcRenderer.invoke( + "worktree-get-git-diff-files", + { + workspaceId, + worktreeId: worktree.id, + }, + ); + + if (result?.success && result.files) { + // Map file list to DiffViewData format (without changes array) + const diffViewData: DiffViewData = { + title: `Changes in ${worktree.branch}`, + description: workspaceName, + timestamp: new Date().toLocaleString(), + files: result.files.map((file) => ({ + ...file, + changes: [], // Empty changes - will be loaded lazily on demand + })), + }; + setDiffData(diffViewData); + if (diffViewData.files.length > 0 && !selectedFile) { + setSelectedFile(diffViewData.files[0].id); + } + } else { + setDiffError(result?.error || "Failed to load diff"); + } + } catch (err) { + setDiffError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setLoadingDiff(false); + } + }, [ + workspaceId, + worktree?.id, + worktree?.branch, + workspaceName, + selectedFile, + ]); + + // Load diff when switching to Changes mode + useEffect(() => { + if (currentMode === "changes" && !diffData && !loadingDiff) { + loadDiff(); + } + }, [currentMode, diffData, loadingDiff, loadDiff]); + + // Early return after all hooks + if (!worktree || !workspaceId) { + return ( +
+

No worktree selected

+
+ ); + } + + // Helper to get icon for tab type + const getTabIcon = (tab: Tab) => { + switch (tab.type) { + case "terminal": + return ; + case "preview": + return ; + case "diff": + return ; + default: + return ; + } + }; + + // Helper to get icon for file diff status + const getFileIcon = (status: FileDiff["status"]) => { + switch (status) { + case "added": + return ; + case "deleted": + return ; + case "modified": + return ; + default: + return ; + } + }; + + // Flatten tabs recursively (handle group tabs) + const flattenTabs = ( + tabs: Tab[], + level = 0, + ): Array<{ tab: Tab; level: number }> => { + const result: Array<{ tab: Tab; level: number }> = []; + for (const tab of tabs) { + result.push({ tab, level }); + if (tab.type === "group" && tab.tabs) { + result.push(...flattenTabs(tab.tabs, level + 1)); + } + } + return result; + }; + + const flatTabs = flattenTabs(tabs); + + // Filter out diff tabs - they should only be accessed through Changes mode + const nonDiffTabs = flatTabs.filter(({ tab }) => tab.type !== "diff"); + + // Render tabs mode content + const renderTabsMode = () => ( + <> + {/* Header with actions */} +
+

Tabs

+
+ + + + + +

New Terminal

+
+
+ + + + + +

New Preview

+
+
+
+
+ + {/* Tab list */} +
+ {nonDiffTabs.length === 0 ? ( +
+ No tabs yet. Create a terminal or preview to get started. +
+ ) : ( +
+ {nonDiffTabs.map(({ tab, level }) => ( + + )} + + ))} +
+ )} +
+ + ); + + // Render changes mode content + const renderChangesMode = () => ( + <> + {/* Header with actions */} +
+

Changes

+
+ + + + + +

Refresh changes

+
+
+
+
+ + {/* Changes content */} +
+ {loadingDiff ? ( +
+
+
+

Loading changes...

+
+
+ ) : diffError ? ( +
+

{diffError}

+ +
+ ) : !diffData || diffData.files.length === 0 ? ( +
+

No changes

+

+ No uncommitted changes in this worktree +

+
+ ) : ( +
+
+

+ Files Changed • {diffData.files.length} +

+
+
+ { + setSelectedFile(fileId); + onDiffFileSelect?.(fileId); + }} + getFileIcon={getFileIcon} + /> +
+
+ )} +
+ + ); + + return ( +
+ {/* Mode Switcher */} +
+
+ {SIDEBAR_MODES.map((mode) => { + const Icon = mode.icon; + const isActive = currentMode === mode.id; + + return ( + + + + + +

{mode.name}

+
+
+ ); + })} +
+
+ + {/* Mode Content Carousel */} +
+
+ {/* Tabs Mode */} +
+ {renderTabsMode()} +
+ + {/* Changes Mode */} +
+ {renderChangesMode()} +
+
+
+
+ ); +}; diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx index bc311ad67d7..3d842e906f5 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx @@ -1,23 +1,17 @@ -import { type MotionValue, useMotionValue } from "framer-motion"; import { useEffect, useState } from "react"; import type { Workspace, Worktree } from "shared/types"; import { CreateWorktreeButton, CreateWorktreeModal, SidebarHeader, - WorkspaceCarousel, - WorkspacePortIndicator, - WorkspaceSwitcher, WorktreeList, } from "./components"; interface SidebarProps { - workspaces: Workspace[]; currentWorkspace: Workspace | null; onCollapse: () => void; onTabSelect: (worktreeId: string, tabId: string) => void; onWorktreeCreated: () => void; - onWorkspaceSelect: (workspaceId: string) => void; onUpdateWorktree: (worktreeId: string, updatedWorktree: Worktree) => void; selectedTabId: string | undefined; isDragging?: boolean; @@ -25,12 +19,10 @@ interface SidebarProps { } export function Sidebar({ - workspaces, currentWorkspace, onCollapse, onTabSelect, onWorktreeCreated, - onWorkspaceSelect, onUpdateWorktree, selectedTabId, isDragging = false, @@ -51,16 +43,6 @@ export function Sidebar({ const [setupStatus, setSetupStatus] = useState(undefined); const [setupOutput, setSetupOutput] = useState(undefined); - // Initialize with current workspace index - const currentIndex = workspaces.findIndex( - (w) => w.id === currentWorkspace?.id, - ); - const initialIndex = currentIndex >= 0 ? currentIndex : 0; - const defaultScrollProgress = useMotionValue(initialIndex); - const [scrollProgress, setScrollProgress] = useState>( - defaultScrollProgress, - ); - // Auto-expand worktree if it contains the selected tab useEffect(() => { if (currentWorkspace && selectedTabId) { @@ -230,43 +212,6 @@ export function Sidebar({ setSetupOutput(undefined); }; - const handleAddWorkspace = () => { - // Trigger the File -> Open Repository menu action - window.ipcRenderer.send("open-repository"); - }; - - const handleRemoveWorkspace = async ( - workspaceId: string, - workspaceName: string, - ) => { - // Confirm deletion - const confirmed = window.confirm( - `Remove workspace "${workspaceName}"?\n\nAll terminal sessions for this workspace will be closed.`, - ); - - if (!confirmed) return; - - try { - const result = await window.ipcRenderer.invoke("workspace-delete", { - id: workspaceId, - removeWorktree: false, - }); - if (result.success) { - // If we deleted the current workspace, clear selection - if (currentWorkspace?.id === workspaceId) { - onWorkspaceSelect(""); - } - // Refresh will happen via workspace-opened event - window.location.reload(); - } else { - alert(`Failed to remove workspace: ${result.error || "Unknown error"}`); - } - } catch (error) { - console.error("Error removing workspace:", error); - alert(`Error: ${error instanceof Error ? error.message : String(error)}`); - } - }; - const handleScanWorktrees = async () => { if (!currentWorkspace) return; @@ -299,38 +244,35 @@ export function Sidebar({ return (
- - {(workspace, isActive) => ( - <> - + + +
+ - {workspace && ( - - )} - + {currentWorkspace && ( + )} - +
void; + selectedDiffFile?: string | null; + onDiffFileSelect?: (fileId: string) => void; } export function DiffTab({ @@ -23,12 +25,17 @@ export function DiffTab({ workspaceName, mainBranch, onClose, + selectedDiffFile, + onDiffFileSelect, }: DiffTabProps) { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [diffData, setDiffData] = useState(null); const [error, setError] = useState(null); + const [loadedFiles, setLoadedFiles] = useState>(new Set()); + const [loadingFiles, setLoadingFiles] = useState>(new Set()); + // Load file list (without detailed changes) const loadDiff = useCallback( async (isRefresh = false) => { if (isRefresh) { @@ -39,8 +46,9 @@ export function DiffTab({ setError(null); try { + // First, get just the file list (fast) const result = await window.ipcRenderer.invoke( - "worktree-get-git-diff", + "worktree-get-git-diff-files", { workspaceId, worktreeId, @@ -52,19 +60,23 @@ export function DiffTab({ typeof result === "object" && "success" in result && result.success && - "diff" in result && - result.diff + "files" in result && + result.files ) { - // Transform the diff data to match DiffViewData format + // Transform the file list to DiffViewData format with empty changes const diffViewData: DiffViewData = { title: `Changes in ${worktree?.branch || "worktree"}`, description: workspaceName ? `Workspace: ${workspaceName}` : undefined, timestamp: new Date().toLocaleString(), - files: result.diff.files, + files: result.files.map((file) => ({ + ...file, + changes: [], // Empty - will be loaded on demand + })), }; setDiffData(diffViewData); + setLoadedFiles(new Set()); // Reset loaded files on refresh } else { const errorMsg = result && typeof result === "object" && "error" in result @@ -85,6 +97,100 @@ export function DiffTab({ [workspaceId, worktreeId, worktree?.branch, workspaceName], ); + // Load individual file diff on demand + const loadFileDiff = useCallback( + async (filePath: string) => { + // Don't reload if already loaded or loading + if (loadedFiles.has(filePath) || loadingFiles.has(filePath)) { + return; + } + + setLoadingFiles((prev) => new Set(prev).add(filePath)); + + try { + const result = await window.ipcRenderer.invoke( + "worktree-get-git-file-diff", + { + workspaceId, + worktreeId, + filePath, + }, + ); + + if ( + result && + typeof result === "object" && + "success" in result && + result.success && + "diff" in result && + result.diff + ) { + // Update the file in diffData with the loaded changes + setDiffData((prev) => { + if (!prev) return prev; + + return { + ...prev, + files: prev.files.map((file) => + file.filePath === filePath + ? { ...file, changes: result.diff?.changes || [] } + : file, + ), + }; + }); + + setLoadedFiles((prev) => new Set(prev).add(filePath)); + } + } catch (err) { + console.error("Failed to load file diff:", err); + } finally { + setLoadingFiles((prev) => { + const next = new Set(prev); + next.delete(filePath); + return next; + }); + } + }, + [workspaceId, worktreeId, loadedFiles, loadingFiles], + ); + + // Load file diff when a file is selected, and preload surrounding files in parallel + useEffect(() => { + if (selectedDiffFile && diffData) { + const selectedIndex = diffData.files.findIndex( + (f) => f.id === selectedDiffFile, + ); + if (selectedIndex === -1) return; + + // Load selected file immediately + const selectedFile = diffData.files[selectedIndex]; + if (!loadedFiles.has(selectedFile.filePath)) { + loadFileDiff(selectedFile.filePath); + } + + // Preload surrounding files in parallel (previous 2 and next 2) + const filesToPreload: string[] = []; + for ( + let i = Math.max(0, selectedIndex - 2); + i <= Math.min(diffData.files.length - 1, selectedIndex + 2); + i++ + ) { + if (i !== selectedIndex) { + const file = diffData.files[i]; + if ( + !loadedFiles.has(file.filePath) && + !loadingFiles.has(file.filePath) + ) { + filesToPreload.push(file.filePath); + } + } + } + + // Load all surrounding files in parallel + Promise.all(filesToPreload.map((filePath) => loadFileDiff(filePath))); + } + }, [selectedDiffFile, diffData, loadedFiles, loadingFiles, loadFileDiff]); + const handleRefresh = useCallback(() => { loadDiff(true); }, [loadDiff]); @@ -159,6 +265,10 @@ export function DiffTab({ onRefresh={handleRefresh} isRefreshing={refreshing} onClose={onClose} + hideFileTree={!!selectedDiffFile} + externalSelectedFile={selectedDiffFile || null} + onFileSelect={onDiffFileSelect} + loadingFiles={loadingFiles} />
); diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx index 97e9f29d9ea..e3942fb6587 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx @@ -5,16 +5,9 @@ import { PanelLeftOpen } from "lucide-react"; interface TopBarProps { isSidebarOpen: boolean; onOpenSidebar: () => void; - workspaceName?: string; - currentBranch?: string; } -export function TopBar({ - isSidebarOpen, - onOpenSidebar, - workspaceName, - currentBranch, -}: TopBarProps) { +export function TopBar({ isSidebarOpen, onOpenSidebar }: TopBarProps) { return (
; + error?: string; + }; + }; + "worktree-get-git-file-diff": { + request: { + workspaceId: string; + worktreeId: string; + filePath: string; + }; + response: { + success: boolean; + diff?: { + id: string; + fileName: string; + filePath: string; + status: "added" | "deleted" | "modified" | "renamed"; + oldPath?: string; + additions: number; + deletions: number; + changes: Array<{ + type: "added" | "removed" | "modified" | "unchanged"; + oldLineNumber: number | null; + newLineNumber: number | null; + content: string; + }>; + }; + error?: string; + }; + }; // Tab operations "tab-create": { @@ -376,6 +418,9 @@ export function isValidChannel(channel: string): channel is IpcChannelName { "worktree-check-settings", "worktree-open-settings", "worktree-get-git-status", + "worktree-get-git-diff", + "worktree-get-git-diff-files", + "worktree-get-git-file-diff", "worktree-update-description", "open-app-settings", "tab-create", diff --git a/apps/docs/src/components/Header.tsx b/apps/docs/src/components/Header.tsx index 928d47bb928..bbdffdbe5e8 100644 --- a/apps/docs/src/components/Header.tsx +++ b/apps/docs/src/components/Header.tsx @@ -40,7 +40,7 @@ export function Header() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1 }} > - {NAV_LINKS.map((link, idx) => ( + {NAV_LINKS.map((link) => ( { - res.headers.set('Access-Control-Allow-Origin', '*'); - res.headers.set('Access-Control-Request-Method', '*'); - res.headers.set('Access-Control-Allow-Methods', 'OPTIONS, GET, POST'); - res.headers.set('Access-Control-Allow-Headers', '*'); - return res; + res.headers.set("Access-Control-Allow-Origin", "*"); + res.headers.set("Access-Control-Request-Method", "*"); + res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST"); + res.headers.set("Access-Control-Allow-Headers", "*"); + return res; }; export const OPTIONS = () => { - return setCorsHeaders(new Response(null, { status: 204 })); + return setCorsHeaders(new Response(null, { status: 204 })); }; const handler = async (req: NextRequest) => { - const response = await fetchRequestHandler({ - endpoint: '/api/trpc', - router: appRouter, - req, - createContext: () => - createTRPCContext({ - headers: req.headers, - }), - onError({ error, path }) { - console.error(`[TRPC Error] ${path ?? ''}:`, error); - }, - }); + const response = await fetchRequestHandler({ + endpoint: "/api/trpc", + router: appRouter, + req, + createContext: () => + createTRPCContext({ + headers: req.headers, + }), + onError({ error, path }) { + console.error(`[TRPC Error] ${path ?? ""}:`, error); + }, + }); - return setCorsHeaders(response); + return setCorsHeaders(response); }; export { handler as GET, handler as POST }; diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx index de5b092fdc0..835a3a55ad5 100644 --- a/apps/website/src/app/layout.tsx +++ b/apps/website/src/app/layout.tsx @@ -28,8 +28,8 @@ export default function RootLayout({ /> - {children} - + {children} + ); } diff --git a/apps/website/src/trpc/query-client.ts b/apps/website/src/trpc/query-client.ts index d0c0d9a9c14..bc21bfbd493 100644 --- a/apps/website/src/trpc/query-client.ts +++ b/apps/website/src/trpc/query-client.ts @@ -1,20 +1,24 @@ -import { defaultShouldDehydrateQuery, QueryClient } from '@tanstack/react-query'; -import SuperJSON from 'superjson'; +import { + defaultShouldDehydrateQuery, + QueryClient, +} from "@tanstack/react-query"; +import SuperJSON from "superjson"; export const createQueryClient = () => - new QueryClient({ - defaultOptions: { - queries: { - staleTime: 30 * 1000, - }, - dehydrate: { - serializeData: SuperJSON.serialize, - shouldDehydrateQuery: (query) => - defaultShouldDehydrateQuery(query) || query.state.status === 'pending', - shouldRedactErrors: () => false, - }, - hydrate: { - deserializeData: SuperJSON.deserialize, - }, - }, - }); + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30 * 1000, + }, + dehydrate: { + serializeData: SuperJSON.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", + shouldRedactErrors: () => false, + }, + hydrate: { + deserializeData: SuperJSON.deserialize, + }, + }, + }); diff --git a/apps/website/src/trpc/react.tsx b/apps/website/src/trpc/react.tsx index fa3b4fa22ef..2aff31fb426 100644 --- a/apps/website/src/trpc/react.tsx +++ b/apps/website/src/trpc/react.tsx @@ -1,65 +1,65 @@ -'use client'; +"use client"; -import type { QueryClient } from '@tanstack/react-query'; -import { QueryClientProvider } from '@tanstack/react-query'; +import type { AppRouter } from "@superset/api"; +import type { QueryClient } from "@tanstack/react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; import { - createTRPCClient, - httpBatchStreamLink, - loggerLink, -} from '@trpc/client'; -import { createTRPCContext } from '@trpc/tanstack-react-query'; -import { useState } from 'react'; -import SuperJSON from 'superjson'; -import type { AppRouter } from '@superset/api'; -import { createQueryClient } from './query-client'; + createTRPCClient, + httpBatchStreamLink, + loggerLink, +} from "@trpc/client"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; +import { useState } from "react"; +import SuperJSON from "superjson"; +import { createQueryClient } from "./query-client"; -let clientQueryClientSingleton: QueryClient | undefined = undefined; +let clientQueryClientSingleton: QueryClient | undefined; const getQueryClient = () => { - if (typeof window === 'undefined') { - // Server: always make a new query client - return createQueryClient(); - } - // Browser: use singleton pattern to keep the same query client - return (clientQueryClientSingleton ??= createQueryClient()); + if (typeof window === "undefined") { + // Server: always make a new query client + return createQueryClient(); + } + // Browser: use singleton pattern to keep the same query client + return (clientQueryClientSingleton ??= createQueryClient()); }; export const { useTRPC, TRPCProvider } = createTRPCContext(); function getBaseUrl() { - if (typeof window !== 'undefined') return window.location.origin; - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; - return `http://localhost:3000`; + if (typeof window !== "undefined") return window.location.origin; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return `http://localhost:3000`; } export function TRPCReactProvider(props: { children: React.ReactNode }) { - const queryClient = getQueryClient(); + const queryClient = getQueryClient(); - const [trpcClient] = useState(() => - createTRPCClient({ - links: [ - loggerLink({ - enabled: (op) => - process.env.NODE_ENV === 'development' || - (op.direction === 'down' && op.result instanceof Error), - }), - httpBatchStreamLink({ - transformer: SuperJSON, - url: `${getBaseUrl()}/api/trpc`, - headers() { - const headers = new Headers(); - headers.set('x-trpc-source', 'nextjs-react'); - return headers; - }, - }), - ], - }), - ); + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + loggerLink({ + enabled: (op) => + process.env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + httpBatchStreamLink({ + transformer: SuperJSON, + url: `${getBaseUrl()}/api/trpc`, + headers() { + const headers = new Headers(); + headers.set("x-trpc-source", "nextjs-react"); + return headers; + }, + }), + ], + }), + ); - return ( - - - {props.children} - - - ); + return ( + + + {props.children} + + + ); } diff --git a/apps/website/src/trpc/server.tsx b/apps/website/src/trpc/server.tsx index 1ed2bc8484f..5b78bc6e8a3 100644 --- a/apps/website/src/trpc/server.tsx +++ b/apps/website/src/trpc/server.tsx @@ -1,52 +1,52 @@ -import 'server-only'; +import "server-only"; -import type { TRPCQueryOptions } from '@trpc/tanstack-react-query'; -import { cache } from 'react'; -import { headers } from 'next/headers'; -import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; -import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; -import { appRouter, createTRPCContext, type AppRouter } from '@superset/api'; -import { createQueryClient } from './query-client'; +import { type AppRouter, appRouter, createTRPCContext } from "@superset/api"; +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import type { TRPCQueryOptions } from "@trpc/tanstack-react-query"; +import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; +import { headers } from "next/headers"; +import { cache } from "react"; +import { createQueryClient } from "./query-client"; /** * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when * handling a tRPC call from a React Server Component. */ const createContext = cache(async () => { - const heads = new Headers(await headers()); - heads.set('x-trpc-source', 'rsc'); + const heads = new Headers(await headers()); + heads.set("x-trpc-source", "rsc"); - return createTRPCContext({ - headers: heads, - }); + return createTRPCContext({ + headers: heads, + }); }); const getQueryClient = cache(createQueryClient); export const trpc = createTRPCOptionsProxy({ - router: appRouter, - ctx: createContext, - queryClient: getQueryClient, + router: appRouter, + ctx: createContext, + queryClient: getQueryClient, }); export function HydrateClient(props: { children: React.ReactNode }) { - const queryClient = getQueryClient(); - return ( - - {props.children} - - ); + const queryClient = getQueryClient(); + return ( + + {props.children} + + ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function prefetch>>( - queryOptions: T, + queryOptions: T, ) { - const queryClient = getQueryClient(); - if (queryOptions.queryKey[1]?.type === 'infinite') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - void queryClient.prefetchInfiniteQuery(queryOptions as any); - } else { - void queryClient.prefetchQuery(queryOptions); - } + const queryClient = getQueryClient(); + if (queryOptions.queryKey[1]?.type === "infinite") { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + void queryClient.prefetchInfiniteQuery(queryOptions as any); + } else { + void queryClient.prefetchQuery(queryOptions); + } } diff --git a/packages/api/package.json b/packages/api/package.json index cc61d755cd6..b97b5706561 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,27 +1,27 @@ { - "name": "@superset/api", - "version": "0.0.0", - "private": true, - "type": "module", - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" - } - }, - "scripts": { - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@superset/db": "*", - "@t3-oss/env-core": "^0.13.8", - "@trpc/server": "^11.7.0", + "name": "@superset/api", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@superset/db": "*", + "@t3-oss/env-core": "^0.13.8", + "@trpc/server": "^11.7.0", "drizzle-orm": "0.44.3", - "superjson": "2.2.3", - "zod": "^4.1.12" - }, - "devDependencies": { - "@superset/typescript": "*", - "typescript": "^5.9.3" - } + "superjson": "2.2.3", + "zod": "^4.1.12" + }, + "devDependencies": { + "@superset/typescript": "*", + "typescript": "^5.9.3" + } } diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts index ed1dfd6e12b..f049168f27c 100644 --- a/packages/api/src/env.ts +++ b/packages/api/src/env.ts @@ -1,12 +1,12 @@ -import { createEnv } from '@t3-oss/env-core'; -import { z } from 'zod'; +import { createEnv } from "@t3-oss/env-core"; +import { z } from "zod"; export const env = createEnv({ - server: { - MOCK_USER_ID: z.string().uuid().optional(), - }, - clientPrefix: 'PUBLIC_', - client: {}, - runtimeEnv: process.env, - emptyStringAsUndefined: true, + server: { + MOCK_USER_ID: z.string().uuid().optional(), + }, + clientPrefix: "PUBLIC_", + client: {}, + runtimeEnv: process.env, + emptyStringAsUndefined: true, }); diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 8b7b66b81fd..2d7d750ad22 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,9 +1,9 @@ -export type { AppRouter } from './root'; -export { appRouter } from './root'; -export { createTRPCContext, createTRPCRouter } from './trpc'; +export type { AppRouter } from "./root"; +export { appRouter } from "./root"; +export { createTRPCContext, createTRPCRouter } from "./trpc"; -import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; -import type { AppRouter } from './root'; +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; +import type { AppRouter } from "./root"; export type RouterInputs = inferRouterInputs; export type RouterOutputs = inferRouterOutputs; diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index 4eadf5e951b..3067c25b671 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -1,17 +1,17 @@ -import { createTRPCRouter } from './trpc'; -import { organizationRouter } from './router/organization'; -import { repositoryRouter } from './router/repository'; -import { taskRouter } from './router/task'; -import { userRouter } from './router/user'; +import { organizationRouter } from "./router/organization"; +import { repositoryRouter } from "./router/repository"; +import { taskRouter } from "./router/task"; +import { userRouter } from "./router/user"; +import { createTRPCRouter } from "./trpc"; export const appRouter = createTRPCRouter({ - organization: organizationRouter, - repository: repositoryRouter, - task: taskRouter, - user: userRouter, + organization: organizationRouter, + repository: repositoryRouter, + task: taskRouter, + user: userRouter, }); export type AppRouter = typeof appRouter; // Export tRPC instance for extending in other packages (e.g., Electron app) -export { createTRPCRouter } from './trpc'; +export { createTRPCRouter } from "./trpc"; diff --git a/packages/api/src/router/organization.ts b/packages/api/src/router/organization.ts index 22616018ec6..e18dd93d6f5 100644 --- a/packages/api/src/router/organization.ts +++ b/packages/api/src/router/organization.ts @@ -1,127 +1,135 @@ -import { z } from 'zod'; -import { and, desc, eq } from 'drizzle-orm'; -import type { TRPCRouterRecord } from '@trpc/server'; -import { organizationMembers, organizations } from '@superset/db/schema'; -import { protectedProcedure, publicProcedure } from '../trpc'; +import { organizationMembers, organizations } from "@superset/db/schema"; +import type { TRPCRouterRecord } from "@trpc/server"; +import { and, desc, eq } from "drizzle-orm"; +import { z } from "zod"; +import { protectedProcedure, publicProcedure } from "../trpc"; export const organizationRouter = { - all: publicProcedure.query(({ ctx }) => { - return ctx.db.query.organizations.findMany({ - orderBy: desc(organizations.createdAt), - with: { - members: { - with: { - user: true, - }, - }, - }, - }); - }), + all: publicProcedure.query(({ ctx }) => { + return ctx.db.query.organizations.findMany({ + orderBy: desc(organizations.createdAt), + with: { + members: { + with: { + user: true, + }, + }, + }, + }); + }), - byId: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { - return ctx.db.query.organizations.findFirst({ - where: eq(organizations.id, input), - with: { - members: { - with: { - user: true, - }, - }, - repositories: true, - }, - }); - }), + byId: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { + return ctx.db.query.organizations.findFirst({ + where: eq(organizations.id, input), + with: { + members: { + with: { + user: true, + }, + }, + repositories: true, + }, + }); + }), - bySlug: publicProcedure.input(z.string()).query(({ ctx, input }) => { - return ctx.db.query.organizations.findFirst({ - where: eq(organizations.slug, input), - with: { - members: { - with: { - user: true, - }, - }, - repositories: true, - }, - }); - }), + bySlug: publicProcedure.input(z.string()).query(({ ctx, input }) => { + return ctx.db.query.organizations.findFirst({ + where: eq(organizations.slug, input), + with: { + members: { + with: { + user: true, + }, + }, + repositories: true, + }, + }); + }), - create: protectedProcedure - .input( - z.object({ - name: z.string().min(1), - slug: z.string().min(1), - githubOrg: z.string().optional(), - avatarUrl: z.string().url().optional(), - }), - ) - .mutation(async ({ ctx, input }) => { - // Create organization - const [organization] = await ctx.db.insert(organizations).values(input).returning(); + create: protectedProcedure + .input( + z.object({ + name: z.string().min(1), + slug: z.string().min(1), + githubOrg: z.string().optional(), + avatarUrl: z.string().url().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + // Create organization + const [organization] = await ctx.db + .insert(organizations) + .values(input) + .returning(); - // Add creator as a member - if (ctx.session?.user.id && organization) { - await ctx.db.insert(organizationMembers).values({ - organizationId: organization.id, - userId: ctx.session.user.id, - }); - } + // Add creator as a member + if (ctx.session?.user.id && organization) { + await ctx.db.insert(organizationMembers).values({ + organizationId: organization.id, + userId: ctx.session.user.id, + }); + } - return organization; - }), + return organization; + }), - update: protectedProcedure - .input( - z.object({ - id: z.string().uuid(), - name: z.string().min(1).optional(), - githubOrg: z.string().optional(), - avatarUrl: z.string().url().optional(), - }), - ) - .mutation(async ({ ctx, input }) => { - const { id, ...data } = input; - const [organization] = await ctx.db - .update(organizations) - .set(data) - .where(eq(organizations.id, id)) - .returning(); - return organization; - }), + update: protectedProcedure + .input( + z.object({ + id: z.string().uuid(), + name: z.string().min(1).optional(), + githubOrg: z.string().optional(), + avatarUrl: z.string().url().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { id, ...data } = input; + const [organization] = await ctx.db + .update(organizations) + .set(data) + .where(eq(organizations.id, id)) + .returning(); + return organization; + }), - delete: protectedProcedure.input(z.string().uuid()).mutation(async ({ ctx, input }) => { - await ctx.db.delete(organizations).where(eq(organizations.id, input)); - return { success: true }; - }), + delete: protectedProcedure + .input(z.string().uuid()) + .mutation(async ({ ctx, input }) => { + await ctx.db.delete(organizations).where(eq(organizations.id, input)); + return { success: true }; + }), - addMember: protectedProcedure - .input( - z.object({ - organizationId: z.string().uuid(), - userId: z.string().uuid(), - }), - ) - .mutation(async ({ ctx, input }) => { - const [member] = await ctx.db.insert(organizationMembers).values(input).returning(); - return member; - }), + addMember: protectedProcedure + .input( + z.object({ + organizationId: z.string().uuid(), + userId: z.string().uuid(), + }), + ) + .mutation(async ({ ctx, input }) => { + const [member] = await ctx.db + .insert(organizationMembers) + .values(input) + .returning(); + return member; + }), - removeMember: protectedProcedure - .input( - z.object({ - organizationId: z.string().uuid(), - userId: z.string().uuid(), - }), - ) - .mutation(async ({ ctx, input }) => { - await ctx.db - .delete(organizationMembers) - .where( - and( - eq(organizationMembers.organizationId, input.organizationId), - eq(organizationMembers.userId, input.userId), - ), - ); - return { success: true }; - }), + removeMember: protectedProcedure + .input( + z.object({ + organizationId: z.string().uuid(), + userId: z.string().uuid(), + }), + ) + .mutation(async ({ ctx, input }) => { + await ctx.db + .delete(organizationMembers) + .where( + and( + eq(organizationMembers.organizationId, input.organizationId), + eq(organizationMembers.userId, input.userId), + ), + ); + return { success: true }; + }), } satisfies TRPCRouterRecord; diff --git a/packages/api/src/router/repository.ts b/packages/api/src/router/repository.ts index d13837b680d..172dd17ec39 100644 --- a/packages/api/src/router/repository.ts +++ b/packages/api/src/router/repository.ts @@ -1,89 +1,99 @@ -import { z } from 'zod'; -import { and, desc, eq } from 'drizzle-orm'; -import type { TRPCRouterRecord } from '@trpc/server'; -import { repositories } from '@superset/db/schema'; -import { protectedProcedure, publicProcedure } from '../trpc'; +import { repositories } from "@superset/db/schema"; +import type { TRPCRouterRecord } from "@trpc/server"; +import { and, desc, eq } from "drizzle-orm"; +import { z } from "zod"; +import { protectedProcedure, publicProcedure } from "../trpc"; export const repositoryRouter = { - all: publicProcedure.query(({ ctx }) => { - return ctx.db.query.repositories.findMany({ - orderBy: desc(repositories.createdAt), - with: { - organization: true, - }, - }); - }), + all: publicProcedure.query(({ ctx }) => { + return ctx.db.query.repositories.findMany({ + orderBy: desc(repositories.createdAt), + with: { + organization: true, + }, + }); + }), - byId: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { - return ctx.db.query.repositories.findFirst({ - where: eq(repositories.id, input), - with: { - organization: true, - tasks: true, - }, - }); - }), + byId: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { + return ctx.db.query.repositories.findFirst({ + where: eq(repositories.id, input), + with: { + organization: true, + tasks: true, + }, + }); + }), - byOrganization: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { - return ctx.db.query.repositories.findMany({ - where: eq(repositories.organizationId, input), - orderBy: desc(repositories.createdAt), - }); - }), + byOrganization: publicProcedure + .input(z.string().uuid()) + .query(({ ctx, input }) => { + return ctx.db.query.repositories.findMany({ + where: eq(repositories.organizationId, input), + orderBy: desc(repositories.createdAt), + }); + }), - byGitHub: publicProcedure - .input( - z.object({ - owner: z.string(), - name: z.string(), - }), - ) - .query(({ ctx, input }) => { - return ctx.db.query.repositories.findFirst({ - where: and(eq(repositories.repoOwner, input.owner), eq(repositories.repoName, input.name)), - with: { - organization: true, - }, - }); - }), + byGitHub: publicProcedure + .input( + z.object({ + owner: z.string(), + name: z.string(), + }), + ) + .query(({ ctx, input }) => { + return ctx.db.query.repositories.findFirst({ + where: and( + eq(repositories.repoOwner, input.owner), + eq(repositories.repoName, input.name), + ), + with: { + organization: true, + }, + }); + }), - create: protectedProcedure - .input( - z.object({ - organizationId: z.string().uuid(), - name: z.string().min(1), - slug: z.string().min(1), - repoUrl: z.string().url(), - repoOwner: z.string().min(1), - repoName: z.string().min(1), - defaultBranch: z.string().default('main'), - }), - ) - .mutation(async ({ ctx, input }) => { - const [repository] = await ctx.db.insert(repositories).values(input).returning(); - return repository; - }), + create: protectedProcedure + .input( + z.object({ + organizationId: z.string().uuid(), + name: z.string().min(1), + slug: z.string().min(1), + repoUrl: z.string().url(), + repoOwner: z.string().min(1), + repoName: z.string().min(1), + defaultBranch: z.string().default("main"), + }), + ) + .mutation(async ({ ctx, input }) => { + const [repository] = await ctx.db + .insert(repositories) + .values(input) + .returning(); + return repository; + }), - update: protectedProcedure - .input( - z.object({ - id: z.string().uuid(), - name: z.string().min(1).optional(), - defaultBranch: z.string().optional(), - }), - ) - .mutation(async ({ ctx, input }) => { - const { id, ...data } = input; - const [repository] = await ctx.db - .update(repositories) - .set(data) - .where(eq(repositories.id, id)) - .returning(); - return repository; - }), + update: protectedProcedure + .input( + z.object({ + id: z.string().uuid(), + name: z.string().min(1).optional(), + defaultBranch: z.string().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { id, ...data } = input; + const [repository] = await ctx.db + .update(repositories) + .set(data) + .where(eq(repositories.id, id)) + .returning(); + return repository; + }), - delete: protectedProcedure.input(z.string().uuid()).mutation(async ({ ctx, input }) => { - await ctx.db.delete(repositories).where(eq(repositories.id, input)); - return { success: true }; - }), + delete: protectedProcedure + .input(z.string().uuid()) + .mutation(async ({ ctx, input }) => { + await ctx.db.delete(repositories).where(eq(repositories.id, input)); + return { success: true }; + }), } satisfies TRPCRouterRecord; diff --git a/packages/api/src/router/task.ts b/packages/api/src/router/task.ts index 71310abd10b..5717e2de415 100644 --- a/packages/api/src/router/task.ts +++ b/packages/api/src/router/task.ts @@ -1,122 +1,128 @@ -import { z } from 'zod'; -import { eq, desc, and } from 'drizzle-orm'; -import type { TRPCRouterRecord } from '@trpc/server'; -import { tasks, taskStatusEnumValues } from '@superset/db/schema'; -import { publicProcedure, protectedProcedure } from '../trpc'; +import { taskStatusEnumValues, tasks } from "@superset/db/schema"; +import type { TRPCRouterRecord } from "@trpc/server"; +import { and, desc, eq } from "drizzle-orm"; +import { z } from "zod"; +import { protectedProcedure, publicProcedure } from "../trpc"; export const taskRouter = { - all: publicProcedure.query(({ ctx }) => { - return ctx.db.query.tasks.findMany({ - with: { - assignee: { - columns: { - id: true, - name: true, - avatarUrl: true, - }, - }, - creator: { - columns: { - id: true, - name: true, - avatarUrl: true, - }, - }, - }, - orderBy: desc(tasks.createdAt), - limit: 50, - }); - }), + all: publicProcedure.query(({ ctx }) => { + return ctx.db.query.tasks.findMany({ + with: { + assignee: { + columns: { + id: true, + name: true, + avatarUrl: true, + }, + }, + creator: { + columns: { + id: true, + name: true, + avatarUrl: true, + }, + }, + }, + orderBy: desc(tasks.createdAt), + limit: 50, + }); + }), - byRepository: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { - return ctx.db.query.tasks.findMany({ - where: eq(tasks.repositoryId, input), - orderBy: desc(tasks.createdAt), - }); - }), + byRepository: publicProcedure + .input(z.string().uuid()) + .query(({ ctx, input }) => { + return ctx.db.query.tasks.findMany({ + where: eq(tasks.repositoryId, input), + orderBy: desc(tasks.createdAt), + }); + }), - byOrganization: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { - return ctx.db.query.tasks.findMany({ - where: eq(tasks.organizationId, input), - orderBy: desc(tasks.createdAt), - }); - }), + byOrganization: publicProcedure + .input(z.string().uuid()) + .query(({ ctx, input }) => { + return ctx.db.query.tasks.findMany({ + where: eq(tasks.organizationId, input), + orderBy: desc(tasks.createdAt), + }); + }), - byId: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { - return ctx.db.query.tasks.findFirst({ - where: eq(tasks.id, input), - }); - }), + byId: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { + return ctx.db.query.tasks.findFirst({ + where: eq(tasks.id, input), + }); + }), - bySlug: publicProcedure.input(z.string()).query(({ ctx, input }) => { - return ctx.db.query.tasks.findFirst({ - where: eq(tasks.slug, input), - }); - }), + bySlug: publicProcedure.input(z.string()).query(({ ctx, input }) => { + return ctx.db.query.tasks.findFirst({ + where: eq(tasks.slug, input), + }); + }), - create: protectedProcedure - .input( - z.object({ - slug: z.string().min(1), - title: z.string().min(1), - description: z.string().optional(), - status: z.enum(taskStatusEnumValues).default('planning'), - repositoryId: z.string().uuid(), - organizationId: z.string().uuid(), - assigneeId: z.string().uuid().optional(), - branch: z.string().optional(), - }), - ) - .mutation(async ({ ctx, input }) => { - const [task] = await ctx.db - .insert(tasks) - .values({ - ...input, - creatorId: ctx.session.user.id, - }) - .returning(); - return task; - }), + create: protectedProcedure + .input( + z.object({ + slug: z.string().min(1), + title: z.string().min(1), + description: z.string().optional(), + status: z.enum(taskStatusEnumValues).default("planning"), + repositoryId: z.string().uuid(), + organizationId: z.string().uuid(), + assigneeId: z.string().uuid().optional(), + branch: z.string().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + const [task] = await ctx.db + .insert(tasks) + .values({ + ...input, + creatorId: ctx.session.user.id, + }) + .returning(); + return task; + }), - update: protectedProcedure - .input( - z.object({ - id: z.string().uuid(), - title: z.string().min(1).optional(), - description: z.string().optional(), - status: z.enum(taskStatusEnumValues).optional(), - assigneeId: z.string().uuid().nullable().optional(), - branch: z.string().nullable().optional(), - }), - ) - .mutation(async ({ ctx, input }) => { - const { id, ...data } = input; - const [task] = await ctx.db - .update(tasks) - .set(data) - .where(eq(tasks.id, id)) - .returning(); - return task; - }), + update: protectedProcedure + .input( + z.object({ + id: z.string().uuid(), + title: z.string().min(1).optional(), + description: z.string().optional(), + status: z.enum(taskStatusEnumValues).optional(), + assigneeId: z.string().uuid().nullable().optional(), + branch: z.string().nullable().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { id, ...data } = input; + const [task] = await ctx.db + .update(tasks) + .set(data) + .where(eq(tasks.id, id)) + .returning(); + return task; + }), - updateStatus: protectedProcedure - .input( - z.object({ - id: z.string().uuid(), - status: z.enum(taskStatusEnumValues), - }), - ) - .mutation(async ({ ctx, input }) => { - const [task] = await ctx.db - .update(tasks) - .set({ status: input.status }) - .where(eq(tasks.id, input.id)) - .returning(); - return task; - }), + updateStatus: protectedProcedure + .input( + z.object({ + id: z.string().uuid(), + status: z.enum(taskStatusEnumValues), + }), + ) + .mutation(async ({ ctx, input }) => { + const [task] = await ctx.db + .update(tasks) + .set({ status: input.status }) + .where(eq(tasks.id, input.id)) + .returning(); + return task; + }), - delete: protectedProcedure.input(z.string().uuid()).mutation(async ({ ctx, input }) => { - await ctx.db.delete(tasks).where(eq(tasks.id, input)); - return { success: true }; - }), + delete: protectedProcedure + .input(z.string().uuid()) + .mutation(async ({ ctx, input }) => { + await ctx.db.delete(tasks).where(eq(tasks.id, input)); + return { success: true }; + }), } satisfies TRPCRouterRecord; diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index fe223fa7b5d..afee26640a0 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -1,25 +1,25 @@ -import { z } from 'zod'; -import { desc, eq } from 'drizzle-orm'; -import type { TRPCRouterRecord } from '@trpc/server'; -import { users } from '@superset/db/schema'; -import { publicProcedure } from '../trpc'; +import { users } from "@superset/db/schema"; +import type { TRPCRouterRecord } from "@trpc/server"; +import { desc, eq } from "drizzle-orm"; +import { z } from "zod"; +import { publicProcedure } from "../trpc"; export const userRouter = { - all: publicProcedure.query(({ ctx }) => { - return ctx.db.query.users.findMany({ - orderBy: desc(users.createdAt), - }); - }), + all: publicProcedure.query(({ ctx }) => { + return ctx.db.query.users.findMany({ + orderBy: desc(users.createdAt), + }); + }), - byId: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { - return ctx.db.query.users.findFirst({ - where: eq(users.id, input), - }); - }), + byId: publicProcedure.input(z.string().uuid()).query(({ ctx, input }) => { + return ctx.db.query.users.findFirst({ + where: eq(users.id, input), + }); + }), - byEmail: publicProcedure.input(z.string().email()).query(({ ctx, input }) => { - return ctx.db.query.users.findFirst({ - where: eq(users.email, input), - }); - }), + byEmail: publicProcedure.input(z.string().email()).query(({ ctx, input }) => { + return ctx.db.query.users.findFirst({ + where: eq(users.email, input), + }); + }), } satisfies TRPCRouterRecord; diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 0600500bdf5..85e4eae7b69 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -1,69 +1,73 @@ -import { initTRPC, TRPCError } from '@trpc/server'; -import { ZodError } from 'zod'; -import SuperJSON from 'superjson'; -import { db } from '@superset/db/client'; -import { env } from './env'; +import { db } from "@superset/db/client"; +import { initTRPC, TRPCError } from "@trpc/server"; +import SuperJSON from "superjson"; +import { ZodError } from "zod"; +import { env } from "./env"; export const createTRPCContext = async (opts: { headers: Headers }) => { - const mockUserId = env.MOCK_USER_ID; + const mockUserId = env.MOCK_USER_ID; - return { - db, - session: mockUserId - ? { - user: { - id: mockUserId, - }, - } - : null, - headers: opts.headers, - }; + return { + db, + session: mockUserId + ? { + user: { + id: mockUserId, + }, + } + : null, + headers: opts.headers, + }; }; const t = initTRPC.context().create({ - transformer: SuperJSON, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, + transformer: SuperJSON, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, }); const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); + const start = Date.now(); - if (process.env.NODE_ENV === 'development') { - const delay = Math.random() * 400 + 100; - await new Promise((resolve) => setTimeout(resolve, delay)); - } + if (process.env.NODE_ENV === "development") { + const delay = Math.random() * 400 + 100; + await new Promise((resolve) => setTimeout(resolve, delay)); + } - const result = await next(); - const end = Date.now(); + const result = await next(); + const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms`); + console.log(`[TRPC] ${path} took ${end - start}ms`); - return result; + return result; }); export const createTRPCRouter = t.router; export const publicProcedure = t.procedure.use(timingMiddleware); -export const protectedProcedure = t.procedure.use(timingMiddleware).use(async ({ ctx, next }) => { - if (!ctx.session?.user) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'Not authenticated. Set MOCK_USER_ID in .env to mock authentication.', - }); - } +export const protectedProcedure = t.procedure + .use(timingMiddleware) + .use(async ({ ctx, next }) => { + if (!ctx.session?.user) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: + "Not authenticated. Set MOCK_USER_ID in .env to mock authentication.", + }); + } - return next({ - ctx: { - session: ctx.session, - }, - }); -}); + return next({ + ctx: { + session: ctx.session, + }, + }); + }); diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index d2047b2e7bd..52989f8e7eb 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,9 +1,9 @@ { - "extends": "@superset/typescript/base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] + "extends": "@superset/typescript/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts index 72044b54102..3e5ea7b7cc2 100644 --- a/packages/db/drizzle.config.ts +++ b/packages/db/drizzle.config.ts @@ -1,11 +1,11 @@ -import type { Config } from 'drizzle-kit'; +import type { Config } from "drizzle-kit"; -import { env } from './src/env'; +import { env } from "./src/env"; export default { - schema: './src/schema/index.ts', - out: './drizzle', - dialect: 'postgresql', - dbCredentials: { url: env.DATABASE_URL }, - casing: 'snake_case', + schema: "./src/schema/index.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { url: env.DATABASE_URL }, + casing: "snake_case", } satisfies Config; diff --git a/packages/db/drizzle/meta/0000_snapshot.json b/packages/db/drizzle/meta/0000_snapshot.json index 7c9ab49c733..d95fd2e5e9f 100644 --- a/packages/db/drizzle/meta/0000_snapshot.json +++ b/packages/db/drizzle/meta/0000_snapshot.json @@ -1,686 +1,646 @@ { - "id": "23f7dd12-475f-4c90-8e8f-9b6ac3670d92", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.organization_members": { - "name": "organization_members", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "organization_id": { - "name": "organization_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "organization_members_organization_id_idx": { - "name": "organization_members_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "organization_members_user_id_idx": { - "name": "organization_members_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "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": { - "organization_members_unique": { - "name": "organization_members_unique", - "nullsNotDistinct": false, - "columns": [ - "organization_id", - "user_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.organizations": { - "name": "organizations", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "github_org": { - "name": "github_org", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "avatar_url": { - "name": "avatar_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "organizations_slug_idx": { - "name": "organizations_slug_idx", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "organizations_slug_unique": { - "name": "organizations_slug_unique", - "nullsNotDistinct": false, - "columns": [ - "slug" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.repositories": { - "name": "repositories", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "organization_id": { - "name": "organization_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "repo_url": { - "name": "repo_url", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "repo_owner": { - "name": "repo_owner", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "repo_name": { - "name": "repo_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "default_branch": { - "name": "default_branch", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'main'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "repositories_organization_id_idx": { - "name": "repositories_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "repositories_slug_idx": { - "name": "repositories_slug_idx", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "repositories_organization_id_organizations_id_fk": { - "name": "repositories_organization_id_organizations_id_fk", - "tableFrom": "repositories", - "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "repositories_org_slug_unique": { - "name": "repositories_org_slug_unique", - "nullsNotDistinct": false, - "columns": [ - "organization_id", - "slug" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.tasks": { - "name": "tasks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "task_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'planning'" - }, - "repository_id": { - "name": "repository_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "assignee_id": { - "name": "assignee_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "creator_id": { - "name": "creator_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "branch": { - "name": "branch", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "tasks_slug_idx": { - "name": "tasks_slug_idx", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tasks_repository_id_idx": { - "name": "tasks_repository_id_idx", - "columns": [ - { - "expression": "repository_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tasks_organization_id_idx": { - "name": "tasks_organization_id_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tasks_assignee_id_idx": { - "name": "tasks_assignee_id_idx", - "columns": [ - { - "expression": "assignee_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tasks_creator_id_idx": { - "name": "tasks_creator_id_idx", - "columns": [ - { - "expression": "creator_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tasks_status_idx": { - "name": "tasks_status_idx", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "tasks_created_at_idx": { - "name": "tasks_created_at_idx", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "tasks_repository_id_repositories_id_fk": { - "name": "tasks_repository_id_repositories_id_fk", - "tableFrom": "tasks", - "tableTo": "repositories", - "columnsFrom": [ - "repository_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "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": { - "tasks_slug_unique": { - "name": "tasks_slug_unique", - "nullsNotDistinct": false, - "columns": [ - "slug" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "avatar_url": { - "name": "avatar_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "users_email_idx": { - "name": "users_email_idx", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.task_status": { - "name": "task_status", - "schema": "public", - "values": [ - "backlog", - "todo", - "planning", - "working", - "needs-feedback", - "ready-to-merge", - "completed", - "canceled" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "id": "23f7dd12-475f-4c90-8e8f-9b6ac3670d92", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.organization_members": { + "name": "organization_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_members_organization_id_idx": { + "name": "organization_members_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_members_user_id_idx": { + "name": "organization_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": { + "organization_members_unique": { + "name": "organization_members_unique", + "nullsNotDistinct": false, + "columns": ["organization_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "repositories_organization_id_idx": { + "name": "repositories_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repositories_slug_idx": { + "name": "repositories_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repositories_organization_id_organizations_id_fk": { + "name": "repositories_organization_id_organizations_id_fk", + "tableFrom": "repositories", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_org_slug_unique": { + "name": "repositories_org_slug_unique", + "nullsNotDistinct": false, + "columns": ["organization_id", "slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'planning'" + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assignee_id": { + "name": "assignee_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_slug_idx": { + "name": "tasks_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_repository_id_idx": { + "name": "tasks_repository_id_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_organization_id_idx": { + "name": "tasks_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_assignee_id_idx": { + "name": "tasks_assignee_id_idx", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_creator_id_idx": { + "name": "tasks_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_created_at_idx": { + "name": "tasks_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_repository_id_repositories_id_fk": { + "name": "tasks_repository_id_repositories_id_fk", + "tableFrom": "tasks", + "tableTo": "repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": { + "tasks_slug_unique": { + "name": "tasks_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "backlog", + "todo", + "planning", + "working", + "needs-feedback", + "ready-to-merge", + "completed", + "canceled" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index f7dbb20a53f..fdb14cd8ad4 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1,13 +1,13 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1762574385218, - "tag": "0000_initial_migration", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1762574385218, + "tag": "0000_initial_migration", + "breakpoints": true + } + ] +} diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 82f79c76aaf..a1d9d43d9b1 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -1,23 +1,23 @@ -import { neon, Pool } from '@neondatabase/serverless'; -import { config } from 'dotenv'; -import { drizzle } from 'drizzle-orm/neon-http'; -import { drizzle as drizzleWs } from 'drizzle-orm/neon-serverless'; +import { neon, Pool } from "@neondatabase/serverless"; +import { config } from "dotenv"; +import { drizzle } from "drizzle-orm/neon-http"; +import { drizzle as drizzleWs } from "drizzle-orm/neon-serverless"; -import { env } from './env'; -import * as schema from './schema'; +import { env } from "./env"; +import * as schema from "./schema"; -config({ path: '.env' }); +config({ path: ".env" }); const sql = neon(env.DATABASE_URL); export const db = drizzle({ - client: sql, - schema, - casing: 'snake_case', + client: sql, + schema, + casing: "snake_case", }); export const dbWs = drizzleWs({ - client: new Pool({ connectionString: process.env.DATABASE_URL }), - schema, - casing: 'snake_case', + client: new Pool({ connectionString: process.env.DATABASE_URL }), + schema, + casing: "snake_case", }); diff --git a/packages/db/src/env.ts b/packages/db/src/env.ts index e8b36b8ce03..ece7a7befb1 100644 --- a/packages/db/src/env.ts +++ b/packages/db/src/env.ts @@ -1,38 +1,38 @@ -import { createEnv } from '@t3-oss/env-core'; -import { z } from 'zod'; +import { createEnv } from "@t3-oss/env-core"; +import { z } from "zod"; export const env = createEnv({ - server: { - DATABASE_URL: z.string().url(), - DATABASE_URL_UNPOOLED: z.string().url().optional(), - }, + server: { + DATABASE_URL: z.string().url(), + DATABASE_URL_UNPOOLED: z.string().url().optional(), + }, - /** - * The prefix that client-side variables must have. This is enforced both at - * a type-level and at runtime. - */ - clientPrefix: 'PUBLIC_', + /** + * The prefix that client-side variables must have. This is enforced both at + * a type-level and at runtime. + */ + clientPrefix: "PUBLIC_", - client: {}, + client: {}, - /** - * What object holds the environment variables at runtime. This is usually - * `process.env` or `import.meta.env`. - */ - runtimeEnv: process.env, + /** + * What object holds the environment variables at runtime. This is usually + * `process.env` or `import.meta.env`. + */ + runtimeEnv: process.env, - /** - * By default, this library will feed the environment variables directly to - * the Zod validator. - * - * This means that if you have an empty string for a value that is supposed - * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag - * it as a type mismatch violation. Additionally, if you have an empty string - * for a value that is supposed to be a string with a default value (e.g. - * `DOMAIN=` in an ".env" file), the default value will never be applied. - * - * In order to solve these issues, we recommend that all new projects - * explicitly specify this option as true. - */ - emptyStringAsUndefined: true, + /** + * By default, this library will feed the environment variables directly to + * the Zod validator. + * + * This means that if you have an empty string for a value that is supposed + * to be a number (e.g. `PORT=` in a ".env" file), Zod will incorrectly flag + * it as a type mismatch violation. Additionally, if you have an empty string + * for a value that is supposed to be a string with a default value (e.g. + * `DOMAIN=` in an ".env" file), the default value will never be applied. + * + * In order to solve these issues, we recommend that all new projects + * explicitly specify this option as true. + */ + emptyStringAsUndefined: true, }); diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 44a847d6581..ad68485737c 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,2 +1,2 @@ -export { db, dbWs } from './client'; -export * as schema from './schema'; +export { db, dbWs } from "./client"; +export * as schema from "./schema"; diff --git a/packages/db/src/schema/enums.ts b/packages/db/src/schema/enums.ts index ec4ea1111da..31e0cce606b 100644 --- a/packages/db/src/schema/enums.ts +++ b/packages/db/src/schema/enums.ts @@ -1,14 +1,14 @@ -import { z } from 'zod'; +import { z } from "zod"; export const taskStatusEnumValues = [ - 'backlog', - 'todo', - 'planning', - 'working', - 'needs-feedback', - 'ready-to-merge', - 'completed', - 'canceled', + "backlog", + "todo", + "planning", + "working", + "needs-feedback", + "ready-to-merge", + "completed", + "canceled", ] as const; export const taskStatusEnum = z.enum(taskStatusEnumValues); export type TaskStatus = z.infer; diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index e3d7a67ce2a..e9191c127d6 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -1,3 +1,3 @@ -export * from './enums'; -export * from './schema'; -export * from './relations'; +export * from "./enums"; +export * from "./relations"; +export * from "./schema"; diff --git a/packages/db/src/schema/relations.ts b/packages/db/src/schema/relations.ts index 975909b7f69..4bf38ce01e8 100644 --- a/packages/db/src/schema/relations.ts +++ b/packages/db/src/schema/relations.ts @@ -1,55 +1,67 @@ -import { relations } from 'drizzle-orm'; +import { relations } from "drizzle-orm"; -import { organizationMembers, organizations, repositories, tasks, users } from './schema'; +import { + organizationMembers, + organizations, + repositories, + tasks, + users, +} from "./schema"; export const usersRelations = relations(users, ({ many }) => ({ - organizationMembers: many(organizationMembers), - createdTasks: many(tasks, { relationName: 'creator' }), - assignedTasks: many(tasks, { relationName: 'assignee' }), + organizationMembers: many(organizationMembers), + createdTasks: many(tasks, { relationName: "creator" }), + assignedTasks: many(tasks, { relationName: "assignee" }), })); export const organizationsRelations = relations(organizations, ({ many }) => ({ - members: many(organizationMembers), - repositories: many(repositories), - tasks: many(tasks), + members: many(organizationMembers), + repositories: many(repositories), + tasks: many(tasks), })); -export const organizationMembersRelations = relations(organizationMembers, ({ one }) => ({ - organization: one(organizations, { - fields: [organizationMembers.organizationId], - references: [organizations.id], - }), - user: one(users, { - fields: [organizationMembers.userId], - references: [users.id], - }), -})); +export const organizationMembersRelations = relations( + organizationMembers, + ({ one }) => ({ + organization: one(organizations, { + fields: [organizationMembers.organizationId], + references: [organizations.id], + }), + user: one(users, { + fields: [organizationMembers.userId], + references: [users.id], + }), + }), +); -export const repositoriesRelations = relations(repositories, ({ one, many }) => ({ - organization: one(organizations, { - fields: [repositories.organizationId], - references: [organizations.id], - }), - tasks: many(tasks), -})); +export const repositoriesRelations = relations( + repositories, + ({ one, many }) => ({ + organization: one(organizations, { + fields: [repositories.organizationId], + references: [organizations.id], + }), + tasks: many(tasks), + }), +); export const tasksRelations = relations(tasks, ({ one }) => ({ - repository: one(repositories, { - fields: [tasks.repositoryId], - references: [repositories.id], - }), - organization: one(organizations, { - fields: [tasks.organizationId], - references: [organizations.id], - }), - assignee: one(users, { - fields: [tasks.assigneeId], - references: [users.id], - relationName: 'assignee', - }), - creator: one(users, { - fields: [tasks.creatorId], - references: [users.id], - relationName: 'creator', - }), + repository: one(repositories, { + fields: [tasks.repositoryId], + references: [repositories.id], + }), + organization: one(organizations, { + fields: [tasks.organizationId], + references: [organizations.id], + }), + assignee: one(users, { + fields: [tasks.assigneeId], + references: [users.id], + relationName: "assignee", + }), + creator: one(users, { + fields: [tasks.creatorId], + references: [users.id], + relationName: "creator", + }), })); diff --git a/packages/db/src/schema/schema.ts b/packages/db/src/schema/schema.ts index e2784504fec..301d94fab9d 100644 --- a/packages/db/src/schema/schema.ts +++ b/packages/db/src/schema/schema.ts @@ -1,141 +1,151 @@ -import { relations } from 'drizzle-orm'; -import { index, pgEnum, pgTable, text, timestamp, unique, uuid } from 'drizzle-orm/pg-core'; +import { relations } from "drizzle-orm"; +import { + index, + pgEnum, + pgTable, + text, + timestamp, + unique, + uuid, +} from "drizzle-orm/pg-core"; -import { taskStatusEnumValues } from './enums'; +import { taskStatusEnumValues } from "./enums"; -export const taskStatus = pgEnum('task_status', taskStatusEnumValues); +export const taskStatus = pgEnum("task_status", taskStatusEnumValues); export const users = pgTable( - 'users', - { - id: uuid().primaryKey().defaultRandom(), - name: text().notNull(), - email: text().notNull().unique(), - avatarUrl: text('avatar_url'), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at') - .notNull() - .defaultNow() - .$onUpdate(() => new Date()), - }, - (table) => [index('users_email_idx').on(table.email)], + "users", + { + id: uuid().primaryKey().defaultRandom(), + name: text().notNull(), + email: text().notNull().unique(), + avatarUrl: text("avatar_url"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at") + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }, + (table) => [index("users_email_idx").on(table.email)], ); export type InsertUser = typeof users.$inferInsert; export type SelectUser = typeof users.$inferSelect; export const organizations = pgTable( - 'organizations', - { - id: uuid().primaryKey().defaultRandom(), - name: text().notNull(), - slug: text().notNull().unique(), - githubOrg: text('github_org'), - avatarUrl: text('avatar_url'), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at') - .notNull() - .defaultNow() - .$onUpdate(() => new Date()), - }, - (table) => [ - index('organizations_slug_idx').on(table.slug), - ], + "organizations", + { + id: uuid().primaryKey().defaultRandom(), + name: text().notNull(), + slug: text().notNull().unique(), + githubOrg: text("github_org"), + avatarUrl: text("avatar_url"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at") + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }, + (table) => [index("organizations_slug_idx").on(table.slug)], ); export type InsertOrganization = typeof organizations.$inferInsert; export type SelectOrganization = typeof organizations.$inferSelect; export const organizationMembers = pgTable( - 'organization_members', - { - id: uuid().primaryKey().defaultRandom(), - organizationId: uuid('organization_id') - .notNull() - .references(() => organizations.id, { onDelete: 'cascade' }), - userId: uuid('user_id') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - createdAt: timestamp('created_at').notNull().defaultNow(), - }, - (table) => [ - index('organization_members_organization_id_idx').on(table.organizationId), - index('organization_members_user_id_idx').on(table.userId), - unique('organization_members_unique').on(table.organizationId, table.userId), - ], + "organization_members", + { + id: uuid().primaryKey().defaultRandom(), + organizationId: uuid("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + userId: uuid("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at").notNull().defaultNow(), + }, + (table) => [ + index("organization_members_organization_id_idx").on(table.organizationId), + index("organization_members_user_id_idx").on(table.userId), + unique("organization_members_unique").on( + table.organizationId, + table.userId, + ), + ], ); export type InsertOrganizationMember = typeof organizationMembers.$inferInsert; export type SelectOrganizationMember = typeof organizationMembers.$inferSelect; export const repositories = pgTable( - 'repositories', - { - id: uuid().primaryKey().defaultRandom(), - organizationId: uuid('organization_id') - .notNull() - .references(() => organizations.id, { onDelete: 'cascade' }), - name: text().notNull(), - slug: text().notNull(), - repoUrl: text('repo_url').notNull(), - repoOwner: text('repo_owner').notNull(), - repoName: text('repo_name').notNull(), - defaultBranch: text('default_branch').notNull().default('main'), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at') - .notNull() - .defaultNow() - .$onUpdate(() => new Date()), - }, - (table) => [ - index('repositories_organization_id_idx').on(table.organizationId), - index('repositories_slug_idx').on(table.slug), - unique('repositories_org_slug_unique').on(table.organizationId, table.slug), - ], + "repositories", + { + id: uuid().primaryKey().defaultRandom(), + organizationId: uuid("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + name: text().notNull(), + slug: text().notNull(), + repoUrl: text("repo_url").notNull(), + repoOwner: text("repo_owner").notNull(), + repoName: text("repo_name").notNull(), + defaultBranch: text("default_branch").notNull().default("main"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at") + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }, + (table) => [ + index("repositories_organization_id_idx").on(table.organizationId), + index("repositories_slug_idx").on(table.slug), + unique("repositories_org_slug_unique").on(table.organizationId, table.slug), + ], ); export type InsertRepository = typeof repositories.$inferInsert; export type SelectRepository = typeof repositories.$inferSelect; export const tasks = pgTable( - 'tasks', - { - id: uuid().primaryKey().defaultRandom(), - slug: text().notNull().unique(), - title: text().notNull(), - description: text(), - status: taskStatus().notNull().default('planning'), - - repositoryId: uuid('repository_id') - .notNull() - .references(() => repositories.id, { onDelete: 'cascade' }), - organizationId: uuid('organization_id') - .notNull() - .references(() => organizations.id, { onDelete: 'cascade' }), - assigneeId: uuid('assignee_id').references(() => users.id, { onDelete: 'set null' }), - creatorId: uuid('creator_id') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - - branch: text(), - - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at') - .notNull() - .defaultNow() - .$onUpdate(() => new Date()), - }, - (table) => [ - index('tasks_slug_idx').on(table.slug), - index('tasks_repository_id_idx').on(table.repositoryId), - index('tasks_organization_id_idx').on(table.organizationId), - index('tasks_assignee_id_idx').on(table.assigneeId), - index('tasks_creator_id_idx').on(table.creatorId), - index('tasks_status_idx').on(table.status), - index('tasks_created_at_idx').on(table.createdAt), - ], + "tasks", + { + id: uuid().primaryKey().defaultRandom(), + slug: text().notNull().unique(), + title: text().notNull(), + description: text(), + status: taskStatus().notNull().default("planning"), + + repositoryId: uuid("repository_id") + .notNull() + .references(() => repositories.id, { onDelete: "cascade" }), + organizationId: uuid("organization_id") + .notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + assigneeId: uuid("assignee_id").references(() => users.id, { + onDelete: "set null", + }), + creatorId: uuid("creator_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + branch: text(), + + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at") + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }, + (table) => [ + index("tasks_slug_idx").on(table.slug), + index("tasks_repository_id_idx").on(table.repositoryId), + index("tasks_organization_id_idx").on(table.organizationId), + index("tasks_assignee_id_idx").on(table.assigneeId), + index("tasks_creator_id_idx").on(table.creatorId), + index("tasks_status_idx").on(table.status), + index("tasks_created_at_idx").on(table.createdAt), + ], ); export type InsertTask = typeof tasks.$inferInsert; export type SelectTask = typeof tasks.$inferSelect; - diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index b0d893462be..ad8f008449b 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -1,141 +1,141 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; +import type * as React from "react"; -import { cn } from "../lib/utils" +import { cn } from "../lib/utils"; function Dialog({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogTrigger({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogPortal({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogClose({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function DialogOverlay({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DialogContent({ - className, - children, - showCloseButton = true, - ...props + className, + children, + showCloseButton = true, + ...props }: React.ComponentProps & { - showCloseButton?: boolean + showCloseButton?: boolean; }) { - return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ) + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function DialogTitle({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function DialogDescription({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -} + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/packages/ui/src/components/input.tsx b/packages/ui/src/components/input.tsx index a16ed8ac1c9..b46432342ca 100644 --- a/packages/ui/src/components/input.tsx +++ b/packages/ui/src/components/input.tsx @@ -1,21 +1,21 @@ -import * as React from "react" +import type * as React from "react"; -import { cn } from "../lib/utils" +import { cn } from "../lib/utils"; function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ) + return ( + + ); } -export { Input } +export { Input }; diff --git a/packages/ui/src/components/label.tsx b/packages/ui/src/components/label.tsx index 35695cb0408..f40a7e376fa 100644 --- a/packages/ui/src/components/label.tsx +++ b/packages/ui/src/components/label.tsx @@ -1,22 +1,22 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" +import * as LabelPrimitive from "@radix-ui/react-label"; +import type * as React from "react"; -import { cn } from "../lib/utils" +import { cn } from "../lib/utils"; function Label({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } -export { Label } +export { Label }; diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index df76db1d7ee..65d9c6b2750 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -1,187 +1,187 @@ -"use client" +"use client"; -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import * as SelectPrimitive from "@radix-ui/react-select"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import type * as React from "react"; -import { cn } from "../lib/utils" +import { cn } from "../lib/utils"; function Select({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function SelectGroup({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function SelectValue({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function SelectTrigger({ - className, - size = "default", - children, - ...props + className, + size = "default", + children, + ...props }: React.ComponentProps & { - size?: "sm" | "default" + size?: "sm" | "default"; }) { - return ( - - {children} - - - - - ) + return ( + + {children} + + + + + ); } function SelectContent({ - className, - children, - position = "popper", - align = "center", - ...props + className, + children, + position = "popper", + align = "center", + ...props }: React.ComponentProps) { - return ( - - - - - {children} - - - - - ) + return ( + + + + + {children} + + + + + ); } function SelectLabel({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function SelectItem({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - - - - - - - {children} - - ) + return ( + + + + + + + {children} + + ); } function SelectSeparator({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function SelectScrollUpButton({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - - - ) + return ( + + + + ); } function SelectScrollDownButton({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - - - ) + return ( + + + + ); } export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -} + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/packages/ui/src/components/textarea.tsx b/packages/ui/src/components/textarea.tsx index 438d640c9ae..da8d48ca2f4 100644 --- a/packages/ui/src/components/textarea.tsx +++ b/packages/ui/src/components/textarea.tsx @@ -1,18 +1,18 @@ -import * as React from "react" +import type * as React from "react"; -import { cn } from "../lib/utils" +import { cn } from "../lib/utils"; function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { - return ( -