Files
RenewCMS/web/src/components/ActivityCardList.vue
T

125 lines
3.9 KiB
Vue

<script setup lang="ts">
import { t } from '@/i18n'
import { computed } from 'vue'
import ActivityActor from '@/components/ActivityActor.vue'
import ActivityBadge from '@/components/ActivityBadge.vue'
import ActivityDetailLine from '@/components/ActivityDetailLine.vue'
import ActivitySentence from '@/components/ActivitySentence.vue'
import { ACTIVITY_SKELETON_CARDS } from '@/composables/useActivity'
import { categoryIcon, outcomeBadge, rowSurface, type ActivityItem } from '@/lib/activity'
import { fullLocalTime, groupByDay, whenLabel } from '@/lib/activityTime'
const SKELETON_SENTENCE_WIDTHS = ['w-9/10', 'w-3/4', 'w-17/20']
const props = defineProps<{
items: ActivityItem[]
loading: boolean
selectedId: number | null
showActor: boolean
}>()
const emit = defineEmits<{
open: [item: ActivityItem, opener: HTMLElement]
}>()
const groups = computed(() => groupByDay(props.items))
function open(item: ActivityItem, event: Event) {
emit('open', item, event.currentTarget as HTMLElement)
}
function cardClass(item: ActivityItem) {
return rowSurface(props.selectedId === item.id, item.security)
}
</script>
<template>
<div
:aria-busy="loading ? 'true' : undefined"
class="flex flex-col overflow-hidden border border-default bg-default"
>
<template v-if="loading">
<span role="status" class="sr-only">{{ t('Loading activity') }}</span>
<div
v-for="n in ACTIVITY_SKELETON_CARDS"
:key="n"
class="flex flex-col gap-2.5 border-t border-default px-4 py-3.5"
data-testid="activity-skeleton-card"
>
<div class="flex items-center gap-2">
<USkeleton class="size-6 shrink-0 rounded-none bg-accented" />
<USkeleton class="h-3 w-22.5 rounded-none bg-accented" />
<span class="flex-1" />
<USkeleton class="h-2.5 w-10 rounded-none bg-accented" />
</div>
<USkeleton
class="h-3.5 rounded-none bg-accented"
:class="SKELETON_SENTENCE_WIDTHS[(n - 1) % 3]"
/>
<USkeleton class="h-5 w-17.5 rounded-none bg-accented" />
</div>
</template>
<template v-else>
<template v-for="group in groups" :key="group.key">
<div
data-testid="activity-day"
class="flex h-8 shrink-0 items-center border-t border-default bg-muted px-4 text-xs font-semibold text-highlighted"
>
{{ group.label }}
</div>
<div
v-for="item in group.items"
:key="item.id"
tabindex="0"
role="button"
data-testid="activity-card"
:data-activity-id="item.id"
:aria-pressed="selectedId === item.id"
:class="cardClass(item)"
class="flex min-h-11 cursor-pointer flex-col gap-2 border-t border-default px-4 py-3 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary"
@click="open(item, $event)"
@keydown.enter.self.prevent="open(item, $event)"
>
<div class="flex min-w-0 items-center gap-2">
<ActivityActor v-if="showActor" :actor="item.actor" class="flex-1" />
<span v-else class="flex-1" />
<span
data-testid="activity-when"
:title="fullLocalTime(item.created_at)"
class="text-xs/5 whitespace-nowrap text-muted"
>{{ whenLabel(item.created_at) }}</span
>
</div>
<div class="flex items-start gap-2">
<UIcon
:name="categoryIcon(item.category)"
class="mt-0.5 size-4 shrink-0 text-muted"
/>
<span class="flex min-w-0 flex-1 flex-col">
<span class="line-clamp-3 text-md wrap-anywhere text-default">
<ActivitySentence :item="item" />
</span>
<ActivityDetailLine :item="item" wrap />
</span>
</div>
<div class="flex flex-wrap items-center gap-1.5">
<ActivityBadge v-bind="outcomeBadge(item.outcome)" />
<ActivityBadge
v-if="item.security"
tone="security"
:label="t('Security')"
/>
<ActivityBadge
v-if="item.target && !item.target.exists"
tone="deleted"
:label="t('Target deleted')"
/>
</div>
</div>
</template>
</template>
</div>
</template>