Improve artwork edit form
This commit is contained in:
@ -83,6 +83,12 @@ interface MultipleSelectorProps {
|
||||
|
||||
/** Optional explicit group ordering (top to bottom). */
|
||||
groupOrder?: string[];
|
||||
|
||||
/**
|
||||
* Customize how a new (creatable) option is represented.
|
||||
* Defaults to value = input text (current behavior).
|
||||
*/
|
||||
createOption?: (raw: string) => Option;
|
||||
}
|
||||
|
||||
export interface MultipleSelectorRef {
|
||||
@ -145,6 +151,10 @@ function isOptionsExist(groupOption: GroupOption, targetOption: Option[]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalizeInput(s: string) {
|
||||
return s.trim().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* The `CommandEmpty` of shadcn/ui will cause the cmdk empty not rendering correctly.
|
||||
* So we create one and copy the `Empty` implementation from `cmdk`.
|
||||
@ -194,6 +204,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
||||
badgeClassName,
|
||||
selectFirstItem = true,
|
||||
creatable = false,
|
||||
createOption,
|
||||
triggerSearchOnFocus = false,
|
||||
commandProps,
|
||||
inputProps,
|
||||
@ -354,45 +365,56 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
||||
|
||||
const CreatableItem = () => {
|
||||
if (!creatable) return undefined;
|
||||
if (
|
||||
isOptionsExist(options, [{ value: inputValue, label: inputValue }]) ||
|
||||
selected.find((s) => s.value === inputValue)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const raw = normalizeInput(inputValue);
|
||||
if (!raw) return undefined;
|
||||
|
||||
// Check if an option with same label already exists (case-insensitive)
|
||||
const labelExistsInOptions = Object.values(options).some((group) =>
|
||||
group.some((o) => o.label.toLowerCase() === raw.toLowerCase()),
|
||||
);
|
||||
|
||||
const labelExistsInSelected = selected.some(
|
||||
(s) => s.label.toLowerCase() === raw.toLowerCase(),
|
||||
);
|
||||
|
||||
if (labelExistsInOptions || labelExistsInSelected) return undefined;
|
||||
|
||||
const created = createOption ? createOption(raw) : { value: raw, label: raw };
|
||||
|
||||
const Item = (
|
||||
<CommandItem
|
||||
value={inputValue}
|
||||
value={raw}
|
||||
className="cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onSelect={(value: string) => {
|
||||
onSelect={() => {
|
||||
if (selected.length >= maxSelected) {
|
||||
onMaxSelected?.(selected.length);
|
||||
return;
|
||||
}
|
||||
setInputValue('');
|
||||
const newOptions = [...selected, { value, label: value }];
|
||||
|
||||
setInputValue("");
|
||||
|
||||
// Guard against duplicates (by value)
|
||||
if (selected.some((s) => s.value === created.value)) return;
|
||||
|
||||
const newOptions = [...selected, created];
|
||||
setSelected(newOptions);
|
||||
onChange?.(newOptions);
|
||||
}}
|
||||
>
|
||||
{`Create "${inputValue}"`}
|
||||
{`Create "${raw}"`}
|
||||
</CommandItem>
|
||||
);
|
||||
|
||||
// For normal creatable
|
||||
if (!onSearch && inputValue.length > 0) {
|
||||
return Item;
|
||||
}
|
||||
if (!onSearch) return Item;
|
||||
|
||||
// For async search creatable. avoid showing creatable item before loading at first.
|
||||
if (onSearch && debouncedSearchTerm.length > 0 && !isLoading) {
|
||||
return Item;
|
||||
}
|
||||
// For async search creatable: show only when user typed something and search isn't loading
|
||||
if (raw.length > 0 && !isLoading) return Item;
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -604,7 +626,7 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
value={option.value}
|
||||
disabled={disabledItem}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user