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;
onChange: (item: ColoredItem | null) => void;
onCreateNew: (name: string, color: string) => Promise<ColoredItem>;
onDelete?: (item: ColoredItem) => Promise<void>;
placeholder?: string;
isLoading?: boolean;
colors?: string[];
@@ -36,6 +37,7 @@ export function ColoredItemCombobox({
value,
onChange,
onCreateNew,
onDelete,
placeholder,
isLoading = false,
colors = DEFAULT_COLORS,
@@ -48,6 +50,7 @@ export function ColoredItemCombobox({
const [search, setSearch] = useState('');
const [newColor, setNewColor] = useState(colors[0] ?? '#00e5a0');
const [isCreating, setIsCreating] = useState(false);
const [deletingId, setDeletingId] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
@@ -137,6 +140,30 @@ export function ColoredItemCombobox({
}
}, [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
const handleKeyDown = useCallback(
(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" />
</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>
))}
</div>

View File

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

View File

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

View File

@@ -445,6 +445,22 @@ export default function AdminNewsCreate() {
[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
const { data: articleData, isLoading: isLoadingArticle } = useQuery({
queryKey: ['admin', 'news', 'article', articleId],
@@ -626,6 +642,7 @@ export default function AdminNewsCreate() {
value={selectedCategory}
onChange={setSelectedCategory}
onCreateNew={handleCreateCategory}
onDelete={handleDeleteCategory}
placeholder={t('news.admin.combobox.selectCategory')}
/>
</div>
@@ -636,6 +653,7 @@ export default function AdminNewsCreate() {
value={selectedTag}
onChange={setSelectedTag}
onCreateNew={handleCreateTag}
onDelete={handleDeleteTag}
placeholder={t('news.admin.combobox.selectTag')}
/>
</div>