refactoring, linting, formatting
This commit is contained in:
208
src/workspace/useWorkspaceState.test.tsx
Normal file
208
src/workspace/useWorkspaceState.test.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, { forwardRef, useImperativeHandle } from "react";
|
||||
import { act, render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { PageRef } from "../pdf/pdfTypes";
|
||||
import type {
|
||||
WorkspaceCommandRecord,
|
||||
WorkspaceCommandState,
|
||||
} from "./workspaceCommands";
|
||||
import { useWorkspaceState } from "./useWorkspaceState";
|
||||
|
||||
function page(id: string, sourcePageIndex: number, rotation = 0): PageRef {
|
||||
return { id, sourcePageIndex, rotation };
|
||||
}
|
||||
|
||||
function state(
|
||||
pages: PageRef[],
|
||||
selectedPageIds: string[] = [],
|
||||
lastSelectedVisualIndex: number | null = null,
|
||||
): WorkspaceCommandState {
|
||||
return { pages, selectedPageIds, lastSelectedVisualIndex };
|
||||
}
|
||||
|
||||
interface HarnessRef {
|
||||
snapshot: () => {
|
||||
pages: PageRef[];
|
||||
selectedPageIds: string[];
|
||||
lastSelectedVisualIndex: number | null;
|
||||
workspaceDirty: boolean;
|
||||
workspaceMessage: string | null;
|
||||
workspaceHistory: WorkspaceCommandRecord[];
|
||||
redoHistory: WorkspaceCommandRecord[];
|
||||
};
|
||||
replaceWorkspaceState: ReturnType<
|
||||
typeof useWorkspaceState
|
||||
>["replaceWorkspaceState"];
|
||||
getCurrentCommandState: ReturnType<
|
||||
typeof useWorkspaceState
|
||||
>["getCurrentCommandState"];
|
||||
createWorkspaceCommand: ReturnType<
|
||||
typeof useWorkspaceState
|
||||
>["createWorkspaceCommand"];
|
||||
executeWorkspaceCommand: ReturnType<
|
||||
typeof useWorkspaceState
|
||||
>["executeWorkspaceCommand"];
|
||||
handleUndo: ReturnType<typeof useWorkspaceState>["handleUndo"];
|
||||
handleRedo: ReturnType<typeof useWorkspaceState>["handleRedo"];
|
||||
}
|
||||
|
||||
const Harness = forwardRef<HarnessRef, { onContentChanged?: () => void }>(
|
||||
({ onContentChanged }, ref) => {
|
||||
const workspace = useWorkspaceState({ onContentChanged });
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
snapshot: () => ({
|
||||
pages: workspace.pages,
|
||||
selectedPageIds: workspace.selectedPageIds,
|
||||
lastSelectedVisualIndex: workspace.lastSelectedVisualIndex,
|
||||
workspaceDirty: workspace.workspaceDirty,
|
||||
workspaceMessage: workspace.workspaceMessage,
|
||||
workspaceHistory: workspace.workspaceHistory,
|
||||
redoHistory: workspace.redoHistory,
|
||||
}),
|
||||
replaceWorkspaceState: workspace.replaceWorkspaceState,
|
||||
getCurrentCommandState: workspace.getCurrentCommandState,
|
||||
createWorkspaceCommand: workspace.createWorkspaceCommand,
|
||||
executeWorkspaceCommand: workspace.executeWorkspaceCommand,
|
||||
handleUndo: workspace.handleUndo,
|
||||
handleRedo: workspace.handleRedo,
|
||||
}));
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
function renderHarness(onContentChanged = vi.fn()) {
|
||||
const ref = React.createRef<HarnessRef>();
|
||||
render(<Harness ref={ref} onContentChanged={onContentChanged} />);
|
||||
|
||||
if (!ref.current) {
|
||||
throw new Error("Harness ref was not initialized");
|
||||
}
|
||||
|
||||
return { ref, onContentChanged };
|
||||
}
|
||||
|
||||
describe("useWorkspaceState", () => {
|
||||
it("replaces workspace state from loaded data without marking it dirty", () => {
|
||||
const { ref } = renderHarness();
|
||||
const loadedPages = [page("p1", 0), page("p2", 1, 90)];
|
||||
|
||||
act(() => {
|
||||
ref.current?.replaceWorkspaceState({
|
||||
pages: loadedPages,
|
||||
selectedPageIds: ["p2"],
|
||||
lastSelectedVisualIndex: 1,
|
||||
history: [],
|
||||
redoHistory: [],
|
||||
dirty: false,
|
||||
message: "Workspace loaded.",
|
||||
});
|
||||
});
|
||||
|
||||
expect(ref.current?.snapshot()).toMatchObject({
|
||||
pages: loadedPages,
|
||||
selectedPageIds: ["p2"],
|
||||
lastSelectedVisualIndex: 1,
|
||||
workspaceDirty: false,
|
||||
workspaceMessage: "Workspace loaded.",
|
||||
workspaceHistory: [],
|
||||
redoHistory: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("executes commands, stores history, clears redo, and marks content changed", () => {
|
||||
const { ref, onContentChanged } = renderHarness();
|
||||
const before = state([page("p1", 0), page("p2", 1)], ["p1"], 0);
|
||||
const after = state([page("p2", 1), page("p1", 0)], ["p2"], 0);
|
||||
|
||||
act(() => {
|
||||
ref.current?.replaceWorkspaceState({
|
||||
...before,
|
||||
redoHistory: [
|
||||
{
|
||||
id: "redo-record",
|
||||
type: "old-redo",
|
||||
label: "Old redo",
|
||||
timestamp: "2026-05-17T10:00:00.000Z",
|
||||
payload: { before, after },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
const command = ref.current?.createWorkspaceCommand({
|
||||
type: "reorder-pages",
|
||||
label: "Move page 2 before page 1",
|
||||
before,
|
||||
after,
|
||||
});
|
||||
|
||||
if (!command) throw new Error("Command was not created");
|
||||
ref.current?.executeWorkspaceCommand(command);
|
||||
});
|
||||
|
||||
const snapshot = ref.current?.snapshot();
|
||||
expect(snapshot?.pages).toEqual(after.pages);
|
||||
expect(snapshot?.selectedPageIds).toEqual(["p2"]);
|
||||
expect(snapshot?.workspaceDirty).toBe(true);
|
||||
expect(snapshot?.workspaceMessage).toBeNull();
|
||||
expect(snapshot?.workspaceHistory).toHaveLength(1);
|
||||
expect(snapshot?.workspaceHistory[0]).toMatchObject({
|
||||
type: "reorder-pages",
|
||||
label: "Move page 2 before page 1",
|
||||
});
|
||||
expect(snapshot?.redoHistory).toHaveLength(0);
|
||||
expect(onContentChanged).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("undoes and redoes command records in stack order", () => {
|
||||
const { ref, onContentChanged } = renderHarness();
|
||||
const initial = state([page("p1", 0), page("p2", 1)], ["p1"], 0);
|
||||
const reordered = state([page("p2", 1), page("p1", 0)], ["p2"], 0);
|
||||
|
||||
act(() => {
|
||||
ref.current?.replaceWorkspaceState(initial);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
const command = ref.current?.createWorkspaceCommand({
|
||||
type: "reorder-pages",
|
||||
label: "Move page",
|
||||
before: initial,
|
||||
after: reordered,
|
||||
});
|
||||
|
||||
if (!command) throw new Error("Command was not created");
|
||||
ref.current?.executeWorkspaceCommand(command);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
ref.current?.handleUndo();
|
||||
});
|
||||
|
||||
expect(ref.current?.snapshot()).toMatchObject({
|
||||
pages: initial.pages,
|
||||
selectedPageIds: initial.selectedPageIds,
|
||||
lastSelectedVisualIndex: initial.lastSelectedVisualIndex,
|
||||
workspaceDirty: true,
|
||||
});
|
||||
expect(ref.current?.snapshot().workspaceHistory).toHaveLength(0);
|
||||
expect(ref.current?.snapshot().redoHistory).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
ref.current?.handleRedo();
|
||||
});
|
||||
|
||||
expect(ref.current?.snapshot()).toMatchObject({
|
||||
pages: reordered.pages,
|
||||
selectedPageIds: reordered.selectedPageIds,
|
||||
lastSelectedVisualIndex: reordered.lastSelectedVisualIndex,
|
||||
workspaceDirty: true,
|
||||
});
|
||||
expect(ref.current?.snapshot().workspaceHistory).toHaveLength(1);
|
||||
expect(ref.current?.snapshot().redoHistory).toHaveLength(0);
|
||||
expect(onContentChanged).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
246
src/workspace/useWorkspaceState.ts
Normal file
246
src/workspace/useWorkspaceState.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import type { PageRef } from "../pdf/pdfTypes";
|
||||
import type {
|
||||
WorkspaceCommand,
|
||||
WorkspaceCommandRecord,
|
||||
WorkspaceCommandState,
|
||||
} from "./workspaceCommands";
|
||||
import {
|
||||
createSnapshotCommand,
|
||||
reviveWorkspaceCommand,
|
||||
toWorkspaceCommandRecord,
|
||||
} from "./workspaceCommands";
|
||||
|
||||
function createId(prefix: string): string {
|
||||
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
export function createWorkspaceId(): string {
|
||||
return createId("workspace");
|
||||
}
|
||||
|
||||
export function defaultWorkspaceNameFromPdfName(pdfName: string): string {
|
||||
return pdfName.replace(/\.pdf$/i, "") || "Untitled workspace";
|
||||
}
|
||||
|
||||
export function createPageRefId(): string {
|
||||
return createId("page");
|
||||
}
|
||||
|
||||
export function createInitialPageRefs(pageCount: number): PageRef[] {
|
||||
return Array.from({ length: pageCount }, (_, sourcePageIndex) => ({
|
||||
id: createPageRefId(),
|
||||
sourcePageIndex,
|
||||
rotation: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeRotation(rotation: number | undefined): number {
|
||||
return (((rotation ?? 0) % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
type SetStateAction<T> = T | ((previous: T) => T);
|
||||
|
||||
interface UseWorkspaceStateOptions {
|
||||
onContentChanged?: () => void;
|
||||
}
|
||||
|
||||
interface ReplaceWorkspaceStateOptions {
|
||||
pages?: PageRef[];
|
||||
selectedPageIds?: string[];
|
||||
lastSelectedVisualIndex?: number | null;
|
||||
history?: WorkspaceCommandRecord[];
|
||||
redoHistory?: WorkspaceCommandRecord[];
|
||||
dirty?: boolean;
|
||||
message?: string | null;
|
||||
}
|
||||
|
||||
export function useWorkspaceState({
|
||||
onContentChanged,
|
||||
}: UseWorkspaceStateOptions = {}) {
|
||||
const [pages, setPagesState] = useState<PageRef[]>([]);
|
||||
const [selectedPageIds, setSelectedPageIdsState] = useState<string[]>([]);
|
||||
const [lastSelectedVisualIndex, setLastSelectedVisualIndexState] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [workspaceDirty, setWorkspaceDirty] = useState(false);
|
||||
const [workspaceMessage, setWorkspaceMessage] = useState<string | null>(null);
|
||||
const [workspaceHistory, setWorkspaceHistory] = useState<
|
||||
WorkspaceCommandRecord[]
|
||||
>([]);
|
||||
const [redoHistory, setRedoHistory] = useState<WorkspaceCommandRecord[]>([]);
|
||||
|
||||
const latestPagesRef = useRef<PageRef[]>([]);
|
||||
const selectedPageIdsRef = useRef<string[]>([]);
|
||||
const lastSelectedVisualIndexRef = useRef<number | null>(null);
|
||||
|
||||
const setPages = useCallback((action: SetStateAction<PageRef[]>) => {
|
||||
setPagesState((previous) => {
|
||||
const next = typeof action === "function" ? action(previous) : action;
|
||||
latestPagesRef.current = next;
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setSelectedPageIds = useCallback((action: SetStateAction<string[]>) => {
|
||||
setSelectedPageIdsState((previous) => {
|
||||
const next = typeof action === "function" ? action(previous) : action;
|
||||
selectedPageIdsRef.current = next;
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setLastSelectedVisualIndex = useCallback(
|
||||
(action: SetStateAction<number | null>) => {
|
||||
setLastSelectedVisualIndexState((previous) => {
|
||||
const next = typeof action === "function" ? action(previous) : action;
|
||||
lastSelectedVisualIndexRef.current = next;
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getCurrentCommandState = useCallback(
|
||||
(): WorkspaceCommandState => ({
|
||||
pages: latestPagesRef.current,
|
||||
selectedPageIds: selectedPageIdsRef.current,
|
||||
lastSelectedVisualIndex: lastSelectedVisualIndexRef.current,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const applyCommandState = useCallback(
|
||||
(state: WorkspaceCommandState) => {
|
||||
setPages(state.pages);
|
||||
setSelectedPageIds(state.selectedPageIds);
|
||||
setLastSelectedVisualIndex(state.lastSelectedVisualIndex);
|
||||
},
|
||||
[setLastSelectedVisualIndex, setPages, setSelectedPageIds],
|
||||
);
|
||||
|
||||
const markWorkspaceChanged = useCallback(() => {
|
||||
setWorkspaceDirty(true);
|
||||
setWorkspaceMessage(null);
|
||||
onContentChanged?.();
|
||||
}, [onContentChanged]);
|
||||
|
||||
const createWorkspaceCommand = useCallback(
|
||||
(params: {
|
||||
type: string;
|
||||
label: string;
|
||||
before: WorkspaceCommandState;
|
||||
after: WorkspaceCommandState;
|
||||
details?: Record<string, unknown>;
|
||||
}): WorkspaceCommand =>
|
||||
createSnapshotCommand({
|
||||
id: createId("command"),
|
||||
type: params.type,
|
||||
label: params.label,
|
||||
before: params.before,
|
||||
after: params.after,
|
||||
details: params.details,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const executeWorkspaceCommand = useCallback(
|
||||
(command: WorkspaceCommand) => {
|
||||
const nextState = command.do(getCurrentCommandState());
|
||||
|
||||
applyCommandState(nextState);
|
||||
setWorkspaceHistory((previous) => [
|
||||
...previous,
|
||||
toWorkspaceCommandRecord(command),
|
||||
]);
|
||||
setRedoHistory([]);
|
||||
markWorkspaceChanged();
|
||||
},
|
||||
[applyCommandState, getCurrentCommandState, markWorkspaceChanged],
|
||||
);
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
const record = workspaceHistory[workspaceHistory.length - 1];
|
||||
if (!record) return;
|
||||
|
||||
const command = reviveWorkspaceCommand(record);
|
||||
const previousState = command.undo(getCurrentCommandState());
|
||||
|
||||
applyCommandState(previousState);
|
||||
setWorkspaceHistory((previous) => previous.slice(0, -1));
|
||||
setRedoHistory((previous) => [...previous, record]);
|
||||
markWorkspaceChanged();
|
||||
}, [
|
||||
applyCommandState,
|
||||
getCurrentCommandState,
|
||||
markWorkspaceChanged,
|
||||
workspaceHistory,
|
||||
]);
|
||||
|
||||
const handleRedo = useCallback(() => {
|
||||
const record = redoHistory[redoHistory.length - 1];
|
||||
if (!record) return;
|
||||
|
||||
const command = reviveWorkspaceCommand(record);
|
||||
const nextState = command.do(getCurrentCommandState());
|
||||
|
||||
applyCommandState(nextState);
|
||||
setRedoHistory((previous) => previous.slice(0, -1));
|
||||
setWorkspaceHistory((previous) => [...previous, record]);
|
||||
markWorkspaceChanged();
|
||||
}, [
|
||||
applyCommandState,
|
||||
getCurrentCommandState,
|
||||
markWorkspaceChanged,
|
||||
redoHistory,
|
||||
]);
|
||||
|
||||
const replaceWorkspaceState = useCallback(
|
||||
(state: ReplaceWorkspaceStateOptions = {}) => {
|
||||
setPages(state.pages ?? []);
|
||||
setSelectedPageIds(state.selectedPageIds ?? []);
|
||||
setLastSelectedVisualIndex(state.lastSelectedVisualIndex ?? null);
|
||||
setWorkspaceHistory(state.history ?? []);
|
||||
setRedoHistory(state.redoHistory ?? []);
|
||||
setWorkspaceDirty(state.dirty ?? false);
|
||||
setWorkspaceMessage(state.message ?? null);
|
||||
},
|
||||
[setLastSelectedVisualIndex, setPages, setSelectedPageIds],
|
||||
);
|
||||
|
||||
const resetWorkspaceState = useCallback(() => {
|
||||
replaceWorkspaceState();
|
||||
}, [replaceWorkspaceState]);
|
||||
|
||||
return {
|
||||
pages,
|
||||
setPages,
|
||||
selectedPageIds,
|
||||
setSelectedPageIds,
|
||||
lastSelectedVisualIndex,
|
||||
setLastSelectedVisualIndex,
|
||||
latestPagesRef,
|
||||
|
||||
workspaceDirty,
|
||||
setWorkspaceDirty,
|
||||
workspaceMessage,
|
||||
setWorkspaceMessage,
|
||||
workspaceHistory,
|
||||
setWorkspaceHistory,
|
||||
redoHistory,
|
||||
setRedoHistory,
|
||||
|
||||
getCurrentCommandState,
|
||||
applyCommandState,
|
||||
createWorkspaceCommand,
|
||||
executeWorkspaceCommand,
|
||||
handleUndo,
|
||||
handleRedo,
|
||||
replaceWorkspaceState,
|
||||
resetWorkspaceState,
|
||||
};
|
||||
}
|
||||
105
src/workspace/workspaceCommands.test.ts
Normal file
105
src/workspace/workspaceCommands.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { WorkspaceCommandState } from "./workspaceCommands";
|
||||
import {
|
||||
cloneCommandState,
|
||||
createSnapshotCommand,
|
||||
reviveWorkspaceCommand,
|
||||
toWorkspaceCommandRecord,
|
||||
} from "./workspaceCommands";
|
||||
|
||||
function makeState(pageIds: string[]): WorkspaceCommandState {
|
||||
return {
|
||||
pages: pageIds.map((id, index) => ({
|
||||
id,
|
||||
sourcePageIndex: index,
|
||||
rotation: index * 90,
|
||||
})),
|
||||
selectedPageIds: pageIds.slice(0, 1),
|
||||
lastSelectedVisualIndex: pageIds.length > 0 ? 0 : null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("workspaceCommands", () => {
|
||||
it("clones command state deeply enough for page and selection changes", () => {
|
||||
const original = makeState(["a", "b"]);
|
||||
const cloned = cloneCommandState(original);
|
||||
|
||||
original.pages[0].rotation = 270;
|
||||
original.selectedPageIds.push("b");
|
||||
original.lastSelectedVisualIndex = 1;
|
||||
|
||||
expect(cloned).toEqual({
|
||||
pages: [
|
||||
{ id: "a", sourcePageIndex: 0, rotation: 0 },
|
||||
{ id: "b", sourcePageIndex: 1, rotation: 90 },
|
||||
],
|
||||
selectedPageIds: ["a"],
|
||||
lastSelectedVisualIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates snapshot commands that are stable after source states mutate", () => {
|
||||
const before = makeState(["a", "b"]);
|
||||
const after = makeState(["b", "a"]);
|
||||
after.selectedPageIds = ["b"];
|
||||
after.lastSelectedVisualIndex = 0;
|
||||
|
||||
const command = createSnapshotCommand({
|
||||
id: "cmd-1",
|
||||
type: "reorder-pages",
|
||||
label: "Move page",
|
||||
timestamp: "2026-05-17T10:00:00.000Z",
|
||||
before,
|
||||
after,
|
||||
details: { moved: 1 },
|
||||
});
|
||||
|
||||
before.pages.length = 0;
|
||||
after.pages[0].rotation = 180;
|
||||
after.selectedPageIds.push("a");
|
||||
|
||||
expect(command.undo(makeState(["ignored"]))).toEqual({
|
||||
pages: [
|
||||
{ id: "a", sourcePageIndex: 0, rotation: 0 },
|
||||
{ id: "b", sourcePageIndex: 1, rotation: 90 },
|
||||
],
|
||||
selectedPageIds: ["a"],
|
||||
lastSelectedVisualIndex: 0,
|
||||
});
|
||||
|
||||
expect(command.do(makeState(["ignored"]))).toEqual({
|
||||
pages: [
|
||||
{ id: "b", sourcePageIndex: 0, rotation: 0 },
|
||||
{ id: "a", sourcePageIndex: 1, rotation: 90 },
|
||||
],
|
||||
selectedPageIds: ["b"],
|
||||
lastSelectedVisualIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("round-trips commands through serializable records", () => {
|
||||
const before = makeState(["a", "b", "c"]);
|
||||
const after: WorkspaceCommandState = {
|
||||
pages: [before.pages[2], before.pages[0], before.pages[1]],
|
||||
selectedPageIds: ["c"],
|
||||
lastSelectedVisualIndex: 0,
|
||||
};
|
||||
|
||||
const command = createSnapshotCommand({
|
||||
id: "cmd-2",
|
||||
type: "copy-pages",
|
||||
label: "Copy pages",
|
||||
timestamp: "2026-05-17T10:05:00.000Z",
|
||||
before,
|
||||
after,
|
||||
});
|
||||
|
||||
const record = toWorkspaceCommandRecord(command);
|
||||
const revived = reviveWorkspaceCommand(record);
|
||||
|
||||
expect(record).not.toHaveProperty("do");
|
||||
expect(record).not.toHaveProperty("undo");
|
||||
expect(revived.do(before)).toEqual(after);
|
||||
expect(revived.undo(after)).toEqual(before);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PageRef } from '../pdf/pdfTypes';
|
||||
import type { PageRef } from "../pdf/pdfTypes";
|
||||
|
||||
export interface WorkspaceCommandState {
|
||||
pages: PageRef[];
|
||||
@@ -26,7 +26,7 @@ export interface WorkspaceCommand extends WorkspaceCommandRecord {
|
||||
}
|
||||
|
||||
export function cloneCommandState(
|
||||
state: WorkspaceCommandState
|
||||
state: WorkspaceCommandState,
|
||||
): WorkspaceCommandState {
|
||||
return {
|
||||
pages: state.pages.map((page) => ({ ...page })),
|
||||
@@ -58,7 +58,7 @@ export function createSnapshotCommand(params: {
|
||||
}
|
||||
|
||||
export function reviveWorkspaceCommand(
|
||||
record: WorkspaceCommandRecord
|
||||
record: WorkspaceCommandRecord,
|
||||
): WorkspaceCommand {
|
||||
return {
|
||||
...record,
|
||||
@@ -68,7 +68,7 @@ export function reviveWorkspaceCommand(
|
||||
}
|
||||
|
||||
export function toWorkspaceCommandRecord(
|
||||
command: WorkspaceCommand
|
||||
command: WorkspaceCommand,
|
||||
): WorkspaceCommandRecord {
|
||||
return {
|
||||
id: command.id,
|
||||
@@ -81,4 +81,4 @@ export function toWorkspaceCommandRecord(
|
||||
details: command.payload.details,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import type {
|
||||
LoadedWorkspace,
|
||||
StoredWorkspace,
|
||||
WorkspaceSummary,
|
||||
} from './workspaceTypes';
|
||||
} from "./workspaceTypes";
|
||||
|
||||
const DB_NAME = 'pdf-tools-workspaces';
|
||||
const DB_NAME = "pdf-tools-workspaces";
|
||||
const DB_VERSION = 1;
|
||||
|
||||
const WORKSPACE_STORE = 'workspaces';
|
||||
const PDF_STORE = 'pdfBinaries';
|
||||
const WORKSPACE_STORE = "workspaces";
|
||||
const PDF_STORE = "pdfBinaries";
|
||||
|
||||
interface PdfBinaryRecord {
|
||||
pdfId: string;
|
||||
@@ -48,21 +48,21 @@ function openWorkspaceDb(): Promise<IDBDatabase> {
|
||||
|
||||
if (!db.objectStoreNames.contains(WORKSPACE_STORE)) {
|
||||
const workspaceStore = db.createObjectStore(WORKSPACE_STORE, {
|
||||
keyPath: 'id',
|
||||
keyPath: "id",
|
||||
});
|
||||
|
||||
workspaceStore.createIndex('updatedAt', 'updatedAt', {
|
||||
workspaceStore.createIndex("updatedAt", "updatedAt", {
|
||||
unique: false,
|
||||
});
|
||||
|
||||
workspaceStore.createIndex('pdfId', 'pdfId', {
|
||||
workspaceStore.createIndex("pdfId", "pdfId", {
|
||||
unique: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(PDF_STORE)) {
|
||||
db.createObjectStore(PDF_STORE, {
|
||||
keyPath: 'pdfId',
|
||||
keyPath: "pdfId",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -76,7 +76,7 @@ export async function listWorkspaces(): Promise<WorkspaceSummary[]> {
|
||||
const db = await openWorkspaceDb();
|
||||
|
||||
try {
|
||||
const tx = db.transaction(WORKSPACE_STORE, 'readonly');
|
||||
const tx = db.transaction(WORKSPACE_STORE, "readonly");
|
||||
const store = tx.objectStore(WORKSPACE_STORE);
|
||||
|
||||
const records = await requestToPromise<StoredWorkspace[]>(store.getAll());
|
||||
@@ -113,13 +113,13 @@ export async function saveWorkspaceToIndexedDb({
|
||||
const pdfRecord: PdfBinaryRecord = {
|
||||
pdfId: workspace.pdfId,
|
||||
name: workspace.pdfName,
|
||||
blob: new Blob([pdfArrayBuffer], { type: 'application/pdf' }),
|
||||
blob: new Blob([pdfArrayBuffer], { type: "application/pdf" }),
|
||||
size: pdfArrayBuffer.byteLength,
|
||||
createdAt: workspace.createdAt,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const tx = db.transaction([WORKSPACE_STORE, PDF_STORE], 'readwrite');
|
||||
const tx = db.transaction([WORKSPACE_STORE, PDF_STORE], "readwrite");
|
||||
|
||||
tx.objectStore(PDF_STORE).put(pdfRecord);
|
||||
tx.objectStore(WORKSPACE_STORE).put(workspace);
|
||||
@@ -131,15 +131,15 @@ export async function saveWorkspaceToIndexedDb({
|
||||
}
|
||||
|
||||
export async function loadWorkspaceFromIndexedDb(
|
||||
workspaceId: string
|
||||
workspaceId: string,
|
||||
): Promise<LoadedWorkspace | null> {
|
||||
const db = await openWorkspaceDb();
|
||||
|
||||
try {
|
||||
const tx = db.transaction([WORKSPACE_STORE, PDF_STORE], 'readonly');
|
||||
const tx = db.transaction([WORKSPACE_STORE, PDF_STORE], "readonly");
|
||||
|
||||
const workspace = await requestToPromise<StoredWorkspace | undefined>(
|
||||
tx.objectStore(WORKSPACE_STORE).get(workspaceId)
|
||||
tx.objectStore(WORKSPACE_STORE).get(workspaceId),
|
||||
);
|
||||
|
||||
if (!workspace) {
|
||||
@@ -148,7 +148,7 @@ export async function loadWorkspaceFromIndexedDb(
|
||||
}
|
||||
|
||||
const pdfRecord = await requestToPromise<PdfBinaryRecord | undefined>(
|
||||
tx.objectStore(PDF_STORE).get(workspace.pdfId)
|
||||
tx.objectStore(PDF_STORE).get(workspace.pdfId),
|
||||
);
|
||||
|
||||
await transactionDone(tx);
|
||||
@@ -169,20 +169,20 @@ export async function loadWorkspaceFromIndexedDb(
|
||||
}
|
||||
|
||||
export async function deleteWorkspaceFromIndexedDb(
|
||||
workspaceId: string
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const db = await openWorkspaceDb();
|
||||
|
||||
try {
|
||||
const lookupTx = db.transaction(WORKSPACE_STORE, 'readonly');
|
||||
const lookupTx = db.transaction(WORKSPACE_STORE, "readonly");
|
||||
const workspace = await requestToPromise<StoredWorkspace | undefined>(
|
||||
lookupTx.objectStore(WORKSPACE_STORE).get(workspaceId)
|
||||
lookupTx.objectStore(WORKSPACE_STORE).get(workspaceId),
|
||||
);
|
||||
await transactionDone(lookupTx);
|
||||
|
||||
if (!workspace) return;
|
||||
|
||||
const deleteTx = db.transaction([WORKSPACE_STORE, PDF_STORE], 'readwrite');
|
||||
const deleteTx = db.transaction([WORKSPACE_STORE, PDF_STORE], "readwrite");
|
||||
deleteTx.objectStore(WORKSPACE_STORE).delete(workspaceId);
|
||||
await transactionDone(deleteTx);
|
||||
|
||||
@@ -190,14 +190,14 @@ export async function deleteWorkspaceFromIndexedDb(
|
||||
const remainingWorkspaces = await listWorkspaces();
|
||||
|
||||
const pdfStillUsed = remainingWorkspaces.some(
|
||||
(summary) => summary.pdfId === workspace.pdfId
|
||||
(summary) => summary.pdfId === workspace.pdfId,
|
||||
);
|
||||
|
||||
if (!pdfStillUsed) {
|
||||
const cleanupDb = await openWorkspaceDb();
|
||||
|
||||
try {
|
||||
const cleanupTx = cleanupDb.transaction(PDF_STORE, 'readwrite');
|
||||
const cleanupTx = cleanupDb.transaction(PDF_STORE, "readwrite");
|
||||
cleanupTx.objectStore(PDF_STORE).delete(workspace.pdfId);
|
||||
await transactionDone(cleanupTx);
|
||||
} finally {
|
||||
@@ -207,4 +207,4 @@ export async function deleteWorkspaceFromIndexedDb(
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PageRef } from '../pdf/pdfTypes';
|
||||
import type { WorkspaceCommandRecord } from './workspaceCommands';
|
||||
import type { PageRef } from "../pdf/pdfTypes";
|
||||
import type { WorkspaceCommandRecord } from "./workspaceCommands";
|
||||
|
||||
export interface StoredWorkspace {
|
||||
schemaVersion: 1;
|
||||
@@ -37,4 +37,4 @@ export interface WorkspaceSummary {
|
||||
export interface LoadedWorkspace {
|
||||
workspace: StoredWorkspace;
|
||||
pdfArrayBuffer: ArrayBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user