feat: sites and authors integration into existing screens (& formated all frontend files kinda accidentally)
This commit is contained in:
1 parent
3d81d2c717
commit
6b8990c55e
239 files changed
+9194
-1354
No files matched your search
@@ -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 }
|
||||
}
|
||||
Reference in new issue
Block a user