Skip to content
Draft
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
36 changes: 36 additions & 0 deletions src/providers/ComponentLibraryProvider/libraries/setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it, vi } from "vitest";

import { createLibraryObject } from "./factory";
import { ensureLibraryFactoriesRegistered } from "./setup";
import type { StoredLibrary } from "./storage";

const { mockGitHubFlatComponentLibrary } = vi.hoisted(() => ({
mockGitHubFlatComponentLibrary: vi.fn(),
}));

vi.mock("@/components/shared/GitHubLibrary/githubFlatComponentLibrary", () => ({
GitHubFlatComponentLibrary: mockGitHubFlatComponentLibrary,
}));

describe("ensureLibraryFactoriesRegistered", () => {
it("registers the GitHub component library factory", () => {
ensureLibraryFactoriesRegistered();

const library = createLibraryObject({
id: "github-lib",
name: "GitHub library",
type: "github",
knownDigests: [],
configuration: {
created_at: "2026-01-01T00:00:00Z",
last_updated_at: "2026-01-01T00:00:00Z",
repo_name: "owner/repo",
access_token: "",
auto_update: false,
},
} satisfies StoredLibrary);

expect(library).toBeInstanceOf(mockGitHubFlatComponentLibrary);
expect(mockGitHubFlatComponentLibrary).toHaveBeenCalledWith("owner/repo");
});
});
28 changes: 28 additions & 0 deletions src/providers/ComponentLibraryProvider/libraries/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { GitHubFlatComponentLibrary } from "@/components/shared/GitHubLibrary/githubFlatComponentLibrary";
import { isGitHubLibraryConfiguration } from "@/components/shared/GitHubLibrary/types";

import { registerLibraryFactory } from "./factory";

/**
* Idempotent registration of library factories. The provider already registers
* the same factories at module load, but the dashboard search page reads
* libraries from Dexie directly (without mounting `ComponentLibraryProvider`,
* which is editor-scoped and depends on `ComponentSpecProvider`). Anywhere
* that needs to instantiate a stored library can call this first.
*/
let registered = false;

export function ensureLibraryFactoriesRegistered() {
if (registered) return;

registerLibraryFactory("github", (library) => {
if (!isGitHubLibraryConfiguration(library.configuration)) {
throw new Error(
`GitHub library configuration is not valid for "${library.id}"`,
);
}
return new GitHubFlatComponentLibrary(library.configuration.repo_name);
});

registered = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from "vitest";

import type { ComponentReference } from "@/utils/componentSpec";

import { writeToSystemClipboard } from "./clipboardEnvelope";
import { copyComponentReferenceToClipboard } from "./copyComponentReferenceToClipboard";

vi.mock("./clipboardEnvelope", () => ({
writeToSystemClipboard: vi.fn(),
}));

describe("copyComponentReferenceToClipboard", () => {
it("writes a single task snapshot for the component reference", async () => {
const reference: ComponentReference = {
digest: "abc",
spec: {
name: "train_model",
inputs: [],
outputs: [],
implementation: { container: { image: "python:3.11" } },
},
};

await copyComponentReferenceToClipboard(reference);

expect(writeToSystemClipboard).toHaveBeenCalledWith(
[
expect.objectContaining({
$type: "task",
name: "train_model",
data: expect.objectContaining({ componentRef: reference }),
}),
],
[],
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { NodeSnapshot } from "@/routes/v2/shared/nodes/types";
import type { ComponentReference } from "@/utils/componentSpec";

import { writeToSystemClipboard } from "./clipboardEnvelope";

/**
* Build a single-task clipboard envelope from a component reference and write
* it to the system clipboard. The user can then paste (Cmd+V) inside the V2
* pipeline editor to drop a new task wired to this component.
*
* The task snapshot is intentionally minimal: no arguments, no execution
* options, no annotations. The paste clone handler assigns a fresh `$id` and
* positions the node at the paste target, so the entityId/position here are
* placeholders that get discarded on paste.
*/
export async function copyComponentReferenceToClipboard(
reference: ComponentReference,
): Promise<void> {
const name = reference.spec?.name ?? reference.name ?? "task";
const snapshot: NodeSnapshot = {
$type: "task",
entityId: "",
name,
position: { x: 0, y: 0 },
data: {
componentRef: reference,
isEnabled: undefined,
arguments: [],
executionOptions: undefined,
annotations: [],
},
};
await writeToSystemClipboard([snapshot], []);
}
Loading