diff --git a/apps/web/src/components/LabelForm.tsx b/apps/web/src/components/LabelForm.tsx index d083aaa66..60a768eee 100644 --- a/apps/web/src/components/LabelForm.tsx +++ b/apps/web/src/components/LabelForm.tsx @@ -4,6 +4,7 @@ import { Fragment, useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { HiChevronUpDown, HiXMark } from "react-icons/hi2"; +import type { Colour } from "@kan/shared/constants"; import { colours } from "@kan/shared/constants"; import Button from "~/components/Button"; @@ -11,6 +12,7 @@ import Input from "~/components/Input"; import Toggle from "~/components/Toggle"; import { useModal } from "~/providers/modal"; import { api } from "~/utils/api"; +import { resolveLabelColour } from "~/utils/labelColours"; interface LabelFormInput { name: string; @@ -18,11 +20,6 @@ interface LabelFormInput { isCreateAnotherEnabled?: boolean; } -interface Colour { - name: string; - code: string; -} - export function LabelForm({ boardPublicId, refetch, @@ -47,9 +44,7 @@ export function LabelForm({ useForm({ values: { name: isEdit && label.data?.name ? label.data.name : "", - colour: (isEdit && label.data?.colourCode - ? colours.find((c) => c.code === label.data?.colourCode) - : colours[0]) as Colour, + colour: resolveLabelColour(isEdit ? label.data?.colourCode : undefined), isCreateAnotherEnabled: false, }, }); diff --git a/apps/web/src/utils/labelColours.test.ts b/apps/web/src/utils/labelColours.test.ts new file mode 100644 index 000000000..22ca1ff89 --- /dev/null +++ b/apps/web/src/utils/labelColours.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { colours } from "@kan/shared/constants"; + +import { resolveLabelColour } from "./labelColours"; + +describe("resolveLabelColour", () => { + it("returns the matching palette colour", () => { + expect(resolveLabelColour(colours[1]?.code)).toEqual(colours[1]); + }); + + it("preserves a custom colour", () => { + expect(resolveLabelColour("#4bce97")).toEqual({ + name: "#4bce97", + code: "#4bce97", + }); + }); + + it("uses the default colour when no colour is stored", () => { + expect(resolveLabelColour(null)).toEqual(colours[0]); + expect(resolveLabelColour(undefined)).toEqual(colours[0]); + }); +}); diff --git a/apps/web/src/utils/labelColours.ts b/apps/web/src/utils/labelColours.ts new file mode 100644 index 000000000..88f58646b --- /dev/null +++ b/apps/web/src/utils/labelColours.ts @@ -0,0 +1,18 @@ +import type { Colour } from "@kan/shared/constants"; +import { colours } from "@kan/shared/constants"; + +export const resolveLabelColour = ( + colourCode: string | null | undefined, +): Colour => { + const defaultColour = colours[0]; + + if (!defaultColour) throw new Error("Label colour palette is empty"); + if (!colourCode) return defaultColour; + + return ( + colours.find((colour) => colour.code === colourCode) ?? { + name: colourCode, + code: colourCode, + } + ); +}; diff --git a/packages/e2e/tests/self-hosted/card-labels.spec.ts b/packages/e2e/tests/self-hosted/card-labels.spec.ts index 138ed2643..65a610e9f 100644 --- a/packages/e2e/tests/self-hosted/card-labels.spec.ts +++ b/packages/e2e/tests/self-hosted/card-labels.spec.ts @@ -6,9 +6,10 @@ import { CardPage } from "../support/pages/card-page"; import { DashboardPage } from "../support/pages/dashboard-page"; import { SelfHostedOnboardingPage } from "../support/pages/self-hosted-onboarding-page"; import { createTestUser } from "../support/test-user"; +import { waitForTrpcMutation } from "../support/wait-for-trpc"; test( - "a label can be created and assigned to a card, and the assignment persists", + "a label can be created, assigned, and edited with a custom colour", { tag: "@self-hosted" }, async ({ page }) => { const user = createTestUser(); @@ -27,11 +28,35 @@ test( await board.createCard("Label test card"); await board.openCard("Label test card"); - await card.createAndAssignLabel("Urgent"); + const labelPublicId = await card.createAndAssignLabel("Urgent"); await expect(card.assignedLabelBadge("Urgent")).toBeVisible(); await page.reload(); await expect(card.assignedLabelBadge("Urgent")).toBeVisible(); + + const response = await page.request.post("/api/trpc/label.update?batch=1", { + data: { + "0": { + json: { + labelPublicId, + name: "Urgent", + colourCode: "#4bce97", + }, + }, + }, + }); + expect(response.ok()).toBe(true); + + await page.reload(); + await card.openLabelEditor("Urgent"); + + const dialog = page.getByRole("dialog"); + await expect(dialog.getByRole("button", { name: "#4bce97" })).toBeVisible(); + + const updated = waitForTrpcMutation(page, "label.update"); + await dialog.getByRole("button", { name: "Update label" }).click(); + const updatedResponse = await updated; + expect(updatedResponse.ok()).toBe(true); }, ); diff --git a/packages/e2e/tests/support/pages/card-page.ts b/packages/e2e/tests/support/pages/card-page.ts index 26c957f59..1f4dc83f2 100644 --- a/packages/e2e/tests/support/pages/card-page.ts +++ b/packages/e2e/tests/support/pages/card-page.ts @@ -69,8 +69,26 @@ export class CardPage { const created = waitForTrpcMutation(this.page, "label.create"); const assigned = waitForTrpcMutation(this.page, "card.addOrRemoveLabel"); await dialog.getByRole("button", { name: "Create label" }).click(); - await created; + const createdResponse = await created; await assigned; + + const body = (await createdResponse.json()) as [ + { result: { data: { json: { publicId: string } } } }, + ]; + + return body[0].result.data.json.publicId; + } + + async openLabelEditor(name: string) { + await this.labelSelectorTrigger().click(); + + const checkbox = this.page + .getByRole("checkbox", { name }) + .filter({ visible: true }); + const row = checkbox.locator(".."); + + await row.hover(); + await row.getByRole("button").click(); } async uploadAttachment(filePath: string) {