feat: add delete buttons for categories and tags in combobox dropdown

- Add onDelete prop to ColoredItemCombobox with X button per item
- Wire up deleteCategory/deleteTag API calls in AdminNewsCreate
- Auto-clear selection if the deleted item was selected
- Spinner on delete, haptic feedback, i18n aria-label
This commit is contained in:
Fringg
2026-03-23 15:48:27 +03:00
parent 13d27a5929
commit be7219ec06
4 changed files with 66 additions and 2 deletions

View File

@@ -25,6 +25,7 @@ interface ColoredItemComboboxProps {
value: ColoredItem | null; value: ColoredItem | null;
onChange: (item: ColoredItem | null) => void; onChange: (item: ColoredItem | null) => void;
onCreateNew: (name: string, color: string) => Promise<ColoredItem>; onCreateNew: (name: string, color: string) => Promise<ColoredItem>;
onDelete?: (item: ColoredItem) => Promise<void>;
placeholder?: string; placeholder?: string;
isLoading?: boolean; isLoading?: boolean;
colors?: string[]; colors?: string[];
@@ -36,6 +37,7 @@ export function ColoredItemCombobox({
value, value,
onChange, onChange,
onCreateNew, onCreateNew,
onDelete,
placeholder, placeholder,
isLoading = false, isLoading = false,
colors = DEFAULT_COLORS, colors = DEFAULT_COLORS,
@@ -48,6 +50,7 @@ export function ColoredItemCombobox({
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [newColor, setNewColor] = useState(colors[0] ?? '#00e5a0'); const [newColor, setNewColor] = useState(colors[0] ?? '#00e5a0');
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [deletingId, setDeletingId] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
@@ -137,6 +140,30 @@ export function ColoredItemCombobox({
} }
}, [search, newColor, isCreating, onCreateNew, onChange, haptic]); }, [search, newColor, isCreating, onCreateNew, onChange, haptic]);
const handleDelete = useCallback(
async (e: React.MouseEvent, item: ColoredItem) => {
e.stopPropagation();
if (!onDelete || deletingId !== null) return;
haptic.buttonPress();
setDeletingId(item.id);
try {
await onDelete(item);
haptic.success();
// Clear selection if deleted item was selected
if (value?.id === item.id) {
onChange(null);
}
} catch {
haptic.error();
} finally {
setDeletingId(null);
}
},
[onDelete, deletingId, value, onChange, haptic],
);
// Keyboard handling // Keyboard handling
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
@@ -257,6 +284,23 @@ export function ColoredItemCombobox({
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" /> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg> </svg>
)} )}
{onDelete && (
<button
type="button"
onClick={(e) => handleDelete(e, item)}
disabled={deletingId === item.id}
className="shrink-0 rounded p-1 text-dark-600 transition-colors hover:bg-red-500/10 hover:text-red-400 disabled:opacity-50"
aria-label={t('news.admin.combobox.delete', { name: item.name })}
>
{deletingId === item.id ? (
<div className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-red-400 border-t-transparent" />
) : (
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
)}
</button>
)}
</button> </button>
))} ))}
</div> </div>

View File

@@ -4522,7 +4522,8 @@
"searchOrCreate": "Search or create...", "searchOrCreate": "Search or create...",
"noItems": "Nothing found", "noItems": "Nothing found",
"placeholder": "Select...", "placeholder": "Select...",
"clear": "Clear" "clear": "Clear",
"delete": "Delete {{name}}"
}, },
"toolbar": { "toolbar": {
"bold": "Bold", "bold": "Bold",

View File

@@ -5089,7 +5089,8 @@
"searchOrCreate": "Поиск или создание...", "searchOrCreate": "Поиск или создание...",
"noItems": "Ничего не найдено", "noItems": "Ничего не найдено",
"placeholder": "Выберите...", "placeholder": "Выберите...",
"clear": "Очистить" "clear": "Очистить",
"delete": "Удалить {{name}}"
}, },
"toolbar": { "toolbar": {
"bold": "Жирный", "bold": "Жирный",

View File

@@ -445,6 +445,22 @@ export default function AdminNewsCreate() {
[queryClient], [queryClient],
); );
const handleDeleteCategory = useCallback(
async (item: { id: number }) => {
await newsApi.deleteCategory(item.id);
await queryClient.invalidateQueries({ queryKey: ['admin', 'news', 'categories'] });
},
[queryClient],
);
const handleDeleteTag = useCallback(
async (item: { id: number }) => {
await newsApi.deleteTag(item.id);
await queryClient.invalidateQueries({ queryKey: ['admin', 'news', 'tags'] });
},
[queryClient],
);
// Fetch article for editing // Fetch article for editing
const { data: articleData, isLoading: isLoadingArticle } = useQuery({ const { data: articleData, isLoading: isLoadingArticle } = useQuery({
queryKey: ['admin', 'news', 'article', articleId], queryKey: ['admin', 'news', 'article', articleId],
@@ -626,6 +642,7 @@ export default function AdminNewsCreate() {
value={selectedCategory} value={selectedCategory}
onChange={setSelectedCategory} onChange={setSelectedCategory}
onCreateNew={handleCreateCategory} onCreateNew={handleCreateCategory}
onDelete={handleDeleteCategory}
placeholder={t('news.admin.combobox.selectCategory')} placeholder={t('news.admin.combobox.selectCategory')}
/> />
</div> </div>
@@ -636,6 +653,7 @@ export default function AdminNewsCreate() {
value={selectedTag} value={selectedTag}
onChange={setSelectedTag} onChange={setSelectedTag}
onCreateNew={handleCreateTag} onCreateNew={handleCreateTag}
onDelete={handleDeleteTag}
placeholder={t('news.admin.combobox.selectTag')} placeholder={t('news.admin.combobox.selectTag')}
/> />
</div> </div>