diff --git a/frontend/src/api/rulebooks.ts b/frontend/src/api/rulebooks.ts index f884999..9665f42 100644 --- a/frontend/src/api/rulebooks.ts +++ b/frontend/src/api/rulebooks.ts @@ -228,6 +228,49 @@ export async function unrelateRules(relationId: number): Promise { return apiDelete(`/api/rule-relations/${relationId}`); } +/** + * One entry in a rule's edit history. + * + * Each entry holds the text the edit REPLACED, not the text it introduced — + * so the newest entry is what the rule said before its most recent change, + * and what that change produced is the rule as it stands now. Read the other + * way round, every diff comes out backwards. + * + * The listing form omits the long fields; open one to get them. + */ +export interface RuleVersion { + id: number; + rule_id: number; + /** Who made the edit. Null when that account has since been deleted. */ + user_id: number | null; + title: string; + created_at: string; + statement?: string; + why?: string; + how_to_apply?: string; + when_to_apply?: string; + tier?: string; + verify_with?: string; + expires_when?: string; +} + +export async function listRuleVersions(ruleId: number): Promise { + const data = await apiGet<{ versions: RuleVersion[] }>( + `/api/rules/${ruleId}/versions`, + ); + return data.versions; +} + +export async function getRuleVersion( + ruleId: number, versionId: number, +): Promise { + return apiGet(`/api/rules/${ruleId}/versions/${versionId}`); +} + +// No restoreRuleVersion, deliberately (milestone 323). Putting an old wording +// back goes through updateRule, which snapshots what it replaces — so the +// undo stays visible in the history like any other edit. + export async function deleteRule(id: number): Promise { return apiDelete(`/api/rules/${id}`); } diff --git a/frontend/src/components/HistoryPanel.vue b/frontend/src/components/HistoryPanel.vue index 0688a9c..56491bc 100644 --- a/frontend/src/components/HistoryPanel.vue +++ b/frontend/src/components/HistoryPanel.vue @@ -2,7 +2,7 @@ import { ref, computed, onMounted } from "vue"; import { apiGet, pinNoteVersion, unpinNoteVersion } from "@/api/client"; import DiffView from "@/components/DiffView.vue"; -import type { DiffLine } from "@/composables/useAssist"; +import { computeDiff, type DiffLine } from "@/utils/diff"; import { fmtStamp } from "@/utils/dateFormat"; interface NoteVersion { @@ -33,28 +33,8 @@ const loadingDetail = ref(false); const diff = computed(() => { if (!selectedVersion.value?.body) return []; - const a = props.currentBody; - const b = selectedVersion.value.body; - const aLines = a.split('\n'); - const bLines = b.split('\n'); - const m = aLines.length, n = bLines.length; - const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); - for (let i = m - 1; i >= 0; i--) - for (let j = n - 1; j >= 0; j--) - dp[i][j] = aLines[i] === bLines[j] - ? dp[i+1][j+1] + 1 - : Math.max(dp[i+1][j], dp[i][j+1]); - const result: DiffLine[] = []; - let i = 0, j = 0; - while (i < m && j < n) { - if (aLines[i] === bLines[j]) { result.push({ type: 'equal', text: aLines[i++] }); j++; } - else if (dp[i+1][j] >= dp[i][j+1]) result.push({ type: 'delete', text: aLines[i++] }); - else result.push({ type: 'insert', text: bLines[j++] }); - } - while (i < m) result.push({ type: 'delete', text: aLines[i++] }); - while (j < n) result.push({ type: 'insert', text: bLines[j++] }); - return result; + return computeDiff(props.currentBody, selectedVersion.value.body); }); async function loadVersions() { diff --git a/frontend/src/components/VersionHistorySection.vue b/frontend/src/components/VersionHistorySection.vue index 6c7cc16..fb344c6 100644 --- a/frontend/src/components/VersionHistorySection.vue +++ b/frontend/src/components/VersionHistorySection.vue @@ -2,7 +2,7 @@ import { ref, computed } from "vue"; import { apiGet } from "@/api/client"; import DiffView from "@/components/DiffView.vue"; -import type { DiffLine } from "@/composables/useAssist"; +import { computeDiff, type DiffLine } from "@/utils/diff"; interface NoteVersion { id: number; @@ -31,25 +31,7 @@ const loadingDetail = ref(false); const diff = computed(() => { if (!selectedVersion.value?.body) return []; - const aLines = props.currentBody.split("\n"); - const bLines = selectedVersion.value.body.split("\n"); - const m = aLines.length, n = bLines.length; - const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); - for (let i = m - 1; i >= 0; i--) - for (let j = n - 1; j >= 0; j--) - dp[i][j] = aLines[i] === bLines[j] - ? dp[i + 1][j + 1] + 1 - : Math.max(dp[i + 1][j], dp[i][j + 1]); - const result: DiffLine[] = []; - let i = 0, j = 0; - while (i < m && j < n) { - if (aLines[i] === bLines[j]) { result.push({ type: "equal", text: aLines[i++] }); j++; } - else if (dp[i + 1][j] >= dp[i][j + 1]) result.push({ type: "delete", text: aLines[i++] }); - else result.push({ type: "insert", text: bLines[j++] }); - } - while (i < m) result.push({ type: "delete", text: aLines[i++] }); - while (j < n) result.push({ type: "insert", text: bLines[j++] }); - return result; + return computeDiff(props.currentBody, selectedVersion.value.body); }); function formatDate(iso: string): string { diff --git a/frontend/src/components/rules/RuleEditorSlideOver.vue b/frontend/src/components/rules/RuleEditorSlideOver.vue index 26523bf..22f54c4 100644 --- a/frontend/src/components/rules/RuleEditorSlideOver.vue +++ b/frontend/src/components/rules/RuleEditorSlideOver.vue @@ -3,6 +3,7 @@ import { computed, ref, watch, onMounted } from "vue"; import { useRulebooksStore } from "@/stores/rulebooks"; import { useCanonicalSystemsStore } from "@/stores/canonicalSystems"; import type { RuleTier } from "@/api/rulebooks"; +import RuleHistoryPanel from "@/components/rules/RuleHistoryPanel.vue"; const props = defineProps<{ ruleId: number | null; topicId: number | null }>(); const emit = defineEmits<{ close: [] }>(); @@ -256,6 +257,17 @@ watch(() => props.ruleId, load); How to apply