Improve artwork edit form

This commit is contained in:
2025-12-29 21:46:01 +01:00
parent eaf822d73a
commit e1e000c8e5
7 changed files with 529 additions and 162 deletions

View File

@ -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();