mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -5089,7 +5089,8 @@
|
|||||||
"searchOrCreate": "Поиск или создание...",
|
"searchOrCreate": "Поиск или создание...",
|
||||||
"noItems": "Ничего не найдено",
|
"noItems": "Ничего не найдено",
|
||||||
"placeholder": "Выберите...",
|
"placeholder": "Выберите...",
|
||||||
"clear": "Очистить"
|
"clear": "Очистить",
|
||||||
|
"delete": "Удалить {{name}}"
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"bold": "Жирный",
|
"bold": "Жирный",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user