From 33d24e48adef43a896b0e23f355a5f4e8d39e2cf Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:52:18 -0500 Subject: [PATCH] fix(skills): simplify source rows, errors, and actions (#16310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip manages agent skills from GitHub repositories. > - Skills → Sources shows saved repositories and their sync status. > - A saved source gained a second repository row when other projects used the same repository and branch. > - That row repeated the GitHub icon and branch, and it exposed project names that did not help with source management. > - The page also repeated the Installed navigation link and showed refresh errors as separate lines of text. > - Connection management also appeared under each source, where it competed with source-specific actions. > - This pull request shows each saved source once, groups error details, and moves connection management into the menu beside Import from GitHub. > - The result is a clearer source list that keeps import and recovery actions available. ## Linked Issues or Issue Description **What happened?** When a saved source shared its repository and branch with other projects, Skills → Sources displayed a second GitHub header and the names of those projects. The page also displayed an extra Installed skills link and repeated Manage GitHub connection under source rows. A failed refresh placed its timestamp and error on separate lines. **Expected behavior** Show one row for each saved source. Keep project names out of that row. Keep the additional import action available in More. Put connection management in the page actions menu beside Import from GitHub. Present refresh errors as one readable notice with the time and review action. **Steps to reproduce** 1. Open Skills → Sources in a company with one saved GitHub source and another project that uses the same repository and branch. 2. Observe the repeated repository row and project names. 3. Open the Storybook journey `Existing source, more projects` to inspect the same state. Open `Retained skills and retry` to inspect a failed refresh. **Paperclip version or commit** The issue is present on base commit `739f9e40a4`. **Deployment mode** Built from source. The issue was also observed in a staging workspace. ## What Changed - Remove project names, the duplicate repository row, and the redundant Installed skills link from saved source rows. - Put imports for additional projects in the source More menu. Show brief import progress in the source metadata. - Group source errors, attempt time, and the review link in a compact notice. Add a Storybook journey and browser coverage for the shared-repository state. - Add a page actions menu beside Import from GitHub. Put Manage GitHub connection there and remove the repeated inline links. Cover the menu and keyboard focus in the Storybook browser suite. ## Verification - `pnpm -r typecheck`, `pnpm build`, `pnpm check:token-gates`, `node scripts/check-maps.mjs`, and `node scripts/storybook-catalog.mjs --check` passed locally. The Skills sources unit suite passed 22 tests. - All 18 repository-skills Storybook browser tests passed, including the shared-source import, connection menu, mobile, and failed-refresh journeys. - A local full `pnpm test:run` was stopped after a server runner-API test timed out. An isolated diagnostic identified macOS embedded PostgreSQL shared-memory exhaustion. The PR passed all 55 CI checks, including server test shards and end-to-end tests. - Open the Storybook journeys `Existing source, more projects`, `Automatically imported skills`, and `Retained skills and retry` to inspect the compact source row, page actions menu, and grouped error notice. ## Risks Low risk. The visible import action for projects that share a saved source moves to More. Connection management moves to a page menu and opens the Apps catalog. People must open those menus to find the actions. The import API and access checks do not change. > I checked `ROADMAP.md`. This source-list correction does not duplicate planned core work. ## Model Used OpenAI Codex, GPT-6 family. The task runtime did not expose an exact serving model ID or context-window size. The model used tool calls, code edits, and browser verification. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- maps/feature-map/skills.md | 8 ++-- .../project-repository-skills.spec.ts | 37 +++++++++++++- .../ProjectSkillRepositoryRow.tsx | 10 ++-- ui/src/pages/SkillSources.test.tsx | 12 +++-- ui/src/pages/SkillSources.tsx | 48 ++++++++++++++----- .../project-repository-skills/README.md | 2 + .../components.stories.tsx | 5 +- .../journeys.stories.tsx | 3 +- .../support/projectRepositorySkills.ts | 13 +++-- 9 files changed, 106 insertions(+), 32 deletions(-) diff --git a/maps/feature-map/skills.md b/maps/feature-map/skills.md index 902649f3781..32d27445937 100644 --- a/maps/feature-map/skills.md +++ b/maps/feature-map/skills.md @@ -69,9 +69,11 @@ Manual: Enable the skill for one test agent and leave another without access. Ru ### Project repository source behavior New GitHub project repositories import skills automatically. Skills → Sources lists -accessible legacy repositories with Import skills, shows source/project provenance, -groups matching repository/ref workspaces into one row, and shows queued/importing feedback, -and provides Refresh and Automatic updates in its source menu. Verify future additions, +accessible legacy repositories with Import skills, shows each saved repository once with +its access scope but without project names, and groups matching repository/ref workspaces. +Additional legacy imports for an existing source live in its More menu. Import progress +appears in the source metadata; Refresh and Automatic updates are also available. +Verify future additions, retained exclusions, error recovery and reload persistence. Private-only sources must be absent for unrelated people/agents; access through either originating project is sufficient. diff --git a/tests/storybook-visual/project-repository-skills.spec.ts b/tests/storybook-visual/project-repository-skills.spec.ts index 01b4a56270c..8377efc9120 100644 --- a/tests/storybook-visual/project-repository-skills.spec.ts +++ b/tests/storybook-visual/project-repository-skills.spec.ts @@ -12,6 +12,28 @@ test('groups matching project repositories and keeps progress visible through ad await expect(page.getByRole('button', { name: 'Import skills', exact: true })).toHaveCount(0); }); +test('keeps an existing source compact while allowing project import from its menu', async ({ page }, info) => { + await page.goto(story('existing-source-more-projects')); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + await expect(source.getByRole('link', { name: 'acme/team-skills', exact: true })).toHaveCount(1); + await expect(source).not.toContainText('Paperclip Cloud'); + await expect(source).not.toContainText('Sentry'); + await expect(source).not.toContainText('Other projects with this repo'); + await expect(page.getByRole('link', { name: 'Installed skills', exact: true })).toHaveCount(0); + await expect(source.getByRole('button', { name: 'Import skills', exact: true })).toHaveCount(0); + await page.screenshot({ path: info.outputPath('existing-source-more-projects.png') }); + await source.getByRole('button', { name: 'More actions for acme/team-skills', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Import project skills', exact: true }).click(); + await expect(source.getByRole('status')).toContainText('Importing skills'); + await expect(source.getByRole('status')).toHaveCount(0); + await source.getByRole('button', { name: 'More actions for acme/team-skills', exact: true }).click(); + await expect(page.getByRole('menuitem', { name: /^(Import (remaining )?project skills|Retry project import)$/ })).toHaveCount(0); + await page.keyboard.press('Escape'); + await expect(source).toBeVisible(); + await expect(source).not.toContainText('Paperclip Cloud'); + await expect(source).not.toContainText('Sentry'); +}); + test('repairs a missing branch without prompting for another connection', async ({ page }) => { await page.goto(story('missing-branch')); await expect(page.getByRole('button', { name: 'Choose connection', exact: true })).toHaveCount(0); @@ -102,7 +124,7 @@ test('legacy adoption imports directly and survives navigation within the journe await expect(source).toBeVisible(); await expect(source.getByText('Code review', { exact: true })).toBeVisible(); await expect(action).toHaveCount(0); - await page.getByRole('link', { name: 'Installed skills', exact: true }).click(); + await page.getByLabel('Skills', { exact: true }).getByRole('link', { name: 'Installed', exact: true }).click(); await expect(source).toHaveCount(0); await expect(page.getByText('Code review', { exact: true }).first()).toBeVisible(); await page.getByLabel('Skills', { exact: true }).getByRole('link', { name: 'Sources', exact: true }).click(); @@ -118,11 +140,24 @@ test('background refresh imports an addition without clicking refresh', async ({ await page.screenshot({ path: info.outputPath('automatic-update.png') }); }); +test('keeps GitHub connection management in the page actions menu', async ({ page }) => { + await page.goto(story('synced')); + const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); + await expect(source.getByRole('link', { name: 'Manage GitHub connection', exact: true })).toHaveCount(0); + await page.getByRole('button', { name: 'More source actions', exact: true }).click(); + const manageConnection = page.getByRole('menuitem', { name: 'Manage GitHub connection', exact: true }); + await expect(manageConnection).toBeVisible(); + await expect(manageConnection).toHaveAttribute('href', /\/apps$/); + await page.keyboard.press('Escape'); + await expect(page.getByRole('button', { name: 'More source actions', exact: true })).toBeFocused(); +}); + test('refresh failure retains content and retry recovers', async ({ page }, info) => { await page.goto(story('failed-refresh')); const source = page.getByRole('region', { name: 'acme/team-skills', exact: true }); await expect(source.getByText('Code review', { exact: true })).toBeVisible(); await expect(source.getByRole('alert')).toContainText('installed skills were kept'); + await page.screenshot({ path: info.outputPath('refresh-error.png') }); await source.getByRole('button', { name: 'Refresh', exact: true }).click(); await expect(source.getByRole('button', { name: 'Refresh', exact: true })).toBeEnabled(); await source.getByRole('button', { name: 'Refresh', exact: true }).click(); diff --git a/ui/src/components/skill-studio/ProjectSkillRepositoryRow.tsx b/ui/src/components/skill-studio/ProjectSkillRepositoryRow.tsx index e7dca5a04e7..f842acbda57 100644 --- a/ui/src/components/skill-studio/ProjectSkillRepositoryRow.tsx +++ b/ui/src/components/skill-studio/ProjectSkillRepositoryRow.tsx @@ -1,15 +1,13 @@ import type { ProjectSkillRepository } from '@paperclipai/shared'; import { GithubIcon } from '@/components/icons/github-icon'; import { Button } from '@/components/ui/button'; -import { Link } from '@/lib/router'; import { LoaderCircle } from 'lucide-react'; -export function ProjectSkillRepositoryRow({ repository, pending, backgroundPending, disabled, showRepository = true, onImport }: { +export function ProjectSkillRepositoryRow({ repository, pending, backgroundPending, disabled, onImport }: { repository: ProjectSkillRepository; pending?: boolean; backgroundPending?: boolean; disabled?: boolean; - showRepository?: boolean; onImport: () => void; }) { const actionPending = pending || repository.status === 'pending'; @@ -18,9 +16,9 @@ export function ProjectSkillRepositoryRow({ repository, pending, backgroundPendi
- {showRepository && + {repository.repositoryUrl.replace('https://github.com/', '')} - } +

{repository.projectName} · {repository.trackingRef === 'HEAD' ? 'Default branch' : repository.trackingRef}{repository.projectVisibility === 'private' ? ' · Project access' : ''}

{importing &&

{pending ? 'Starting import…' : repository.importState === 'importing' ? 'Checking repository for skills… This can take a few minutes for large repositories.' : 'Import queued. Skills will appear here when ready.'}

} - {repository.lastError &&

{repository.lastError} Manage GitHub connection

} + {repository.lastError &&

{repository.lastError}

} ; } diff --git a/ui/src/pages/SkillSources.test.tsx b/ui/src/pages/SkillSources.test.tsx index ffc16d6f7b3..7cd9b8a88d0 100644 --- a/ui/src/pages/SkillSources.test.tsx +++ b/ui/src/pages/SkillSources.test.tsx @@ -119,16 +119,20 @@ describe('GitHub skill source import', () => { expect(document.querySelector('button[aria-label="Branch"]')?.textContent).toContain('master'); }); - it('keeps pending project links inside the installed source instead of duplicating its repository', async () => { + it('keeps an existing source compact while project import is pending', async () => { context.sourceId = ''; - vi.mocked(skillSourcesApi.list).mockResolvedValue([{ ...sourceFixture(), trackingRef: 'HEAD' }]); + vi.mocked(skillSourcesApi.list).mockResolvedValue([{ ...sourceFixture(), trackingRef: 'HEAD', projects: [{ id: 'paperclip', name: 'Paperclip', visibility: 'open', active: true }] }]); vi.mocked(skillSourcesApi.projectRepositories).mockResolvedValue([{ projectId: 'sentry', projectName: 'Sentry', projectVisibility: 'open', workspaceId: 'second-workspace', repositoryUrl: sourceFixture().repositoryUrl, trackingRef: 'HEAD', bindingId: 'binding', sourceId: null, status: 'pending', lastError: null }]); await mount(); expect(document.querySelectorAll(`a[href="${sourceFixture().repositoryUrl}"]`)).toHaveLength(1); expect(document.querySelector('[aria-label="Project repositories"]')).toBeNull(); - expect(button('Importing…').disabled).toBe(true); - expect(document.querySelector('[role="status"]')?.textContent).toContain('queued'); + expect(document.querySelector('[aria-label="Skills from Sentry"]')).toBeNull(); + expect(document.body.textContent).not.toContain('Other projects with this repo'); + expect(document.body.textContent).not.toContain('Sentry'); + expect(document.body.textContent).not.toContain('Paperclip'); + expect(document.querySelector('a[href="/skills"]')).toBeNull(); + expect(document.querySelector('[role="status"]')?.textContent).toContain('Importing skills'); }); it('allows remaining project imports while another matching workspace is pending', async () => { diff --git a/ui/src/pages/SkillSources.tsx b/ui/src/pages/SkillSources.tsx index ec21881480c..4e7d115f856 100644 --- a/ui/src/pages/SkillSources.tsx +++ b/ui/src/pages/SkillSources.tsx @@ -3,9 +3,9 @@ import { SkillSourceConnectionRepair } from "@/components/skill-studio/SkillSour import { SkillSourceBranchRepair } from "@/components/skill-studio/SkillSourceBranchRepair"; import { SkillSourceSyncControls } from "@/components/skill-studio/SkillSourceSyncControls"; import { GithubIcon } from "@/components/icons/github-icon"; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, type ReactNode } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { RefreshCw, Plus, ExternalLink, Check, Lock, GitBranch, FileText, MoreVertical } from 'lucide-react'; +import { RefreshCw, Plus, ExternalLink, Check, Lock, GitBranch, FileText, MoreVertical, CircleAlert } from 'lucide-react'; import { parseGitHubSkillRepositoryUrl, type SkillSourceDisconnectRequest, type SkillSource, type SkillSourceDiscovery, type SkillSourceRefreshResult, type SkillSourceScanProgress } from '@paperclipai/shared'; import { Link, useNavigate, useParams } from '@/lib/router'; import { useCompany } from '@/context/CompanyContext'; @@ -85,9 +85,18 @@ export function SkillSources() { return

Skill sources

Skills from your repositories stay up to date automatically.

- +
+ + + + + + + Manage GitHub connection + + +
- Installed skills {query.isPending &&

Loading sources…

} {query.error &&

{query.error.message}

} {query.data?.length === 0 && availableProjects.length === 0 &&

No repositories added yet. Import your skills to make them available in {selectedCompany?.name ?? 'this company'}.

} @@ -103,6 +112,7 @@ export function SkillSources() { const newCount = source.entries.filter(entry => entry.selection === 'new' && entry.present).length; const result = results[source.id]; const projectGroup = availableProjects.find(group => groupSource(group)?.id === source.id); + const importingProjectSkills = projectGroup && (projectGroup.hasPending || (adopt.isPending && adopt.variables?.key === projectGroup.key)); const localError = result && result.message !== source.lastError && (result.operation === 'settings' || !source.lastSuccessAt || new Date(source.lastSuccessAt).getTime() < result.failedAt) ? result.message : null; @@ -110,8 +120,7 @@ export function SkillSources() {
{source.fullName} -

{source.trackingRef === 'HEAD' ? 'Default branch' : source.trackingRef}{source.lastSuccessAt ? ` · Updated ${timeAgo(source.lastSuccessAt)}` : ''}{source.accessScope === 'projects' ? ' · Project access' : ''}

-
{source.projects?.map(project => {project.name})}
+

{source.trackingRef === 'HEAD' ? 'Default branch' : source.trackingRef}{source.lastSuccessAt ? ` · Updated ${timeAgo(source.lastSuccessAt)}` : ''}{source.accessScope === 'projects' ? ' · Project access' : ''}{importingProjectSkills && · Importing skills…}

{source.enabled && source.automaticUpdates === false &&

Automatic updates paused

} {source.trackingActive === false &&

Repository tracking stopped · installed skills kept

} {!source.enabled &&

Disconnected

} @@ -130,14 +139,16 @@ export function SkillSources() { settings.mutate({ source, enabled })}>Automatic updates navigate(`/skills/sources/${source.id}`)}>Select skills + {projectGroup && projectGroup.importableWorkspaces.length > 0 && adopt.mutate(projectGroup)}> + {projectGroup.repository.status === 'error' ? 'Retry project import' : projectGroup.hasPending ? 'Import remaining project skills' : 'Import project skills'} + } setRepairSourceId(source.id)}>Choose connection {source.accessScope !== 'projects' && !source.projects?.some(project => project.active) && setBranchSourceId(source.id)}>Choose a new branch} { disconnect.reset(); reloadRemoval.reset(); setDisconnectSource(source); }}>{source.enabled ? 'Disconnect source' : 'Remove source'}
- {projectGroup && adopt.mutate(projectGroup)} />} + {projectGroup?.repository.lastError && } {installed.length > 0 &&
    {installed.map(entry =>
  • @@ -151,15 +162,15 @@ export function SkillSources() { {installed.length === 0 && source.entries.length === 0 && source.syncState !== 'refreshing' && !source.lastError &&

    No skills found yet.{source.enabled && source.automaticUpdates && source.importNewSkills ? ' New skills will be imported automatically.' : ''}

    } - {localError &&

    {localError}

    } + {localError && } entry.selection === 'selected'), source.repositoryPackage)} /> - {source.lastError && source.lastAttemptAt &&

    Last attempt {timeAgo(source.lastAttemptAt)}

    } - {source.lastError &&

    {source.lastError} Review source

    } + {source.lastError && Review source} />} {source.lastError && (source.repairAction === 'connection' || (!source.repairAction && !source.connectionId)) && } {source.lastError && source.repairAction === 'branch' && (source.accessScope === 'projects' || source.projects?.some(project => project.active) ?
    {source.projects?.filter(project => project.active).map(project => Update {project.name} repository)}
    : )} - {source.connectionId && Manage GitHub connection} ; })}
@@ -180,6 +191,19 @@ export function SkillSources() {
; } +function SourceErrorNotice({ message, detail, action }: { message: string; detail?: string; action?: ReactNode }) { + return
+
; +} + function DisconnectSourceDialog({ source, pending, error, stale, onReload, onClose, onConfirm, onChoiceChange }: { source: SkillSource; pending: boolean; error?: string; stale: boolean; onReload: () => void; onClose: () => void; onConfirm: (skills: SkillSourceDisconnectRequest['skills']) => void; onChoiceChange: () => void; diff --git a/ui/storybook/stories/skills/project-repository-skills/README.md b/ui/storybook/stories/skills/project-repository-skills/README.md index e75ccdd71cf..ae1d2a1757a 100644 --- a/ui/storybook/stories/skills/project-repository-skills/README.md +++ b/ui/storybook/stories/skills/project-repository-skills/README.md @@ -9,6 +9,8 @@ Open **Agents and reusable capabilities → Skills and Skill Studio → Project navigate to Skills → Sources. Legacy adoption starts before import. Background update introduces skills without a click. Retry the failed refresh, pause updates, inspect the private/shared source and try the mobile and light-theme variants. + **Existing source, more projects** reproduces a saved source with additional + project imports available in its More menu. The source row shows no project names. Connection repair chooses an existing authorized account without changing skill selections. Also inspect removed skills, invalid upstream content, two private projects and the large-list story. diff --git a/ui/storybook/stories/skills/project-repository-skills/components.stories.tsx b/ui/storybook/stories/skills/project-repository-skills/components.stories.tsx index a523f59504c..982f6519898 100644 --- a/ui/storybook/stories/skills/project-repository-skills/components.stories.tsx +++ b/ui/storybook/stories/skills/project-repository-skills/components.stories.tsx @@ -7,12 +7,13 @@ import { projectRepository } from '../../../support/projectRepositorySkills'; function Components({ state = 'available' }: { state?: 'available' | 'pending' | 'importing' | 'partial' | 'error' | 'private' | 'paused' | 'refreshing' }) { const [imported, setImported] = useState(false), [automatic, setAutomatic] = useState(state !== 'paused'); return
- setImported(true)} /> setImported(true)} />
; } -const meta = { id: 'skills-project-repository-skills-01-components', title: 'Agents and reusable capabilities/Skills and Skill Studio/Project repository skills/01 Components', component: Components, parameters: { paperclip: {"capabilities": ["skills/discovery", "skills/versions"], "status": "current", "implementation": "production", "boundary": "fixtures", "notes": "Production Sources controls with fixture data cover empty, updating, paused, removed, invalid and private-project states. Provider access, persistence and runtime delivery require live acceptance."}, layout: 'fullscreen' } } satisfies Meta; +const meta = { id: 'skills-project-repository-skills-01-components', title: 'Agents and reusable capabilities/Skills and Skill Studio/Project repository skills/01 Components', component: Components, parameters: { paperclip: {"capabilities": ["skills/discovery", "skills/versions"], "status": "current", "implementation": "production", "boundary": "fixtures", "notes": "Production repository row and source controls cover legacy availability, import progress, errors, private access, paused updates and refresh. Existing-source adoption is covered by the journey story. Provider access, persistence and runtime delivery require live acceptance."}, layout: 'fullscreen' } } satisfies Meta; export default meta; type Story = StoryObj; export const Available: Story = {}; diff --git a/ui/storybook/stories/skills/project-repository-skills/journeys.stories.tsx b/ui/storybook/stories/skills/project-repository-skills/journeys.stories.tsx index 22262b2cabc..31ef2b698c2 100644 --- a/ui/storybook/stories/skills/project-repository-skills/journeys.stories.tsx +++ b/ui/storybook/stories/skills/project-repository-skills/journeys.stories.tsx @@ -36,13 +36,14 @@ function Journey({ scenario = 'ready' }: { scenario?: Scenario }) { ; } const meta = { id: 'skills-project-repository-skills-02-journeys', title: 'Agents and reusable capabilities/Skills and Skill Studio/Project repository skills/02 Journeys', component: Journey, tags: ['!autodocs'], - parameters: { paperclip: {"capabilities": ["skills/discovery", "skills/versions", "projects/repositories", "routines/run-now", "routines/history"], "status": "current", "implementation": "production", "boundary": "fixtures", "notes": "Production app shell and pages exercise project creation/attachment, legacy adoption, refresh/repair and routine revision journeys with in-memory GitHub and execution fixtures. Live authorization and GitHub-to-run delivery are verified separately."}, layout: 'fullscreen', docs: { description: { component: 'Production pages and app shell. Imports, GitHub responses, background refresh and routine executions are in-memory fixtures. Start with Create project or Legacy adoption; all actions remain interactive. Live integration is verified separately.' } } }, + parameters: { paperclip: {"capabilities": ["skills/discovery", "skills/versions", "projects/repositories", "routines/run-now", "routines/history"], "status": "current", "implementation": "production", "boundary": "fixtures", "notes": "Production app shell and pages exercise project creation/attachment, legacy and existing-source adoption, refresh/repair and routine revision journeys with in-memory GitHub and execution fixtures. Live authorization and GitHub-to-run delivery are verified separately."}, layout: 'fullscreen', docs: { description: { component: 'Production pages and app shell. Imports, GitHub responses, background refresh and routine executions are in-memory fixtures. Start with Create project or Legacy adoption; all actions remain interactive. Live integration is verified separately.' } } }, } satisfies Meta; export default meta; type Story = StoryObj; export const CreateProject: Story = { name: '01 · Create a project', args: { scenario: 'new-project' } }; export const LegacyAdoption: Story = { name: '02 · Import from an older project', args: { scenario: 'legacy' } }; export const DuplicateProjects: Story = { name: 'Shared repository import across projects', args: { scenario: 'duplicate' } }; +export const ExistingSourceMoreProjects: Story = { name: 'Existing source, more projects', args: { scenario: 'partial-adoption' } }; export const MissingBranch: Story = { name: 'Repair an unavailable branch', args: { scenario: 'missing-branch' } }; export const Synced: Story = { name: '03 · Automatically imported skills' }; export const AutomaticUpdate: Story = { name: '04 · Background update and a new skill', args: { scenario: 'automatic' } }; diff --git a/ui/storybook/support/projectRepositorySkills.ts b/ui/storybook/support/projectRepositorySkills.ts index 2191c7c825e..e5edd46cf3c 100644 --- a/ui/storybook/support/projectRepositorySkills.ts +++ b/ui/storybook/support/projectRepositorySkills.ts @@ -15,7 +15,7 @@ import { baseRoutine } from '../fixtures/routineWebhooks'; import { storybookProjects, storybookIssues } from '../fixtures/paperclipData'; import { COMPANY_ID, SOURCE_ID, COMMIT, installFixtures, candidates, sourceFixture } from '../fixtures/githubSkillSources'; -export type Scenario = 'new-project' | 'attach-repository' | 'legacy' | 'duplicate' | 'missing-branch' | 'ready' | 'refreshing' | 'error' | 'empty' | 'private' | 'shared' | 'shared-private' | 'long' | 'many' | 'paused' | 'removed' | 'validation' | 'repair' | 'repair-conflict' | 'automatic' | 'latest'; +export type Scenario = 'new-project' | 'attach-repository' | 'legacy' | 'duplicate' | 'partial-adoption' | 'missing-branch' | 'ready' | 'refreshing' | 'error' | 'empty' | 'private' | 'shared' | 'shared-private' | 'long' | 'many' | 'paused' | 'removed' | 'validation' | 'repair' | 'repair-conflict' | 'automatic' | 'latest'; export const projectRepository: ProjectSkillRepository = { projectId: 'project-skills', projectName: 'Acme platform', projectVisibility: 'open', workspaceId: 'workspace-skills', repositoryUrl: 'https://github.com/acme/team-skills', trackingRef: 'main', @@ -38,9 +38,14 @@ export function installProjectSkillFixtures(scenario: Scenario, client: QueryCli let live = true, automaticUpdates = scenario !== 'paused', failed = ['error', 'repair', 'repair-conflict', 'missing-branch'].includes(scenario); let sourceRevision = sourceFixture().revision, repairConflict = scenario === 'repair-conflict'; const privateProject = scenario === 'private'; - let repository = { ...projectRepository, projectVisibility: privateProject ? 'private' as const : 'open' as const }; + let repository = { ...projectRepository, projectName: scenario === 'partial-adoption' ? 'Paperclip' : projectRepository.projectName, + projectVisibility: privateProject ? 'private' as const : 'open' as const }; if (!['new-project', 'legacy', 'duplicate'].includes(scenario)) repository = { ...repository, sourceId: SOURCE_ID, bindingId: 'binding-skills', status: 'ready' }; - let additionalRepositories: ProjectSkillRepository[] = scenario === 'duplicate' ? [{ ...repository, workspaceId: 'second-workspace', projectId: 'second-project', projectName: 'Sentry' }] : []; + let additionalRepositories: ProjectSkillRepository[] = scenario === 'duplicate' ? [{ ...repository, workspaceId: 'second-workspace', projectId: 'second-project', projectName: 'Sentry' }] + : scenario === 'partial-adoption' ? [ + { ...repository, workspaceId: 'cloud-workspace', projectId: 'paperclip-cloud', projectName: 'Paperclip Cloud', sourceId: null, bindingId: null, status: 'available' }, + { ...repository, workspaceId: 'sentry-workspace', projectId: 'sentry', projectName: 'Sentry', sourceId: null, bindingId: null, status: 'available' }, + ] : []; let project = { ...storybookProjects[0]!, id: repository.projectId, urlKey: repository.projectId, name: repository.projectName, companyId: COMPANY_ID, workspaces: storybookProjects[0]!.workspaces.slice(0, 1).map(workspace => ({ ...workspace, id: repository.workspaceId, projectId: repository.projectId, repoUrl: repository.repositoryUrl, repoRef: 'main', defaultRef: 'main', name: 'acme/team-skills', metadata: { ...workspace.metadata, githubRepositoryId: '123456' } })) }; const workspaceTemplate = project.workspaces[0]!; @@ -89,6 +94,8 @@ export function installProjectSkillFixtures(scenario: Scenario, client: QueryCli repairAction: !failed ? null : scenario === 'missing-branch' ? 'branch' : scenario.startsWith('repair') ? 'connection' : 'retry', projects: scenario === 'missing-branch' ? [] : ['shared', 'shared-private'].includes(scenario) ? [{ id: project.id, name: 'Private research', visibility: 'private', active: true }, { id: 'project-open', name: 'Engineering', visibility: scenario === 'shared' ? 'open' : 'private', active: true }] + : scenario === 'partial-adoption' ? [repository, ...additionalRepositories].filter(row => row.sourceId === source.id && row.status === 'ready') + .map(row => ({ id: row.projectId, name: row.projectName, visibility: row.projectVisibility, active: true })) : [{ id: project.id, name: project.name, visibility: repository.projectVisibility, active: true }], entries: scenario === 'empty' ? [] : scenario === 'many' ? Array.from({ length: 80 }, (_, index) => ({ ...source.entries[0]!, id: 'entry-' + index, name: 'Project workflow ' + (index + 1), skillId: 'skill-' + index,