From 098fc41b2f9b04a2cb8f88ebaeede7972d02a740 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 20 Apr 2026 10:04:50 -0500 Subject: [PATCH] feat(ui): add dialogs-modals and projects-goals-workspaces stories New story files for dialog/modal components (830 lines) and project/ goal/workspace components (516 lines). Updates data-viz-misc stories. Co-Authored-By: Paperclip --- .../stories/data-viz-misc.stories.tsx | 1 - .../stories/dialogs-modals.stories.tsx | 826 ++++++++++++++++++ .../projects-goals-workspaces.stories.tsx | 516 +++++++++++ 3 files changed, 1342 insertions(+), 1 deletion(-) create mode 100644 ui/storybook/stories/dialogs-modals.stories.tsx create mode 100644 ui/storybook/stories/projects-goals-workspaces.stories.tsx diff --git a/ui/storybook/stories/data-viz-misc.stories.tsx b/ui/storybook/stories/data-viz-misc.stories.tsx index 47d310fb7ec..d3474ee9a30 100644 --- a/ui/storybook/stories/data-viz-misc.stories.tsx +++ b/ui/storybook/stories/data-viz-misc.stories.tsx @@ -129,7 +129,6 @@ function makeHeartbeatRun(overrides: Partial): HeartbeatRun { lastUsefulActionAt: null, nextAction: null, contextSnapshot: null, - updatedAt: createdAt, ...overrides, createdAt, updatedAt: overrides.updatedAt ?? createdAt, diff --git a/ui/storybook/stories/dialogs-modals.stories.tsx b/ui/storybook/stories/dialogs-modals.stories.tsx new file mode 100644 index 00000000000..e43910db02b --- /dev/null +++ b/ui/storybook/stories/dialogs-modals.stories.tsx @@ -0,0 +1,826 @@ +import { useEffect, useLayoutEffect, useState, type ReactNode } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { + DocumentRevision, + ExecutionWorkspaceCloseReadiness, + Goal, + IssueAttachment, +} from "@paperclipai/shared"; +import { useQueryClient } from "@tanstack/react-query"; +import { Badge } from "@/components/ui/badge"; +import { DocumentDiffModal } from "@/components/DocumentDiffModal"; +import { ExecutionWorkspaceCloseDialog } from "@/components/ExecutionWorkspaceCloseDialog"; +import { ImageGalleryModal } from "@/components/ImageGalleryModal"; +import { NewAgentDialog } from "@/components/NewAgentDialog"; +import { NewGoalDialog } from "@/components/NewGoalDialog"; +import { NewIssueDialog } from "@/components/NewIssueDialog"; +import { NewProjectDialog } from "@/components/NewProjectDialog"; +import { PathInstructionsModal } from "@/components/PathInstructionsModal"; +import { useDialog } from "@/context/DialogContext"; +import { queryKeys } from "@/lib/queryKeys"; +import { + storybookAgents, + storybookAuthSession, + storybookCompanies, + storybookExecutionWorkspaces, + storybookIssueDocuments, + storybookIssueLabels, + storybookIssues, + storybookProjects, +} from "../fixtures/paperclipData"; + +const COMPANY_ID = "company-storybook"; +const SELECTED_COMPANY_STORAGE_KEY = "paperclip.selectedCompanyId"; +const ISSUE_DRAFT_STORAGE_KEY = "paperclip:issue-draft"; + +const storybookGoals: Goal[] = [ + { + id: "goal-company", + companyId: COMPANY_ID, + title: "Build Paperclip", + description: "Make autonomous companies easier to run and govern.", + level: "company", + status: "active", + parentId: null, + ownerAgentId: "agent-cto", + createdAt: new Date("2026-04-01T09:00:00.000Z"), + updatedAt: new Date("2026-04-20T11:00:00.000Z"), + }, + { + id: "goal-storybook", + companyId: COMPANY_ID, + title: "Complete Storybook coverage", + description: "Expose dense board UI states for review before release.", + level: "team", + status: "active", + parentId: "goal-company", + ownerAgentId: "agent-codex", + createdAt: new Date("2026-04-17T09:00:00.000Z"), + updatedAt: new Date("2026-04-20T11:10:00.000Z"), + }, + { + id: "goal-governance", + companyId: COMPANY_ID, + title: "Tighten governance review", + description: "Make review and approval gates visible in every operator flow.", + level: "team", + status: "planned", + parentId: "goal-company", + ownerAgentId: "agent-cto", + createdAt: new Date("2026-04-18T09:00:00.000Z"), + updatedAt: new Date("2026-04-20T11:15:00.000Z"), + }, +]; + +const documentRevisions: DocumentRevision[] = [ + { + id: "revision-plan-1", + companyId: COMPANY_ID, + documentId: "document-plan-storybook", + issueId: "issue-storybook-1", + key: "plan", + revisionNumber: 1, + title: "Plan", + format: "markdown", + body: [ + "# Plan", + "", + "- Add overview stories for the dashboard.", + "- Create issue list stories for filters and grouping.", + "- Ask QA to review the final Storybook build.", + ].join("\n"), + changeSummary: "Initial plan", + createdByAgentId: "agent-codex", + createdByUserId: null, + createdAt: new Date("2026-04-20T08:00:00.000Z"), + }, + { + id: "revision-plan-2", + companyId: COMPANY_ID, + documentId: "document-plan-storybook", + issueId: "issue-storybook-1", + key: "plan", + revisionNumber: 2, + title: "Plan", + format: "markdown", + body: [ + "# Plan", + "", + "- Add overview stories for the dashboard.", + "- Create issue list stories for filters, grouping, and workspace state.", + "- Add dialog stories for issue, goal, project, and workspace workflows.", + "- Ask QA to review the final Storybook build.", + ].join("\n"), + changeSummary: "Expanded component coverage", + createdByAgentId: "agent-codex", + createdByUserId: null, + createdAt: new Date("2026-04-20T10:00:00.000Z"), + }, + { + id: "revision-plan-3", + companyId: COMPANY_ID, + documentId: "document-plan-storybook", + issueId: "issue-storybook-1", + key: "plan", + revisionNumber: 3, + title: "Plan", + format: "markdown", + body: storybookIssueDocuments[0]?.body ?? "", + changeSummary: "Aligned with current issue scope", + createdByAgentId: "agent-codex", + createdByUserId: null, + createdAt: new Date("2026-04-20T11:30:00.000Z"), + }, +]; + +const closeReadinessReady: ExecutionWorkspaceCloseReadiness = { + workspaceId: "execution-workspace-storybook", + state: "ready_with_warnings", + blockingReasons: [], + warnings: [ + "The branch is still two commits ahead of master.", + "One shared runtime service will be stopped during cleanup.", + ], + linkedIssues: [ + { + id: "issue-storybook-1", + identifier: "PAP-1641", + title: "Create super-detailed storybooks for the project", + status: "done", + isTerminal: true, + }, + { + id: "issue-storybook-6", + identifier: "PAP-1670", + title: "Publish static Storybook preview", + status: "todo", + isTerminal: false, + }, + ], + plannedActions: [ + { + kind: "stop_runtime_services", + label: "Stop Storybook preview", + description: "Stops the managed Storybook preview service before archiving the workspace record.", + command: "pnpm dev:stop", + }, + { + kind: "git_worktree_remove", + label: "Remove git worktree", + description: "Removes the issue worktree from the local worktree parent directory.", + command: "git worktree remove .paperclip/worktrees/PAP-1641-create-super-detailed-storybooks-for-our-project", + }, + { + kind: "archive_record", + label: "Archive workspace record", + description: "Keeps audit history while removing the workspace from active workspace views.", + command: null, + }, + ], + isDestructiveCloseAllowed: true, + isSharedWorkspace: false, + isProjectPrimaryWorkspace: false, + git: { + repoRoot: "/Users/dotta/paperclip", + workspacePath: "/Users/dotta/paperclip/.paperclip/worktrees/PAP-1641-create-super-detailed-storybooks-for-our-project", + branchName: "PAP-1641-create-super-detailed-storybooks-for-our-project", + baseRef: "master", + hasDirtyTrackedFiles: true, + hasUntrackedFiles: false, + dirtyEntryCount: 3, + untrackedEntryCount: 0, + aheadCount: 2, + behindCount: 0, + isMergedIntoBase: false, + createdByRuntime: true, + }, + runtimeServices: storybookExecutionWorkspaces[0]?.runtimeServices ?? [], +}; + +const closeReadinessBlocked: ExecutionWorkspaceCloseReadiness = { + ...closeReadinessReady, + state: "blocked", + blockingReasons: [ + "PAP-1670 is still open and references this execution workspace.", + "The worktree has dirty tracked files that have not been committed.", + ], + warnings: [], + plannedActions: closeReadinessReady.plannedActions.slice(0, 1), +}; + +const galleryImages: IssueAttachment[] = [ + { + id: "attachment-storybook-dashboard", + companyId: COMPANY_ID, + issueId: "issue-storybook-1", + issueCommentId: null, + assetId: "asset-dashboard", + provider: "storybook", + objectKey: "storybook/dashboard-preview.svg", + contentType: "image/svg+xml", + byteSize: 1480, + sha256: "storybook-dashboard-preview", + originalFilename: "dashboard-preview.png", + createdByAgentId: "agent-codex", + createdByUserId: null, + createdAt: new Date("2026-04-20T10:30:00.000Z"), + updatedAt: new Date("2026-04-20T10:30:00.000Z"), + contentPath: + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1400' height='900' viewBox='0 0 1400 900'%3E%3Crect width='1400' height='900' fill='%230f172a'/%3E%3Crect x='88' y='96' width='1224' height='708' rx='28' fill='%23111827' stroke='%23334155' stroke-width='4'/%3E%3Crect x='136' y='148' width='264' height='604' rx='18' fill='%231e293b'/%3E%3Crect x='444' y='148' width='380' height='190' rx='18' fill='%230f766e'/%3E%3Crect x='860' y='148' width='348' height='190' rx='18' fill='%232563eb'/%3E%3Crect x='444' y='382' width='764' height='104' rx='18' fill='%23334155'/%3E%3Crect x='444' y='526' width='764' height='104' rx='18' fill='%23334155'/%3E%3Crect x='444' y='670' width='520' height='82' rx='18' fill='%23334155'/%3E%3Ccircle cx='236' cy='236' r='58' fill='%2314b8a6'/%3E%3Crect x='188' y='334' width='164' height='18' rx='9' fill='%2394a3b8'/%3E%3Crect x='188' y='386' width='128' height='18' rx='9' fill='%2364748b'/%3E%3Crect x='188' y='438' width='176' height='18' rx='9' fill='%2364748b'/%3E%3C/svg%3E", + }, + { + id: "attachment-storybook-diff", + companyId: COMPANY_ID, + issueId: "issue-storybook-1", + issueCommentId: null, + assetId: "asset-diff", + provider: "storybook", + objectKey: "storybook/diff-preview.svg", + contentType: "image/svg+xml", + byteSize: 1320, + sha256: "storybook-diff-preview", + originalFilename: "document-diff-preview.png", + createdByAgentId: "agent-qa", + createdByUserId: null, + createdAt: new Date("2026-04-20T10:40:00.000Z"), + updatedAt: new Date("2026-04-20T10:40:00.000Z"), + contentPath: + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1400' height='900' viewBox='0 0 1400 900'%3E%3Crect width='1400' height='900' fill='%23171717'/%3E%3Crect x='110' y='104' width='1180' height='692' rx='24' fill='%230a0a0a' stroke='%23333333' stroke-width='4'/%3E%3Crect x='160' y='164' width='1080' height='48' rx='12' fill='%23262626'/%3E%3Crect x='160' y='260' width='1080' height='52' fill='%2315221a'/%3E%3Crect x='160' y='312' width='1080' height='52' fill='%23231818'/%3E%3Crect x='160' y='364' width='1080' height='52' fill='%2315221a'/%3E%3Crect x='160' y='468' width='1080' height='52' fill='%23231818'/%3E%3Crect x='160' y='520' width='1080' height='52' fill='%2315221a'/%3E%3Crect x='220' y='276' width='720' height='18' rx='9' fill='%2374c69d'/%3E%3Crect x='220' y='328' width='540' height='18' rx='9' fill='%23fca5a5'/%3E%3Crect x='220' y='380' width='820' height='18' rx='9' fill='%2374c69d'/%3E%3Crect x='220' y='484' width='480' height='18' rx='9' fill='%23fca5a5'/%3E%3Crect x='220' y='536' width='760' height='18' rx='9' fill='%2374c69d'/%3E%3C/svg%3E", + }, +]; + +function Section({ + eyebrow, + title, + description, + children, +}: { + eyebrow: string; + title: string; + description?: string; + children: ReactNode; +}) { + return ( +
+
+
{eyebrow}
+
+
+

{title}

+ {description ? ( +

{description}

+ ) : null} +
+
+
+
{children}
+
+ ); +} + +function StoryShell({ children }: { children: ReactNode }) { + return ( +
+
{children}
+
+ ); +} + +function DialogBackdropFrame({ + eyebrow, + title, + description, + badges, +}: { + eyebrow: string; + title: string; + description: string; + badges: string[]; +}) { + return ( + +
+
+
+
+
+
+
+
+
+
+
+
+
Story state
+
+ {badges.map((badge) => ( + + {badge} + + ))} +
+
+
+
+
+ ); +} + +function hydrateDialogQueries(queryClient: ReturnType) { + queryClient.setQueryData(queryKeys.companies.all, storybookCompanies); + queryClient.setQueryData(queryKeys.auth.session, storybookAuthSession); + queryClient.setQueryData(queryKeys.agents.list(COMPANY_ID), storybookAgents); + queryClient.setQueryData(queryKeys.projects.list(COMPANY_ID), storybookProjects); + queryClient.setQueryData(queryKeys.goals.list(COMPANY_ID), storybookGoals); + queryClient.setQueryData(queryKeys.issues.list(COMPANY_ID), storybookIssues); + queryClient.setQueryData(queryKeys.issues.labels(COMPANY_ID), storybookIssueLabels); + queryClient.setQueryData(queryKeys.issues.documents("issue-storybook-1"), storybookIssueDocuments); + queryClient.setQueryData(queryKeys.issues.documentRevisions("issue-storybook-1", "plan"), documentRevisions); + queryClient.setQueryData(queryKeys.executionWorkspaces.closeReadiness("execution-workspace-storybook"), closeReadinessReady); + queryClient.setQueryData(queryKeys.executionWorkspaces.closeReadiness("execution-workspace-blocked"), closeReadinessBlocked); + queryClient.setQueryData( + queryKeys.executionWorkspaces.list(COMPANY_ID, { + projectId: "project-board-ui", + projectWorkspaceId: "workspace-board-ui", + reuseEligible: true, + }), + storybookExecutionWorkspaces, + ); + queryClient.setQueryData(queryKeys.instance.experimentalSettings, { + enableIsolatedWorkspaces: true, + enableRoutineTriggers: true, + }); + queryClient.setQueryData(queryKeys.access.companyUserDirectory(COMPANY_ID), { + users: [ + { + principalId: "user-board", + status: "active", + user: { + id: "user-board", + email: "riley@paperclip.local", + name: "Riley Board", + image: null, + }, + }, + ], + }); + queryClient.setQueryData( + queryKeys.sidebarPreferences.projectOrder(COMPANY_ID, storybookAuthSession.user.id), + { orderedIds: storybookProjects.map((project) => project.id), updatedAt: null }, + ); + queryClient.setQueryData(queryKeys.adapters.all, [ + { + type: "codex_local", + label: "Codex local", + source: "builtin", + modelsCount: 5, + loaded: true, + disabled: false, + capabilities: { + supportsInstructionsBundle: true, + supportsSkills: true, + supportsLocalAgentJwt: true, + requiresMaterializedRuntimeSkills: false, + }, + }, + { + type: "claude_local", + label: "Claude local", + source: "builtin", + modelsCount: 4, + loaded: true, + disabled: false, + capabilities: { + supportsInstructionsBundle: true, + supportsSkills: true, + supportsLocalAgentJwt: true, + requiresMaterializedRuntimeSkills: false, + }, + }, + ]); + queryClient.setQueryData(queryKeys.agents.adapterModels(COMPANY_ID, "codex_local"), [ + { id: "gpt-5.4", label: "GPT-5.4" }, + { id: "gpt-5.4-mini", label: "GPT-5.4 Mini" }, + ]); +} + +function StorybookDialogFixtures({ children }: { children: ReactNode }) { + const queryClient = useQueryClient(); + const [ready] = useState(() => { + if (typeof window !== "undefined") { + window.localStorage.setItem(SELECTED_COMPANY_STORAGE_KEY, COMPANY_ID); + window.localStorage.removeItem(ISSUE_DRAFT_STORAGE_KEY); + } + hydrateDialogQueries(queryClient); + return true; + }); + + return ready ? children : null; +} + +function useIssueCreateErrorMock(enabled: boolean) { + useLayoutEffect(() => { + if (!enabled || typeof window === "undefined") return undefined; + + const originalFetch = window.fetch.bind(window); + window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const rawUrl = + typeof input === "string" + ? input + : input instanceof URL + ? input.href + : input.url; + const url = new URL(rawUrl, window.location.origin); + if (url.pathname === `/api/companies/${COMPANY_ID}/issues` && init?.method === "POST") { + return Response.json( + { error: "Validation failed: add a reviewer before creating governed release work." }, + { status: 422 }, + ); + } + return originalFetch(input, init); + }; + + return () => { + window.fetch = originalFetch; + }; + }, [enabled]); +} + +function setFieldValue(element: HTMLInputElement | HTMLTextAreaElement, value: string) { + const prototype = Object.getPrototypeOf(element) as HTMLInputElement | HTMLTextAreaElement; + const valueSetter = Object.getOwnPropertyDescriptor(prototype, "value")?.set; + valueSetter?.call(element, value); + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); +} + +function fillFirstField(selector: string, value: string) { + const element = document.querySelector(selector); + if (!element) return false; + setFieldValue(element, value); + return true; +} + +function clickButtonByText(text: string) { + const buttons = Array.from(document.querySelectorAll("button")); + const button = buttons.find((candidate) => candidate.textContent?.trim().includes(text)); + button?.click(); +} + +function useOpenOnMount(open: () => void) { + useEffect(() => { + open(); + }, [open]); +} + +function IssueDialogOpener({ + variant, +}: { + variant: "empty" | "prefilled" | "validation"; +}) { + const { openNewIssue } = useDialog(); + useIssueCreateErrorMock(variant === "validation"); + + useOpenOnMount(() => { + openNewIssue( + variant === "empty" + ? {} + : { + title: variant === "validation" ? "Ship guarded release checklist" : "Create dialog Storybook coverage", + description: [ + "Cover modal flows with fixture-backed states.", + "", + "- Keep dialogs open by default", + "- Show project workspace selection", + "- Include reviewer and approver context", + ].join("\n"), + status: "todo", + priority: "high", + projectId: "project-board-ui", + projectWorkspaceId: "workspace-board-ui", + assigneeAgentId: "agent-codex", + executionWorkspaceMode: "isolated_workspace", + }, + ); + }); + + useEffect(() => { + if (variant !== "validation") return undefined; + const timer = window.setTimeout(() => { + clickButtonByText("Create Issue"); + }, 500); + return () => window.clearTimeout(timer); + }, [variant]); + + return ; +} + +function AgentDialogOpener({ advanced }: { advanced?: boolean }) { + const { openNewAgent } = useDialog(); + + useOpenOnMount(() => { + openNewAgent(); + }); + + useEffect(() => { + if (!advanced) return undefined; + const timer = window.setTimeout(() => { + clickButtonByText("advanced configuration"); + }, 250); + return () => window.clearTimeout(timer); + }, [advanced]); + + return ; +} + +function GoalDialogOpener({ populated }: { populated?: boolean }) { + const { openNewGoal } = useDialog(); + + useOpenOnMount(() => { + openNewGoal(populated ? { parentId: "goal-company" } : {}); + }); + + useEffect(() => { + if (!populated) return undefined; + const timer = window.setTimeout(() => { + fillFirstField("input[placeholder='Goal title']", "Add modal review coverage"); + }, 250); + return () => window.clearTimeout(timer); + }, [populated]); + + return ; +} + +function ProjectDialogOpener({ populated }: { populated?: boolean }) { + const { openNewProject } = useDialog(); + + useOpenOnMount(() => { + openNewProject(); + }); + + useEffect(() => { + if (!populated) return undefined; + const timer = window.setTimeout(() => { + fillFirstField("input[placeholder='Project name']", "Storybook review workspace"); + fillFirstField("input[placeholder='https://github.com/org/repo']", "https://github.com/paperclipai/paperclip"); + fillFirstField("input[placeholder='/absolute/path/to/workspace']", "/Users/dotta/paperclip/ui"); + fillFirstField("input[type='date']", "2026-04-30"); + }, 250); + return () => window.clearTimeout(timer); + }, [populated]); + + return ; +} + +function DialogStory({ + eyebrow, + title, + description, + badges, + children, +}: { + eyebrow: string; + title: string; + description: string; + badges: string[]; + children: ReactNode; +}) { + return ( + + + {children} + + ); +} + +function ExecutionWorkspaceDialogStory({ blocked }: { blocked?: boolean }) { + const workspace = storybookExecutionWorkspaces[0]!; + return ( + + undefined} + /> + + ); +} + +function DocumentDiffModalStory() { + return ( + + undefined} + /> + + ); +} + +function ImageGalleryModalStory() { + return ( + + undefined} /> + + ); +} + +function PathInstructionsModalStory() { + return ( + + undefined} /> + + ); +} + +const meta = { + title: "Product/Dialogs & Modals", + parameters: { + docs: { + description: { + component: + "Open-state stories for Paperclip creation dialogs, workspace confirmations, document diffing, image attachments, and path helper modals.", + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const NewIssueEmpty: Story = { + name: "New Issue - Empty", + render: () => ( + + + + ), +}; + +export const NewIssuePrefilled: Story = { + name: "New Issue - Prefilled", + render: () => ( + + + + ), +}; + +export const NewIssueValidationError: Story = { + name: "New Issue - Validation Error", + render: () => ( + + + + ), +}; + +export const NewAgentRecommendation: Story = { + name: "New Agent - Recommendation", + render: () => ( + + + + ), +}; + +export const NewAgentAdapterSelection: Story = { + name: "New Agent - Adapter Selection", + render: () => ( + + + + ), +}; + +export const NewGoalEmpty: Story = { + name: "New Goal - Empty", + render: () => ( + + + + ), +}; + +export const NewGoalWithParent: Story = { + name: "New Goal - Parent Selected", + render: () => ( + + + + ), +}; + +export const NewProjectEmpty: Story = { + name: "New Project - Empty", + render: () => ( + + + + ), +}; + +export const NewProjectWorkspaceConfig: Story = { + name: "New Project - Workspace Config", + render: () => ( + + + + ), +}; + +export const ExecutionWorkspaceCloseReady: Story = { + name: "Execution Workspace Close - Ready", + render: () => , +}; + +export const ExecutionWorkspaceCloseBlocked: Story = { + name: "Execution Workspace Close - Blocked", + render: () => , +}; + +export const DocumentDiffOpen: Story = { + name: "Document Diff", + render: () => , +}; + +export const ImageGalleryOpen: Story = { + name: "Image Gallery", + render: () => , +}; + +export const PathInstructionsOpen: Story = { + name: "Path Instructions", + render: () => , +}; diff --git a/ui/storybook/stories/projects-goals-workspaces.stories.tsx b/ui/storybook/stories/projects-goals-workspaces.stories.tsx new file mode 100644 index 00000000000..76d83d9d2a6 --- /dev/null +++ b/ui/storybook/stories/projects-goals-workspaces.stories.tsx @@ -0,0 +1,516 @@ +import { useMemo, useState, type ReactNode } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useQueryClient } from "@tanstack/react-query"; +import type { Goal, Project } from "@paperclipai/shared"; +import { Archive, Boxes, FolderGit2, GitBranch, Network, Play, RotateCcw, Square } from "lucide-react"; +import { GoalProperties } from "@/components/GoalProperties"; +import { GoalTree } from "@/components/GoalTree"; +import { ProjectProperties, type ProjectConfigFieldKey, type ProjectFieldSaveState } from "@/components/ProjectProperties"; +import { ProjectWorkspacesContent } from "@/components/ProjectWorkspacesContent"; +import { ProjectWorkspaceSummaryCard } from "@/components/ProjectWorkspaceSummaryCard"; +import { + WorkspaceRuntimeControls, + buildWorkspaceRuntimeControlSections, + type WorkspaceRuntimeControlRequest, +} from "@/components/WorkspaceRuntimeControls"; +import { WorktreeBanner } from "@/components/WorktreeBanner"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { queryKeys } from "@/lib/queryKeys"; +import { buildProjectWorkspaceSummaries } from "@/lib/project-workspaces-tab"; +import { + storybookAgents, + storybookAuthSession, + storybookCompanies, + storybookExecutionWorkspaces, + storybookGoals, + storybookIssues, + storybookProjectWorkspaces, + storybookProjects, +} from "../fixtures/paperclipData"; + +const COMPANY_ID = "company-storybook"; +const boardProject = storybookProjects.find((project) => project.id === "project-board-ui") ?? storybookProjects[0]!; +const archivedProject = + storybookProjects.find((project) => project.id === "project-archived-import") + ?? storybookProjects[storybookProjects.length - 1]!; + +const goalProgress = new Map([ + ["goal-company", 62], + ["goal-board-ux", 74], + ["goal-agent-runtime", 48], + ["goal-storybook", 88], + ["goal-budget-safety", 100], + ["goal-archived-import", 18], +]); + +function Section({ + eyebrow, + title, + children, +}: { + eyebrow: string; + title: string; + children: ReactNode; +}) { + return ( +
+
+
{eyebrow}
+

{title}

+
+
{children}
+
+ ); +} + +function hydrateStorybookQueries(queryClient: ReturnType) { + queryClient.setQueryData(queryKeys.auth.session, storybookAuthSession); + queryClient.setQueryData(queryKeys.companies.all, storybookCompanies); + queryClient.setQueryData(queryKeys.agents.list(COMPANY_ID), storybookAgents); + queryClient.setQueryData(queryKeys.projects.list(COMPANY_ID), storybookProjects); + queryClient.setQueryData(queryKeys.projects.detail(boardProject.id), boardProject); + queryClient.setQueryData(queryKeys.projects.detail(boardProject.urlKey), boardProject); + queryClient.setQueryData(queryKeys.projects.detail(archivedProject.id), archivedProject); + queryClient.setQueryData(queryKeys.goals.list(COMPANY_ID), storybookGoals); + for (const goal of storybookGoals) { + queryClient.setQueryData(queryKeys.goals.detail(goal.id), goal); + } + queryClient.setQueryData(queryKeys.issues.list(COMPANY_ID), storybookIssues); + queryClient.setQueryData(queryKeys.issues.listByProject(COMPANY_ID, boardProject.id), storybookIssues); + queryClient.setQueryData(queryKeys.secrets.list(COMPANY_ID), []); + queryClient.setQueryData(queryKeys.instance.experimentalSettings, { + enableIsolatedWorkspaces: true, + enableRoutineTriggers: true, + }); + queryClient.setQueryData(queryKeys.executionWorkspaces.list(COMPANY_ID), storybookExecutionWorkspaces); + queryClient.setQueryData( + queryKeys.executionWorkspaces.list(COMPANY_ID, { projectId: boardProject.id }), + storybookExecutionWorkspaces, + ); + queryClient.setQueryData( + queryKeys.executionWorkspaces.summaryList(COMPANY_ID), + storybookExecutionWorkspaces.map((workspace) => ({ + id: workspace.id, + name: workspace.name, + mode: workspace.mode, + projectWorkspaceId: workspace.projectWorkspaceId, + })), + ); +} + +function StorybookData({ children }: { children: ReactNode }) { + const queryClient = useQueryClient(); + const [ready] = useState(() => { + hydrateStorybookQueries(queryClient); + return true; + }); + + return ready ? children : null; +} + +function stateForProjectField(field: ProjectConfigFieldKey): ProjectFieldSaveState { + if (field === "env" || field === "execution_workspace_branch_template") return "saved"; + if (field === "execution_workspace_worktree_parent_dir") return "saving"; + return "idle"; +} + +function ProjectPropertiesMatrix() { + const editableProject: Project = useMemo( + () => ({ + ...boardProject, + env: { + STORYBOOK_REVIEW: { type: "plain", value: "enabled" }, + OPENAI_API_KEY: { type: "secret_ref", secretId: "secret-openai", version: "latest" }, + }, + }), + [], + ); + + return ( +
+
+ undefined} + getFieldSaveState={stateForProjectField} + onArchive={() => undefined} + /> +
+
+
+
+
+
{archivedProject.name}
+
Archived, no workspace configured
+
+ + + archived + +
+ undefined} + onArchive={() => undefined} + archivePending={false} + /> +
+
+ {[ + { label: "Goals linked", value: boardProject.goalIds.length, icon: Network }, + { label: "Workspaces", value: boardProject.workspaces.length, icon: Boxes }, + { label: "Runtime services", value: boardProject.primaryWorkspace?.runtimeServices?.length ?? 0, icon: Play }, + ].map((item) => { + const Icon = item.icon; + return ( +
+ +
{item.value}
+
{item.label}
+
+ ); + })} +
+
+
+ ); +} + +function WorkspacesMatrix() { + const summaries = buildProjectWorkspaceSummaries({ + project: boardProject, + issues: storybookIssues.filter((issue) => issue.projectId === boardProject.id), + executionWorkspaces: storybookExecutionWorkspaces, + }); + const localSummary = summaries.find((summary) => summary.kind === "project_workspace" && summary.workspaceId === "workspace-board-ui"); + const remoteSummary = summaries.find((summary) => summary.workspaceId === "workspace-docs-remote"); + const cleanupSummary = summaries.find((summary) => summary.executionWorkspaceStatus === "cleanup_failed"); + const featuredSummaries = [localSummary, remoteSummary, cleanupSummary].filter( + (summary): summary is NonNullable => Boolean(summary), + ); + + return ( +
+ +
+ {featuredSummaries.map((summary) => ( + 0 ? `${summary.key}:stop` : null} + runtimeActionPending={summary.runningServiceCount > 0} + onRuntimeAction={() => undefined} + onCloseWorkspace={() => undefined} + /> + ))} +
+
+ +
+
+ ); +} + +function GoalProgressRow({ goal }: { goal: Goal }) { + const progress = goalProgress.get(goal.id) ?? 0; + const childCount = storybookGoals.filter((candidate) => candidate.parentId === goal.id).length; + + return ( +
+
+
+
{goal.title}
+
+ {goal.level} ยท {childCount} child goal{childCount === 1 ? "" : "s"} +
+
+ {progress}% +
+
+
+
+
+ ); +} + +function GoalPropertiesMatrix() { + const selectedGoal = storybookGoals.find((goal) => goal.id === "goal-board-ux") ?? storybookGoals[0]!; + const childGoals = storybookGoals.filter((goal) => goal.parentId === selectedGoal.id); + const linkedProjects = storybookProjects.filter((project) => project.goalIds.includes(selectedGoal.id)); + + return ( +
+
+
+
+
+
Goal detail composition
+

{selectedGoal.title}

+

{selectedGoal.description}

+
+ {selectedGoal.status} +
+
+ +
+
{childGoals.length}
+
Child goals
+
+
+
{linkedProjects.length}
+
Linked projects
+
+
+
+
+ {childGoals.map((goal) => ( + + ))} +
+
+
+ undefined} /> +
+
+ ); +} + +function GoalTreeMatrix() { + const [selectedGoal, setSelectedGoal] = useState(storybookGoals[1] ?? null); + + return ( +
+
+ +
+
+
Selected goal
+ {selectedGoal ? ( +
+
+
{selectedGoal.title}
+
{selectedGoal.description}
+
+ +
+ ) : ( +

Select a goal row to inspect its progress state.

+ )} +
+
+ ); +} + +function RuntimeControlsMatrix() { + const primaryWorkspace = storybookProjectWorkspaces[0]!; + const remoteWorkspace = storybookProjectWorkspaces.find((workspace) => workspace.id === "workspace-docs-remote")!; + const runningSections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: primaryWorkspace.runtimeConfig?.workspaceRuntime, + runtimeServices: primaryWorkspace.runtimeServices, + canStartServices: true, + canRunJobs: true, + }); + const stoppedSections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: remoteWorkspace.runtimeConfig?.workspaceRuntime, + runtimeServices: remoteWorkspace.runtimeServices, + canStartServices: true, + canRunJobs: true, + }); + const disabledSections = buildWorkspaceRuntimeControlSections({ + runtimeConfig: { + commands: [ + { id: "web", name: "Web app", kind: "service", command: "pnpm dev" }, + { id: "migrate", name: "Migrate database", kind: "job", command: "pnpm db:migrate" }, + ], + }, + runtimeServices: [], + canStartServices: false, + canRunJobs: false, + }); + const pendingRequest: WorkspaceRuntimeControlRequest = { + action: "restart", + workspaceCommandId: "storybook", + runtimeServiceId: "service-storybook", + serviceIndex: 0, + }; + + return ( +
+ + + + + Running services + + Stop and restart actions with a pending request spinner. + + + undefined} + /> + + + + + + + Stopped remote preview + + Startable remote workspace service with URL history. + + + undefined} /> + + + + + + + Missing prerequisites + + Disabled runtime controls when no workspace path is available. + + + undefined} + /> + + +
+ ); +} + +function setWorktreeMeta(name: string, content: string) { + if (typeof document === "undefined") return; + let element = document.querySelector(`meta[name="${name}"]`); + if (!element) { + element = document.createElement("meta"); + element.setAttribute("name", name); + document.head.appendChild(element); + } + element.setAttribute("content", content); +} + +function WorktreeBannerMatrix() { + setWorktreeMeta("paperclip-worktree-enabled", "true"); + setWorktreeMeta("paperclip-worktree-name", "PAP-1675-projects-goals-workspaces"); + setWorktreeMeta("paperclip-worktree-color", "#0f766e"); + setWorktreeMeta("paperclip-worktree-text-color", "#ecfeff"); + + return ( +
+
+ +
+
+ {[ + { label: "Branch", value: "PAP-1675-projects-goals-workspaces", icon: GitBranch }, + { label: "Workspace", value: "Project Storybook worktree", icon: FolderGit2 }, + { label: "Context", value: "visible before layout chrome", icon: Boxes }, + ].map((item) => { + const Icon = item.icon; + return ( +
+ +
{item.label}
+
{item.value}
+
+ ); + })} +
+
+ ); +} + +function ProjectsGoalsWorkspacesStories() { + return ( + +
+
+
+
+
+
Projects, goals, and workspaces
+

Hierarchical planning and runtime surfaces

+

+ Fixture-backed project and goal stories cover editable project properties, local and remote workspace + cards, cleanup failures, goal hierarchy states, and runtime command controls. +

+
+
+ active + archived + local workspace + remote workspace +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ ); +} + +const meta = { + title: "Product/Projects Goals Workspaces", + component: ProjectsGoalsWorkspacesStories, + parameters: { + docs: { + description: { + component: + "Projects, goals, and workspaces stories cover project properties, workspace cards/lists, goal hierarchy panels, runtime controls, and worktree branding states.", + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const SurfaceMatrix: Story = {};