feat(ui): confine read-only users to the transcription read view (#697)
On the document detail page, pass canEdit={canWrite} to the panel header,
guard onModeChange so a reader can never flip to edit, and default panelMode
to 'read' for readers. Thread canAnnotate={canWrite} through DocumentViewer
to PdfViewer so the annotation layer's canDraw (which also gates delete and
resize) is off for readers — they can open and read, but not draw, edit, or
delete. The writer-only OCR status check is also skipped for readers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ type Props = {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
transcribeMode?: boolean;
|
transcribeMode?: boolean;
|
||||||
|
canAnnotate?: boolean;
|
||||||
blockNumbers?: Record<string, number>;
|
blockNumbers?: Record<string, number>;
|
||||||
annotationReloadKey?: number;
|
annotationReloadKey?: number;
|
||||||
activeAnnotationId: string | null;
|
activeAnnotationId: string | null;
|
||||||
@@ -33,6 +34,7 @@ let {
|
|||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
transcribeMode = false,
|
transcribeMode = false,
|
||||||
|
canAnnotate = false,
|
||||||
blockNumbers = {},
|
blockNumbers = {},
|
||||||
annotationReloadKey = 0,
|
annotationReloadKey = 0,
|
||||||
activeAnnotationId = $bindable(),
|
activeAnnotationId = $bindable(),
|
||||||
@@ -93,6 +95,7 @@ let {
|
|||||||
url={fileUrl}
|
url={fileUrl}
|
||||||
documentId={doc.id}
|
documentId={doc.id}
|
||||||
transcribeMode={transcribeMode}
|
transcribeMode={transcribeMode}
|
||||||
|
canAnnotate={canAnnotate}
|
||||||
blockNumbers={blockNumbers}
|
blockNumbers={blockNumbers}
|
||||||
annotationReloadKey={annotationReloadKey}
|
annotationReloadKey={annotationReloadKey}
|
||||||
bind:activeAnnotationId={activeAnnotationId}
|
bind:activeAnnotationId={activeAnnotationId}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ let {
|
|||||||
url,
|
url,
|
||||||
documentId = '',
|
documentId = '',
|
||||||
transcribeMode = false,
|
transcribeMode = false,
|
||||||
|
canAnnotate = false,
|
||||||
blockNumbers = {},
|
blockNumbers = {},
|
||||||
annotationReloadKey = 0,
|
annotationReloadKey = 0,
|
||||||
activeAnnotationId = $bindable<string | null>(null),
|
activeAnnotationId = $bindable<string | null>(null),
|
||||||
@@ -28,6 +29,7 @@ let {
|
|||||||
url: string;
|
url: string;
|
||||||
documentId?: string;
|
documentId?: string;
|
||||||
transcribeMode?: boolean;
|
transcribeMode?: boolean;
|
||||||
|
canAnnotate?: boolean;
|
||||||
blockNumbers?: Record<string, number>;
|
blockNumbers?: Record<string, number>;
|
||||||
annotationReloadKey?: number;
|
annotationReloadKey?: number;
|
||||||
activeAnnotationId?: string | null;
|
activeAnnotationId?: string | null;
|
||||||
@@ -262,7 +264,7 @@ function handleAnnotationClick(id: string) {
|
|||||||
annotations={visibleAnnotations.filter(
|
annotations={visibleAnnotations.filter(
|
||||||
(a) => a.pageNumber === renderer.currentPage
|
(a) => a.pageNumber === renderer.currentPage
|
||||||
)}
|
)}
|
||||||
canDraw={transcribeMode}
|
canDraw={transcribeMode && canAnnotate}
|
||||||
color={TRANSCRIPTION_COLOR}
|
color={TRANSCRIPTION_COLOR}
|
||||||
blockNumbers={blockNumbers}
|
blockNumbers={blockNumbers}
|
||||||
activeAnnotationId={activeAnnotationId}
|
activeAnnotationId={activeAnnotationId}
|
||||||
|
|||||||
@@ -86,6 +86,38 @@ describe('PdfViewer — loaded state', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('makes the annotation surface drawable (crosshair) when transcribeMode and canAnnotate', async () => {
|
||||||
|
render(PdfViewer, {
|
||||||
|
url: '/api/documents/test/file',
|
||||||
|
documentId: 'test',
|
||||||
|
transcribeMode: true,
|
||||||
|
canAnnotate: true,
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
const surface = document.querySelector('[role="presentation"]');
|
||||||
|
expect(surface).not.toBeNull();
|
||||||
|
expect(surface?.getAttribute('style') ?? '').toContain('crosshair');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not make the annotation surface drawable when canAnnotate is false (read-only user)', async () => {
|
||||||
|
render(PdfViewer, {
|
||||||
|
url: '/api/documents/test/file',
|
||||||
|
documentId: 'test',
|
||||||
|
transcribeMode: true,
|
||||||
|
canAnnotate: false,
|
||||||
|
libLoader: makeFakeLibLoader()
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(document.querySelector('.bg-pdf-bg')).not.toBeNull();
|
||||||
|
});
|
||||||
|
const surface = document.querySelector('[role="presentation"]');
|
||||||
|
expect(surface?.getAttribute('style') ?? '').not.toContain('crosshair');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the canvas region when documentFileHash is provided', async () => {
|
it('renders the canvas region when documentFileHash is provided', async () => {
|
||||||
render(PdfViewer, {
|
render(PdfViewer, {
|
||||||
url: '/api/documents/test/file',
|
url: '/api/documents/test/file',
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const ocrJob = createOcrJob({
|
|||||||
onJobFinished: async () => {
|
onJobFinished: async () => {
|
||||||
await transcription.load();
|
await transcription.load();
|
||||||
transcription.bumpAnnotationReloadKey();
|
transcription.bumpAnnotationReloadKey();
|
||||||
panelMode = transcription.hasBlocks ? 'read' : 'edit';
|
panelMode = canWrite && !transcription.hasBlocks ? 'edit' : 'read';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,10 +148,10 @@ $effect(() => {
|
|||||||
if (skipInitialPanelMode) {
|
if (skipInitialPanelMode) {
|
||||||
skipInitialPanelMode = false;
|
skipInitialPanelMode = false;
|
||||||
} else {
|
} else {
|
||||||
panelMode = transcription.hasBlocks ? 'read' : 'edit';
|
panelMode = canWrite && !transcription.hasBlocks ? 'edit' : 'read';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ocrJob.checkStatus();
|
if (canWrite) ocrJob.checkStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -252,6 +252,7 @@ onMount(() => {
|
|||||||
isLoading={fileLoader.isLoading}
|
isLoading={fileLoader.isLoading}
|
||||||
error={fileLoader.fileError}
|
error={fileLoader.fileError}
|
||||||
transcribeMode={transcribeMode && !ocrJob.running}
|
transcribeMode={transcribeMode && !ocrJob.running}
|
||||||
|
canAnnotate={canWrite}
|
||||||
blockNumbers={transcription.blockNumbers}
|
blockNumbers={transcription.blockNumbers}
|
||||||
annotationReloadKey={transcription.annotationReloadKey}
|
annotationReloadKey={transcription.annotationReloadKey}
|
||||||
annotationsDimmed={transcribeMode && panelMode === 'read'}
|
annotationsDimmed={transcribeMode && panelMode === 'read'}
|
||||||
@@ -293,7 +294,10 @@ onMount(() => {
|
|||||||
hasBlocks={transcription.hasBlocks}
|
hasBlocks={transcription.hasBlocks}
|
||||||
blockCount={transcription.blocks.length}
|
blockCount={transcription.blocks.length}
|
||||||
lastEditedAt={transcription.lastEditedAt}
|
lastEditedAt={transcription.lastEditedAt}
|
||||||
onModeChange={(newMode) => (panelMode = newMode)}
|
canEdit={canWrite}
|
||||||
|
onModeChange={(newMode) => {
|
||||||
|
if (canWrite) panelMode = newMode;
|
||||||
|
}}
|
||||||
onClose={() => (transcribeMode = false)}
|
onClose={() => (transcribeMode = false)}
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
|||||||
Reference in New Issue
Block a user