refactor(tender-details): enhance translation page tests and layout
- Updated Cypress tests for the tender translation page to improve visibility checks and streamline assertions. - Refactored test structure to utilize a dedicated function for fetching the visible title heading. - Adjusted test cases to ensure accurate language switching and URL handling for tender details. - Improved layout consistency in the translation page by refining element visibility checks and ensuring proper section rendering.
This commit is contained in:
@@ -3,6 +3,7 @@ import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
|
||||
import { Switch } from "@/components/FormElements/switch";
|
||||
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import IsVisible from "@/components/ui/IsVisible";
|
||||
import ListHeader from "@/components/ui/ListHeader";
|
||||
import ListWrapper from "@/components/ui/ListWrapper";
|
||||
import Modal from "@/components/ui/modal";
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import Pagination from "@/components/ui/pagination";
|
||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||
import { getPaginatedRowNumber, unixToDate } from "@/utils/shared";
|
||||
import { startTransition } from "react";
|
||||
@@ -44,6 +46,9 @@ const CompanyCategoriesTable = () => {
|
||||
setFilterValue,
|
||||
columns,
|
||||
isMutating,
|
||||
metadata,
|
||||
setParams,
|
||||
shouldShowPagination,
|
||||
} = useCompanyCategoriesPresenter();
|
||||
|
||||
return (
|
||||
@@ -70,6 +75,7 @@ const CompanyCategoriesTable = () => {
|
||||
list={optimisticCategories}
|
||||
columns={columns}
|
||||
emptyMessage="No categories were found"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{optimisticCategories.map((category, index) => (
|
||||
<TableRow
|
||||
@@ -78,7 +84,11 @@ const CompanyCategoriesTable = () => {
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell colSpan={100}>
|
||||
{getPaginatedRowNumber({ index })}
|
||||
{getPaginatedRowNumber({
|
||||
index,
|
||||
page: metadata?.page,
|
||||
limit: metadata?.limit,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell colSpan={100}>{category.name}</TableCell>
|
||||
<TableCell colSpan={100}>{category.description}</TableCell>
|
||||
@@ -179,6 +189,19 @@ const CompanyCategoriesTable = () => {
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<IsVisible condition={shouldShowPagination}>
|
||||
<Pagination
|
||||
currentPage={metadata?.page ? metadata.page - 1 : 0}
|
||||
totalPages={metadata?.pages ?? 1}
|
||||
onPageChange={(e) => {
|
||||
setParams((currentParams) => ({
|
||||
...currentParams,
|
||||
offset: e.selected * (metadata?.limit ?? 10),
|
||||
limit: metadata?.limit ?? 10,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</IsVisible>
|
||||
</ListWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,6 +35,8 @@ const useCompanyCategoriesPresenter = () => {
|
||||
useEffect(() => {
|
||||
const newParams: Record<string, any> = {
|
||||
...apiDefaultParams,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
};
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
newParams[key] = value;
|
||||
@@ -45,6 +47,10 @@ const useCompanyCategoriesPresenter = () => {
|
||||
|
||||
const { data, isLoading, isError } = useCompanyCategoriesQuery(params);
|
||||
const categories = data?.data.categories;
|
||||
const metadata = data?.meta;
|
||||
const shouldShowPagination =
|
||||
!isLoading &&
|
||||
(data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
|
||||
const [optimisticCategories, addOptimisticPublished] = useOptimistic<
|
||||
TCompanyCategory[],
|
||||
TogglePublishedOptimistic
|
||||
@@ -58,7 +64,7 @@ const useCompanyCategoriesPresenter = () => {
|
||||
useToggleCompanyCategoryPublished();
|
||||
|
||||
const search = (data: any) => {
|
||||
const newParams = { ...params, ...data };
|
||||
const newParams = { ...params, ...data, offset: 0 };
|
||||
const cleanedParams = deleteEmptyKeys(newParams);
|
||||
const queryString = new URLSearchParams(cleanedParams).toString();
|
||||
router.push(`${pathName}?${queryString}`);
|
||||
@@ -102,6 +108,8 @@ const useCompanyCategoriesPresenter = () => {
|
||||
watch,
|
||||
setFilterValue,
|
||||
isMutating,
|
||||
metadata,
|
||||
shouldShowPagination,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
} from "@/assets/icons";
|
||||
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
|
||||
import ConfirmationModal from "@/components/ui/ConfirmationModal";
|
||||
import IsVisible from "@/components/ui/IsVisible";
|
||||
import ListHeader from "@/components/ui/ListHeader";
|
||||
import ListWrapper from "@/components/ui/ListWrapper";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import Pagination from "@/components/ui/pagination";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -45,6 +47,9 @@ const CompaniesTable = () => {
|
||||
watch,
|
||||
setFilterValue,
|
||||
isMutating,
|
||||
metadata,
|
||||
setParams,
|
||||
shouldShowPagination,
|
||||
} = useCompanyListPresenter();
|
||||
const companies = allCompanies.filter(
|
||||
(c): c is NonNullable<typeof c> => c != null,
|
||||
@@ -75,6 +80,7 @@ const CompaniesTable = () => {
|
||||
list={companies}
|
||||
columns={columns}
|
||||
emptyMessage="No companies were found"
|
||||
isLoading={isPending}
|
||||
>
|
||||
{companies.map((company, index) => (
|
||||
<TableRow
|
||||
@@ -82,7 +88,11 @@ const CompaniesTable = () => {
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{getPaginatedRowNumber({ index })}
|
||||
{getPaginatedRowNumber({
|
||||
index,
|
||||
page: metadata?.page,
|
||||
limit: metadata?.limit,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{company.name}
|
||||
@@ -198,6 +208,19 @@ const CompaniesTable = () => {
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<IsVisible condition={shouldShowPagination}>
|
||||
<Pagination
|
||||
currentPage={metadata?.page ? metadata.page - 1 : 0}
|
||||
totalPages={metadata?.pages ?? 1}
|
||||
onPageChange={(e) => {
|
||||
setParams((currentParams) => ({
|
||||
...currentParams,
|
||||
offset: e.selected * (metadata?.limit ?? 10),
|
||||
limit: metadata?.limit ?? 10,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</IsVisible>
|
||||
</ListWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
"use client";
|
||||
import { ConfirmationModalProps } from "@/components/ui/ConfirmationModal";
|
||||
import { apiDefaultParams } from "@/constants/Api_Params";
|
||||
import {
|
||||
useCompaniesInfiniteQuery,
|
||||
useCompaniesQuery,
|
||||
useDeleteCompanyQuery,
|
||||
} from "@/hooks/queries";
|
||||
import { TCompany } from "@/lib/api";
|
||||
import { deleteEmptyKeys } from "@/utils/shared";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
export const useCompanyListPresenter = () => {
|
||||
const deleteModal = useRef<HTMLDialogElement | null>(null);
|
||||
const { inView } = useInView({ threshold: 0.5 });
|
||||
const [currentCompany, setCurrentCompany] = useState<TCompany | null>(null);
|
||||
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||
const [modalConfig, setModalConfig] = useState<
|
||||
@@ -50,7 +48,9 @@ export const useCompanyListPresenter = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const newParams: Record<string, any> = {
|
||||
sort: "created_at",
|
||||
...apiDefaultParams,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
};
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
newParams[key] = value;
|
||||
@@ -59,28 +59,17 @@ export const useCompanyListPresenter = () => {
|
||||
filterFormReset(newParams);
|
||||
}, [searchParams, filterFormReset]);
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isPending,
|
||||
isError,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useCompaniesInfiniteQuery(params);
|
||||
const { data, error, isPending, isError } = useCompaniesQuery(params);
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && hasNextPage && data && data.pages.length < 5) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [inView, hasNextPage, data, fetchNextPage]);
|
||||
const shouldShowPagination =
|
||||
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
|
||||
|
||||
const { mutate: deleteCompany } = useDeleteCompanyQuery(() =>
|
||||
setIsModalOpen(false),
|
||||
);
|
||||
|
||||
const search = (data: any) => {
|
||||
const newParams = { ...params, ...data };
|
||||
const newParams = { ...params, ...data, offset: 0 };
|
||||
const cleanedParams = deleteEmptyKeys(newParams);
|
||||
const queryString = new URLSearchParams(cleanedParams).toString();
|
||||
router.push(`${pathName}?${queryString}`);
|
||||
@@ -91,22 +80,18 @@ export const useCompanyListPresenter = () => {
|
||||
deleteCompany(currentCompany.id);
|
||||
}
|
||||
};
|
||||
const allCompanies = data?.pages.flatMap((page) => page.data.companies) ?? [];
|
||||
const allCompanies = data?.data.companies ?? [];
|
||||
const metadata = data?.meta;
|
||||
return {
|
||||
allCompanies,
|
||||
onConfirmDelete,
|
||||
deleteCompany,
|
||||
isFetchingNextPage,
|
||||
data,
|
||||
metadata,
|
||||
columns,
|
||||
pathName,
|
||||
currentCompany,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
modalConfig,
|
||||
setModalConfig,
|
||||
setCurrentCompany,
|
||||
deleteModal,
|
||||
error,
|
||||
isModalOpen,
|
||||
setIsModalOpen,
|
||||
@@ -121,5 +106,7 @@ export const useCompanyListPresenter = () => {
|
||||
watch,
|
||||
setFilterValue,
|
||||
isMutating,
|
||||
setParams,
|
||||
shouldShowPagination,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DatePicker from "@/components/FormElements/DatePicker/DatePicker";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { Button } from "@/components/ui-elements/button";
|
||||
@@ -6,6 +7,7 @@ import { TenderStatus } from "@/constants/enums";
|
||||
import { Languages } from "@/constants/languages";
|
||||
import { getEnumAsArray } from "@/utils/shared";
|
||||
import {
|
||||
Control,
|
||||
FieldValues,
|
||||
UseFormHandleSubmit,
|
||||
UseFormRegister,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
|
||||
interface TenderListFiltersProps {
|
||||
filterRegister: UseFormRegister<FieldValues>;
|
||||
control: Control<FieldValues>;
|
||||
setIsFilterModalOpen: (isOpen: boolean) => void;
|
||||
isMutating: number;
|
||||
handleFilterSubmit: UseFormHandleSubmit<FieldValues>;
|
||||
@@ -25,6 +28,7 @@ interface TenderListFiltersProps {
|
||||
|
||||
const TenderListFilters = ({
|
||||
filterRegister,
|
||||
control,
|
||||
setIsFilterModalOpen,
|
||||
isMutating,
|
||||
handleFilterSubmit,
|
||||
@@ -37,11 +41,22 @@ const TenderListFilters = ({
|
||||
className="grid !min-w-full grid-cols-1 gap-4 xl:grid-cols-2"
|
||||
onSubmit={handleFilterSubmit(search)}
|
||||
>
|
||||
<InputGroup
|
||||
{...filterRegister("q")}
|
||||
name="q"
|
||||
label="keyword"
|
||||
type="text"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("title")}
|
||||
name="title"
|
||||
label="title"
|
||||
placeholder="Title"
|
||||
type="text"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("description")}
|
||||
name="description"
|
||||
label="description"
|
||||
type="text"
|
||||
/>
|
||||
<Select
|
||||
@@ -49,27 +64,100 @@ const TenderListFilters = ({
|
||||
items={getEnumAsArray(TenderStatus)}
|
||||
label="status"
|
||||
name="status"
|
||||
placeholder="Status"
|
||||
clearable
|
||||
value={watch("status")}
|
||||
onClear={() => setValue("status", undefined)}
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("country")}
|
||||
name="country"
|
||||
label="country"
|
||||
type="text"
|
||||
/>
|
||||
<Select
|
||||
{...filterRegister("country_code")}
|
||||
items={Countries}
|
||||
label="country code"
|
||||
name="country_code"
|
||||
placeholder="Country Code"
|
||||
clearable
|
||||
value={watch("country_code")}
|
||||
onClear={() => setValue("country_code", undefined)}
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("country_codes")}
|
||||
name="country_codes"
|
||||
label="country codes"
|
||||
type="text"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("notice_type")}
|
||||
name="notice_type"
|
||||
label="notice type"
|
||||
type="text"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("notice_types")}
|
||||
name="notice_types"
|
||||
label="notice types"
|
||||
type="text"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("form_types")}
|
||||
name="form_types"
|
||||
label="form types"
|
||||
type="text"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("main_classification")}
|
||||
name="main_classification"
|
||||
label="main classification"
|
||||
type="text"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("classifications")}
|
||||
name="classifications"
|
||||
label="classifications"
|
||||
type="text"
|
||||
/>
|
||||
<InputGroup
|
||||
{...filterRegister("cpv_codes")}
|
||||
name="cpv_codes"
|
||||
label="cpv codes"
|
||||
type="text"
|
||||
/>
|
||||
<DatePicker
|
||||
control={control}
|
||||
name="created_at_range"
|
||||
label="created at range"
|
||||
range
|
||||
timePicker
|
||||
/>
|
||||
<DatePicker
|
||||
control={control}
|
||||
name="tender_deadline_range"
|
||||
label="tender deadline range"
|
||||
range
|
||||
timePicker
|
||||
/>
|
||||
<DatePicker
|
||||
control={control}
|
||||
name="publication_date_range"
|
||||
label="publication date range"
|
||||
range
|
||||
timePicker
|
||||
/>
|
||||
<DatePicker
|
||||
control={control}
|
||||
name="submission_deadline_range"
|
||||
label="submission deadline range"
|
||||
range
|
||||
timePicker
|
||||
/>
|
||||
<Select
|
||||
{...filterRegister("lang")}
|
||||
items={Languages}
|
||||
label="language"
|
||||
name="lang"
|
||||
placeholder="Language"
|
||||
clearable
|
||||
value={watch("lang")}
|
||||
onClear={() => setValue("lang", undefined)}
|
||||
@@ -83,10 +171,10 @@ const TenderListFilters = ({
|
||||
className="mt-6 w-fit capitalize"
|
||||
label={
|
||||
isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
) : (
|
||||
<span>Search</span>
|
||||
)
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
) : (
|
||||
<span>Search</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -41,6 +41,7 @@ const TendersTable = () => {
|
||||
search,
|
||||
watch,
|
||||
setFilterValue,
|
||||
control,
|
||||
isMutating,
|
||||
shouldShowPagination,
|
||||
} = useTenderListPresenter();
|
||||
@@ -167,6 +168,7 @@ const TendersTable = () => {
|
||||
<TenderListFilters
|
||||
setValue={setFilterValue}
|
||||
filterRegister={filterRegister}
|
||||
control={control}
|
||||
handleFilterSubmit={handleFilterSubmit}
|
||||
isMutating={isMutating as number}
|
||||
search={search}
|
||||
|
||||
@@ -8,6 +8,110 @@ import { useIsMutating } from "@tanstack/react-query";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
const COMMA_SEPARATED_FILTER_KEYS = new Set([
|
||||
"country_codes",
|
||||
"notice_types",
|
||||
"form_types",
|
||||
"classifications",
|
||||
"cpv_codes",
|
||||
]);
|
||||
|
||||
const normalizeFilterValues = (values: Record<string, any>) => {
|
||||
const normalized: Record<string, any> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const trimmedValue = value.trim();
|
||||
if (!trimmedValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (COMMA_SEPARATED_FILTER_KEYS.has(key)) {
|
||||
normalized[key] = trimmedValue
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized[key] = trimmedValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized[key] = value;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const mapDateRangeFilters = (values: Record<string, any>) => {
|
||||
const mappedValues = { ...values };
|
||||
|
||||
const mapRange = ({
|
||||
key,
|
||||
fromKey,
|
||||
toKey,
|
||||
aliases = [],
|
||||
}: {
|
||||
key: string;
|
||||
fromKey: string;
|
||||
toKey: string;
|
||||
aliases?: Array<{ from: string; to: string }>;
|
||||
}) => {
|
||||
const rangeValue = mappedValues[key];
|
||||
if (!Array.isArray(rangeValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [from, to] = rangeValue;
|
||||
if (from !== undefined && from !== null && from !== "") {
|
||||
mappedValues[fromKey] = from;
|
||||
aliases.forEach((alias) => {
|
||||
mappedValues[alias.from] = from;
|
||||
});
|
||||
}
|
||||
|
||||
if (to !== undefined && to !== null && to !== "") {
|
||||
mappedValues[toKey] = to;
|
||||
aliases.forEach((alias) => {
|
||||
mappedValues[alias.to] = to;
|
||||
});
|
||||
}
|
||||
|
||||
delete mappedValues[key];
|
||||
};
|
||||
|
||||
mapRange({
|
||||
key: "created_at_range",
|
||||
fromKey: "created_at_from",
|
||||
toKey: "created_at_to",
|
||||
});
|
||||
mapRange({
|
||||
key: "tender_deadline_range",
|
||||
fromKey: "tender_deadline_from",
|
||||
toKey: "tender_deadline_to",
|
||||
aliases: [{ from: "deadline_from", to: "deadline_to" }],
|
||||
});
|
||||
mapRange({
|
||||
key: "publication_date_range",
|
||||
fromKey: "publication_date_from",
|
||||
toKey: "publication_date_to",
|
||||
});
|
||||
mapRange({
|
||||
key: "submission_deadline_range",
|
||||
fromKey: "submission_deadline_from",
|
||||
toKey: "submission_deadline_to",
|
||||
aliases: [{ from: "submission_date_from", to: "submission_date_to" }],
|
||||
});
|
||||
|
||||
return mappedValues;
|
||||
};
|
||||
const useTenderListPresenter = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||
@@ -27,6 +131,7 @@ const useTenderListPresenter = () => {
|
||||
reset: filterFormReset,
|
||||
watch,
|
||||
setValue: setFilterValue,
|
||||
control,
|
||||
} = useForm();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -48,7 +153,9 @@ const useTenderListPresenter = () => {
|
||||
!isPending && (data?.meta?.pages ?? 1) * (data?.meta?.limit ?? 10) > 10;
|
||||
|
||||
const search = (data: any) => {
|
||||
const newParams = { ...params, ...data, offset: 0 };
|
||||
const mappedDateRanges = mapDateRangeFilters(data);
|
||||
const normalizedData = normalizeFilterValues(mappedDateRanges);
|
||||
const newParams = { ...params, ...normalizedData, offset: 0 };
|
||||
const cleanedParams = deleteEmptyKeys(newParams);
|
||||
const queryString = new URLSearchParams(cleanedParams).toString();
|
||||
router.push(`${pathName}?${queryString}`);
|
||||
@@ -88,6 +195,7 @@ const useTenderListPresenter = () => {
|
||||
search,
|
||||
watch,
|
||||
setFilterValue,
|
||||
control,
|
||||
isMutating,
|
||||
shouldShowPagination,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user