mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
refactor(FE): Support inputPrefix/inputSuffix on SelectInput
This commit is contained in:
@@ -54,6 +54,9 @@ interface SelectInputBaseProps<T = OptionType> {
|
|||||||
wrapper?: string;
|
wrapper?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
select?: string;
|
select?: string;
|
||||||
|
inputPrefix?: string;
|
||||||
|
inputSuffix?: string;
|
||||||
|
inputPrefixSuffixWrapper?: string;
|
||||||
};
|
};
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
@@ -62,6 +65,8 @@ interface SelectInputBaseProps<T = OptionType> {
|
|||||||
delay?: number;
|
delay?: number;
|
||||||
onInputChange?: (search: string) => void;
|
onInputChange?: (search: string) => void;
|
||||||
startAdornment?: ReactNode;
|
startAdornment?: ReactNode;
|
||||||
|
inputPrefix?: ReactNode;
|
||||||
|
inputSuffix?: ReactNode;
|
||||||
menuPortalTarget?: HTMLElement | null;
|
menuPortalTarget?: HTMLElement | null;
|
||||||
closeMenuOnSelect?: boolean;
|
closeMenuOnSelect?: boolean;
|
||||||
hideSelectedOptions?: boolean;
|
hideSelectedOptions?: boolean;
|
||||||
@@ -153,6 +158,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
createables = false,
|
createables = false,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
startAdornment,
|
startAdornment,
|
||||||
|
inputPrefix,
|
||||||
|
inputSuffix,
|
||||||
menuPortalTarget,
|
menuPortalTarget,
|
||||||
closeMenuOnSelect,
|
closeMenuOnSelect,
|
||||||
hideSelectedOptions,
|
hideSelectedOptions,
|
||||||
@@ -227,6 +234,155 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{inputPrefix || inputSuffix ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex text-sm',
|
||||||
|
className?.inputPrefixSuffixWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputPrefix && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||||
|
{
|
||||||
|
'bg-gray-100 border-base-content/10': !isDisabled,
|
||||||
|
'bg-gray-50 border-base-content/10': isDisabled,
|
||||||
|
'border-error': isError,
|
||||||
|
},
|
||||||
|
className?.inputPrefix
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputPrefix}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectComponent<T, boolean, GroupBase<T>>
|
||||||
|
instanceId='select'
|
||||||
|
value={value ?? (isMulti ? [] : null)}
|
||||||
|
onChange={onChange ? handleChange : undefined}
|
||||||
|
options={options}
|
||||||
|
menuIsOpen={openMenu}
|
||||||
|
inputValue={internalInputValue}
|
||||||
|
onInputChange={internalInputChangeHandler}
|
||||||
|
onMenuClose={() => setInternalInputValue('')}
|
||||||
|
isMulti={isMulti}
|
||||||
|
isDisabled={isDisabled || readOnly}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isClearable={isClearable}
|
||||||
|
isRtl={isRtl}
|
||||||
|
isSearchable={isSearchable}
|
||||||
|
placeholder={placeholder}
|
||||||
|
closeMenuOnSelect={closeMenuOnSelect}
|
||||||
|
hideSelectedOptions={hideSelectedOptions}
|
||||||
|
className={cn('w-full flex-1', className?.select)}
|
||||||
|
classNames={{
|
||||||
|
...(!startAdornment && {
|
||||||
|
control: ({ isFocused, isDisabled }) =>
|
||||||
|
cn('w-full rounded-lg! border bg-white transition-shadow', {
|
||||||
|
'cursor-pointer!': !readOnly && !isDisabled,
|
||||||
|
'border-red-500! ring-2 ring-red-200': isError,
|
||||||
|
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
|
||||||
|
'border-base-content/10!': !isError && !isFocused,
|
||||||
|
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
||||||
|
isDisabled && !readOnly,
|
||||||
|
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||||
|
'rounded-l-none!': inputPrefix,
|
||||||
|
'rounded-r-none!': inputSuffix,
|
||||||
|
}),
|
||||||
|
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
||||||
|
}),
|
||||||
|
placeholder: () =>
|
||||||
|
cn({
|
||||||
|
'text-gray-400 text-sm leading-tight': !isError,
|
||||||
|
'text-red-300!': isError,
|
||||||
|
}),
|
||||||
|
singleValue: () =>
|
||||||
|
cn({
|
||||||
|
'm-0! text-gray-900 text-sm leading-tight': !isError,
|
||||||
|
'text-error!': isError,
|
||||||
|
'text-gray-900!': readOnly,
|
||||||
|
}),
|
||||||
|
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
||||||
|
indicatorsContainer: () =>
|
||||||
|
cn('flex items-center gap-1 pr-3 py-2'),
|
||||||
|
dropdownIndicator: ({ isFocused }) =>
|
||||||
|
cn('p-0! rounded hover:bg-gray-100', {
|
||||||
|
'text-gray-900': isFocused,
|
||||||
|
'text-gray-500': !isFocused,
|
||||||
|
'text-error!': isError,
|
||||||
|
}),
|
||||||
|
clearIndicator: () => cn('p-0! rounded hover:bg-gray-100'),
|
||||||
|
menu: () =>
|
||||||
|
cn(
|
||||||
|
'border border-base-content/5 rounded-xl! bg-base-100 shadow-lg! my-1.5!'
|
||||||
|
),
|
||||||
|
menuList: () => cn('p-0! max-h-60 overflow-auto'),
|
||||||
|
option: ({ isFocused, isSelected }) =>
|
||||||
|
cn('px-3 py-2 rounded-md cursor-pointer!', {
|
||||||
|
'bg-indigo-600 text-white': isFocused,
|
||||||
|
'bg-blue-500!': isSelected,
|
||||||
|
'text-gray-700': !isFocused && !isSelected,
|
||||||
|
}),
|
||||||
|
multiValue: ({ getValue, index }) => {
|
||||||
|
const selectedValues = getValue() as T[];
|
||||||
|
return cn(
|
||||||
|
'bg-base-200! rounded-lg! py-[3px] px-2.5 m-0! flex items-center gap-1! w-fit gap-2!',
|
||||||
|
selectedValues[index]?.className
|
||||||
|
);
|
||||||
|
},
|
||||||
|
multiValueRemove: () => cn('p-0! w-3 h-3'),
|
||||||
|
multiValueLabel: ({ getValue, index }) => {
|
||||||
|
const selectedValues = getValue() as T[];
|
||||||
|
return cn(
|
||||||
|
'p-0! text-base-content! text-xs!',
|
||||||
|
selectedValues[index]?.labelClassName
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
...components,
|
||||||
|
...(optionComponent ? { Option: optionComponent } : {}),
|
||||||
|
MenuList: CustomMenuList,
|
||||||
|
}}
|
||||||
|
{...(startAdornment && {
|
||||||
|
shouldShowAdornment,
|
||||||
|
startAdornment,
|
||||||
|
})}
|
||||||
|
menuPortalTarget={
|
||||||
|
typeof document !== 'undefined'
|
||||||
|
? (menuPortalTarget ?? document.body)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
styles={{
|
||||||
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
|
multiValue(base) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
borderRadius: '8px',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onMenuScrollToBottom={onMenuScrollToBottom}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{inputSuffix && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||||
|
{
|
||||||
|
'bg-gray-100 border-base-content/10': !isDisabled,
|
||||||
|
'bg-gray-50 border-base-content/10': isDisabled,
|
||||||
|
'border-error': isError,
|
||||||
|
},
|
||||||
|
className?.inputSuffix
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputSuffix}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<SelectComponent<T, boolean, GroupBase<T>>
|
<SelectComponent<T, boolean, GroupBase<T>>
|
||||||
instanceId='select'
|
instanceId='select'
|
||||||
value={value ?? (isMulti ? [] : null)}
|
value={value ?? (isMulti ? [] : null)}
|
||||||
@@ -332,6 +488,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
}}
|
}}
|
||||||
onMenuScrollToBottom={onMenuScrollToBottom}
|
onMenuScrollToBottom={onMenuScrollToBottom}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
|
|||||||
@@ -874,32 +874,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
(warehouseId: number) => {
|
(warehouseId: number) => {
|
||||||
const stockInfo = warehouseStockMap.get(warehouseId);
|
const stockInfo = warehouseStockMap.get(warehouseId);
|
||||||
if (!stockInfo) {
|
if (!stockInfo) {
|
||||||
return (
|
return <span className='text-xs'>Kosong</span>;
|
||||||
<Badge
|
|
||||||
variant='ghost'
|
|
||||||
color='neutral'
|
|
||||||
size='sm'
|
|
||||||
className={{ badge: 'whitespace-nowrap font-semibold' }}
|
|
||||||
>
|
|
||||||
Kosong
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { productCount } = stockInfo;
|
const { productCount } = stockInfo;
|
||||||
let color: 'neutral' | 'success' | 'warning' = 'neutral';
|
|
||||||
if (productCount === 0) color = 'warning';
|
|
||||||
else if (productCount > 0) color = 'success';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<span className='text-xs whitespace-nowrap'>
|
||||||
variant='soft'
|
|
||||||
color={color}
|
|
||||||
size='sm'
|
|
||||||
className={{ badge: 'whitespace-nowrap font-semibold' }}
|
|
||||||
>
|
|
||||||
Tersedia {productCount} produk
|
Tersedia {productCount} produk
|
||||||
</Badge>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[warehouseStockMap]
|
[warehouseStockMap]
|
||||||
@@ -1360,7 +1343,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
errorMessage={formik.errors.source_warehouse_id as string}
|
errorMessage={formik.errors.source_warehouse_id as string}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
startAdornment={
|
inputPrefix={
|
||||||
formik.values.source_warehouse_id
|
formik.values.source_warehouse_id
|
||||||
? getWarehouseStockAdornment(
|
? getWarehouseStockAdornment(
|
||||||
formik.values.source_warehouse_id
|
formik.values.source_warehouse_id
|
||||||
@@ -1418,7 +1401,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
errorMessage={formik.errors.destination_warehouse_id as string}
|
errorMessage={formik.errors.destination_warehouse_id as string}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
startAdornment={
|
inputPrefix={
|
||||||
formik.values.destination_warehouse_id
|
formik.values.destination_warehouse_id
|
||||||
? getWarehouseStockAdornment(
|
? getWarehouseStockAdornment(
|
||||||
formik.values.destination_warehouse_id
|
formik.values.destination_warehouse_id
|
||||||
|
|||||||
Reference in New Issue
Block a user