This commit is contained in:
David Arranz 2026-10-14 12:57:21 +02:00
parent 5934773331
commit c5a40e32a3
12 changed files with 141 additions and 33 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "@uecko-presupuestador/client", "name": "@uecko-presupuestador/client",
"private": true, "private": true,
"version": "1.2.6", "version": "1.2.7",
"author": "Rodax Software <dev@rodax-software.com>", "author": "Rodax Software <dev@rodax-software.com>",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -1,5 +1,4 @@
import { ScrollArea } from "@/ui"; import { ScrollArea } from "@/ui";
import { useTranslation } from "react-i18next";
import esBlocks from "./locales/es.json"; import esBlocks from "./locales/es.json";
import enBlocks from "./locales/en.json"; import enBlocks from "./locales/en.json";
import frBlocks from "./locales/fr.json"; import frBlocks from "./locales/fr.json";
@ -15,12 +14,12 @@ const blockData: Record<string, BlockDataType[]> = { es: esBlocks, en: enBlocks,
export const BlockList = ({ export const BlockList = ({
onSelect, onSelect,
languageCode,
}: { }: {
languageCode: string;
onSelect: (data: BlockDataType, quantity: number) => void; onSelect: (data: BlockDataType, quantity: number) => void;
}) => { }) => {
const { i18n } = useTranslation(); const listState = blockData[languageCode.toLowerCase()] ?? blockData.es;
const language = i18n.resolvedLanguage || "es";
const listState = blockData[language.toLowerCase()] ?? blockData.es;
return ( return (
<ScrollArea className='h-96'> <ScrollArea className='h-96'>

View File

@ -0,0 +1,105 @@
import type { ReactNode } from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { useMediaQuery } from "@/lib/hooks";
import { useTranslation } from "react-i18next";
import { BlocksPickerDialog } from "./BlocksPickerDialog";
import esBlocks from "./locales/es.json";
import enBlocks from "./locales/en.json";
import frBlocks from "./locales/fr.json";
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
jest.mock("@/lib/hooks", () => ({ useMediaQuery: jest.fn() }));
jest.mock("react-i18next", () => ({ useTranslation: jest.fn() }));
jest.mock("i18next", () => ({ t: (key: string) => key }));
jest.mock("@/ui", () => {
const Container = ({ children }: { children: ReactNode }) => <div>{children}</div>;
return {
Button: Container,
Dialog: Container,
DialogContent: Container,
DialogDescription: Container,
DialogFooter: Container,
DialogHeader: Container,
DialogTitle: Container,
Drawer: Container,
DrawerClose: Container,
DrawerContent: Container,
DrawerDescription: Container,
DrawerFooter: Container,
DrawerHeader: Container,
DrawerTitle: Container,
ScrollArea: Container,
};
});
describe.each([true, false])("text block picker (desktop: %s)", (isDesktop) => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
jest.mocked(useMediaQuery).mockReturnValue(isDesktop);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it.each([
{ languageCode: "es", interfaceLanguage: "en", blocks: esBlocks },
{ languageCode: "en", interfaceLanguage: "es", blocks: enBlocks },
{ languageCode: "FR", interfaceLanguage: "es", blocks: frBlocks },
])("displays and selects document text in $languageCode with the interface in $interfaceLanguage", ({
languageCode, interfaceLanguage, blocks,
}) => {
jest.mocked(useTranslation).mockReturnValue({
i18n: { resolvedLanguage: interfaceLanguage },
} as unknown as ReturnType<typeof useTranslation>);
const onSelect = jest.fn();
act(() => {
root.render(
<BlocksPickerDialog
isOpen
languageCode={languageCode}
onOpenChange={jest.fn()}
onSelect={onSelect}
/>
);
});
const expectedBlocks = blocks.filter(({ title, body }) => title || body);
const buttons = Array.from(container.querySelectorAll("button"));
expect(buttons).toHaveLength(expectedBlocks.length);
expect(buttons.map((button) => button.querySelector(".font-semibold")?.textContent).sort())
.toEqual(expectedBlocks.map(({ title }) => title).sort());
const selectedBlock = expectedBlocks[0];
const selectedButton = buttons.find((button) =>
button.querySelector(".font-semibold")?.textContent === selectedBlock.title
);
expect(selectedButton).toBeDefined();
act(() => selectedButton!.click());
// The insertion handler receives the full localized title and body, including line breaks.
expect(onSelect).toHaveBeenCalledWith(selectedBlock, 1);
expect(container.textContent).toContain("quotes.blocks_picker_dialog.title");
});
it("updates available blocks when the document language changes while open", () => {
const onSelect = jest.fn();
const props = { isOpen: true, onOpenChange: jest.fn(), onSelect };
act(() => root.render(<BlocksPickerDialog {...props} languageCode='en' />));
act(() => root.render(<BlocksPickerDialog {...props} languageCode='fr' />));
const selectedButton = Array.from(container.querySelectorAll("button")).find((button) =>
button.querySelector(".font-semibold")?.textContent === frBlocks[0].title
);
expect(selectedButton).toBeDefined();
act(() => selectedButton!.click());
expect(onSelect).toHaveBeenCalledWith(frBlocks[0], 1);
});
});

View File

@ -23,7 +23,9 @@ export const BlocksPickerDialog = ({
isOpen, isOpen,
onOpenChange, onOpenChange,
onSelect, onSelect,
languageCode,
}: { }: {
languageCode: string;
isOpen: boolean; isOpen: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onSelect: (data: unknown) => void; onSelect: (data: unknown) => void;
@ -39,7 +41,7 @@ export const BlocksPickerDialog = ({
<DialogDescription>{t("quotes.blocks_picker_dialog.description")}</DialogDescription> <DialogDescription>{t("quotes.blocks_picker_dialog.description")}</DialogDescription>
</DialogHeader> </DialogHeader>
<BlockList onSelect={onSelect} /> <BlockList languageCode={languageCode} onSelect={onSelect} />
<DialogFooter> <DialogFooter>
<Button type='submit' onClick={() => onOpenChange(false)}> <Button type='submit' onClick={() => onOpenChange(false)}>
@ -59,7 +61,7 @@ export const BlocksPickerDialog = ({
<DrawerDescription>{t("quotes.blocks_picker_dialog.description")}</DrawerDescription> <DrawerDescription>{t("quotes.blocks_picker_dialog.description")}</DrawerDescription>
</DrawerHeader> </DrawerHeader>
<BlockList onSelect={onSelect} /> <BlockList languageCode={languageCode} onSelect={onSelect} />
<DrawerFooter className='pt-2'> <DrawerFooter className='pt-2'>
<DrawerClose asChild> <DrawerClose asChild>

View File

@ -31,6 +31,7 @@ export const QuoteDetailsCardEditor = ({
}) => { }) => {
const { toast } = useToast(); const { toast } = useToast();
const { control, getValues, setValue, watch } = useFormContext(); const { control, getValues, setValue, watch } = useFormContext();
const documentLanguageCode = watch("lang_code") || Language.DEFAULT_LANGUAGE_CODE;
const showLineDiscountColumn = watch("show_line_discount_column") ?? true; const showLineDiscountColumn = watch("show_line_discount_column") ?? true;
const hasLineDiscounts = Boolean( const hasLineDiscounts = Boolean(
getValues("items")?.some((item: { discount?: { amount?: number | null } }) => getValues("items")?.some((item: { discount?: { amount?: number | null } }) =>
@ -279,6 +280,7 @@ export const QuoteDetailsCardEditor = ({
/> />
<BlocksPickerDialog <BlocksPickerDialog
languageCode={documentLanguageCode}
onSelect={handleAppendBlock} onSelect={handleAppendBlock}
isOpen={blockPickerDialogOpen} isOpen={blockPickerDialogOpen}
onOpenChange={setBlockPickerDialogOpen} onOpenChange={setBlockPickerDialogOpen}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" /> <link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
<title>Presupuestador Uecko</title> <title>Presupuestador Uecko</title>
<script type="module" crossorigin src="/assets/index-AMb79QFB.js"></script> <script type="module" crossorigin src="/assets/index-zvH6W8yt.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cipqnmx2.css"> <link rel="stylesheet" crossorigin href="/assets/index-Cipqnmx2.css">
</head> </head>

View File

@ -1 +1 @@
{"buildId":"dfdc0c24-169e-420e-b97a-aa406689e0d0"} {"buildId":"073e7786-0ae3-4df4-8650-2f0d78bdc9e9"}

View File

@ -1,6 +1,6 @@
{ {
"name": "uecko-presupuestador", "name": "uecko-presupuestador",
"version": "1.2.6", "version": "1.2.7",
"author": "Rodax Software <dev@rodax-software.com>", "author": "Rodax Software <dev@rodax-software.com>",
"license": "ISC", "license": "ISC",
"private": true, "private": true,

View File

@ -1,7 +1,7 @@
{ {
"name": "@uecko-presupuestador/server", "name": "@uecko-presupuestador/server",
"private": true, "private": true,
"version": "1.2.6", "version": "1.2.7",
"author": "Rodax Software <dev@rodax-software.com>", "author": "Rodax Software <dev@rodax-software.com>",
"main": "./src/index.ts", "main": "./src/index.ts",
"scripts": { "scripts": {

View File

@ -1,7 +1,7 @@
{ {
"name": "@uecko-presupuestador/shared", "name": "@uecko-presupuestador/shared",
"private": false, "private": false,
"version": "1.2.6", "version": "1.2.7",
"main": "./index.ts", "main": "./index.ts",
"author": "Rodax Software <dev@rodax-software.com>", "author": "Rodax Software <dev@rodax-software.com>",
"license": "ISC", "license": "ISC",