feat(multi-select): enhance multi-select component with search and load more functionality
- Added onSearch and onLoadMore props to the MultiSelect component for improved user interaction. - Refactored state management to handle selected items and search queries more effectively. - Updated the CreateNotificationForm to integrate the new multi-select features for recipient selection. - Implemented infinite scrolling logic in the useCompaniesInfiniteQuery, useCustomersInfiniteQuery, and useAdminListInfiniteQuery hooks to support dynamic data loading. - Enhanced the useCreateNotificationFormPresenter to manage recipient search and loading states.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { useEffect, useId, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useId, useRef, useState } from "react";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FieldValues,
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
type MultiSelectProps<T extends FieldValues> = {
|
||||
@@ -26,6 +25,11 @@ type MultiSelectProps<T extends FieldValues> = {
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
height?: "sm" | "default";
|
||||
onSearch?: (query: string) => void;
|
||||
onLoadMore?: () => void;
|
||||
hasMore?: boolean;
|
||||
isLoading?: boolean;
|
||||
isLoadingMore?: boolean;
|
||||
} & UseFormRegisterReturn<Path<T>>;
|
||||
|
||||
function MultiSelect<T extends FieldValues>({
|
||||
@@ -43,84 +47,112 @@ function MultiSelect<T extends FieldValues>({
|
||||
ref,
|
||||
value,
|
||||
required,
|
||||
onSearch,
|
||||
onLoadMore,
|
||||
hasMore,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
}: MultiSelectProps<T>) {
|
||||
const id = useId();
|
||||
const error = errors && errors[name];
|
||||
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const [selectedItems, setSelectedItems] = useState<Option[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [show, setShow] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const trigger = useRef<HTMLDivElement | null>(null);
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
// Initialize selectedItems from external value + items (edit mode)
|
||||
useEffect(() => {
|
||||
if (initializedRef.current) return;
|
||||
if (!value || value.length === 0) return;
|
||||
const safeItems = Array.isArray(items) ? items : [];
|
||||
|
||||
const newOptions = safeItems.map((item) => ({
|
||||
...item,
|
||||
selected: value?.includes(item.value) ?? false,
|
||||
}));
|
||||
setOptions(newOptions);
|
||||
|
||||
const newSelectedIndices = safeItems
|
||||
.map((item, index) => (value?.includes(item.value) ? index : -1))
|
||||
.filter((index) => index !== -1);
|
||||
setSelected(newSelectedIndices);
|
||||
const found = safeItems.filter((item) => value.includes(item.value));
|
||||
if (found.length > 0) {
|
||||
setSelectedItems(found);
|
||||
initializedRef.current = true;
|
||||
}
|
||||
}, [items, value]);
|
||||
|
||||
const open = () => {
|
||||
if (!disabled) {
|
||||
setShow(true);
|
||||
// Reset when value is cleared externally (form reset)
|
||||
useEffect(() => {
|
||||
if (!value || value.length === 0) {
|
||||
setSelectedItems([]);
|
||||
initializedRef.current = false;
|
||||
}
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
const isOpen = () => {
|
||||
return show === true;
|
||||
// Update labels for selected items as new pages load
|
||||
useEffect(() => {
|
||||
const safeItems = Array.isArray(items) ? items : [];
|
||||
if (!safeItems.length) return;
|
||||
setSelectedItems((prev) => {
|
||||
const needsUpdate = prev.some((s) => {
|
||||
const found = safeItems.find((item) => item.value === s.value);
|
||||
return found && found.label !== s.label;
|
||||
});
|
||||
if (!needsUpdate) return prev;
|
||||
return prev.map((s) => {
|
||||
const found = safeItems.find((item) => item.value === s.value);
|
||||
return found ? { value: found.value, label: found.label } : s;
|
||||
});
|
||||
});
|
||||
}, [items]);
|
||||
|
||||
const selectedValueSet = new Set(selectedItems.map((i) => i.value));
|
||||
const options = (Array.isArray(items) ? items : []).map((item) => ({
|
||||
...item,
|
||||
selected: selectedValueSet.has(item.value),
|
||||
}));
|
||||
|
||||
const open = () => {
|
||||
if (!disabled) setShow(true);
|
||||
};
|
||||
|
||||
const combinedRef = (el: HTMLDivElement) => {
|
||||
trigger.current = el;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
}
|
||||
if (typeof ref === "function") ref(el);
|
||||
};
|
||||
|
||||
const handleSelect = (index: number) => {
|
||||
const handleSelect = (option: Option) => {
|
||||
if (disabled) return;
|
||||
|
||||
const newOptions = [...options];
|
||||
let newSelected = [...selected];
|
||||
|
||||
if (!newOptions[index].selected) {
|
||||
newOptions[index].selected = true;
|
||||
newSelected.push(index);
|
||||
let newSelected: Option[];
|
||||
if (selectedValueSet.has(option.value)) {
|
||||
newSelected = selectedItems.filter((i) => i.value !== option.value);
|
||||
} else {
|
||||
newOptions[index].selected = false;
|
||||
newSelected = newSelected.filter((i) => i !== index);
|
||||
newSelected = [...selectedItems, option];
|
||||
}
|
||||
|
||||
setOptions(newOptions);
|
||||
setSelected(newSelected);
|
||||
|
||||
const selectedValues = newSelected.map((i) => newOptions[i].value);
|
||||
onChange?.({ target: { name, value: selectedValues } });
|
||||
setSelectedItems(newSelected);
|
||||
onChange?.({ target: { name, value: newSelected.map((i) => i.value) } });
|
||||
};
|
||||
|
||||
const remove = (index: number, event: React.MouseEvent) => {
|
||||
const remove = (itemValue: string, event: React.MouseEvent) => {
|
||||
if (disabled) return;
|
||||
|
||||
event.stopPropagation();
|
||||
const newOptions = [...options];
|
||||
const newSelected = selected.filter((i) => i !== index);
|
||||
newOptions[index].selected = false;
|
||||
|
||||
setOptions(newOptions);
|
||||
setSelected(newSelected);
|
||||
|
||||
const selectedValues = newSelected.map((i) => newOptions[i].value);
|
||||
onChange?.({ target: { name, value: selectedValues } });
|
||||
const newSelected = selectedItems.filter((i) => i.value !== itemValue);
|
||||
setSelectedItems(newSelected);
|
||||
onChange?.({ target: { name, value: newSelected.map((i) => i.value) } });
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const q = e.target.value;
|
||||
setSearchQuery(q);
|
||||
onSearch?.(q);
|
||||
};
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (!listRef.current || !onLoadMore || !hasMore || isLoadingMore) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
|
||||
if (scrollHeight - scrollTop - clientHeight < 40) {
|
||||
onLoadMore();
|
||||
}
|
||||
}, [onLoadMore, hasMore, isLoadingMore]);
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
const clickHandler = ({ target }: MouseEvent) => {
|
||||
if (!dropdownRef.current) return;
|
||||
@@ -132,7 +164,6 @@ function MultiSelect<T extends FieldValues>({
|
||||
return;
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
document?.addEventListener("click", clickHandler);
|
||||
return () => document?.removeEventListener("click", clickHandler);
|
||||
}, [show]);
|
||||
@@ -163,16 +194,16 @@ function MultiSelect<T extends FieldValues>({
|
||||
data-active={active}
|
||||
>
|
||||
<div className="flex flex-1 flex-wrap items-center gap-2">
|
||||
{selected.map((index) => (
|
||||
{selectedItems.map((item) => (
|
||||
<span
|
||||
key={index}
|
||||
key={item.value}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-primary/10 px-2.5 py-1 text-sm font-medium text-primary dark:bg-primary/20"
|
||||
>
|
||||
{options[index]?.label}
|
||||
{item.label}
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => remove(index, e)}
|
||||
onClick={(e) => remove(item.value, e)}
|
||||
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-xs hover:bg-primary/20"
|
||||
>
|
||||
×
|
||||
@@ -180,10 +211,8 @@ function MultiSelect<T extends FieldValues>({
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
{selected.length === 0 && (
|
||||
<span className="text-dark-6 dark:text-dark-6">
|
||||
{placeholder}
|
||||
</span>
|
||||
{selectedItems.length === 0 && (
|
||||
<span className="text-dark-6 dark:text-dark-6">{placeholder}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -191,7 +220,7 @@ function MultiSelect<T extends FieldValues>({
|
||||
<svg
|
||||
className={cn(
|
||||
"fill-current text-dark-4 transition-transform duration-200 dark:text-dark-6",
|
||||
isOpen() && "rotate-180",
|
||||
show && "rotate-180",
|
||||
)}
|
||||
width="20"
|
||||
height="20"
|
||||
@@ -211,51 +240,78 @@ function MultiSelect<T extends FieldValues>({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 top-full z-40 mt-1 max-h-60 w-full overflow-y-auto rounded-lg border border-stroke bg-white shadow-lg dark:border-dark-3 dark:bg-dark-2",
|
||||
!isOpen() && "hidden",
|
||||
"absolute left-0 top-full z-40 mt-1 w-full rounded-lg border border-stroke bg-white shadow-lg dark:border-dark-3 dark:bg-dark-2",
|
||||
!show && "hidden",
|
||||
)}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div className="py-1">
|
||||
{options.length === 0 ? (
|
||||
{onSearch && (
|
||||
<div className="border-b border-stroke p-2 dark:border-dark-3">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={handleSearch}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
placeholder="Search..."
|
||||
className="w-full rounded-md border border-stroke bg-transparent px-3 py-1.5 text-sm outline-none focus:border-primary dark:border-dark-3 dark:text-white dark:focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={listRef}
|
||||
onScroll={handleScroll}
|
||||
className="max-h-52 overflow-y-auto py-1"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
) : options.length === 0 && !isLoadingMore ? (
|
||||
<div className="px-3 py-2 text-sm text-dark-6 dark:text-dark-6">
|
||||
No items are available
|
||||
</div>
|
||||
) : (
|
||||
options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center px-3 py-2 text-sm text-dark hover:bg-primary/5 dark:text-white dark:hover:bg-primary/5",
|
||||
option.selected && "bg-primary/10 dark:bg-primary/20",
|
||||
)}
|
||||
onClick={() => handleSelect(index)}
|
||||
>
|
||||
<>
|
||||
{options.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
"mr-3 h-4 w-4 rounded border-2",
|
||||
option.selected
|
||||
? "border-primary bg-primary"
|
||||
: "border-stroke dark:border-dark-3",
|
||||
"flex cursor-pointer items-center px-3 py-2 text-sm text-dark hover:bg-primary/5 dark:text-white dark:hover:bg-primary/5",
|
||||
option.selected && "bg-primary/10 dark:bg-primary/20",
|
||||
)}
|
||||
onClick={() => handleSelect(option)}
|
||||
>
|
||||
{option.selected && (
|
||||
<svg
|
||||
className="h-full w-full text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"mr-3 h-4 w-4 rounded border-2",
|
||||
option.selected
|
||||
? "border-primary bg-primary"
|
||||
: "border-stroke dark:border-dark-3",
|
||||
)}
|
||||
>
|
||||
{option.selected && (
|
||||
<svg
|
||||
className="h-full w-full text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{option.label}
|
||||
</div>
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
))}
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,11 @@ const CreateNotificationForm = () => {
|
||||
recipient,
|
||||
onSubmit,
|
||||
onSelectTarget,
|
||||
onSearchRecipient,
|
||||
onLoadMoreRecipients,
|
||||
hasMoreRecipients,
|
||||
isLoadingMoreRecipients,
|
||||
isLoadingRecipients,
|
||||
priorities,
|
||||
targets,
|
||||
types,
|
||||
@@ -85,6 +90,11 @@ const CreateNotificationForm = () => {
|
||||
recipient && recipient.length > 0 ? "Choose recipients" : ""
|
||||
}
|
||||
name="recipient"
|
||||
onSearch={recipient ? onSearchRecipient : undefined}
|
||||
onLoadMore={onLoadMoreRecipients}
|
||||
hasMore={hasMoreRecipients}
|
||||
isLoading={isLoadingRecipients}
|
||||
isLoadingMore={isLoadingMoreRecipients}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -6,20 +6,29 @@ import {
|
||||
NotificationTypes,
|
||||
} from "@/constants/enums";
|
||||
import {
|
||||
useCompanyFullList,
|
||||
useGetAllCustomers,
|
||||
useGetUsersQuery,
|
||||
useAdminListInfiniteQuery,
|
||||
useCompaniesInfiniteQuery,
|
||||
useCustomersInfiniteQuery,
|
||||
} from "@/hooks/queries";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { useCreateNotificationQuery } from "@/hooks/queries/useNotificationQueries";
|
||||
import { TCreateNotificationCredentials } from "@/lib/api/types/NotificationHistory";
|
||||
import { ILabelValue } from "@/types/shared";
|
||||
import { getEnumAsArray } from "@/utils/shared";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { ChangeEvent, useCallback, useMemo, useState } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
const ROLE_ITEMS: ILabelValue[] = [
|
||||
{ label: "Admin", value: "admin" },
|
||||
{ label: "Analyst", value: "analyst" },
|
||||
];
|
||||
|
||||
const useCreateNotificationFormPresenter = () => {
|
||||
const [recipient, setRecipient] = useState<ILabelValue[] | null>(null);
|
||||
const [target, setTarget] = useState<NotificationTargetGroups | null>(null);
|
||||
const [recipientSearch, setRecipientSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(recipientSearch, 400);
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
@@ -30,13 +39,128 @@ const useCreateNotificationFormPresenter = () => {
|
||||
control,
|
||||
} = useForm<TCreateNotificationCredentials>();
|
||||
const router = useRouter();
|
||||
const { data: companies } = useCompanyFullList();
|
||||
const { data: users } = useGetUsersQuery();
|
||||
const { data: customers } = useGetAllCustomers();
|
||||
|
||||
const isCompany = target === NotificationTargetGroups.SPECIFIC_COMPANY;
|
||||
const isUser = target === NotificationTargetGroups.SPECIFIC_USERS;
|
||||
const isCustomer = target === NotificationTargetGroups.SPECIFIC_CUSTOMER;
|
||||
|
||||
const searchParam = debouncedSearch ? { search: debouncedSearch } : undefined;
|
||||
|
||||
const companiesQuery = useCompaniesInfiniteQuery(searchParam, {
|
||||
enabled: isCompany,
|
||||
});
|
||||
const usersQuery = useAdminListInfiniteQuery(searchParam, {
|
||||
enabled: isUser,
|
||||
});
|
||||
const customersQuery = useCustomersInfiniteQuery(searchParam, {
|
||||
enabled: isCustomer,
|
||||
});
|
||||
|
||||
const companiesItems = useMemo(() => {
|
||||
if (!companiesQuery.data) return null;
|
||||
return companiesQuery.data.pages.flatMap(
|
||||
(page) =>
|
||||
page.data.companies?.map((c) => ({ label: c.name, value: c.id })) ?? [],
|
||||
);
|
||||
}, [companiesQuery.data]);
|
||||
|
||||
const usersItems = useMemo(() => {
|
||||
if (!usersQuery.data) return null;
|
||||
return usersQuery.data.pages.flatMap(
|
||||
(page) =>
|
||||
page.data.users?.map((u) => ({
|
||||
label: u.full_name ?? u.username ?? u.id,
|
||||
value: u.id,
|
||||
})) ?? [],
|
||||
);
|
||||
}, [usersQuery.data]);
|
||||
|
||||
const customersItems = useMemo(() => {
|
||||
if (!customersQuery.data) return null;
|
||||
return customersQuery.data.pages.flatMap(
|
||||
(page) =>
|
||||
page.data.customers?.map((c) => ({
|
||||
label: c.full_name,
|
||||
value: c.id,
|
||||
})) ?? [],
|
||||
);
|
||||
}, [customersQuery.data]);
|
||||
|
||||
const recipient = useMemo((): ILabelValue[] | null => {
|
||||
if (!target) return null;
|
||||
const map: Partial<Record<NotificationTargetGroups, ILabelValue[] | null>> =
|
||||
{
|
||||
[NotificationTargetGroups.SPECIFIC_COMPANY]: companiesItems,
|
||||
[NotificationTargetGroups.SPECIFIC_USERS]: usersItems,
|
||||
[NotificationTargetGroups.SPECIFIC_CUSTOMER]: customersItems,
|
||||
[NotificationTargetGroups.SPECIFIC_ROLE]: ROLE_ITEMS,
|
||||
};
|
||||
return map[target] ?? null;
|
||||
}, [target, companiesItems, usersItems, customersItems]);
|
||||
|
||||
const hasMoreRecipients = useMemo(() => {
|
||||
if (isCompany) return companiesQuery.hasNextPage;
|
||||
if (isUser) return usersQuery.hasNextPage;
|
||||
if (isCustomer) return customersQuery.hasNextPage;
|
||||
return false;
|
||||
}, [
|
||||
isCompany,
|
||||
isUser,
|
||||
isCustomer,
|
||||
companiesQuery.hasNextPage,
|
||||
usersQuery.hasNextPage,
|
||||
customersQuery.hasNextPage,
|
||||
]);
|
||||
|
||||
const isLoadingMoreRecipients = useMemo(() => {
|
||||
if (isCompany) return companiesQuery.isFetchingNextPage;
|
||||
if (isUser) return usersQuery.isFetchingNextPage;
|
||||
if (isCustomer) return customersQuery.isFetchingNextPage;
|
||||
return false;
|
||||
}, [
|
||||
isCompany,
|
||||
isUser,
|
||||
isCustomer,
|
||||
companiesQuery.isFetchingNextPage,
|
||||
usersQuery.isFetchingNextPage,
|
||||
customersQuery.isFetchingNextPage,
|
||||
]);
|
||||
|
||||
// True during initial/search fetches (not load-more)
|
||||
const isLoadingRecipients = useMemo(() => {
|
||||
if (isCompany)
|
||||
return companiesQuery.isFetching && !companiesQuery.isFetchingNextPage;
|
||||
if (isUser) return usersQuery.isFetching && !usersQuery.isFetchingNextPage;
|
||||
if (isCustomer)
|
||||
return customersQuery.isFetching && !customersQuery.isFetchingNextPage;
|
||||
return false;
|
||||
}, [
|
||||
isCompany,
|
||||
isUser,
|
||||
isCustomer,
|
||||
companiesQuery.isFetching,
|
||||
companiesQuery.isFetchingNextPage,
|
||||
usersQuery.isFetching,
|
||||
usersQuery.isFetchingNextPage,
|
||||
customersQuery.isFetching,
|
||||
customersQuery.isFetchingNextPage,
|
||||
]);
|
||||
|
||||
const onLoadMoreRecipients = useCallback(() => {
|
||||
if (isCompany) companiesQuery.fetchNextPage();
|
||||
else if (isUser) usersQuery.fetchNextPage();
|
||||
else if (isCustomer) customersQuery.fetchNextPage();
|
||||
}, [isCompany, isUser, isCustomer, companiesQuery, usersQuery, customersQuery]);
|
||||
|
||||
const onSearchRecipient = useCallback((query: string) => {
|
||||
setRecipientSearch(query);
|
||||
}, []);
|
||||
|
||||
const { mutate } = useCreateNotificationQuery(() => {
|
||||
router.back();
|
||||
reset();
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<TCreateNotificationCredentials> = (data) => {
|
||||
const credentials = {
|
||||
...data,
|
||||
@@ -44,6 +168,7 @@ const useCreateNotificationFormPresenter = () => {
|
||||
};
|
||||
mutate({ credentials });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
reset();
|
||||
router.back();
|
||||
@@ -51,33 +176,11 @@ const useCreateNotificationFormPresenter = () => {
|
||||
|
||||
const onSelectTarget = (event: ChangeEvent<HTMLSelectElement>): void => {
|
||||
const type = event.target.value as NotificationTargetGroups;
|
||||
|
||||
const recipientMaps: Partial<
|
||||
Record<NotificationTargetGroups, Array<ILabelValue> | null>
|
||||
> = {
|
||||
[NotificationTargetGroups.SPECIFIC_COMPANY]:
|
||||
companies?.data.companies?.map((company) => ({
|
||||
label: company.name,
|
||||
value: company.id,
|
||||
})) ?? null,
|
||||
[NotificationTargetGroups.SPECIFIC_ROLE]: [
|
||||
{ label: "Admin", value: "admin" },
|
||||
{ label: "Analyst", value: "analyst" },
|
||||
],
|
||||
[NotificationTargetGroups.SPECIFIC_USERS]:
|
||||
users?.data.users?.map((user) => ({
|
||||
label: user.full_name ?? "",
|
||||
value: user.id,
|
||||
})) ?? null,
|
||||
[NotificationTargetGroups.SPECIFIC_CUSTOMER]:
|
||||
customers?.data.customers?.map((customer) => ({
|
||||
label: customer.full_name,
|
||||
value: customer.id,
|
||||
})) ?? null,
|
||||
};
|
||||
|
||||
setRecipient(recipientMaps[type] ?? null);
|
||||
setTarget(type);
|
||||
setRecipientSearch("");
|
||||
setValue("recipient", undefined);
|
||||
};
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
watch,
|
||||
@@ -92,6 +195,11 @@ const useCreateNotificationFormPresenter = () => {
|
||||
onSubmit,
|
||||
control,
|
||||
recipient,
|
||||
onSearchRecipient,
|
||||
onLoadMoreRecipients,
|
||||
hasMoreRecipients,
|
||||
isLoadingMoreRecipients,
|
||||
isLoadingRecipients,
|
||||
targets: getEnumAsArray(NotificationTargetGroups),
|
||||
types: getEnumAsArray(NotificationTypes),
|
||||
channels: getEnumAsArray(NotificationChannels),
|
||||
|
||||
@@ -103,7 +103,11 @@ export const useCompaniesQuery = (params?: Record<string, any>) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCompaniesInfiniteQuery = (params?: Record<string, any>) => {
|
||||
export const useCompaniesInfiniteQuery = (
|
||||
params?: Record<string, any>,
|
||||
options?: { enabled?: boolean },
|
||||
) => {
|
||||
const enabled = options?.enabled ?? true;
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.COMPANIES.READ_ALL, params],
|
||||
[params],
|
||||
@@ -116,6 +120,7 @@ export const useCompaniesInfiniteQuery = (params?: Record<string, any>) => {
|
||||
companiesService.getCompanies({ ...params, offset: pageParam }),
|
||||
|
||||
initialPageParam: 0,
|
||||
enabled,
|
||||
|
||||
getNextPageParam: (lastPage: any) => {
|
||||
const { offset, limit, total } = lastPage.meta;
|
||||
|
||||
@@ -12,7 +12,11 @@ import { toast } from "react-toastify";
|
||||
import { API_ENDPOINTS } from "@/lib/api";
|
||||
import { customersService } from "@/lib/api/services/customers-service";
|
||||
|
||||
export const useCustomersInfiniteQuery = (params?: Record<string, any>) => {
|
||||
export const useCustomersInfiniteQuery = (
|
||||
params?: Record<string, any>,
|
||||
options?: { enabled?: boolean },
|
||||
) => {
|
||||
const enabled = options?.enabled ?? true;
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.CUSTOMERS.READ_ALL, params],
|
||||
[params],
|
||||
@@ -25,6 +29,7 @@ export const useCustomersInfiniteQuery = (params?: Record<string, any>) => {
|
||||
customersService.getCustomers({ ...params, offset: pageParam }),
|
||||
|
||||
initialPageParam: 0,
|
||||
enabled,
|
||||
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (!lastPage.meta) return;
|
||||
|
||||
@@ -100,7 +100,11 @@ export const useAdminDetails = (id: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useAdminListInfiniteQuery = (params?: Record<string, any>) => {
|
||||
export const useAdminListInfiniteQuery = (
|
||||
params?: Record<string, any>,
|
||||
options?: { enabled?: boolean },
|
||||
) => {
|
||||
const enabled = options?.enabled ?? true;
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.USER.ADMINS, params], [params]);
|
||||
|
||||
return useInfiniteQuery({
|
||||
@@ -110,6 +114,7 @@ export const useAdminListInfiniteQuery = (params?: Record<string, any>) => {
|
||||
userService.adminsList({ ...params, offset: pageParam }),
|
||||
|
||||
initialPageParam: 0,
|
||||
enabled,
|
||||
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (!lastPage.meta) {
|
||||
|
||||
Reference in New Issue
Block a user