From 9e847deb6b27c8774d786b45bdb95e98aac0f0ea Mon Sep 17 00:00:00 2001 From: mbeaulne Date: Wed, 27 May 2026 16:21:07 -0400 Subject: [PATCH] Add component search support utilities --- .../libraries/setup.test.ts | 36 ++++++++++++++++++ .../libraries/setup.ts | 28 ++++++++++++++ .../copyComponentReferenceToClipboard.test.ts | 37 +++++++++++++++++++ .../copyComponentReferenceToClipboard.ts | 34 +++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 src/providers/ComponentLibraryProvider/libraries/setup.test.ts create mode 100644 src/providers/ComponentLibraryProvider/libraries/setup.ts create mode 100644 src/routes/v2/shared/clipboard/copyComponentReferenceToClipboard.test.ts create mode 100644 src/routes/v2/shared/clipboard/copyComponentReferenceToClipboard.ts diff --git a/src/providers/ComponentLibraryProvider/libraries/setup.test.ts b/src/providers/ComponentLibraryProvider/libraries/setup.test.ts new file mode 100644 index 000000000..0bcde09a5 --- /dev/null +++ b/src/providers/ComponentLibraryProvider/libraries/setup.test.ts @@ -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"); + }); +}); diff --git a/src/providers/ComponentLibraryProvider/libraries/setup.ts b/src/providers/ComponentLibraryProvider/libraries/setup.ts new file mode 100644 index 000000000..b72c351f2 --- /dev/null +++ b/src/providers/ComponentLibraryProvider/libraries/setup.ts @@ -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; +} diff --git a/src/routes/v2/shared/clipboard/copyComponentReferenceToClipboard.test.ts b/src/routes/v2/shared/clipboard/copyComponentReferenceToClipboard.test.ts new file mode 100644 index 000000000..b73faf66d --- /dev/null +++ b/src/routes/v2/shared/clipboard/copyComponentReferenceToClipboard.test.ts @@ -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 }), + }), + ], + [], + ); + }); +}); diff --git a/src/routes/v2/shared/clipboard/copyComponentReferenceToClipboard.ts b/src/routes/v2/shared/clipboard/copyComponentReferenceToClipboard.ts new file mode 100644 index 000000000..d2210a765 --- /dev/null +++ b/src/routes/v2/shared/clipboard/copyComponentReferenceToClipboard.ts @@ -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 { + 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], []); +}