Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/api/entities/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,14 @@ export default app => {
_id: { type: 'string' },
withPdf: { type: 'string' },
omitRelationships: { type: 'boolean' },
documentsFullText: { type: 'boolean' },
include: { type: 'array', items: { type: 'string', enum: ['permissions'] } },
},
},
},
}),
(req, res, next) => {
const { omitRelationships, include = [], ...query } = req.query;
const { omitRelationships, include = [], documentsFullText, ...query } = req.query;
const action = omitRelationships ? 'get' : 'getWithRelationships';
const published = req.user ? {} : { published: true };
const language = req.language ? { language: req.language } : {};
Expand All @@ -178,6 +179,7 @@ export default app => {
include.map(field => `+${field}`).join(' '),
{
limit: 1,
documentsFullText,
}
)
.then(_entities => {
Expand Down
9 changes: 9 additions & 0 deletions app/api/entities/specs/routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ describe('entities routes', () => {
]);
});
});

it('should return documents with fullText when requested', async () => {
const response: SuperTestResponse = await request(app)
.get('/api/entities')
.query({ sharedId: 'shared', documentsFullText: true });

expect(response.status).toBe(200);
expect(response.body.rows[0].documents[0].fullText).toBeDefined();
});
});

describe('POST', () => {
Expand Down
1 change: 1 addition & 0 deletions app/react/V2/Components/Metadata/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { MetadataDisplay } from './MetadataDisplay';
export { TemplateLabel } from './TemplateLabel';
43 changes: 39 additions & 4 deletions app/react/V2/Components/PDFViewer/PDF.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ const PDF = ({
const pdfContainerRef = useRef<HTMLDivElement>(null);
const [pdf, setPDF] = useState<PDFDocumentProxy>();
const [error, setError] = useState<string>();
const [containerWidth, setContainerWidth] = useState<number | undefined>(undefined);
const resizeTimeoutRef = useRef<NodeJS.Timeout>();

const padding = 10;
const containerStyles = {
height: size?.height || '100%',
width: size?.width || '100%',
overflow: size?.overflow || 'auto',
paddingLeft: '10px',
paddingRight: '10px',
paddingLeft: `${padding}px`,
paddingRight: `${padding}px`,
};

useEffect(() => {
Expand All @@ -72,6 +75,40 @@ const PDF = ({
};
}, [scrollToPage, pdf]);

// ResizeObserver to track container width changes with debouncing
useEffect(() => {
const container = pdfContainerRef.current;
if (!container) return;

// Set initial width
const initialWidth = container.offsetWidth - padding * 2; // Account for padding
setContainerWidth(initialWidth);

const resizeObserver = new ResizeObserver(entries => {
const [entry] = entries;
if (entry && entry.contentRect) {
// Debounce the resize updates
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}

resizeTimeoutRef.current = setTimeout(() => {
const newWidth = entry.contentRect.width - padding * 2; // Account for padding
setContainerWidth(newWidth);
}, 150); // 150ms debounce
}
});

resizeObserver.observe(container);

return () => {
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
resizeObserver.disconnect();
};
}, []);

if (error) {
return <div>{error}</div>;
}
Expand All @@ -84,8 +121,6 @@ const PDF = ({
const regionId = number.toString();
const pageHighlights = highlights ? highlights[regionId] : undefined;
const shouldScrollToPage = scrollToPage === regionId;
const containerWidth =
pdfContainerRef.current?.offsetWidth && pdfContainerRef.current.offsetWidth - 20;

return (
<div
Expand Down
40 changes: 39 additions & 1 deletion app/react/V2/Components/PDFViewer/PDFPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ const PDFPage = ({ pdf, page, eventBus, containerWidth, highlights }: PDFPagePro
const [error, setError] = useState<string>();
const [pdfScale, setPdfScale] = useAtom(pdfScaleAtom);
const pageContainerRef = useRef<HTMLDivElement>(null);
const pageViewerRef = useRef<any>(null);
const pdfPageRef = useRef<any>(null);

// Initial setup - only runs once
useEffect(() => {
const currentContainer = pageContainerRef.current;
let observer: IntersectionObserver;
Expand All @@ -30,6 +33,8 @@ const PDFPage = ({ pdf, page, eventBus, containerWidth, highlights }: PDFPagePro
.getPage(page)
.then(pdfPage => {
if (currentContainer && pdfPage) {
pdfPageRef.current = pdfPage;

const originalViewport = pdfPage.getViewport({ scale: 1 });
const scale = calculateScaling(
originalViewport.width * PDFJS.PixelsPerInch.PDF_TO_CSS_UNITS,
Expand All @@ -49,6 +54,7 @@ const PDFPage = ({ pdf, page, eventBus, containerWidth, highlights }: PDFPagePro
});

pageViewer.setPdfPage(pdfPage);
pageViewerRef.current = pageViewer;

const handleIntersection: IntersectionObserverCallback = entries => {
const [entry] = entries;
Expand Down Expand Up @@ -81,12 +87,44 @@ const PDFPage = ({ pdf, page, eventBus, containerWidth, highlights }: PDFPagePro
});

return () => {
if (currentContainer) observer.unobserve(currentContainer);
if (currentContainer && observer) observer.unobserve(currentContainer);
if (pageViewerRef.current) {
pageViewerRef.current.destroy();
}
};
//pdf rendering is expensive and we want to make sure there's a single effect that runs only on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Update scale dynamically when containerWidth changes
useEffect(() => {
const pageViewer = pageViewerRef.current;
const pdfPage = pdfPageRef.current;

if (!pageViewer || !pdfPage || !containerWidth) return;

const originalViewport = pdfPage.getViewport({ scale: 1 });
const newScale = calculateScaling(
originalViewport.width * PDFJS.PixelsPerInch.PDF_TO_CSS_UNITS,
containerWidth
);

// Only update if scale actually changed
if (Math.abs(pageViewer.scale - newScale) > 0.01) {
setPdfScale(newScale);

// Update the viewer's scale
pageViewer.update({ scale: newScale });

// If the page is already rendered, re-draw it at the new scale
if (pageViewer.renderingState === PDFJSViewer.RenderingStates.FINISHED) {
pageViewer.draw().catch((e: Error) => {
setError(e.message);
});
}
}
}, [containerWidth, setPdfScale]);

if (error) {
return <div>{error}</div>;
}
Expand Down
30 changes: 30 additions & 0 deletions app/react/V2/Components/PDFViewer/PlainText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { FileType } from 'shared/types/fileType';

interface PlainTextProps {
file?: FileType;
page?: number | string;
className?: string;
dir?: 'ltr' | 'rtl';
}

export const PlainText = ({ className = '', dir, page, file }: PlainTextProps) => {
const firstPage = file?.fullText
? Object.keys(file.fullText)
.map(k => Number(k))
.filter(n => !Number.isNaN(n))
.sort((a, b) => a - b)[0]
: 1;

const pageKey = page ? String(page) : String(firstPage);

const pageText = file?.fullText?.[pageKey] ?? '';

return (
<div className={`${className} whitespace-pre-line`} dir={dir}>
{pageText}
</div>
);
};

export type { PlainTextProps };
1 change: 1 addition & 0 deletions app/react/V2/Components/PDFViewer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ const PDF = loadable(async () => import(/* webpackChunkName: "LazyLoadPDF" */ '.

export { PDF, selectionHandlers };
export { calculateScaling } from './functions/calculateScaling';
export { PlainText } from './PlainText';
55 changes: 55 additions & 0 deletions app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ import * as helpers from '../functions/helpers';
configMocks({ act });
const oberserverMock = mockIntersectionObserver();

// Mock ResizeObserver
class ResizeObserverMock {
callback: ResizeObserverCallback;

constructor(callback: ResizeObserverCallback) {
this.callback = callback;
}

observe = jest.fn();

unobserve = jest.fn();

disconnect = jest.fn();
}

global.ResizeObserver = ResizeObserverMock as any;

const highlights: PDFProps['highlights'] = {
2: [
{
Expand Down Expand Up @@ -64,6 +81,8 @@ jest.mock('../pdfjs.ts', () => ({
}),
destroy: mockPageDestroy,
renderingState: 0,
scale: args.scale,
update: jest.fn(),
cancelRendering: jest.fn(),
};
}),
Expand Down Expand Up @@ -168,4 +187,40 @@ describe('PDF', () => {
expect(unobserveMock).toHaveBeenCalledTimes(5);
});
});

describe('resize observer', () => {
let resizeObserverInstance: ResizeObserverMock | null = null;

beforeEach(() => {
const OriginalResizeObserver = global.ResizeObserver;
global.ResizeObserver = jest.fn().mockImplementation(function(callback: ResizeObserverCallback) {
resizeObserverInstance = new (OriginalResizeObserver as any)(callback);
return resizeObserverInstance;
}) as any;
});

afterEach(() => {
resizeObserverInstance = null;
});

it('should set up ResizeObserver for the PDF container', async () => {
await act(() => {
renderComponet();
});

// Should observe the PDF container
expect(resizeObserverInstance?.observe).toHaveBeenCalledTimes(1);
});

it('should disconnect ResizeObserver on unmount', async () => {
await act(() => {
renderComponet();
});

cleanup();

// Should disconnect on unmount
expect(resizeObserverInstance?.disconnect).toHaveBeenCalledTimes(1);
});
});
});
43 changes: 43 additions & 0 deletions app/react/V2/Components/PDFViewer/specs/PlainText.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @jest-environment jsdom
*/

import React from 'react';
import { render, cleanup } from '@testing-library/react';
import { PlainText } from '../PlainText';

afterEach(() => cleanup());

describe('PlainText', () => {
it('renders the first page when page prop is not provided', () => {
const file = { fullText: { 2: 'Second page', 10: 'Tenth page' } };
const { container } = render(<PlainText file={file} className="my-class" />);

const el = container.querySelector('.whitespace-pre-line');
expect(el?.textContent).toBe('Second page');
});

it('renders the requested page when page prop is a number', () => {
const file = { fullText: { 1: 'One', 3: 'Three' } };
const { container } = render(<PlainText file={file} page={3} />);

const el = container.querySelector('.whitespace-pre-line');
expect(el?.textContent).toBe('Three');
});

it('renders the requested page when page prop is a string', () => {
const file = { fullText: { 1: 'One', 3: 'Three' } };
const { container } = render(<PlainText file={file} page="1" />);

const el = container.querySelector('.whitespace-pre-line');
expect(el?.textContent).toBe('One');
});

it('renders an empty string when the requested page does not exist', () => {
const file = { fullText: { 5: 'Five' } };
const { container } = render(<PlainText file={file} page={2} />);

const el = container.querySelector('.whitespace-pre-line');
expect(el?.textContent).toBe('');
});
});
6 changes: 3 additions & 3 deletions app/react/V2/Components/UI/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ const Tabs = ({
}
};

const activeClass = 'text-gray-900 bg-gray-50';
const activeClass = 'text-primary-900 bg-primary-50';
const inactiveClass = 'text-gray-500';

return (
<div className={`flex flex-col h-full ${className ?? ''}`}>
<div className={`flex flex-col h-full gap-4 ${className ?? ''}`}>
<div
role="tablist"
aria-orientation="horizontal"
Expand All @@ -74,7 +74,7 @@ const Tabs = ({
</button>
))}
</div>
<div className="py-4 grow overflow-y-auto">
<div className="grow overflow-y-auto">
{tabChildren.map(child => {
const isActive = child.props.id === activeTab;

Expand Down
Loading