Implement AsciiDoc and TipTap conversion logic, including new Admonition node and CodeBlock extension; add DiffViewer and HistoryPanel components for document version comparison; introduce password hashing utility with SHA-256.
This commit is contained in:
170
frontend/src/components/history/HistoryPanel.vue
Normal file
170
frontend/src/components/history/HistoryPanel.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { gql } from '@/lib/gql'
|
||||
|
||||
export interface CommitEntry {
|
||||
hash: string
|
||||
author: string
|
||||
email: string
|
||||
date: string
|
||||
subject: string
|
||||
added: number
|
||||
removed: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
slug: string
|
||||
isOpen: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
(e: 'view-version', entry: CommitEntry): void
|
||||
(e: 'compare-with-latest', entry: CommitEntry, latestHash: string): void
|
||||
}>()
|
||||
|
||||
const history = ref<CommitEntry[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
async function loadHistory() {
|
||||
if (!props.slug || props.slug === 'new') return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await gql<{ history: CommitEntry[] }>(
|
||||
`query History($slug: String!) {
|
||||
history(slug: $slug) {
|
||||
hash author email date subject added removed
|
||||
}
|
||||
}`,
|
||||
{ slug: props.slug },
|
||||
)
|
||||
history.value = data.history ?? []
|
||||
} catch (e: any) {
|
||||
error.value = e.message ?? 'Failed to load history'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.isOpen, props.slug] as const,
|
||||
([open]) => {
|
||||
if (open) loadHistory()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
const d = new Date(dateStr)
|
||||
return d.toLocaleString('sv-SE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="history-slide">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="flex flex-col w-80 flex-shrink-0 border-l border-slate-200 dark:border-slate-700/60 bg-slate-50 dark:bg-slate-800/60 overflow-hidden"
|
||||
>
|
||||
<!-- Panel header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-700/60 flex-shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h2 class="text-sm font-semibold text-slate-700 dark:text-slate-200">History</h2>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 rounded hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors"
|
||||
aria-label="Close history panel"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div v-if="loading" class="p-4 text-sm text-slate-400 animate-pulse">Loading history…</div>
|
||||
|
||||
<div v-else-if="error" class="p-4 text-sm text-red-400">{{ error }}</div>
|
||||
|
||||
<div v-else-if="history.length === 0" class="p-4 text-sm text-slate-400">
|
||||
No commits found for this document.
|
||||
</div>
|
||||
|
||||
<ul v-else class="divide-y divide-slate-200 dark:divide-slate-700/40">
|
||||
<li
|
||||
v-for="(entry, idx) in history"
|
||||
:key="entry.hash"
|
||||
class="p-4 hover:bg-white/60 dark:hover:bg-slate-700/30 transition-colors"
|
||||
>
|
||||
<!-- Commit subject -->
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-100 mb-1 leading-snug line-clamp-2">
|
||||
{{ entry.subject || '(no message)' }}
|
||||
</p>
|
||||
|
||||
<!-- Author + date -->
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mb-2">
|
||||
<span class="font-medium">{{ entry.author }}</span>
|
||||
· {{ formatDate(entry.date) }}
|
||||
</p>
|
||||
|
||||
<!-- Line diff stats -->
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="flex items-center gap-1 text-xs font-mono font-semibold text-green-600 dark:text-green-400">
|
||||
<span>+{{ entry.added }}</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1 text-xs font-mono font-semibold text-red-500 dark:text-red-400">
|
||||
<span>-{{ entry.removed }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-slate-400 font-mono truncate flex-1" :title="entry.hash">
|
||||
{{ entry.hash.slice(0, 7) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="text-xs px-2.5 py-1 rounded bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-200 transition-colors"
|
||||
@click="emit('view-version', entry)"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
v-if="idx > 0"
|
||||
class="text-xs px-2.5 py-1 rounded bg-blue-100 dark:bg-blue-900/50 hover:bg-blue-200 dark:hover:bg-blue-800/60 text-blue-700 dark:text-blue-300 transition-colors"
|
||||
@click="emit('compare-with-latest', entry, history[0].hash)"
|
||||
>
|
||||
Compare with latest
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.history-slide-enter-active,
|
||||
.history-slide-leave-active {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.history-slide-enter-from,
|
||||
.history-slide-leave-to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user