From c0b194ed4aae2b0612b59815f2754418727bf7cd Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 9 Jul 2026 19:28:00 +0000 Subject: [PATCH] Stabilize Clip import and slug conflicts Co-Authored-By: Paperclip --- server/src/__tests__/clips-service.test.ts | 26 ++++++++++++++++++++++ server/src/services/clips.ts | 4 ++-- ui/src/pages/Clips.tsx | 13 +++++++---- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/server/src/__tests__/clips-service.test.ts b/server/src/__tests__/clips-service.test.ts index 4a759875478..a966dc31da8 100644 --- a/server/src/__tests__/clips-service.test.ts +++ b/server/src/__tests__/clips-service.test.ts @@ -138,6 +138,32 @@ describeEmbeddedPostgres("clipService", () => { expect(detail).not.toHaveProperty("sourceObjectId"); }); + it("returns a clean conflict when publishing a duplicate slug", async () => { + const companyId = await seedCompany(); + const { clip } = await publishFixture(companyId); + + await expect(svc.publish(companyId, { + creatorProfile: { + handle: "duplicate-creator-" + companyId.slice(0, 8), + displayName: "Duplicate Creator", + }, + slug: clip.slug, + type: "agent", + title: "Duplicate Slug Agent", + summary: "Attempts to reuse an existing slug.", + visibility: "public", + status: "published", + sourceKind: "paperclip_company_object", + sourceObjectType: "agent", + sourceObjectId: "duplicate-agent-id", + revision: { + manifestChecksum: "sha256:manifest-duplicate", + artifactChecksum: "sha256:artifact-duplicate", + manifestPayload: { schema: "paperclip.clip/v1", duplicate: true }, + }, + })).rejects.toThrow("Clip slug already exists"); + }); + it("rolls back initial publish when revision dependency insertion fails", async () => { const companyId = await seedCompany(); const slug = "rollback-test-" + companyId.slice(0, 8); diff --git a/server/src/services/clips.ts b/server/src/services/clips.ts index 6c82953d9ed..ce4700cb03d 100644 --- a/server/src/services/clips.ts +++ b/server/src/services/clips.ts @@ -351,8 +351,6 @@ export function clipService(db: Db) { } async function publish(companyId: string, input: PublishClip) { - const existing = await getClipBySlug(input.slug); - if (existing) throw conflict("Clip slug already exists"); const { clip: updated, revision, creatorProfile } = await db.transaction(async (tx) => { const creatorProfile = await resolveCreatorProfile(tx, companyId, input); const [clip] = await tx @@ -376,7 +374,9 @@ export function clipService(db: Db) { sourceObjectType: input.sourceObjectType ?? null, sourceObjectId: input.sourceObjectId ?? null, }) + .onConflictDoNothing({ target: clips.slug }) .returning(); + if (!clip) throw conflict("Clip slug already exists"); const revision = await insertRevision(tx, clip.id, input.revision, 1); const visibleRevisionId = clip.status === "published" ? revision.id : null; const [updated] = await tx diff --git a/ui/src/pages/Clips.tsx b/ui/src/pages/Clips.tsx index ff4d5903c44..3bbb908637c 100644 --- a/ui/src/pages/Clips.tsx +++ b/ui/src/pages/Clips.tsx @@ -15,6 +15,7 @@ import { Upload, } from "lucide-react"; import type { + ClipImportPreviewResult, ClipSharePreviewResult, ClipType, ClipVisibility, @@ -225,10 +226,10 @@ export function Clips() { }); const importMutation = useMutation({ - mutationFn: () => { + mutationFn: (preview: ClipImportPreviewResult) => { if (!selectedCompanyId) throw new Error("Select a company first."); return clipsApi.importClip(selectedCompanyId, { - url: importUrl.trim(), + url: preview.source.url, collisionStrategy: "rename", selectedOptions: { routineTriggers: "review_required" }, }); @@ -301,7 +302,11 @@ export function Clips() {
setImportUrl(event.target.value)} + onChange={(event) => { + setImportUrl(event.target.value); + importPreviewMutation.reset(); + importMutation.reset(); + }} placeholder="https://paperclip.ing/clips/support-triage or support-triage" />