From c70bbd9977cbed03b604a6c35ebbf4657c1ad704 Mon Sep 17 00:00:00 2001 From: hoyla Date: Sat, 14 Mar 2026 10:18:18 +0000 Subject: [PATCH 1/3] Hide empty Text view for documents with no text content When a document has OCR text but no extracted text content (e.g. an OCR'd image), the Text tab was still shown in the viewer and could be selected or defaulted to, displaying a blank page. Changes: - Add hasTextContent() helper to check for non-empty text contents - Update PreviewSwitcher to hide the Text tab when text is empty - Update PreviewSwitcher validation and fallback to skip empty text - Update getDefaultView() to prefer preview over empty text view - Add previewStatus to the frontend Resource type (already sent by backend, used by JS components, but missing from the TS type) - Add tests for getDefaultView() and hasTextContent() --- .../js/components/viewer/PreviewSwitcher.js | 10 +- frontend/src/js/types/Resource.ts | 1 + frontend/src/js/util/resourceUtils.spec.ts | 130 ++++++++++++++++++ frontend/src/js/util/resourceUtils.ts | 14 ++ 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 frontend/src/js/util/resourceUtils.spec.ts diff --git a/frontend/src/js/components/viewer/PreviewSwitcher.js b/frontend/src/js/components/viewer/PreviewSwitcher.js index 1f419e86..4782a9ff 100644 --- a/frontend/src/js/components/viewer/PreviewSwitcher.js +++ b/frontend/src/js/components/viewer/PreviewSwitcher.js @@ -3,6 +3,7 @@ import PropTypes from "prop-types"; import { resourcePropType } from "../../types/Resource"; import _ from "lodash"; +import { hasTextContent } from "../../util/resourceUtils"; import { keyboardShortcuts } from "../../util/keyboardShortcuts"; import { KeyboardShortcut } from "../UtilComponents/KeyboardShortcut"; @@ -34,7 +35,7 @@ class PreviewSwitcher extends React.Component { return false; } - if (this.props.view === "text" && !resource.text) { + if (this.props.view === "text" && !hasTextContent(resource)) { return false; } @@ -65,8 +66,8 @@ class PreviewSwitcher extends React.Component { this.props.view && !this.currentViewModeIsValid(this.props.resource) ) { - // Automatically switch to a preview if you can preview but there's no extracted text or OCR - if (this.props.resource.text) { + // Automatically switch to a valid view when the current one is not available + if (hasTextContent(this.props.resource)) { this.props.setResourceView("text"); } else if (this.props.resource.ocr) { const languages = Object.keys(this.props.resource.ocr); @@ -136,7 +137,8 @@ class PreviewSwitcher extends React.Component { shortcut={keyboardShortcuts.showPreview} func={this.showPreview} /> - {this.props.resource.text && !this.props.resource.transcript ? ( + {hasTextContent(this.props.resource) && + !this.props.resource.transcript ? ( & { text: HighlightableText }, +): Resource { + return { + uri: "test/doc.pdf", + type: "blob", + isExpandable: false, + processingStage: { type: "processed" }, + extracted: true, + mimeTypes: ["application/pdf"], + fileSize: 1024, + parents: [], + children: [], + comments: [], + previewStatus: "disabled", + ...overrides, + } as Resource; +} + +describe("hasTextContent", () => { + test("returns true when text has content", () => { + const resource = makeResource({ + text: makeHighlightableText("Hello world"), + }); + expect(hasTextContent(resource)).toBe(true); + }); + + test("returns false when text is empty", () => { + const resource = makeResource({ + text: makeHighlightableText(""), + }); + expect(hasTextContent(resource)).toBe(false); + }); + + test("returns false when text is only whitespace", () => { + const resource = makeResource({ + text: makeHighlightableText(" \n\t "), + }); + expect(hasTextContent(resource)).toBe(false); + }); +}); + +describe("getDefaultView", () => { + test("returns undefined for non-blob resources", () => { + const resource = makeResource({ + text: makeHighlightableText("content"), + type: "file", + }); + expect(getDefaultView(resource)).toBeUndefined(); + }); + + test("returns 'text' when document has text content", () => { + const resource = makeResource({ + text: makeHighlightableText("Hello world"), + }); + expect(getDefaultView(resource)).toBe("text"); + }); + + test("returns transcript view when transcript exists", () => { + const resource = makeResource({ + text: makeHighlightableText(""), + transcript: { + english: makeHighlightableText("transcript content"), + }, + }); + expect(getDefaultView(resource)).toBe("transcript.english"); + }); + + test("returns first OCR language when text is empty but OCR has content", () => { + const resource = makeResource({ + text: makeHighlightableText(""), + ocr: { + english: makeHighlightableText("OCR text"), + }, + }); + expect(getDefaultView(resource)).toBe("ocr.english"); + }); + + test("skips empty OCR entries and returns first non-empty one", () => { + const resource = makeResource({ + text: makeHighlightableText(""), + ocr: { + french: makeHighlightableText(""), + english: makeHighlightableText("OCR text"), + }, + }); + expect(getDefaultView(resource)).toBe("ocr.english"); + }); + + test("returns 'preview' when text is empty, no usable OCR, but preview is available", () => { + const resource = makeResource({ + text: makeHighlightableText(""), + previewStatus: "pass_through", + }); + expect(getDefaultView(resource)).toBe("preview"); + }); + + test("returns 'preview' when text is empty and all OCR entries are also empty", () => { + const resource = makeResource({ + text: makeHighlightableText(""), + ocr: { + english: makeHighlightableText(""), + }, + previewStatus: "pdf_generated", + }); + expect(getDefaultView(resource)).toBe("preview"); + }); + + test("returns 'text' as last resort when text is empty and preview is disabled", () => { + const resource = makeResource({ + text: makeHighlightableText(""), + previewStatus: "disabled", + }); + expect(getDefaultView(resource)).toBe("text"); + }); + + test("returns undefined when resource.text is undefined", () => { + const resource = makeResource({ + text: undefined as unknown as HighlightableText, + }); + expect(getDefaultView(resource)).toBeUndefined(); + }); +}); diff --git a/frontend/src/js/util/resourceUtils.ts b/frontend/src/js/util/resourceUtils.ts index 6f6efc1d..20b1f279 100644 --- a/frontend/src/js/util/resourceUtils.ts +++ b/frontend/src/js/util/resourceUtils.ts @@ -77,6 +77,12 @@ export function getCurrentResource(prefix: string): string { return window.location.pathname.split("/").slice(2).join("/"); } +export function hasTextContent(resource: Resource): boolean { + return ( + resource.text !== undefined && resource.text.contents.trim().length > 0 + ); +} + export function getDefaultView(resource: Resource): string | undefined { if (resource.type !== "blob") { return undefined; @@ -104,6 +110,14 @@ export function getDefaultView(resource: Resource): string | undefined { } } + // If text is empty and there's no usable OCR, prefer preview over an empty text view + if ( + resource.text.contents.trim().length === 0 && + resource.previewStatus !== "disabled" + ) { + return "preview"; + } + return "text"; } From b4e3cd635262324dc800c1ecd8dfaf0ccaa57c76 Mon Sep 17 00:00:00 2001 From: hoyla Date: Sat, 14 Mar 2026 10:42:45 +0000 Subject: [PATCH 2/3] Simplify hasTextContent and use it consistently in getDefaultView - Remove unnecessary undefined guard from hasTextContent (text is non-optional on Resource) - Use hasTextContent() in getDefaultView instead of repeating the inline trim/length check - Use idiomatic trim() !== '' comparisons instead of trim().length > 0 --- frontend/src/js/util/resourceUtils.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/frontend/src/js/util/resourceUtils.ts b/frontend/src/js/util/resourceUtils.ts index 20b1f279..fed944f9 100644 --- a/frontend/src/js/util/resourceUtils.ts +++ b/frontend/src/js/util/resourceUtils.ts @@ -78,9 +78,7 @@ export function getCurrentResource(prefix: string): string { } export function hasTextContent(resource: Resource): boolean { - return ( - resource.text !== undefined && resource.text.contents.trim().length > 0 - ); + return resource.text.contents.trim() !== ""; } export function getDefaultView(resource: Resource): string | undefined { @@ -99,10 +97,10 @@ export function getDefaultView(resource: Resource): string | undefined { return undefined; } - if (resource.text.contents.trim().length === 0 && resource.ocr) { + if (!hasTextContent(resource) && resource.ocr) { const ocrEntries = Object.entries(resource.ocr); const firstNonEmptyEntry = ocrEntries.find( - ([_, { contents }]) => contents.trim().length > 0, + ([_, { contents }]) => contents.trim() !== "", ); if (firstNonEmptyEntry) { @@ -111,10 +109,7 @@ export function getDefaultView(resource: Resource): string | undefined { } // If text is empty and there's no usable OCR, prefer preview over an empty text view - if ( - resource.text.contents.trim().length === 0 && - resource.previewStatus !== "disabled" - ) { + if (!hasTextContent(resource) && resource.previewStatus !== "disabled") { return "preview"; } From 828018a08693c29eb3f12cfd9e0d1a1ffc909eb1 Mon Sep 17 00:00:00 2001 From: hoyla Date: Sat, 14 Mar 2026 10:44:00 +0000 Subject: [PATCH 3/3] Simplify makeResource test helper Give makeResource a default empty text so callers only need to pass overrides for the fields relevant to each test case. --- frontend/src/js/util/resourceUtils.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/js/util/resourceUtils.spec.ts b/frontend/src/js/util/resourceUtils.spec.ts index 6d774795..6b675c21 100644 --- a/frontend/src/js/util/resourceUtils.spec.ts +++ b/frontend/src/js/util/resourceUtils.spec.ts @@ -5,9 +5,7 @@ function makeHighlightableText(contents: string): HighlightableText { return { contents, highlights: [] }; } -function makeResource( - overrides: Partial & { text: HighlightableText }, -): Resource { +function makeResource(overrides?: Partial): Resource { return { uri: "test/doc.pdf", type: "blob", @@ -20,6 +18,7 @@ function makeResource( children: [], comments: [], previewStatus: "disabled", + text: makeHighlightableText(""), ...overrides, } as Resource; }