diff --git a/apps/client/src/features/base/components/property/__tests__/conversion-warning.spec.ts b/apps/client/src/features/base/components/property/__tests__/conversion-warning.spec.ts new file mode 100644 index 000000000..2db7945ca --- /dev/null +++ b/apps/client/src/features/base/components/property/__tests__/conversion-warning.spec.ts @@ -0,0 +1,104 @@ +import { + conversionWarning, + NON_USER_TARGET_TYPES, +} from "../conversion-warning"; + +describe("conversionWarning", () => { + it("returns the choice-name copy for select → text", () => { + expect(conversionWarning("select", "text")).toBe( + "Cells will be replaced with the option name.", + ); + }); + + it("returns the same copy for status → text", () => { + expect(conversionWarning("status", "text")).toBe( + "Cells will be replaced with the option name.", + ); + }); + + it("returns comma-list copy for multiSelect → text", () => { + expect(conversionWarning("multiSelect", "text")).toBe( + "Cells will be replaced with a comma-separated list of option names.", + ); + }); + + it("returns person-name copy for person → text", () => { + expect(conversionWarning("person", "text")).toBe( + "Cells will be replaced with the person's name.", + ); + }); + + it("returns file-name list copy for file → text", () => { + expect(conversionWarning("file", "text")).toBe( + "Cells will be replaced with a comma-separated list of file names.", + ); + }); + + it("returns page-title copy for page → text", () => { + expect(conversionWarning("page", "text")).toBe( + "Cells will be replaced with the page title.", + ); + }); + + it("returns first-item-kept copy for multiSelect → select", () => { + expect(conversionWarning("multiSelect", "select")).toBe( + "Only the first selected item per row will be kept; the rest will be discarded.", + ); + }); + + it("returns single-item-list copy for select → multiSelect", () => { + expect(conversionWarning("select", "multiSelect")).toBe( + "Existing values become single-item lists. No data is lost.", + ); + }); + + it("returns page-cleared copy when target is page from non-page", () => { + expect(conversionWarning("text", "page")).toBe( + "Cells that aren't already a page reference will be cleared.", + ); + expect(conversionWarning("number", "page")).toBe( + "Cells that aren't already a page reference will be cleared.", + ); + }); + + it("returns number-parse-cleared copy when target is number from non-numeric", () => { + expect(conversionWarning("select", "number")).toBe( + "Cells that can't be parsed as a number will be cleared.", + ); + }); + + it("returns date-parse-cleared copy when target is date", () => { + expect(conversionWarning("text", "date")).toBe( + "Cells that can't be parsed as a date will be cleared.", + ); + }); + + it("returns checkbox-coercion copy when target is checkbox", () => { + expect(conversionWarning("text", "checkbox")).toBe( + "Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).", + ); + }); + + it("returns the default safe copy for text → url", () => { + expect(conversionWarning("text", "url")).toBe( + "Cells will be reinterpreted under the new type.", + ); + }); + + it("returns the default safe copy for number → text", () => { + expect(conversionWarning("number", "text")).toBe( + "Cells will be reinterpreted under the new type.", + ); + }); + + describe("NON_USER_TARGET_TYPES", () => { + it("contains exactly the 4 non-user types", () => { + expect(Array.from(NON_USER_TARGET_TYPES).sort()).toEqual([ + "createdAt", + "formula", + "lastEditedAt", + "lastEditedBy", + ]); + }); + }); +}); diff --git a/apps/client/src/features/base/components/property/conversion-warning.ts b/apps/client/src/features/base/components/property/conversion-warning.ts new file mode 100644 index 000000000..9bc795414 --- /dev/null +++ b/apps/client/src/features/base/components/property/conversion-warning.ts @@ -0,0 +1,65 @@ +import type { BasePropertyType } from "@/features/base/types/base.types"; + +export const NON_USER_TARGET_TYPES = new Set([ + "createdAt", + "lastEditedAt", + "lastEditedBy", + "formula", +]); + +/* + * Returns the warning copy shown in the property-menu's + * `confirmTypeChange` panel before the user applies a type change. + * Strings are i18n source keys (translation files key them by their + * exact text). Buckets are ordered most-specific first; the default + * branch covers safe reinterpretations like text ↔ number, text → url, + * text → email. + */ +export function conversionWarning( + from: BasePropertyType, + to: BasePropertyType, +): string { + if (to === "text") { + if (from === "select" || from === "status") { + return "Cells will be replaced with the option name."; + } + if (from === "multiSelect") { + return "Cells will be replaced with a comma-separated list of option names."; + } + if (from === "person") { + return "Cells will be replaced with the person's name."; + } + if (from === "file") { + return "Cells will be replaced with a comma-separated list of file names."; + } + if (from === "page") { + return "Cells will be replaced with the page title."; + } + } + + if (to === "select" && from === "multiSelect") { + return "Only the first selected item per row will be kept; the rest will be discarded."; + } + + if (to === "multiSelect" && from === "select") { + return "Existing values become single-item lists. No data is lost."; + } + + if (to === "page") { + return "Cells that aren't already a page reference will be cleared."; + } + + if (to === "number" && from !== "number") { + return "Cells that can't be parsed as a number will be cleared."; + } + + if (to === "date" && from !== "date") { + return "Cells that can't be parsed as a date will be cleared."; + } + + if (to === "checkbox" && from !== "checkbox") { + return "Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared)."; + } + + return "Cells will be reinterpreted under the new type."; +}