mirror of
https://github.com/zadam/trilium.git
synced 2025-10-29 10:39:00 +01:00
feat: show source diff between note and revision
This commit is contained in:
parent
1c451fb98a
commit
c60c738c7e
@ -2376,3 +2376,13 @@ footer.webview-footer button {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
.revision-diff-added {
|
||||
background: rgba(100, 200, 100, 0.5);
|
||||
}
|
||||
|
||||
.revision-diff-removed {
|
||||
background: rgba(255, 100, 100, 0.5);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@ -263,10 +263,10 @@
|
||||
"confirm_delete_all": "Do you want to delete all revisions of this note?",
|
||||
"no_revisions": "No revisions for this note yet...",
|
||||
"restore_button": "Restore",
|
||||
"diff_button": "Diff",
|
||||
"content_button": "Content",
|
||||
"diff_button_title": "Show note source diff",
|
||||
"content_button_title": "Show revision content",
|
||||
"diff_on": "Show diff",
|
||||
"diff_off": "Show content",
|
||||
"diff_on_hint": "Click to show note source diff",
|
||||
"diff_off_hint": "Click to show note content",
|
||||
"diff_not_available": "Diff isn't available.",
|
||||
"confirm_restore": "Do you want to restore this revision? This will overwrite the current title and content of the note with this revision.",
|
||||
"delete_button": "Delete",
|
||||
|
||||
@ -7,6 +7,7 @@ import { t } from "../../services/i18n";
|
||||
import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import Button from "../react/Button";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import Modal from "../react/Modal";
|
||||
import FormList, { FormListItem } from "../react/FormList";
|
||||
import utils from "../../services/utils";
|
||||
@ -59,7 +60,23 @@ export default function RevisionsDialog() {
|
||||
helpPageId="vZWERwf8U3nx"
|
||||
bodyStyle={{ display: "flex", height: "80vh" }}
|
||||
header={
|
||||
(!!revisions?.length && <Button text={t("revisions.delete_all_revisions")} size="small" style={{ padding: "0 10px" }}
|
||||
!!revisions?.length && (
|
||||
<>
|
||||
{["text", "code", "mermaid"].includes(currentRevision?.type ?? "") && (
|
||||
<FormToggle
|
||||
currentValue={showDiff}
|
||||
onChange={(newValue) => setShowDiff(newValue)}
|
||||
switchOnName={t("revisions.diff_on")}
|
||||
switchOffName={t("revisions.diff_off")}
|
||||
switchOnTooltip={t("revisions.diff_on_hint")}
|
||||
switchOffTooltip={t("revisions.diff_off_hint")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
text={t("revisions.delete_all_revisions")}
|
||||
size="small"
|
||||
style={{ padding: "0 10px" }}
|
||||
onClick={async () => {
|
||||
const text = t("revisions.confirm_delete_all");
|
||||
|
||||
@ -69,7 +86,10 @@ export default function RevisionsDialog() {
|
||||
setCurrentRevision(undefined);
|
||||
toast.showMessage(t("revisions.revisions_deleted"));
|
||||
}
|
||||
}}/>)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
footer={<RevisionFooter note={note} />}
|
||||
footerStyle={{ paddingTop: 0, paddingBottom: 0 }}
|
||||
@ -104,9 +124,8 @@ export default function RevisionsDialog() {
|
||||
<RevisionPreview
|
||||
noteContent={noteContent}
|
||||
revisionItem={currentRevision}
|
||||
setShown={setShown}
|
||||
showDiff={showDiff}
|
||||
setShowDiff={setShowDiff}
|
||||
setShown={setShown}
|
||||
onRevisionDeleted={() => {
|
||||
setRefreshCounter(c => c + 1);
|
||||
setCurrentRevision(undefined);
|
||||
@ -131,12 +150,11 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
|
||||
</FormList>);
|
||||
}
|
||||
|
||||
function RevisionPreview({noteContent, revisionItem, setShown, showDiff, setShowDiff, onRevisionDeleted }: {
|
||||
function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevisionDeleted }: {
|
||||
noteContent?: string,
|
||||
revisionItem?: RevisionItem,
|
||||
setShown: Dispatch<StateUpdater<boolean>>,
|
||||
showDiff: boolean,
|
||||
setShowDiff: Dispatch<StateUpdater<boolean>>,
|
||||
setShown: Dispatch<StateUpdater<boolean>>,
|
||||
onRevisionDeleted?: () => void
|
||||
}) {
|
||||
const [ fullRevision, setFullRevision ] = useState<RevisionPojo>();
|
||||
@ -156,17 +174,6 @@ function RevisionPreview({noteContent, revisionItem, setShown, showDiff, setShow
|
||||
{(revisionItem && <div className="revision-title-buttons">
|
||||
{(!revisionItem.isProtected || protected_session_holder.isProtectedSessionAvailable()) &&
|
||||
<>
|
||||
{["text", "code", "mermaid"].includes(revisionItem.type) && (
|
||||
<Button
|
||||
icon={showDiff ? "bx bx-detail" : "bx bx-outline"}
|
||||
text={showDiff ? t("revisions.content_button") : t("revisions.diff_button")}
|
||||
title={showDiff ? t("revisions.content_button_title") : t("revisions.diff_button_title")}
|
||||
onClick={async () => {
|
||||
setShowDiff(!showDiff);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
icon="bx bx-history"
|
||||
text={t("revisions.restore_button")}
|
||||
@ -294,32 +301,46 @@ function RevisionContentText({ content }: { content: string | Buffer<ArrayBuffer
|
||||
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
|
||||
}
|
||||
|
||||
function RevisionContentDiff({ noteContent, itemContent, itemType }: { noteContent?: string, itemContent: string | Buffer<ArrayBufferLike> | undefined, itemType: string }) {
|
||||
let diffHtml: string;
|
||||
function RevisionContentDiff({ noteContent, itemContent, itemType }: {
|
||||
noteContent?: string,
|
||||
itemContent: string | Buffer<ArrayBufferLike> | undefined,
|
||||
itemType: string
|
||||
}) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
if (noteContent && typeof itemContent === "string") {
|
||||
if (itemType === "text") {
|
||||
noteContent = utils.formatHtml(noteContent);
|
||||
itemContent = utils.formatHtml(itemContent);
|
||||
useEffect(() => {
|
||||
if (!noteContent || typeof itemContent !== "string") {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.textContent = t("revisions.diff_not_available");
|
||||
}
|
||||
const diff = diffWords(noteContent, itemContent);
|
||||
diffHtml = diff.map(part => {
|
||||
return;
|
||||
}
|
||||
|
||||
let processedNoteContent = noteContent;
|
||||
let processedItemContent = itemContent;
|
||||
|
||||
if (itemType === "text") {
|
||||
processedNoteContent = utils.formatHtml(noteContent);
|
||||
processedItemContent = utils.formatHtml(itemContent);
|
||||
}
|
||||
|
||||
const diff = diffWords(processedNoteContent, processedItemContent);
|
||||
const diffHtml = diff.map(part => {
|
||||
if (part.added) {
|
||||
return `<span style="background:rgba(100, 200, 100, 0.5)">${utils.escapeHtml(part.value)}</span>`;
|
||||
return `<span class="revision-diff-added">${utils.escapeHtml(part.value)}</span>`;
|
||||
} else if (part.removed) {
|
||||
return `<span style="background:rgba(255, 100, 100, 0.5);text-decoration:line-through;">${utils.escapeHtml(part.value)}</span>`;
|
||||
return `<span class="revision-diff-removed">${utils.escapeHtml(part.value)}</span>`;
|
||||
} else {
|
||||
return utils.escapeHtml(part.value);
|
||||
}
|
||||
}).join("");
|
||||
} else {
|
||||
return <>{t("revisions.diff_not_available")}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ck-content" style={{ whiteSpace: "pre-wrap" }}
|
||||
dangerouslySetInnerHTML={{ __html: diffHtml }}></div>
|
||||
);
|
||||
if (contentRef.current) {
|
||||
contentRef.current.innerHTML = diffHtml;
|
||||
}
|
||||
}, [noteContent, itemContent, itemType]);
|
||||
|
||||
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }}></div>;
|
||||
}
|
||||
|
||||
function RevisionFooter({ note }: { note?: FNote }) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user