Files
RenewCMS/web/src/views/ActivityView.vue
T

314 lines
8.3 KiB
Vue

<script setup lang="ts">
import { t } from '@/i18n'
import { computed, nextTick, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useMediaQuery } from '@vueuse/core'
import ActivityCardList from '@/components/ActivityCardList.vue'
import ActivityChips from '@/components/ActivityChips.vue'
import ActivityDetailsPanel from '@/components/ActivityDetailsPanel.vue'
import ActivityEmptyPanel from '@/components/ActivityEmptyPanel.vue'
import ActivityExportDialog from '@/components/ActivityExportDialog.vue'
import ActivityExportMenu from '@/components/ActivityExportMenu.vue'
import ActivityFilterBar from '@/components/ActivityFilterBar.vue'
import ActivityFiltersSheet from '@/components/ActivityFiltersSheet.vue'
import ActivityPager from '@/components/ActivityPager.vue'
import ActivitySummaryStrip from '@/components/ActivitySummaryStrip.vue'
import ActivityTable from '@/components/ActivityTable.vue'
import DashboardShell from '@/components/DashboardShell.vue'
import { useActivityExport } from '@/composables/useActivityExport'
import { useActivityPage } from '@/composables/useActivityPage'
import { useSessionStore } from '@/stores/session'
import type { ActivityFilters } from '@/lib/activity'
import type { StripKey } from '@/lib/activitySummary'
const DESKTOP_QUERY = '(min-width: 48rem)'
const router = useRouter()
const session = useSessionStore()
const isDesktop = useMediaQuery(DESKTOP_QUERY)
const activity = useActivityPage()
const {
filters,
items,
total,
page,
size,
listState,
summary,
summaryState,
people,
chips,
isFullView,
isEmpty,
exportDisabled,
showListSkeleton,
showSummarySkeleton,
selectedId,
selectedItem,
position,
canPrevious,
canNext,
} = activity
const exportFlow = useActivityExport(filters)
const sheetOpen = ref(false)
let started = false
let opener: HTMLElement | null = null
const sessionReady = computed(() => session.status === 'ready' && !!session.session?.verified)
const title = computed(() =>
isFullView.value || session.status !== 'ready' ? t('Activity') : t('My activity'),
)
const subtitle = computed(() =>
isFullView.value
? t('Who did what, and when, across your site.')
: t("Everything you've done on this site."),
)
const forbidden = computed(() => listState.value === 'forbidden')
const showStrip = computed(
() =>
isFullView.value &&
!forbidden.value &&
((summaryState.value !== 'failed' && summary.value !== null) || showSummarySkeleton.value),
)
const showRows = computed(
() =>
listState.value === 'ready' ||
(listState.value === 'loading' && (showListSkeleton.value || items.value.length > 0)),
)
const showPager = computed(
() => showRows.value && !showListSkeleton.value && !isEmpty.value && items.value.length > 0,
)
const emptyIsFiltered = computed(() => chips.value.length > 0)
watch(
sessionReady,
(ready) => {
if (ready && !started) {
started = true
activity.start()
}
},
{ immediate: true },
)
function onFilters(next: ActivityFilters) {
activity.applyFilters(next)
}
function onStripCard(key: StripKey) {
if (key === 'total') activity.clearAll()
else if (key === 'failed') activity.toggleFailed()
else if (key === 'security') activity.toggleSecurity()
}
function onOpen(item: { id: number }, element: HTMLElement) {
opener = element
activity.openDetails(item.id)
}
async function onClose() {
const id = selectedId.value
activity.closeDetails()
await nextTick()
const row = document.querySelector<HTMLElement>(`[data-activity-id="${id}"]`)
;(row ?? opener)?.focus()
}
function goHome() {
router.push('/home')
}
</script>
<template>
<DashboardShell compact-padding :mobile-title="title">
<template #breadcrumb>
<UBreadcrumb
:items="[{ label: title }]"
color="neutral"
class="hidden lg:block"
:ui="{ link: 'text-sm font-medium' }"
/>
</template>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-end gap-4">
<div class="flex min-w-45 flex-1 flex-col gap-0.5">
<h1 class="text-xl font-semibold text-highlighted">
{{ title }}
</h1>
<p class="text-muted">{{ subtitle }}</p>
</div>
<ActivityExportMenu
v-if="isFullView && !forbidden"
:total="total"
:is-desktop="isDesktop"
:disabled="exportDisabled || exportFlow.currentBusy.value"
:nothing-to-export="isEmpty"
:forbidden="exportFlow.forbidden.value"
@current="exportFlow.exportCurrentView"
@all="exportFlow.openDialog"
/>
</div>
<ActivityEmptyPanel
v-if="forbidden"
icon="i-lucide-lock"
:title="t('You can\'t view this activity')"
:description="
t(
'Your role doesn\'t include this page. Ask an owner or admin if you need access.',
)
"
:action-label="t('Back to home')"
@action="goHome"
/>
<template v-else>
<ActivitySummaryStrip
v-if="showStrip"
:summary="summary"
:range="filters.range"
:loading="showSummarySkeleton"
:is-desktop="isDesktop"
:outcome="filters.outcome"
:security="filters.security"
@apply-filter="onStripCard"
/>
<ActivityFilterBar
:filters="filters"
:people="people"
:can-filter-people="isFullView"
:is-desktop="isDesktop"
:chip-count="chips.length"
@update:filters="onFilters"
@open-people="activity.ensurePeople"
@open-sheet="sheetOpen = true"
/>
<ActivityChips
:chips="chips"
@remove="activity.removeChip"
@clear="activity.clearAll"
/>
<UAlert
v-if="listState === 'error'"
role="alert"
color="error"
variant="soft"
orientation="vertical"
:close="false"
icon="i-lucide-circle-x"
:title="t('Couldn\'t load activity')"
:description="t('Check your connection and try again.')"
:ui="{ actions: 'mt-2' }"
>
<template #actions>
<UButton
color="neutral"
variant="outline"
class="h-11 border border-accented bg-default px-3 text-xs text-highlighted ring-0 md:h-7"
@click="activity.retry"
>
{{ t('Try again') }}
</UButton>
</template>
</UAlert>
<ActivityEmptyPanel
v-else-if="isEmpty && !emptyIsFiltered"
icon="i-lucide-history"
:title="t('No activity yet')"
:description="
isFullView
? t('Actions on articles, media, users and API keys will appear here.')
: t('Your actions will appear here once you start working on the site.')
"
/>
<ActivityEmptyPanel
v-else-if="isEmpty"
icon="i-lucide-search-x"
:title="t('No activity matches these filters')"
:description="t('Try a different search or a wider date range.')"
:action-label="t('Clear filters')"
@action="activity.clearAll"
/>
<template v-else-if="showRows">
<ActivityTable
v-if="isDesktop"
:items="items"
:loading="showListSkeleton"
:selected-id="selectedId"
:show-actor="isFullView"
@open="onOpen"
/>
<ActivityCardList
v-else
:items="items"
:loading="showListSkeleton"
:selected-id="selectedId"
:show-actor="isFullView"
@open="onOpen"
/>
</template>
<ActivityPager
v-if="showPager"
:page="page"
:page-count="activity.pageCount.value"
:total="total"
:size="size"
:from="activity.rangeStart.value"
:to="activity.rangeEnd.value"
:is-desktop="isDesktop"
@update:page="activity.goToPage"
@update:size="activity.setSize"
/>
</template>
</div>
<ActivityExportDialog
v-if="isFullView && !forbidden"
v-model:open="exportFlow.dialogOpen.value"
v-model:range="exportFlow.range.value"
v-model:from="exportFlow.from.value"
v-model:to="exportFlow.to.value"
:stage="exportFlow.stage.value"
:can-submit="exportFlow.canSubmit.value"
:file-name="exportFlow.startedFileName.value"
@submit="exportFlow.submit"
@close="exportFlow.closeDialog"
/>
<ActivityDetailsPanel
:item="selectedItem"
:position="position"
:total="total"
:can-previous="canPrevious"
:can-next="canNext"
:is-desktop="isDesktop"
@previous="activity.previous"
@next="activity.next"
@close="onClose"
/>
<ActivityFiltersSheet
v-if="!isDesktop && !forbidden"
v-model:open="sheetOpen"
:filters="filters"
:people="people"
:can-filter-people="isFullView"
:chip-count="chips.length"
:total="total"
@update:filters="onFilters"
@open-people="activity.ensurePeople"
@clear="activity.clearAll"
/>
</DashboardShell>
</template>