feat(#38): document edit history with diff and compare mode #52

Merged
marcel merged 13 commits from feat/38-document-edit-history into main 2026-03-23 17:27:57 +01:00
Showing only changes of commit 0020d1e773 - Show all commits

View File

@@ -112,6 +112,44 @@ function personLabel(p: { firstName: string; lastName: string }): string {
return `${p.firstName} ${p.lastName}`.trim();
}
const DIFF_CONTEXT_WORDS = 4;
type DiffPart = { value: string; added?: boolean; removed?: boolean };
function trimContextParts(parts: DiffPart[]): DiffPart[] {
return parts.flatMap((part, i) => {
if (part.added || part.removed) return [part];
const tokens = part.value.split(/(\s+)/).filter(Boolean);
const wordCount = tokens.filter((t) => /\S/.test(t)).length;
if (wordCount <= DIFF_CONTEXT_WORDS * 2) return [part];
function keepFirst(n: number): string {
let count = 0;
const out: string[] = [];
for (const t of tokens) {
out.push(t);
if (/\S/.test(t) && ++count >= n) break;
}
return out.join('');
}
function keepLast(n: number): string {
let count = 0;
const out: string[] = [];
for (const t of [...tokens].reverse()) {
out.unshift(t);
if (/\S/.test(t) && ++count >= n) break;
}
return out.join('');
}
const isFirst = i === 0;
const isLast = i === parts.length - 1;
if (isFirst) return [{ value: '… ' + keepLast(DIFF_CONTEXT_WORDS) }];
if (isLast) return [{ value: keepFirst(DIFF_CONTEXT_WORDS) + ' …' }];
return [{ value: keepFirst(DIFF_CONTEXT_WORDS) + ' … ' + keepLast(DIFF_CONTEXT_WORDS) }];
});
}
function buildDiff(older: SnapshotDoc | null, newer: SnapshotDoc): DiffEntry[] {
const entries: DiffEntry[] = [];
@@ -119,7 +157,7 @@ function buildDiff(older: SnapshotDoc | null, newer: SnapshotDoc): DiffEntry[] {
const a = older?.[field] ?? '';
const b = newer[field] ?? '';
if (a === b) continue;
const parts = diffWords(a, b);
const parts = trimContextParts(diffWords(a, b));
entries.push({ kind: 'text', field, label: fieldLabels[field](), parts });
}
@@ -844,14 +882,14 @@ function versionLabel(v: VersionSummary, index: number): string {
</div>
{:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')}
<iframe
src={fileUrl}
src="{fileUrl}#zoom=page-width"
title={m.doc_preview_iframe_title()}
class="h-full w-full border-none bg-white"
></iframe>
{:else if fileUrl}
<div class="flex h-full w-full items-center justify-center overflow-auto p-8">
<img
src={fileUrl}
src="{fileUrl}#zoom=page-width"
alt={m.doc_image_alt()}
class="max-h-full max-w-full object-contain shadow-2xl"
/>