web: fix previewing note history where only title has changed

This commit is contained in:
Abdullah Atta
2026-01-16 12:41:12 +05:00
parent 2b0e5c41c8
commit 29f789493e
2 changed files with 202 additions and 183 deletions

View File

@@ -17,13 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState
} from "react";
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Copy, Restore } from "../icons";
import ContentToggle from "./content-toggle";
@@ -59,6 +53,8 @@ function DiffViewer(props: DiffViewerProps) {
const onResolveContent = useCallback(
(saveCopy: boolean) => {
if (!conflictedContent) return;
const toKeep =
selectedContent === 1 ? session.content?.conflicted : session.content;
const toCopy =
@@ -69,7 +65,7 @@ function DiffViewer(props: DiffViewerProps) {
toKeep,
toCopy: saveCopy ? toCopy : undefined,
toKeepDateEdited: toKeep.dateEdited,
dateResolved: conflictedContent!.dateModified
dateResolved: conflictedContent.dateModified
});
},
[conflictedContent, selectedContent, session.content, session.note]
@@ -82,8 +78,6 @@ function DiffViewer(props: DiffViewerProps) {
});
}, [session]);
if (!conflictedContent || !content) return null;
return (
<Flex
ref={(el) => {
@@ -110,8 +104,8 @@ function DiffViewer(props: DiffViewerProps) {
}}
dangerouslySetInnerHTML={{
__html:
session.type === "diff"
? diff(session.oldContentTitle || "", session.note.title)
session.type === "diff" && session.oldTitle
? diff(session.oldTitle || "", session.note.title)
: session.note.title
}}
></Text>
@@ -145,28 +139,34 @@ function DiffViewer(props: DiffViewerProps) {
<Restore size={18} />
<Text ml={1}>{strings.restoreThisVersion()}</Text>
</Button>
<Button
variant="secondary"
onClick={async () => {
const { closeTabs, openSession } = useEditorStore.getState();
{!content || !conflictedContent ? null : (
<Button
variant="secondary"
onClick={async () => {
const { closeTabs, openSession } = useEditorStore.getState();
const noteId = await createCopy(session.note, content);
const noteId = await createCopy(
session.note,
content,
session.oldTitle
);
closeTabs(session.id);
closeTabs(session.id);
await notesStore.refresh();
await openSession(noteId);
}}
mr={2}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Copy size={18} />
<Text ml={1}>{strings.saveACopy()}</Text>
</Button>
await notesStore.refresh();
await openSession(noteId);
}}
mr={2}
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Copy size={18} />
<Text ml={1}>{strings.saveACopy()}</Text>
</Button>
)}
<Button
variant="errorSecondary"
onClick={() => {
@@ -179,165 +179,183 @@ function DiffViewer(props: DiffViewerProps) {
</>
) : null}
</Flex>
<ScrollSync>
<Flex
{!conflictedContent || !content ? (
<Text
variant="body"
sx={{
flex: "1 1 auto",
flexDirection: ["column", "column", "row"],
overflow: "hidden"
px: 2,
py: 1,
my: 1,
borderTop: "1px solid var(--border)"
}}
>
{strings.noContent()}
</Text>
) : (
<ScrollSync>
<Flex
className="firstEditor"
data-test-id="first-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"]
flexDirection: ["column", "column", "row"],
overflow: "hidden"
}}
>
{content.locked ? (
<UnlockView
title={getFormattedDate(content.dateEdited)}
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
content,
password
);
setContent({
...content,
...decryptedContent,
locked: false
});
}}
/>
) : (
<>
<ContentToggle
label={
session.type === "diff"
? strings.olderVersion()
: strings.currentNote()
}
readonly={session.type === "diff"}
dateEdited={content.dateEdited}
isSelected={selectedContent === 0}
isOtherSelected={selectedContent === 1}
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
resolveConflict={onResolveContent}
sx={{
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1
<Flex
className="firstEditor"
data-test-id="first-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"]
}}
>
{content.locked ? (
<UnlockView
title={getFormattedDate(content.dateEdited)}
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
content,
password
);
setContent({
...content,
...decryptedContent,
locked: false
});
}}
/>
<ScrollSyncPane>
<Flex
) : (
<>
<ContentToggle
label={
session.type === "diff"
? strings.olderVersion()
: strings.currentNote()
}
readonly={session.type === "diff"}
dateEdited={content.dateEdited}
isSelected={selectedContent === 0}
isOtherSelected={selectedContent === 1}
onToggle={() =>
setSelectedContent((s) => (s === 0 ? -1 : 0))
}
resolveConflict={onResolveContent}
sx={{
px: 2,
overflowY: "auto",
flex: 1,
borderStyle: "solid",
borderWidth: 0,
borderRightWidth: [0, 0, 1],
borderBottomWidth: [1, 1, 0],
borderColor: "border"
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1
}}
>
<Editor
id={content.id}
content={() => content.data}
session={session}
nonce={content.dateEdited}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
</Flex>
<Flex
className="secondEditor"
data-test-id="second-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"],
borderLeft: conflictedContent.locked
? "1px solid var(--border)"
: "none"
}}
>
{conflictedContent.locked ? (
<UnlockView
title={getFormattedDate(conflictedContent.dateEdited)}
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
conflictedContent,
password
);
setConflictedContent({
...conflictedContent,
...decryptedContent,
locked: false
});
}}
/>
) : (
<>
<ContentToggle
readonly={session.type === "diff"}
resolveConflict={onResolveContent}
label={
session.type === "diff"
? strings.currentNote()
: strings.incomingNote()
}
isSelected={selectedContent === 1}
isOtherSelected={selectedContent === 0}
dateEdited={conflictedContent.dateEdited}
onToggle={() => setSelectedContent((s) => (s === 1 ? -1 : 1))}
sx={{
alignItems: "flex-end",
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1,
pt: [1, 1, 0]
/>
<ScrollSyncPane>
<Flex
sx={{
px: 2,
overflowY: "auto",
flex: 1,
borderStyle: "solid",
borderWidth: 0,
borderRightWidth: [0, 0, 1],
borderBottomWidth: [1, 1, 0],
borderColor: "border"
}}
>
<Editor
id={content.id}
content={() => content.data}
session={session}
nonce={content.dateEdited}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
</Flex>
<Flex
className="secondEditor"
data-test-id="second-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
width: ["100%", "100%", "50%"],
height: ["50%", "50%", "100%"],
borderLeft: conflictedContent?.locked
? "1px solid var(--border)"
: "none"
}}
>
{conflictedContent.locked ? (
<UnlockView
title={getFormattedDate(conflictedContent.dateEdited)}
subtitle={strings.enterPasswordToUnlockVersion()}
buttonTitle={strings.unlock()}
unlock={async (password) => {
const decryptedContent = await db.vault.decryptContent(
conflictedContent,
password
);
setConflictedContent({
...conflictedContent,
...decryptedContent,
locked: false
});
}}
/>
<ScrollSyncPane>
<Flex sx={{ px: 2, overflow: "auto" }}>
<Editor
id={`${conflictedContent.id}-conflicted`}
session={session}
content={() =>
content.locked
? conflictedContent.data
: diff(content.data, conflictedContent.data)
}
nonce={conflictedContent.dateEdited}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
) : (
<>
<ContentToggle
readonly={session.type === "diff"}
resolveConflict={onResolveContent}
label={
session.type === "diff"
? strings.currentNote()
: strings.incomingNote()
}
isSelected={selectedContent === 1}
isOtherSelected={selectedContent === 0}
dateEdited={conflictedContent.dateEdited}
onToggle={() =>
setSelectedContent((s) => (s === 1 ? -1 : 1))
}
sx={{
alignItems: "flex-end",
borderStyle: "solid",
borderWidth: 0,
borderBottomWidth: 1,
borderColor: "border",
px: 2,
pb: 1,
pt: [1, 1, 0]
}}
/>
<ScrollSyncPane>
<Flex sx={{ px: 2, overflow: "auto" }}>
<Editor
id={`${conflictedContent.id}-conflicted`}
session={session}
content={() =>
content.locked
? conflictedContent.data
: diff(content.data, conflictedContent.data)
}
nonce={conflictedContent.dateEdited}
options={{ readonly: true, headless: true }}
/>
</Flex>
</ScrollSyncPane>
</>
)}
</Flex>
</Flex>
</Flex>
</ScrollSync>
</ScrollSync>
)}
</Flex>
);
}
@@ -379,7 +397,7 @@ async function resolveConflict({
await notesStore.refresh();
}
async function createCopy(note: Note, content: ContentItem) {
async function createCopy(note: Note, content: ContentItem, title?: string) {
if (content.locked) {
const contentId = await db.content.add({
locked: true,
@@ -389,7 +407,7 @@ async function createCopy(note: Note, content: ContentItem) {
});
return await db.notes.add({
contentId,
title: note.title + " (COPY)"
title: title || note.title + " (COPY)"
});
} else {
return await db.notes.add({
@@ -397,7 +415,7 @@ async function createCopy(note: Note, content: ContentItem) {
type: "tiptap",
data: content.data
},
title: note.title + " (COPY)"
title: title || note.title + " (COPY)"
});
}
}

View File

@@ -144,8 +144,8 @@ export type DiffEditorSession = BaseEditorSession & {
type: "diff";
note: Note;
content: ContentItem;
oldTitle?: string;
historySessionId: string;
oldContentTitle?: string;
};
export type EditorSession =
@@ -620,12 +620,13 @@ class EditorStore extends BaseStore<EditorStore> {
openDiffSession = async (noteId: string, sessionId: string) => {
const session = await db.noteHistory.session(sessionId);
const note = await db.notes.note(noteId);
if (!session || !note || !note.contentId) return;
if (!session || !note) return;
const currentContent = await db.content.get(note.contentId);
const currentContent = note.contentId
? await db.content.get(note.contentId)
: undefined;
const oldContent = await db.noteHistory.content(session.id);
if (!oldContent || !currentContent) return;
if (!oldContent) return;
const {
getSession,
@@ -659,7 +660,7 @@ class EditorStore extends BaseStore<EditorStore> {
note,
tabId,
title: label,
oldContentTitle: oldContent.title,
oldTitle: oldContent.title,
historySessionId: session.id,
content: {
type: oldContent.type || "tiptap",
@@ -1356,7 +1357,7 @@ const useEditorStore = createPersistedStore(EditorStore, {
}, [] as EditorSession[])
}),
storage: db.config() as PersistStorage<Partial<EditorStore>>
});
}) as ReturnType<typeof createPersistedStore<EditorStore>>;
export { useEditorStore, SESSION_STATES };
const MILLISECONDS_IN_A_MINUTE = 60 * 1000;