162 lines
3.8 KiB
Vue
162 lines
3.8 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { useRouter } from 'vue-router'
|
|
import ArticleStatusBadge from '@/components/ArticleStatusBadge.vue'
|
|
import { formatRelativeDate } from '@/lib/relativeDate'
|
|
import type { Article } from '@/composables/useArticles'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
articles: Article[]
|
|
loading: boolean
|
|
skeletonRows?: number
|
|
busyIds: Set<number>
|
|
now?: Date
|
|
}>(),
|
|
{ skeletonRows: 5 },
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
publish: [id: number]
|
|
unpublish: [id: number]
|
|
delete: [article: Article]
|
|
'copy-url': [id: number]
|
|
}>()
|
|
|
|
const router = useRouter()
|
|
|
|
function editPath(article: Article) {
|
|
return `/article/${article.id}/edit`
|
|
}
|
|
|
|
function isBusy(article: Article) {
|
|
return props.busyIds.has(article.id)
|
|
}
|
|
|
|
function pendingFor(article: Article) {
|
|
if (!isBusy(article)) {
|
|
return undefined
|
|
}
|
|
return article.is_online ? ('unpublish' as const) : ('publish' as const)
|
|
}
|
|
|
|
function actionsFor(article: Article) {
|
|
return [
|
|
[
|
|
{
|
|
get label() {
|
|
return t('Edit')
|
|
},
|
|
icon: 'i-lucide-pencil',
|
|
onSelect: () => router.push(editPath(article)),
|
|
},
|
|
{
|
|
get label() {
|
|
return article.is_online ? t('Unpublish') : t('Publish')
|
|
},
|
|
icon: article.is_online ? 'i-lucide-globe-lock' : 'i-lucide-globe',
|
|
disabled: isBusy(article),
|
|
onSelect: () => {
|
|
if (article.is_online) {
|
|
emit('unpublish', article.id)
|
|
} else {
|
|
emit('publish', article.id)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
get label() {
|
|
return t('Copy API URL')
|
|
},
|
|
icon: 'i-lucide-link',
|
|
onSelect: () => emit('copy-url', article.id),
|
|
},
|
|
],
|
|
[
|
|
{
|
|
get label() {
|
|
return t('Delete…')
|
|
},
|
|
icon: 'i-lucide-trash-2',
|
|
color: 'error' as const,
|
|
onSelect: () => emit('delete', article),
|
|
},
|
|
],
|
|
]
|
|
}
|
|
|
|
function onRowClick(article: Article, event: MouseEvent) {
|
|
if ((event.target as HTMLElement).closest('[data-row-menu]')) {
|
|
return
|
|
}
|
|
router.push(editPath(article))
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<ul
|
|
class="-mx-4 border-y border-default bg-default md:hidden"
|
|
:aria-busy="loading ? 'true' : undefined"
|
|
>
|
|
<template v-if="loading">
|
|
<li
|
|
v-for="n in skeletonRows"
|
|
:key="n"
|
|
class="flex min-h-17 flex-col justify-center gap-2 border-t border-muted py-3 pl-4 pr-1 first:border-t-0"
|
|
>
|
|
<USkeleton class="h-3.5 w-3/4 rounded-none bg-accented" />
|
|
<USkeleton class="h-3 w-2/5 rounded-none bg-accented" />
|
|
</li>
|
|
</template>
|
|
<template v-else>
|
|
<li v-if="articles.length === 0">
|
|
<slot name="empty" />
|
|
</li>
|
|
<li
|
|
v-for="article in articles"
|
|
:key="article.id"
|
|
class="relative flex min-h-17 items-center gap-2 border-t border-muted py-3 pl-4 pr-1 first:border-t-0"
|
|
:class="{ 'opacity-55': isBusy(article) }"
|
|
@click="onRowClick(article, $event)"
|
|
>
|
|
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
<RouterLink
|
|
:to="editPath(article)"
|
|
:data-row-title="article.id"
|
|
class="truncate text-[0.9375rem]/[21px] font-medium text-highlighted"
|
|
@click.stop
|
|
>
|
|
{{ article.title }}
|
|
</RouterLink>
|
|
<div class="flex items-center gap-2.5 text-xs text-muted">
|
|
<ArticleStatusBadge
|
|
:online="article.is_online"
|
|
size="sm"
|
|
:pending="pendingFor(article)"
|
|
/>
|
|
<span :title="article.updated_at"
|
|
>{{ t('Updated') }}
|
|
{{ formatRelativeDate(article.updated_at, now) }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<UDropdownMenu
|
|
:items="actionsFor(article)"
|
|
:content="{ align: 'end' }"
|
|
data-row-menu
|
|
>
|
|
<UButton
|
|
icon="i-lucide-ellipsis-vertical"
|
|
color="neutral"
|
|
variant="ghost"
|
|
:aria-label="t('Actions for {0}', [article.title])"
|
|
class="size-11 justify-center text-toned"
|
|
@click.stop
|
|
/>
|
|
</UDropdownMenu>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</template>
|