diff --git a/ui/src/pages/CompanySkills.test.tsx b/ui/src/pages/CompanySkills.test.tsx index 180fd9d45a..ef381ae1bc 100644 --- a/ui/src/pages/CompanySkills.test.tsx +++ b/ui/src/pages/CompanySkills.test.tsx @@ -279,6 +279,59 @@ async function renderDiscoveryGrid(props: Partial>; +} + function buttonsNamed(node: ParentNode, name: string) { return Array.from(node.querySelectorAll("button")).filter((button) => button.textContent?.trim() === name); } @@ -341,6 +394,55 @@ describe("DiscoveryGrid Studio entry points", () => { expect(onCreate).toHaveBeenCalledTimes(2); }); + it("keeps folder creation in the compact rail control", async () => { + const props = projectFolderGridProps(); + const node = await renderDiscoveryGrid(props); + const compactCreateButton = node.querySelector('button[title="New folder"]'); + + expect(buttonsNamed(node, "New folder")).toHaveLength(0); + expect(compactCreateButton).not.toBeNull(); + + await click(compactCreateButton!); + + expect(props.onCreateFolderIn).toHaveBeenCalledWith(null); + expect(props.onCreateFolder).not.toHaveBeenCalled(); + }); + + it("keeps folder creation available when no folder rail exists", async () => { + const onCreateFolder = vi.fn(); + const node = await renderDiscoveryGrid({ + ...projectFolderGridProps(), + folderResult: { ...projectFolderResult, folders: [] }, + onCreateFolder, + }); + const createButton = buttonsNamed(node, "New folder")[0] as HTMLButtonElement; + + expect(createButton).toBeDefined(); + + await click(createButton); + + expect(onCreateFolder).toHaveBeenCalledOnce(); + }); + + it("refreshes only the project represented by the active project folder", async () => { + const onScan = vi.fn(); + const node = await renderDiscoveryGrid({ + ...projectFolderGridProps(), + folderSelection: "project-folder", + onScan, + }); + const refreshButton = node.querySelector( + 'button[aria-label="Refresh Acme project skills"]', + ); + + expect(refreshButton).not.toBeNull(); + + await click(refreshButton!); + + expect(onScan).toHaveBeenCalledOnce(); + expect(onScan).toHaveBeenCalledWith("project-1"); + }); + it("does not open a skill when keyboard-activating its actions button", async () => { const onOpenCard = vi.fn(); const card = { diff --git a/ui/src/pages/CompanySkills.tsx b/ui/src/pages/CompanySkills.tsx index 2bb0859f07..42a9325d46 100644 --- a/ui/src/pages/CompanySkills.tsx +++ b/ui/src/pages/CompanySkills.tsx @@ -1084,7 +1084,7 @@ export function DiscoveryGrid({ onImport: () => void; onImportFromProject: () => void; onBrowseCatalog: () => void; - onScan: () => void; + onScan: (projectId?: string) => void; scanPending: boolean; scanStatus: string | null; folderResult?: FolderListResult | null; @@ -1138,7 +1138,15 @@ export function DiscoveryGrid({ ); // The nested folder tree owns the left rail whenever folders (reserved roots // or user folders) exist for the installed view. - const showFolderRail = Boolean(folderResult && folderResult.folders.length > 0 && onFolderSelect && folderActionsReady); + const showFolderRail = Boolean( + folderResult && folderResult.folders.length > 0 && onFolderSelect && folderActionsReady, + ); + const activeProjectFolder = useMemo(() => { + if (!folderResult || folderSelection === "all" || folderSelection === "unfiled") return null; + const folder = folderResult.folders.find((candidate) => candidate.id === folderSelection); + return folder?.systemKey?.startsWith("project:") ? folder : null; + }, [folderResult, folderSelection]); + const activeProjectId = activeProjectFolder?.systemKey?.slice("project:".length) || null; return ( // On desktop the store is bounded to the viewport so the category sidebar @@ -1240,8 +1248,9 @@ export function DiscoveryGrid({ + ) : null} ) : null} {folderNudgeStorageKey && onCreateFolder && folderResult && folderResult.folders.length === 0 && !loading && cards.length > 0 ? ( @@ -4278,13 +4300,21 @@ export function CompanySkills() { }); const scanProjects = useMutation({ - mutationFn: () => companySkillsApi.scanProjects(selectedCompanyId!), - onMutate: () => { - setScanStatusMessage("Scanning project workspaces for skills..."); + mutationFn: (projectId?: string) => companySkillsApi.scanProjects( + selectedCompanyId!, + projectId ? { projectIds: [projectId] } : {}, + ), + onMutate: (projectId) => { + setScanStatusMessage( + projectId ? "Refreshing project skills..." : "Scanning project workspaces for skills...", + ); }, onSuccess: async (result) => { setScanStatusMessage("Refreshing skills list..."); - await queryClient.invalidateQueries({ queryKey: queryKeys.companySkills.list(selectedCompanyId!) }); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: queryKeys.companySkills.list(selectedCompanyId!) }), + queryClient.invalidateQueries({ queryKey: queryKeys.folders.list(selectedCompanyId!, "skill") }), + ]); const summary = formatProjectScanSummary(result); setScanStatusMessage(summary); pushToast({ @@ -5308,7 +5338,7 @@ export function CompanySkills() { onImport={() => setImportDialogOpen(true)} onImportFromProject={() => setImportFromProjectOpen(true)} onBrowseCatalog={() => setDiscoveryTab("catalog")} - onScan={() => scanProjects.mutate()} + onScan={(projectId) => scanProjects.mutate(projectId)} scanPending={scanProjects.isPending} scanStatus={scanStatusMessage} folderResult={showInstalledFolders ? railSkillFolderResult : null}