feat: sites and authors integration into existing screens (& formated all frontend files kinda accidentally)

This commit is contained in:
Florian Sylvain committed 2026-10-15 05:25:44 +02:00
1 parent 3d81d2c717
commit 6b8990c55e
239 files changed
+9194 -1354

No files matched your search

+192
View File
@@ -0,0 +1,192 @@
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<Article[]>
total: Ref<number>
counts: Ref<ArticleCounts>
shown: Ref<ArticleListQuery>
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<Set<number>>(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<HTMLElement>(`[data-row-title="${nextId}"]`)?.focus()
return
}
document.querySelector<HTMLElement>('[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 }
}