mostly formatting, dependency fix

This commit is contained in:
2026-05-17 02:39:32 +02:00
parent a5dc70aabf
commit cf9a0dd0b7
32 changed files with 837 additions and 836 deletions

View File

@@ -1,12 +1,12 @@
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 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";
} from './workspaceCommands';
import { useWorkspaceState } from './useWorkspaceState';
function page(id: string, sourcePageIndex: number, rotation = 0): PageRef {
return { id, sourcePageIndex, rotation };
@@ -15,7 +15,7 @@ function page(id: string, sourcePageIndex: number, rotation = 0): PageRef {
function state(
pages: PageRef[],
selectedPageIds: string[] = [],
lastSelectedVisualIndex: number | null = null,
lastSelectedVisualIndex: number | null = null
): WorkspaceCommandState {
return { pages, selectedPageIds, lastSelectedVisualIndex };
}
@@ -32,18 +32,18 @@ interface HarnessRef {
};
replaceWorkspaceState: ReturnType<
typeof useWorkspaceState
>["replaceWorkspaceState"];
>['replaceWorkspaceState'];
getCurrentCommandState: ReturnType<
typeof useWorkspaceState
>["getCurrentCommandState"];
>['getCurrentCommandState'];
createWorkspaceCommand: ReturnType<
typeof useWorkspaceState
>["createWorkspaceCommand"];
>['createWorkspaceCommand'];
executeWorkspaceCommand: ReturnType<
typeof useWorkspaceState
>["executeWorkspaceCommand"];
handleUndo: ReturnType<typeof useWorkspaceState>["handleUndo"];
handleRedo: ReturnType<typeof useWorkspaceState>["handleRedo"];
>['executeWorkspaceCommand'];
handleUndo: ReturnType<typeof useWorkspaceState>['handleUndo'];
handleRedo: ReturnType<typeof useWorkspaceState>['handleRedo'];
}
const Harness = forwardRef<HarnessRef, { onContentChanged?: () => void }>(
@@ -69,7 +69,7 @@ const Harness = forwardRef<HarnessRef, { onContentChanged?: () => void }>(
}));
return null;
},
}
);
function renderHarness(onContentChanged = vi.fn()) {
@@ -77,54 +77,54 @@ function renderHarness(onContentChanged = vi.fn()) {
render(<Harness ref={ref} onContentChanged={onContentChanged} />);
if (!ref.current) {
throw new Error("Harness ref was not initialized");
throw new Error('Harness ref was not initialized');
}
return { ref, onContentChanged };
}
describe("useWorkspaceState", () => {
it("replaces workspace state from loaded data without marking it dirty", () => {
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)];
const loadedPages = [page('p1', 0), page('p2', 1, 90)];
act(() => {
ref.current?.replaceWorkspaceState({
pages: loadedPages,
selectedPageIds: ["p2"],
selectedPageIds: ['p2'],
lastSelectedVisualIndex: 1,
history: [],
redoHistory: [],
dirty: false,
message: "Workspace loaded.",
message: 'Workspace loaded.',
});
});
expect(ref.current?.snapshot()).toMatchObject({
pages: loadedPages,
selectedPageIds: ["p2"],
selectedPageIds: ['p2'],
lastSelectedVisualIndex: 1,
workspaceDirty: false,
workspaceMessage: "Workspace loaded.",
workspaceMessage: 'Workspace loaded.',
workspaceHistory: [],
redoHistory: [],
});
});
it("executes commands, stores history, clears redo, and marks content changed", () => {
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);
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",
id: 'redo-record',
type: 'old-redo',
label: 'Old redo',
timestamp: '2026-05-17T10:00:00.000Z',
payload: { before, after },
},
],
@@ -133,34 +133,34 @@ describe("useWorkspaceState", () => {
act(() => {
const command = ref.current?.createWorkspaceCommand({
type: "reorder-pages",
label: "Move page 2 before page 1",
type: 'reorder-pages',
label: 'Move page 2 before page 1',
before,
after,
});
if (!command) throw new Error("Command was not created");
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?.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",
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", () => {
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);
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);
@@ -168,13 +168,13 @@ describe("useWorkspaceState", () => {
act(() => {
const command = ref.current?.createWorkspaceCommand({
type: "reorder-pages",
label: "Move page",
type: 'reorder-pages',
label: 'Move page',
before: initial,
after: reordered,
});
if (!command) throw new Error("Command was not created");
if (!command) throw new Error('Command was not created');
ref.current?.executeWorkspaceCommand(command);
});

View File

@@ -1,18 +1,18 @@
import { useCallback, useRef, useState } from "react";
import type { PageRef } from "../pdf/pdfTypes";
import { useCallback, useRef, useState } from 'react';
import type { PageRef } from '../pdf/pdfTypes';
import type {
WorkspaceCommand,
WorkspaceCommandRecord,
WorkspaceCommandState,
} from "./workspaceCommands";
} from './workspaceCommands';
import {
createSnapshotCommand,
reviveWorkspaceCommand,
toWorkspaceCommandRecord,
} from "./workspaceCommands";
} from './workspaceCommands';
function createId(prefix: string): string {
if (typeof crypto !== "undefined" && crypto.randomUUID) {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
@@ -20,7 +20,7 @@ function createId(prefix: string): string {
}
export function createWorkspaceId(): string {
return createId("workspace");
return createId('workspace');
}
export function createPdfId(): string {
@@ -28,11 +28,11 @@ export function createPdfId(): string {
}
export function defaultWorkspaceNameFromPdfName(pdfName: string): string {
return pdfName.replace(/\.pdf$/i, "") || "Untitled workspace";
return pdfName.replace(/\.pdf$/i, '') || 'Untitled workspace';
}
export function createPageRefId(): string {
return createId("page");
return createId('page');
}
export function createInitialPageRefs(pageCount: number): PageRef[] {
@@ -84,7 +84,7 @@ export function useWorkspaceState({
const setPages = useCallback((action: SetStateAction<PageRef[]>) => {
setPagesState((previous) => {
const next = typeof action === "function" ? action(previous) : action;
const next = typeof action === 'function' ? action(previous) : action;
latestPagesRef.current = next;
return next;
});
@@ -92,7 +92,7 @@ export function useWorkspaceState({
const setSelectedPageIds = useCallback((action: SetStateAction<string[]>) => {
setSelectedPageIdsState((previous) => {
const next = typeof action === "function" ? action(previous) : action;
const next = typeof action === 'function' ? action(previous) : action;
selectedPageIdsRef.current = next;
return next;
});
@@ -101,12 +101,12 @@ export function useWorkspaceState({
const setLastSelectedVisualIndex = useCallback(
(action: SetStateAction<number | null>) => {
setLastSelectedVisualIndexState((previous) => {
const next = typeof action === "function" ? action(previous) : action;
const next = typeof action === 'function' ? action(previous) : action;
lastSelectedVisualIndexRef.current = next;
return next;
});
},
[],
[]
);
const getCurrentCommandState = useCallback(
@@ -115,7 +115,7 @@ export function useWorkspaceState({
selectedPageIds: selectedPageIdsRef.current,
lastSelectedVisualIndex: lastSelectedVisualIndexRef.current,
}),
[],
[]
);
const applyCommandState = useCallback(
@@ -124,7 +124,7 @@ export function useWorkspaceState({
setSelectedPageIds(state.selectedPageIds);
setLastSelectedVisualIndex(state.lastSelectedVisualIndex);
},
[setLastSelectedVisualIndex, setPages, setSelectedPageIds],
[setLastSelectedVisualIndex, setPages, setSelectedPageIds]
);
const markWorkspaceChanged = useCallback(() => {
@@ -142,14 +142,14 @@ export function useWorkspaceState({
details?: Record<string, unknown>;
}): WorkspaceCommand =>
createSnapshotCommand({
id: createId("command"),
id: createId('command'),
type: params.type,
label: params.label,
before: params.before,
after: params.after,
details: params.details,
}),
[],
[]
);
const executeWorkspaceCommand = useCallback(
@@ -164,7 +164,7 @@ export function useWorkspaceState({
setRedoHistory([]);
markWorkspaceChanged();
},
[applyCommandState, getCurrentCommandState, markWorkspaceChanged],
[applyCommandState, getCurrentCommandState, markWorkspaceChanged]
);
const handleUndo = useCallback(() => {
@@ -213,7 +213,7 @@ export function useWorkspaceState({
setWorkspaceDirty(state.dirty ?? false);
setWorkspaceMessage(state.message ?? null);
},
[setLastSelectedVisualIndex, setPages, setSelectedPageIds],
[setLastSelectedVisualIndex, setPages, setSelectedPageIds]
);
const resetWorkspaceState = useCallback(() => {

View File

@@ -1,11 +1,11 @@
import { describe, expect, it } from "vitest";
import type { WorkspaceCommandState } from "./workspaceCommands";
import { describe, expect, it } from 'vitest';
import type { WorkspaceCommandState } from './workspaceCommands';
import {
cloneCommandState,
createSnapshotCommand,
reviveWorkspaceCommand,
toWorkspaceCommandRecord,
} from "./workspaceCommands";
} from './workspaceCommands';
function makeState(pageIds: string[]): WorkspaceCommandState {
return {
@@ -19,36 +19,36 @@ function makeState(pageIds: string[]): WorkspaceCommandState {
};
}
describe("workspaceCommands", () => {
it("clones command state deeply enough for page and selection changes", () => {
const original = makeState(["a", "b"]);
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.selectedPageIds.push('b');
original.lastSelectedVisualIndex = 1;
expect(cloned).toEqual({
pages: [
{ id: "a", sourcePageIndex: 0, rotation: 0 },
{ id: "b", sourcePageIndex: 1, rotation: 90 },
{ id: 'a', sourcePageIndex: 0, rotation: 0 },
{ id: 'b', sourcePageIndex: 1, rotation: 90 },
],
selectedPageIds: ["a"],
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"];
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",
id: 'cmd-1',
type: 'reorder-pages',
label: 'Move page',
timestamp: '2026-05-17T10:00:00.000Z',
before,
after,
details: { moved: 1 },
@@ -56,40 +56,40 @@ describe("workspaceCommands", () => {
before.pages.length = 0;
after.pages[0].rotation = 180;
after.selectedPageIds.push("a");
after.selectedPageIds.push('a');
expect(command.undo(makeState(["ignored"]))).toEqual({
expect(command.undo(makeState(['ignored']))).toEqual({
pages: [
{ id: "a", sourcePageIndex: 0, rotation: 0 },
{ id: "b", sourcePageIndex: 1, rotation: 90 },
{ id: 'a', sourcePageIndex: 0, rotation: 0 },
{ id: 'b', sourcePageIndex: 1, rotation: 90 },
],
selectedPageIds: ["a"],
selectedPageIds: ['a'],
lastSelectedVisualIndex: 0,
});
expect(command.do(makeState(["ignored"]))).toEqual({
expect(command.do(makeState(['ignored']))).toEqual({
pages: [
{ id: "b", sourcePageIndex: 0, rotation: 0 },
{ id: "a", sourcePageIndex: 1, rotation: 90 },
{ id: 'b', sourcePageIndex: 0, rotation: 0 },
{ id: 'a', sourcePageIndex: 1, rotation: 90 },
],
selectedPageIds: ["b"],
selectedPageIds: ['b'],
lastSelectedVisualIndex: 0,
});
});
it("round-trips commands through serializable records", () => {
const before = makeState(["a", "b", "c"]);
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"],
selectedPageIds: ['c'],
lastSelectedVisualIndex: 0,
};
const command = createSnapshotCommand({
id: "cmd-2",
type: "copy-pages",
label: "Copy pages",
timestamp: "2026-05-17T10:05:00.000Z",
id: 'cmd-2',
type: 'copy-pages',
label: 'Copy pages',
timestamp: '2026-05-17T10:05:00.000Z',
before,
after,
});
@@ -97,8 +97,8 @@ describe("workspaceCommands", () => {
const record = toWorkspaceCommandRecord(command);
const revived = reviveWorkspaceCommand(record);
expect(record).not.toHaveProperty("do");
expect(record).not.toHaveProperty("undo");
expect(record).not.toHaveProperty('do');
expect(record).not.toHaveProperty('undo');
expect(revived.do(before)).toEqual(after);
expect(revived.undo(after)).toEqual(before);
});

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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;