Corregir sincronización de idioma tras login y reactividad i18n
This commit is contained in:
parent
f8d0979059
commit
e6001d62a9
@ -7,15 +7,16 @@ import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
||||
import { Button, Input } from "@/ui";
|
||||
import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts";
|
||||
import { CellContext, ColumnDef, createColumnHelper, Row, RowData } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { PackagePlusIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type QuantityValue = number | "";
|
||||
|
||||
type CatalogPickerTableMeta = {
|
||||
quantities: QuantityValue[];
|
||||
t: (key: string) => string;
|
||||
setQuantity: (index: number, quantity: QuantityValue) => void;
|
||||
selectArticle: (article: IListArticles_Response_DTO, index: number) => void;
|
||||
};
|
||||
@ -68,7 +69,7 @@ function AddArticleCell({ row, table }: CellContext<IListArticles_Response_DTO,
|
||||
}}
|
||||
>
|
||||
<PackagePlusIcon className='h-3.5 w-3.5' />
|
||||
<span className='sr-only xl:not-sr-only xl:whitespace-nowrap'>{t("common.add")}</span>
|
||||
<span className='sr-only xl:not-sr-only xl:whitespace-nowrap'>{catalogPicker?.t("common.add")}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -79,6 +80,7 @@ export const CatalogPickerDataTable = ({
|
||||
onSelect: (data: unknown, quantity: number) => void;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { pagination, globalFilter, isFiltered } = useDataTableContext();
|
||||
|
||||
const [quantities, setQuantities] = useState<QuantityValue[]>([]);
|
||||
@ -111,12 +113,13 @@ export const CatalogPickerDataTable = ({
|
||||
() => ({
|
||||
catalogPicker: {
|
||||
quantities,
|
||||
t,
|
||||
setQuantity: handleQuantity,
|
||||
selectArticle: (article, index) =>
|
||||
onSelect(article, quantities[index] === "" ? 1 : (quantities[index] ?? 1)),
|
||||
},
|
||||
}),
|
||||
[handleQuantity, onSelect, quantities]
|
||||
[handleQuantity, onSelect, quantities, t]
|
||||
);
|
||||
|
||||
const columns = useMemo<Array<ColumnDef<IListArticles_Response_DTO, any>>>(
|
||||
@ -155,7 +158,7 @@ export const CatalogPickerDataTable = ({
|
||||
cell: AddArticleCell,
|
||||
}),
|
||||
],
|
||||
[]
|
||||
[t, i18n.resolvedLanguage]
|
||||
);
|
||||
|
||||
/*const columns2 = useMemo<ColumnDef<IListArticles_Response_DTO>[]>(() => {
|
||||
|
||||
@ -12,9 +12,9 @@ import { Button, Card, CardContent, Tooltip, TooltipContent, TooltipTrigger } fr
|
||||
import { useToast } from "@/ui/use-toast";
|
||||
import { IListQuotes_Response_DTO, UTCDateValue } from "@shared/contexts";
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { ArchiveIcon, CopyIcon, DownloadIcon, EditIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useId, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuotes } from "../hooks";
|
||||
import { DownloadQuoteDialog } from "./DownloadQuoteDialog";
|
||||
@ -30,6 +30,7 @@ export const QuotesDataTable = ({
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const tableId = useId();
|
||||
const previewId = useId();
|
||||
@ -356,7 +357,7 @@ export const QuotesDataTable = ({
|
||||
];
|
||||
|
||||
return preview ? columns : [...columns, ...actionColumn];
|
||||
}, [preview]);
|
||||
}, [preview, t, i18n.resolvedLanguage]);
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: data?.items ?? [],
|
||||
|
||||
@ -6,7 +6,7 @@ import { Checkbox } from "@/ui";
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { QuoteItemsSortableDragHandleCell } from "../components/QuoteItemsSortableDragHandleCell";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ -19,6 +19,7 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
|
||||
rowActionFn?: DataTablaRowActionFunction<TData>;
|
||||
} = {}
|
||||
): ColumnDef<TData, TValue>[] {
|
||||
const { t, i18n } = useTranslation();
|
||||
const {
|
||||
enableDragHandleColumn = false,
|
||||
enableSelectionColumn = false,
|
||||
@ -94,5 +95,5 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
|
||||
}
|
||||
|
||||
return columns;
|
||||
}, [enableActionsColumn, enableDragHandleColumn, enableSelectionColumn]);
|
||||
}, [enableActionsColumn, enableDragHandleColumn, enableSelectionColumn, t, i18n.resolvedLanguage]);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { ProtectedRoute } from "./ProtectedRoute";
|
||||
const useIsLoggedIn = jest.fn();
|
||||
const useGetProfile = jest.fn();
|
||||
const changeLanguage = jest.fn();
|
||||
const i18n = { language: "en", resolvedLanguage: "en", changeLanguage };
|
||||
|
||||
jest.mock("@/lib/hooks", () => ({
|
||||
useIsLoggedIn: () => useIsLoggedIn(),
|
||||
@ -18,12 +19,11 @@ jest.mock("@/components", () => ({
|
||||
}));
|
||||
|
||||
jest.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ i18n: { language: "en", changeLanguage } }),
|
||||
useTranslation: () => ({ i18n }),
|
||||
}));
|
||||
|
||||
const renderProtectedRoute = () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const renderProtectedRoute = (container = document.createElement("div")) => {
|
||||
if (!container.parentElement) document.body.appendChild(container);
|
||||
|
||||
act(() => {
|
||||
renderDom(
|
||||
@ -54,6 +54,8 @@ describe("ProtectedRoute", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
changeLanguage.mockClear();
|
||||
i18n.language = "en";
|
||||
i18n.resolvedLanguage = "en";
|
||||
useIsLoggedIn.mockReturnValue({
|
||||
isSuccess: true,
|
||||
isLoading: false,
|
||||
@ -75,7 +77,7 @@ describe("ProtectedRoute", () => {
|
||||
expect(container.querySelector("[data-testid='login']")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the protected route after resolving a valid profile", () => {
|
||||
it("waits for the profile language before mounting the protected route", () => {
|
||||
useGetProfile.mockReturnValue({
|
||||
isSuccess: true,
|
||||
isLoading: false,
|
||||
@ -85,8 +87,29 @@ describe("ProtectedRoute", () => {
|
||||
|
||||
const container = renderProtectedRoute();
|
||||
|
||||
expect(container.querySelector("[data-testid='quotes']")).not.toBeNull();
|
||||
expect(container.querySelector("[data-testid='loading']")).not.toBeNull();
|
||||
expect(container.querySelector("[data-testid='quotes']")).toBeNull();
|
||||
expect(changeLanguage).toHaveBeenCalledWith("es");
|
||||
|
||||
i18n.language = "es";
|
||||
i18n.resolvedLanguage = "es";
|
||||
renderProtectedRoute(container);
|
||||
|
||||
expect(container.querySelector("[data-testid='quotes']")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("mounts immediately when the active language already matches the profile", () => {
|
||||
useGetProfile.mockReturnValue({
|
||||
isSuccess: true,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
data: { id: "current-user", lang_code: "en" },
|
||||
});
|
||||
|
||||
const container = renderProtectedRoute();
|
||||
|
||||
expect(container.querySelector("[data-testid='quotes']")).not.toBeNull();
|
||||
expect(changeLanguage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders an error instead of treating a profile request failure as a logout", () => {
|
||||
|
||||
@ -29,13 +29,22 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
||||
enabled: isLoggedInSuccess && authenticated,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isProfileSuccess && profile?.lang_code && i18n.language !== profile.lang_code) {
|
||||
void i18n.changeLanguage(profile.lang_code);
|
||||
}
|
||||
}, [isProfileSuccess, profile?.lang_code, i18n]);
|
||||
const profileLanguage = profile?.lang_code;
|
||||
const activeLanguage = i18n.resolvedLanguage ?? i18n.language;
|
||||
const isLanguageSynchronized = !profileLanguage || activeLanguage === profileLanguage;
|
||||
|
||||
if (isLoggedInLoading || isProfileLoading || (isProfileSuccess && profile === null && isProfileFetching)) {
|
||||
React.useEffect(() => {
|
||||
if (isProfileSuccess && profileLanguage && !isLanguageSynchronized) {
|
||||
void i18n.changeLanguage(profileLanguage);
|
||||
}
|
||||
}, [isProfileSuccess, profileLanguage, isLanguageSynchronized, i18n]);
|
||||
|
||||
if (
|
||||
isLoggedInLoading ||
|
||||
isProfileLoading ||
|
||||
(isProfileSuccess && profile === null && isProfileFetching) ||
|
||||
(isProfileSuccess && profile?.id && !isLanguageSynchronized)
|
||||
) {
|
||||
return <LoadingOverlay />;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user