feat: activity screen in the SPA, route /activity
This commit is contained in:
1 parent
e37a46a027
commit
35db442cd3
47 files changed
+7582
-11
No files matched your search
@@ -0,0 +1,122 @@
|
||||
<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 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="line-clamp-3 min-w-0 flex-1 text-md wrap-anywhere text-default"
|
||||
>
|
||||
<ActivitySentence :item="item" />
|
||||
</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>
|
||||
Reference in new issue
Block a user