Merge branch 'feat/select-input-lazy-loading' into 'development'

[FEAT/FE] Implement SelectInput lazy loading

See merge request mbugroup/lti-web-client!173
This commit is contained in:
Rivaldi A N S
2026-01-14 04:53:27 +00:00
7 changed files with 230 additions and 201 deletions
+112 -18
View File
@@ -9,15 +9,20 @@ import Select, {
SingleValue, SingleValue,
components as ReactSelectComponents, components as ReactSelectComponents,
ControlProps, ControlProps,
MenuListProps,
} from 'react-select'; } from 'react-select';
import CreatableSelect from 'react-select/creatable'; import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated'; import makeAnimated from 'react-select/animated';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { cn, getByPath } from '@/lib/helper'; import { cn, getByPath } from '@/lib/helper';
import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite';
import { httpClientFetcher } from '@/services/http/client'; import { httpClientFetcher } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general'; import {
import { isResponseSuccess } from '@/lib/api-helper'; BaseApiResponse,
ErrorApiResponse,
SuccessApiResponse,
} from '@/types/api/api-general';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
export interface OptionType { export interface OptionType {
value: string | number; value: string | number;
@@ -56,6 +61,7 @@ interface SelectInputBaseProps<T = OptionType> {
onInputChange?: (search: string) => void; onInputChange?: (search: string) => void;
startAdornment?: ReactNode; startAdornment?: ReactNode;
menuPortalTarget?: HTMLElement | null; menuPortalTarget?: HTMLElement | null;
onMenuScrollToBottom?: ((event: WheelEvent | TouchEvent) => void) | undefined;
} }
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> { interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
@@ -93,6 +99,29 @@ const CustomControl = <
); );
}; };
const CustomMenuList = <
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>,
>(
props: MenuListProps<Option, IsMulti, Group>
) => {
const { children, selectProps, options } = props;
const { isLoading } = selectProps;
return (
<ReactSelectComponents.MenuList {...props}>
{children}
{options.length > 0 && isLoading && (
<div className='mt-1 px-3 py-2 rounded-md text-center text-gray-400'>
<span className='loading loading-spinner loading-md' />
</div>
)}
</ReactSelectComponents.MenuList>
);
};
const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => { const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const { const {
label, label,
@@ -119,6 +148,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
onInputChange, onInputChange,
startAdornment, startAdornment,
menuPortalTarget, menuPortalTarget,
onMenuScrollToBottom,
} = props; } = props;
const [internalInputValue, setInternalInputValue] = useState(''); const [internalInputValue, setInternalInputValue] = useState('');
@@ -256,6 +286,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
components={{ components={{
...components, ...components,
...(optionComponent ? { Option: optionComponent } : {}), ...(optionComponent ? { Option: optionComponent } : {}),
MenuList: CustomMenuList,
}} }}
{...(startAdornment && { {...(startAdornment && {
shouldShowAdornment, shouldShowAdornment,
@@ -269,6 +300,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
styles={{ styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }), menuPortal: (base) => ({ ...base, zIndex: 9999 }),
}} }}
onMenuScrollToBottom={onMenuScrollToBottom}
/> />
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>} {isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
@@ -288,34 +320,96 @@ const useSelect = <T,>(
) => { ) => {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const optionsUrlParams = useMemo(() => { const pageKey = 'page';
return new URLSearchParams({ const limitKey = 'limit';
const limit = params?.['limit'] ?? 10;
const getKey = (
pageIndex: number,
previousPageData?: BaseApiResponse<T[]>
) => {
// stop when backend says no more pages
if (previousPageData && isResponseSuccess(previousPageData)) {
const meta = previousPageData.meta;
if (meta && meta.page >= meta.total_pages) return null;
}
const qs = new URLSearchParams({
...(params ?? {}),
[searchKey]: inputValue ?? '', [searchKey]: inputValue ?? '',
...params, [pageKey]: String(pageIndex + 1),
[limitKey]: String(limit),
}).toString(); }).toString();
}, [inputValue, searchKey, params]);
const optionsUrl = `${basePath}?${optionsUrlParams}`; return `${basePath}?${qs}`;
};
const { data, isLoading } = useSWR(optionsUrl, async (url) => { const {
return await httpClientFetcher<BaseApiResponse<T[]>>(url); data: pages,
}); isLoading,
isValidating,
size,
setSize,
} = useSWRInfinite<BaseApiResponse<T[]>>(getKey, (url) =>
httpClientFetcher<BaseApiResponse<T[]>>(url)
);
const options = isResponseSuccess(data) const options = useMemo(() => {
? data.data.map((item) => { if (!pages) return [];
return {
return pages.flatMap((page) =>
isResponseSuccess(page)
? page.data.map((item) => ({
value: getByPath<T, number>(item, valueKey as string), value: getByPath<T, number>(item, valueKey as string),
label: getByPath<T, string>(item, labelKey as string), label: getByPath<T, string>(item, labelKey as string),
}))
: []
);
}, [pages, valueKey, labelKey]);
const lastPage = pages?.[pages.length - 1];
const hasMore =
!!lastPage &&
isResponseSuccess(lastPage) &&
!!lastPage.meta &&
lastPage.meta.page < lastPage.meta.total_pages;
const loadMore = () => {
if (!hasMore) return;
setSize(size + 1);
}; };
})
: []; let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
if (isResponseSuccess(pages?.[latestPagesIndex])) {
formattedSuccessRawData = {
...pages?.[latestPagesIndex],
data:
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ??
[],
};
}
if (isResponseError(pages?.[latestPagesIndex])) {
formattedErrorRawData = pages?.[latestPagesIndex];
}
return { return {
inputValue, inputValue,
setInputValue, setInputValue,
options, options,
isLoadingOptions: isLoading, rawData: isResponseSuccess(pages?.[latestPagesIndex])
rawData: data, ? formattedSuccessRawData
: formattedErrorRawData,
isLoadingOptions: isLoading || isValidating,
isLoadingMore: isValidating && size > 1,
hasMore,
loadMore,
}; };
}; };
@@ -23,13 +23,17 @@ import TextInput from '@/components/input/TextInput';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import TextArea from '@/components/input/TextArea'; import TextArea from '@/components/input/TextArea';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import useSWR from 'swr'; import useSWR from 'swr';
import { UserApi } from '@/services/api/user'; import { UserApi } from '@/services/api/user';
import { TYPE_OPTIONS } from '@/config/constant'; import { TYPE_OPTIONS } from '@/config/constant';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { User } from '@/types/api/api-general';
interface CustomerFormProps { interface CustomerFormProps {
formType?: 'add' | 'edit' | 'detail'; formType?: 'add' | 'edit' | 'detail';
@@ -47,25 +51,15 @@ const CustomerForm = ({
// Setup State // Setup State
const [customerFormErrorMessage, setCustomerFormErrorMessage] = useState(''); const [customerFormErrorMessage, setCustomerFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [picSelectInputValue, setPicSelectInputValue] = useState('');
// Fetch Data const {
const picUrl = `${UserApi.basePath}?${new URLSearchParams({ setInputValue: setPicSelectInputValue,
search: picSelectInputValue ?? '', options: picOptions,
})}`; isLoadingOptions: isLoadingPicOptions,
loadMore: loadMorePic,
const { data: pic, isLoading: isLoadingPic } = useSWR( } = useSelect<User>(UserApi.basePath, 'id', 'name');
picUrl,
UserApi.getAllFetcher
);
// -- Options data mapping // -- Options data mapping
const picOptions = isResponseSuccess(pic)
? pic?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const typeOptions = TYPE_OPTIONS; const typeOptions = TYPE_OPTIONS;
// Handler Event // Handler Event
@@ -240,11 +234,12 @@ const CustomerForm = ({
required required
placeholder='Pilih PIC' placeholder='Pilih PIC'
label='PIC' label='PIC'
value={formik.values.pic ?? undefined} value={formik.values.pic?.value ? formik.values.pic : undefined}
onChange={picChangeHandler} onChange={picChangeHandler}
options={picOptions} options={picOptions}
onInputChange={setPicSelectInputValue} onInputChange={setPicSelectInputValue}
isLoading={isLoadingPic} onMenuScrollToBottom={loadMorePic}
isLoading={isLoadingPicOptions}
isError={formik.touched.picId && Boolean(formik.errors.picId)} isError={formik.touched.picId && Boolean(formik.errors.picId)}
errorMessage={formik.errors.picId as string} errorMessage={formik.errors.picId as string}
isDisabled={formType === 'detail'} isDisabled={formType === 'detail'}
@@ -9,7 +9,10 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
@@ -31,6 +34,7 @@ import { UserApi } from '@/services/api/user';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { User } from '@/types/api/api-general';
interface KandangFormProps { interface KandangFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -128,23 +132,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
// location // location
const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); const {
setInputValue: setLocationSelectInputValue,
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ options: locationOptions,
search: locationSelectInputValue ?? '', isLoadingOptions: isLoadingLocationOptions,
}).toString()}`; loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const { data: locations, isLoading: isLoadingLocations } = useSWR(
locationsUrl,
LocationApi.getAllFetcher
);
const locationOptions = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('location', true); formik.setFieldTouched('location', true);
@@ -155,23 +148,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
}; };
// PIC // PIC
const [picSelectInputValue, setPicSelectInputValue] = useState(''); const {
setInputValue: setPicSelectInputValue,
const picsUrl = `${UserApi.basePath}?${new URLSearchParams({ options: picOptions,
search: picSelectInputValue ?? '', isLoadingOptions: isLoadingPicOptions,
}).toString()}`; loadMore: loadMorePics,
} = useSelect<User>(UserApi.basePath, 'id', 'name');
const { data: pics, isLoading: isLoadingPics } = useSWR(
picsUrl,
LocationApi.getAllFetcher
);
const picOptions = isResponseSuccess(pics)
? pics?.data.map((pic) => ({
value: pic.id,
label: pic.name,
}))
: [];
const picChangeHandler = (val: OptionType | OptionType[] | null) => { const picChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('pic', true); formik.setFieldTouched('pic', true);
@@ -249,7 +231,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
onChange={locationChangeHandler} onChange={locationChangeHandler}
options={locationOptions} options={locationOptions}
onInputChange={setLocationSelectInputValue} onInputChange={setLocationSelectInputValue}
isLoading={isLoadingLocations} onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocationOptions}
isError={ isError={
formik.touched.locationId && Boolean(formik.errors.locationId) formik.touched.locationId && Boolean(formik.errors.locationId)
} }
@@ -280,7 +263,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
onChange={picChangeHandler} onChange={picChangeHandler}
options={picOptions} options={picOptions}
onInputChange={setPicSelectInputValue} onInputChange={setPicSelectInputValue}
isLoading={isLoadingPics} onMenuScrollToBottom={loadMorePics}
isLoading={isLoadingPicOptions}
isError={formik.touched.picId && Boolean(formik.errors.picId)} isError={formik.touched.picId && Boolean(formik.errors.picId)}
errorMessage={formik.errors.picId as string} errorMessage={formik.errors.picId as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
@@ -9,7 +9,10 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
@@ -29,6 +32,7 @@ import { AreaApi, LocationApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { Area } from '@/types/api/master-data/area';
interface LocationFormProps { interface LocationFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -117,23 +121,12 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); const {
setInputValue: setAreaSelectInputValue,
const areasUrl = `${AreaApi.basePath}?${new URLSearchParams({ options: areaOptions,
search: areaSelectInputValue ?? '', isLoadingOptions: isLoadingAreaOptions,
}).toString()}`; loadMore: loadMoreAreas,
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
const { data: areas, isLoading: isLoadingAreas } = useSWR(
areasUrl,
AreaApi.getAllFetcher
);
const areaOptions = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const areaChangeHandler = (val: OptionType | OptionType[] | null) => { const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('area', true); formik.setFieldTouched('area', true);
@@ -224,7 +217,8 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
onChange={areaChangeHandler} onChange={areaChangeHandler}
options={areaOptions} options={areaOptions}
onInputChange={setAreaSelectInputValue} onInputChange={setAreaSelectInputValue}
isLoading={isLoadingAreas} onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreaOptions}
isError={formik.touched.areaId && Boolean(formik.errors.areaId)} isError={formik.touched.areaId && Boolean(formik.errors.areaId)}
errorMessage={formik.errors.areaId as string} errorMessage={formik.errors.areaId as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
@@ -9,7 +9,10 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
@@ -31,6 +34,8 @@ import { flags } from '@/types/api/api-general';
import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant'; import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { Supplier } from '@/types/api/master-data/supplier';
import { Uom } from '@/types/api/master-data/uom';
interface NonstockFormProps { interface NonstockFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -129,23 +134,12 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
// UOM // UOM
const [uomSelectInputValue, setUomSelectInputValue] = useState(''); const {
setInputValue: setUomSelectInputValue,
const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ options: uomOptions,
search: uomSelectInputValue ?? '', isLoadingOptions: isLoadingUomOptions,
}).toString()}`; loadMore: loadMoreUoms,
} = useSelect<Uom>(UomApi.basePath, 'id', 'name');
const { data: uoms, isLoading: isLoadingUoms } = useSWR(
uomsUrl,
UomApi.getAllFetcher
);
const uomOptions = isResponseSuccess(uoms)
? uoms?.data.map((uom) => ({
value: uom.id,
label: uom.name,
}))
: [];
const uomChangeHandler = (val: OptionType | OptionType[] | null) => { const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('uom', true); formik.setFieldTouched('uom', true);
@@ -156,25 +150,12 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
}; };
// supplier // supplier
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); const {
setInputValue: setSupplierSelectInputValue,
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ options: supplierOptions,
search: supplierSelectInputValue ?? '', isLoadingOptions: isLoadingSupplierOptions,
}).toString()}`; loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
suppliersUrl,
SupplierApi.getAllFetcher
);
const supplierOptions = isResponseSuccess(suppliers)
? suppliers?.data
.filter((sup) => sup.category === 'BOP')
.map((supplier) => ({
value: supplier.id,
label: supplier.name,
}))
: [];
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('suppliers', true); formik.setFieldTouched('suppliers', true);
@@ -264,7 +245,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
onChange={uomChangeHandler} onChange={uomChangeHandler}
options={uomOptions} options={uomOptions}
onInputChange={setUomSelectInputValue} onInputChange={setUomSelectInputValue}
isLoading={isLoadingUoms} isLoading={isLoadingUomOptions}
onMenuScrollToBottom={loadMoreUoms}
isError={formik.touched.uomId && Boolean(formik.errors.uomId)} isError={formik.touched.uomId && Boolean(formik.errors.uomId)}
errorMessage={formik.errors.uomId as string} errorMessage={formik.errors.uomId as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
@@ -278,7 +260,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
options={supplierOptions ?? []} options={supplierOptions ?? []}
onInputChange={setSupplierSelectInputValue} onInputChange={setSupplierSelectInputValue}
isLoading={isLoadingSuppliers} onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSupplierOptions}
isError={ isError={
formik.touched.suppliers && Boolean(formik.errors.suppliers) formik.touched.suppliers && Boolean(formik.errors.suppliers)
} }
@@ -40,6 +40,7 @@ import {
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant'; import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import { Supplier } from '@/types/api/master-data/supplier';
interface ProductFormProps { interface ProductFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -145,6 +146,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
setInputValue: setUomSelectInputValue, setInputValue: setUomSelectInputValue,
options: uomOptions, options: uomOptions,
isLoadingOptions: isLoadingUoms, isLoadingOptions: isLoadingUoms,
loadMore: loadMoreUoms,
} = useSelect(UomApi.basePath, 'id', 'name'); } = useSelect(UomApi.basePath, 'id', 'name');
const uomChangeHandler = (val: OptionType | OptionType[] | null) => { const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('uom', true); formik.setFieldTouched('uom', true);
@@ -158,6 +160,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
setInputValue: setCategorySelectInputValue, setInputValue: setCategorySelectInputValue,
options: categoryOptions, options: categoryOptions,
isLoadingOptions: isLoadingCategories, isLoadingOptions: isLoadingCategories,
loadMore: loadMoreCategories,
} = useSelect(ProductCategoryApi.basePath, 'id', 'name'); } = useSelect(ProductCategoryApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('product_category', true); formik.setFieldTouched('product_category', true);
@@ -167,17 +170,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
}; };
// Supplier (multi select) - using SWR to filter by category // Supplier (multi select) - using SWR to filter by category
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); const {
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`; setInputValue: setSupplierSelectInputValue,
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( options: supplierOptions,
suppliersUrl, isLoadingOptions: isLoadingSuppliers,
SupplierApi.getAllFetcher loadMore: loadMoreSuppliers,
); } = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
const supplierOptions = isResponseSuccess(suppliers) category: 'SAPRONAK',
? suppliers?.data });
.filter((sup) => sup.category === 'SAPRONAK')
.map((sup) => ({ value: sup.id, label: sup.name }))
: [];
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
const arr = Array.isArray(val) ? val : val ? [val] : []; const arr = Array.isArray(val) ? val : val ? [val] : [];
formik.setFieldTouched('supplier_ids', true); formik.setFieldTouched('supplier_ids', true);
@@ -291,6 +292,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
onChange={uomChangeHandler} onChange={uomChangeHandler}
options={uomOptions} options={uomOptions}
onInputChange={setUomSelectInputValue} onInputChange={setUomSelectInputValue}
onMenuScrollToBottom={loadMoreUoms}
isLoading={isLoadingUoms} isLoading={isLoadingUoms}
isError={ isError={
(formik.touched.uom || formik.touched.uom_id) && (formik.touched.uom || formik.touched.uom_id) &&
@@ -308,6 +310,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
onChange={categoryChangeHandler} onChange={categoryChangeHandler}
options={categoryOptions} options={categoryOptions}
onInputChange={setCategorySelectInputValue} onInputChange={setCategorySelectInputValue}
onMenuScrollToBottom={loadMoreCategories}
isLoading={isLoadingCategories} isLoading={isLoadingCategories}
isError={ isError={
(formik.touched.product_category || (formik.touched.product_category ||
@@ -412,6 +415,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
options={supplierOptions} options={supplierOptions}
onInputChange={setSupplierSelectInputValue} onInputChange={setSupplierSelectInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSuppliers} isLoading={isLoadingSuppliers}
isError={ isError={
formik.touched.supplier_ids && formik.touched.supplier_ids &&
@@ -9,7 +9,10 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
@@ -35,6 +38,8 @@ import { cn } from '@/lib/helper';
import { WAREHOUSE_TYPE_OPTIONS } from '@/config/constant'; import { WAREHOUSE_TYPE_OPTIONS } from '@/config/constant';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { Area } from '@/types/api/master-data/area';
import { Kandang } from '@/types/api/master-data/kandang';
interface WarehouseFormProps { interface WarehouseFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -221,61 +226,28 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
// Area // Area
const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); const {
setInputValue: setAreaSelectInputValue,
const areasUrl = `${AreaApi.basePath}?${new URLSearchParams({ options: areaOptions,
search: areaSelectInputValue ?? '', isLoadingOptions: isLoadingAreaOptions,
}).toString()}`; loadMore: loadMoreAreas,
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
const { data: areas, isLoading: isLoadingAreas } = useSWR(
areasUrl,
AreaApi.getAllFetcher
);
const areaOptions = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
// Location // Location
const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); const {
setInputValue: setLocationSelectInputValue,
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ options: locationOptions,
search: locationSelectInputValue ?? '', isLoadingOptions: isLoadingLocationOptions,
}).toString()}`; loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const { data: locations, isLoading: isLoadingLocations } = useSWR(
locationsUrl,
LocationApi.getAllFetcher
);
const locationOptions = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
// Kandang // Kandang
const [kandangSelectInputValue, setKandangSelectInputValue] = useState(''); const {
setInputValue: setKandangSelectInputValue,
const kandangsUrl = `${KandangApi.basePath}?${new URLSearchParams({ options: kandangOptions,
search: kandangSelectInputValue ?? '', isLoadingOptions: isLoadingKandangOptions,
}).toString()}`; loadMore: loadMoreKandangs,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const { data: kandangs, isLoading: isLoadingKandangs } = useSWR(
kandangsUrl,
KandangApi.getAllFetcher
);
const kandangOptions = isResponseSuccess(kandangs)
? kandangs?.data.map((kandang) => ({
value: kandang.id,
label: kandang.name,
}))
: [];
const typeChangeHandler = (val: OptionType | OptionType[] | null) => { const typeChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('type', true); formik.setFieldTouched('type', true);
@@ -393,7 +365,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
onChange={areaChangeHandler} onChange={areaChangeHandler}
options={areaOptions} options={areaOptions}
onInputChange={setAreaSelectInputValue} onInputChange={setAreaSelectInputValue}
isLoading={isLoadingAreas} onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreaOptions}
isError={formik.touched.areaId && Boolean(formik.errors.areaId)} isError={formik.touched.areaId && Boolean(formik.errors.areaId)}
errorMessage={formik.errors.areaId as string} errorMessage={formik.errors.areaId as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
@@ -409,7 +382,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
onChange={locationChangeHandler} onChange={locationChangeHandler}
options={locationOptions} options={locationOptions}
onInputChange={setLocationSelectInputValue} onInputChange={setLocationSelectInputValue}
isLoading={isLoadingLocations} onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocationOptions}
isError={ isError={
formik.touched.locationId && Boolean(formik.errors.locationId) formik.touched.locationId && Boolean(formik.errors.locationId)
} }
@@ -427,7 +401,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
options={kandangOptions} options={kandangOptions}
onInputChange={setKandangSelectInputValue} onInputChange={setKandangSelectInputValue}
isLoading={isLoadingKandangs} onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangOptions}
isError={ isError={
formik.touched.kandangId && Boolean(formik.errors.kandangId) formik.touched.kandangId && Boolean(formik.errors.kandangId)
} }