import { nextTick, ref, type Ref } from 'vue' import { t } from '@/i18n' import { useLocalizedToast } from '@/i18n/presentation' import { useArticleActions } from '@/composables/useArticleActions' import type { Article, ArticleCounts } from '@/composables/useArticles' import type { ArticleListQuery } from '@/lib/articleListQuery' import { pageCountFor } from '@/lib/pagination' interface ArticleRowActionsState { items: Ref total: Ref counts: Ref shown: Ref reload: (query: ArticleListQuery, mode: 'push' | 'replace') => void } export function useArticleRowActions({ items, total, counts, shown, reload, }: ArticleRowActionsState) { const actions = useArticleActions() const toast = useLocalizedToast() const busyIds = ref>(new Set()) function setBusy(id: number, busy: boolean) { const next = new Set(busyIds.value) if (busy) { next.add(id) } else { next.delete(id) } busyIds.value = next } function shiftCounts(article: Article, delta: number) { const key = article.is_online ? 'online' : 'draft' counts.value = { ...counts.value, all: Math.max(0, counts.value.all + delta), [key]: Math.max(0, counts.value[key] + delta), } } function detachRow(id: number) { if (!items.value.some((article) => article.id === id)) { return } items.value = items.value.filter((article) => article.id !== id) total.value = Math.max(0, total.value - 1) if (items.value.length === 0 && total.value > 0) { const page = Math.min(shown.value.page, pageCountFor(total.value, shown.value.size)) reload({ ...shown.value, page }, 'replace') } } function removeArticle(article: Article) { shiftCounts(article, -1) detachRow(article.id) } function replaceArticle(updated: Article, previous: Article) { items.value = items.value.map((existing) => existing.id === updated.id ? updated : existing, ) if (updated.is_online === previous.is_online) { return } counts.value = { ...counts.value, online: counts.value.online + (updated.is_online ? 1 : -1), draft: counts.value.draft + (updated.is_online ? -1 : 1), } const filter = shown.value.status if (filter && (filter === 'online') !== updated.is_online) { detachRow(updated.id) } } function notFoundToast(article: Article) { return { id: `article-not-found-${article.id}`, color: 'info' as const, icon: 'i-lucide-info', get title() { return t('That article no longer exists') }, get description() { return t('It was deleted somewhere else, so we removed it from the list.') }, } } async function setOnline(article: Article, makeOnline: boolean) { setBusy(article.id, true) try { await actions.publish(article, makeOnline, { onUpdated: (updated) => replaceArticle(updated, article), onGone: () => goneFromList(article), retry: () => setOnline(article, makeOnline), }) } finally { setBusy(article.id, false) } } function findItem(id: number) { return items.value.find((existing) => existing.id === id) } function handlePublish(id: number) { const article = findItem(id) if (article) { setOnline(article, true) } } function handleUnpublish(id: number) { const article = findItem(id) if (article) { setOnline(article, false) } } async function handleCopyUrl(id: number) { const article = findItem(id) if (!article) { return } const url = `${window.location.origin}/v1/article/${id}` try { await navigator.clipboard.writeText(url) toast.add(() => ({ id: `article-link-copied-${id}`, color: 'success', icon: 'i-lucide-circle-check', get title() { return t('Link copied') }, get description() { return t('The API URL for \u0022{0}\u0022 is on your clipboard.', [ article.title, ]) }, })) } catch { toast.add(() => ({ id: `article-link-copy-failed-${id}`, color: 'error', icon: 'i-lucide-circle-x', get title() { return t("Couldn't copy the link") }, })) } } function goneFromList(article: Article) { removeArticle(article) toast.add(() => notFoundToast(article)) } function performDelete(article: Article) { return actions.deleteArticle(article, { onDeleted: () => removeArticle(article), onGone: () => goneFromList(article), }) } async function focusRowOrHeading(idsAtOpen: number[], deletedId: number) { await nextTick() const index = idsAtOpen.indexOf(deletedId) const nextId = idsAtOpen[index + 1] ?? idsAtOpen.slice(0, index).reverse()[0] if (nextId != null) { document.querySelector(`[data-row-title="${nextId}"]`)?.focus() return } document.querySelector('[data-testid="articles-heading"]')?.focus() } async function handleDelete(article: Article) { const idsAtOpen = items.value.map((existing) => existing.id) const confirmed = await actions.confirmDelete(article, () => performDelete(article)) if (confirmed) { focusRowOrHeading(idsAtOpen, article.id) } } return { busyIds, handlePublish, handleUnpublish, handleCopyUrl, handleDelete } }