314 lines
8.3 KiB
Vue
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>
|