114 lines
3.4 KiB
Vue
114 lines
3.4 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
import { bodyDeltaLabel, changeValue, fieldLabel, type ActivityChange } from '@/lib/activity'
|
|
|
|
const props = defineProps<{
|
|
changes: ActivityChange[]
|
|
isDesktop: boolean
|
|
}>()
|
|
|
|
const expanded = ref<Record<number, boolean>>({})
|
|
const overflowing = ref<Record<number, boolean>>({})
|
|
const valueBoxes = new Map<number, HTMLElement[]>()
|
|
|
|
function register(index: number, element: unknown) {
|
|
if (!(element instanceof HTMLElement)) return
|
|
const boxes = valueBoxes.get(index) ?? []
|
|
if (!boxes.includes(element)) valueBoxes.set(index, [...boxes, element])
|
|
}
|
|
|
|
function measure() {
|
|
const next: Record<number, boolean> = { ...overflowing.value }
|
|
for (const [index, boxes] of valueBoxes) {
|
|
if (expanded.value[index]) continue
|
|
next[index] = boxes.some((box) => box.scrollHeight > box.clientHeight)
|
|
}
|
|
overflowing.value = next
|
|
}
|
|
|
|
function toggle(index: number) {
|
|
expanded.value = { ...expanded.value, [index]: !expanded.value[index] }
|
|
}
|
|
|
|
watch(
|
|
() => props.changes,
|
|
() => {
|
|
expanded.value = {}
|
|
overflowing.value = {}
|
|
valueBoxes.clear()
|
|
nextTick(measure)
|
|
},
|
|
)
|
|
|
|
onMounted(() => {
|
|
nextTick(measure)
|
|
window.addEventListener('resize', measure)
|
|
})
|
|
|
|
onBeforeUnmount(() => window.removeEventListener('resize', measure))
|
|
</script>
|
|
|
|
<template>
|
|
<div data-testid="activity-diff" class="flex flex-col border border-default">
|
|
<div
|
|
v-for="(change, index) in changes"
|
|
:key="index"
|
|
data-testid="activity-change"
|
|
class="flex flex-col gap-2 border-t border-muted p-3 first:border-t-0"
|
|
>
|
|
<span class="font-medium text-highlighted">{{ fieldLabel(change.field) }}</span>
|
|
|
|
<span
|
|
v-if="change.kind === 'body'"
|
|
data-testid="activity-body-change"
|
|
class="flex items-center gap-1.5 text-toned"
|
|
>
|
|
<UIcon name="i-lucide-file-pen-line" class="size-3.5 shrink-0" />
|
|
{{ bodyDeltaLabel(change.delta) }}
|
|
</span>
|
|
|
|
<template v-else>
|
|
<div
|
|
class="grid items-start gap-1.5 md:grid-cols-[minmax(0,1fr)_16px_minmax(0,1fr)] md:gap-2"
|
|
>
|
|
<div class="flex min-w-0 flex-col gap-0.5">
|
|
<span class="text-2xs text-muted">{{ t('Before') }}</span>
|
|
<span
|
|
:ref="(element) => register(index, element)"
|
|
data-testid="activity-before"
|
|
class="border border-default bg-elevated px-2.5 py-2 wrap-anywhere text-toned"
|
|
:class="expanded[index] ? '' : 'line-clamp-2'"
|
|
>{{ changeValue(change.field, change.before) }}</span
|
|
>
|
|
</div>
|
|
<UIcon
|
|
:name="isDesktop ? 'i-lucide-arrow-right' : 'i-lucide-arrow-down'"
|
|
class="size-3.5 shrink-0 justify-self-center text-muted md:mt-7.5"
|
|
/>
|
|
<div class="flex min-w-0 flex-col gap-0.5">
|
|
<span class="text-2xs text-muted">{{ t('After') }}</span>
|
|
<span
|
|
:ref="(element) => register(index, element)"
|
|
data-testid="activity-after"
|
|
class="border border-accented bg-default px-2.5 py-2 wrap-anywhere text-highlighted"
|
|
:class="expanded[index] ? '' : 'line-clamp-2'"
|
|
>{{ changeValue(change.field, change.after) }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<UButton
|
|
v-if="overflowing[index]"
|
|
color="neutral"
|
|
variant="link"
|
|
class="min-h-8 self-start px-0 text-xs font-medium text-highlighted underline underline-offset-3 md:min-h-4"
|
|
@click="toggle(index)"
|
|
>
|
|
{{ expanded[index] ? t('Show less') : t('Show more') }}
|
|
</UButton>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|