Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions ui/src/pages/CompanySkills.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,59 @@ async function renderDiscoveryGrid(props: Partial<ComponentProps<typeof Discover
return container;
}

const projectFolderResult: FolderListResult = {
kind: "skill",
allCount: 1,
unfiledCount: 0,
folders: [
{
id: "projects-root",
companyId: "company-1",
kind: "skill",
parentId: null,
name: "Projects",
slug: "projects",
systemKey: "projects",
path: "projects",
depth: 1,
color: null,
position: 0,
createdAt: new Date("2026-08-01T00:00:00Z"),
updatedAt: new Date("2026-08-01T00:00:00Z"),
itemCount: 1,
},
{
id: "project-folder",
companyId: "company-1",
kind: "skill",
parentId: "projects-root",
name: "Acme",
slug: "acme",
systemKey: "project:project-1",
path: "projects/acme",
depth: 2,
color: null,
position: 0,
createdAt: new Date("2026-08-01T00:00:00Z"),
updatedAt: new Date("2026-08-01T00:00:00Z"),
itemCount: 1,
},
],
};

function projectFolderGridProps() {
return {
folderResult: projectFolderResult,
onFolderSelect: vi.fn(),
onCreateFolder: vi.fn(),
onCreateFolderIn: vi.fn(),
onRenameFolder: vi.fn(),
onEditFolder: vi.fn(),
onMoveFolder: vi.fn(),
onDeleteFolder: vi.fn(),
} satisfies Partial<ComponentProps<typeof DiscoveryGrid>>;
}

function buttonsNamed(node: ParentNode, name: string) {
return Array.from(node.querySelectorAll("button")).filter((button) => button.textContent?.trim() === name);
}
Expand Down Expand Up @@ -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<HTMLButtonElement>('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<HTMLButtonElement>(
'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 = {
Expand Down
50 changes: 40 additions & 10 deletions ui/src/pages/CompanySkills.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1240,8 +1248,9 @@ export function DiscoveryGrid({
<Button
variant="ghost"
size="icon-sm"
onClick={onScan}
onClick={() => onScan()}
disabled={scanPending}
aria-label="Scan project workspaces for skills"
title="Scan project workspaces for skills"
>
<RefreshCw className={cn("h-4 w-4", scanPending && "animate-spin")} />
Expand Down Expand Up @@ -1289,7 +1298,7 @@ export function DiscoveryGrid({
/>
Comment thread
cryppadotta marked this conversation as resolved.
</div>
) : null}
{onCreateFolder ? (
{onCreateFolder && !showFolderRail ? (
<Button variant="outline" size="sm" onClick={onCreateFolder}>
<Plus className="mr-1 h-3.5 w-3.5" />
New folder
Expand Down Expand Up @@ -1357,8 +1366,21 @@ export function DiscoveryGrid({
<div className="min-h-0 flex-1 overflow-auto p-4">
{scanStatus ? <p className="mb-3 text-xs text-muted-foreground">{scanStatus}</p> : null}
{showFolderRail && onFolderSelect ? (
<div className="mb-4">
<div className="mb-4 flex items-center justify-between gap-2">
<FolderBreadcrumb result={folderResult} selection={folderSelection} onSelect={onFolderSelect} />
{activeProjectFolder && activeProjectId ? (
<Button
variant="ghost"
size="sm"
onClick={() => onScan(activeProjectId)}
disabled={scanPending}
aria-label={`Refresh ${activeProjectFolder.name} project skills`}
title={`Refresh skills from ${activeProjectFolder.name}`}
>
<RefreshCw className={cn("h-3.5 w-3.5", scanPending && "animate-spin")} />
Refresh
</Button>
) : null}
</div>
) : null}
{folderNudgeStorageKey && onCreateFolder && folderResult && folderResult.folders.length === 0 && !loading && cards.length > 0 ? (
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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}
Expand Down
Loading