From 4da00ddce1c87a97f4f4cb03b7fee8feefcc26e1 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Mon, 28 Sep 2026 19:07:03 +0200 Subject: [PATCH] fix(web): start a resume cover letter from a library letter again The entry dialogs removed with the Write panel carried "Import from library", which copied a saved letter's recipient and text into a new cover-letter entry. A new, empty cover-letter entry now offers the same picker inline. The copy stays independent of the library letter. The cover-letter-library spec drives the inline picker and waits for the renamed JSON copy before checking the original, which removes a race with the list refetch. --- .../features/resume/editor/write/entries.tsx | 35 +++++++++++++++++++ tests/e2e/specs/cover-letter-library.spec.ts | 23 ++++++------ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/web/src/features/resume/editor/write/entries.tsx b/apps/web/src/features/resume/editor/write/entries.tsx index b8fb8ca1e..aaf1361f7 100644 --- a/apps/web/src/features/resume/editor/write/entries.tsx +++ b/apps/web/src/features/resume/editor/write/entries.tsx @@ -5,6 +5,8 @@ import type { EntryWriter } from "./fields"; import type { Entry } from "./model"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; +import { useQuery } from "@tanstack/react-query"; +import { useId } from "react"; import { EMPTY_RESUME_DATES } from "@reactive-resume/schema/resume/dates"; import { Button } from "@reactive-resume/ui/components/button"; import { FormControl, FormDescription, FormItem, FormLabel } from "@reactive-resume/ui/components/form"; @@ -15,6 +17,8 @@ import { generateId } from "@reactive-resume/utils/string"; import { ChipInput } from "@/components/input/chip-input"; import { ColorPicker } from "@/components/input/color-picker"; import { IconPicker } from "@/components/input/icon-picker"; +import { Combobox } from "@/components/ui/combobox"; +import { orpc } from "@/libs/orpc/client"; import { DatesField } from "./dates-field"; import { MoreOptions, TextField, WebsiteField } from "./fields"; import { RichTextEditor } from "./rich-text-editor"; @@ -112,6 +116,36 @@ function Description({ ); } +/** A new, empty letter can start as a copy of one saved in the library; the copy doesn't follow later edits. */ +function ImportFromLibrary({ entry, write }: FieldSetProps) { + const id = useId(); + const letters = useQuery(orpc.coverLetters.list.queryOptions({ input: { limit: 100 } })); + const values = valuesOf(entry); + if (str(values.recipient) || str(values.content) || !letters.data?.items.length) return null; + + return ( +
+ + ({ value: letter.id, label: letter.name }))} + onValueChange={(letterId) => { + const letter = letters.data?.items.find((item) => item.id === letterId); + if (!letter) return; + write("import", (target) => { + target.recipient = letter.recipient; + target.content = letter.content; + }); + }} + /> +
+ ); +} + function Keywords({ entry, write, label }: FieldSetProps & { label: ReactNode }) { return ( @@ -444,6 +478,7 @@ export function EntryFields({ case "cover-letter": return ( <> + diff --git a/tests/e2e/specs/cover-letter-library.spec.ts b/tests/e2e/specs/cover-letter-library.spec.ts index 498882b92..4e964252d 100644 --- a/tests/e2e/specs/cover-letter-library.spec.ts +++ b/tests/e2e/specs/cover-letter-library.spec.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import { readFile } from "node:fs/promises"; import { Pool } from "pg"; -import { createSampleResumeFromDashboard } from "../fixtures/resume"; +import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume"; import { expect, test } from "../fixtures/test"; test("imports a library letter into the builder as an independent copy", async ({ authPage: page }, testInfo) => { @@ -45,21 +45,19 @@ test("imports a library letter into the builder as an independent copy", async ( await editor.getByRole("button", { name: "Close", exact: true }).click(); await page.goto(builderUrl); - await page.getByRole("button", { name: "Cover Letter", exact: true }).click(); - await page.getByRole("button", { name: "Add a new item", exact: true }).last().click(); - const createItem = page.getByRole("dialog", { name: "Create a new cover letter", exact: true }); - await createItem.getByLabel("Import from library", { exact: true }).click(); + await openSidebarSection(page, "Cover Letter"); + await page.getByRole("button", { name: "Add cover letter", exact: true }).click(); + await page.getByLabel("Import from library", { exact: true }).click(); await page.getByRole("option", { name: "Platform engineer letter", exact: true }).click(); - const createEditors = createItem.locator('[data-editor="true"]'); - await expect(createEditors.nth(0)).toContainText("Dear hiring team"); - await expect(createEditors.nth(1)).toContainText("reliable platforms"); - await createEditors.nth(1).fill("Updated independent resume copy."); + const letter = page.getByRole("textbox", { name: "Letter", exact: true }); + await expect(page.getByRole("textbox", { name: "Recipient", exact: true })).toContainText("Dear hiring team"); + await expect(letter).toContainText("reliable platforms"); const resumeSaved = page.waitForResponse( (response) => new URL(response.url()).pathname === "/api/rpc/resume/update" && response.ok(), ); - await createItem.getByRole("button", { name: "Create", exact: true }).click(); + await letter.fill("Updated independent resume copy."); await resumeSaved; - await expect(page.getByRole("button", { name: /^Dear hiring team, Updated independent resume copy/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /^Updated independent resume copy\./ })).toBeVisible(); await page.goto("/dashboard/cover-letters"); await page.getByRole("button", { name: "Edit Platform engineer letter", exact: true }).click(); @@ -72,8 +70,9 @@ test("imports a library letter into the builder as an independent copy", async ( await editor.getByRole("button", { name: "Save Changes", exact: true }).click(); await expect(editor.getByRole("button", { name: "Save Changes", exact: true })).toBeDisabled(); await editor.getByRole("button", { name: "Close", exact: true }).click(); - await expect(page.getByRole("button", { name: "Edit Platform engineer letter", exact: true })).toBeVisible(); + // The renamed copy shows once the list has refetched; until then both rows carry the original name. await expect(page.getByRole("button", { name: "Edit Imported independent copy", exact: true })).toBeVisible(); + await expect(page.getByRole("button", { name: "Edit Platform engineer letter", exact: true })).toBeVisible(); }); test("keeps the application PDF snapshot after the library letter is deleted", async ({ authPage: page, account }) => {