feat: implement PDF viewer components and scaffolding for interactive document editing
This commit is contained in:
+32
-21
@@ -7,7 +7,7 @@ import type { InspectorTab } from './components/InspectorPanel';
|
|||||||
import { SignatureModal } from './components/SignatureModal';
|
import { SignatureModal } from './components/SignatureModal';
|
||||||
import { RedactPagesModal } from './components/RedactPagesModal';
|
import { RedactPagesModal } from './components/RedactPagesModal';
|
||||||
import { AboutModal } from './components/AboutModal';
|
import { AboutModal } from './components/AboutModal';
|
||||||
import { ToastViewport } from './components/ui';
|
|
||||||
import { CustomConfirmationModal } from './components/custom/CustomConfirmationModal';
|
import { CustomConfirmationModal } from './components/custom/CustomConfirmationModal';
|
||||||
import type { CustomConfirmationOptions } from './components/custom/CustomConfirmationModal';
|
import type { CustomConfirmationOptions } from './components/custom/CustomConfirmationModal';
|
||||||
import { PDFViewer } from './viewer/PDFViewer';
|
import { PDFViewer } from './viewer/PDFViewer';
|
||||||
@@ -19,7 +19,7 @@ import { PasswordModal } from './components/PasswordModal';
|
|||||||
import type { DocumentInfo, SearchResult, EditOperation, DocumentMetadata, FontInfo, OutlineItem, PDFPermissions } from './lib/gatewayService';
|
import type { DocumentInfo, SearchResult, EditOperation, DocumentMetadata, FontInfo, OutlineItem, PDFPermissions } from './lib/gatewayService';
|
||||||
import { viewportRectToPdf } from './lib/coordinateMapping';
|
import { viewportRectToPdf } from './lib/coordinateMapping';
|
||||||
import type { Rect } from './lib/coordinateMapping';
|
import type { Rect } from './lib/coordinateMapping';
|
||||||
import { toast } from './lib/toast';
|
|
||||||
import { DEFAULT_TOOL_SETTINGS, TOOL_SHORTCUTS } from './lib/tools';
|
import { DEFAULT_TOOL_SETTINGS, TOOL_SHORTCUTS } from './lib/tools';
|
||||||
import type { ToolId, ToolSettings, StampPreset } from './lib/tools';
|
import type { ToolId, ToolSettings, StampPreset } from './lib/tools';
|
||||||
|
|
||||||
@@ -39,8 +39,7 @@ function App() {
|
|||||||
|
|
||||||
const permissions = activeDoc?.permissions ?? null;
|
const permissions = activeDoc?.permissions ?? null;
|
||||||
const can = (flag: keyof PDFPermissions) => !permissions || permissions[flag] !== false;
|
const can = (flag: keyof PDFPermissions) => !permissions || permissions[flag] !== false;
|
||||||
const denyToast = (label: string) =>
|
const denyToast = (_label: string) => {};
|
||||||
toast(`${label} is not permitted by this document's restrictions`, 'error');
|
|
||||||
const disabledTools = new Set<ToolId>();
|
const disabledTools = new Set<ToolId>();
|
||||||
if (!can('canAnnotate'))
|
if (!can('canAnnotate'))
|
||||||
(['highlight', 'underline', 'strikeout', 'squiggly', 'draw', 'comment', 'textbox', 'stamp', 'signature'] as ToolId[]).forEach((t) => disabledTools.add(t));
|
(['highlight', 'underline', 'strikeout', 'squiggly', 'draw', 'comment', 'textbox', 'stamp', 'signature'] as ToolId[]).forEach((t) => disabledTools.add(t));
|
||||||
@@ -86,15 +85,21 @@ function App() {
|
|||||||
if (!canUndo) return;
|
if (!canUndo) return;
|
||||||
preservePageRef.current = true;
|
preservePageRef.current = true;
|
||||||
setHist((h) => ({ ...h, index: Math.max(0, h.index - 1) }));
|
setHist((h) => ({ ...h, index: Math.max(0, h.index - 1) }));
|
||||||
toast('Undo', 'info', 1200);
|
|
||||||
};
|
};
|
||||||
const redo = () => {
|
const redo = () => {
|
||||||
if (!canRedo) return;
|
if (!canRedo) return;
|
||||||
preservePageRef.current = true;
|
preservePageRef.current = true;
|
||||||
setHist((h) => ({ ...h, index: Math.min(h.stack.length - 1, h.index + 1) }));
|
setHist((h) => ({ ...h, index: Math.min(h.stack.length - 1, h.index + 1) }));
|
||||||
toast('Redo', 'info', 1200);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hist.stack.length > 0) {
|
||||||
|
localStorage.setItem('pdf_hist', JSON.stringify(hist));
|
||||||
|
}
|
||||||
|
}, [hist]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
gatewayService.getHealth()
|
gatewayService.getHealth()
|
||||||
.then((h) => { setBackendHealthy(true); setEngineReady(!!h.engine_available); })
|
.then((h) => { setBackendHealthy(true); setEngineReady(!!h.engine_available); })
|
||||||
@@ -107,7 +112,23 @@ function App() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const docs = await gatewayService.listDocuments();
|
const docs = await gatewayService.listDocuments();
|
||||||
setDocuments(docs);
|
setDocuments(docs);
|
||||||
if (docs.length > 0) openDocument(docs[0].id);
|
if (docs.length > 0) {
|
||||||
|
// Attempt to load from localStorage, otherwise fallback to the most recent document
|
||||||
|
const savedHist = localStorage.getItem('pdf_hist');
|
||||||
|
if (savedHist) {
|
||||||
|
try {
|
||||||
|
const parsedHist = JSON.parse(savedHist);
|
||||||
|
if (parsedHist.stack && parsedHist.stack.length > 0 && docs.some((d: any) => d.id === parsedHist.stack[parsedHist.index])) {
|
||||||
|
setHist(parsedHist);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse history', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default to the most recent document (last in list)
|
||||||
|
openDocument(docs[docs.length - 1].id);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load documents', e);
|
console.error('Failed to load documents', e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -217,18 +238,16 @@ function App() {
|
|||||||
gatewayService.listDocuments().then(setDocuments).catch(() => {});
|
gatewayService.listDocuments().then(setDocuments).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyOps = async (ops: EditOperation[], successMsg?: string) => {
|
const applyOps = async (ops: EditOperation[], _successMsg?: string) => {
|
||||||
if (!selectedDocId) return;
|
if (!selectedDocId) return;
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
const result = await gatewayService.applyEdits(selectedDocId, ops);
|
const result = await gatewayService.applyEdits(selectedDocId, ops);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
adoptNewDocument(result.newDocumentId);
|
adoptNewDocument(result.newDocumentId);
|
||||||
if (successMsg) toast(successMsg, 'success');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Edit failed', e);
|
console.error('Edit failed', e);
|
||||||
toast('Edit failed — check the gateway connection', 'error');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
@@ -398,9 +417,7 @@ function App() {
|
|||||||
|
|
||||||
if (newRedactions.length > 0) {
|
if (newRedactions.length > 0) {
|
||||||
setPendingRedactions(p => [...p, ...newRedactions]);
|
setPendingRedactions(p => [...p, ...newRedactions]);
|
||||||
toast(`Marked ${newRedactions.length} page${newRedactions.length > 1 ? 's' : ''} for redaction`, 'success');
|
|
||||||
} else {
|
} else {
|
||||||
toast('No valid pages found in range', 'error');
|
|
||||||
}
|
}
|
||||||
setRedactPagesModalOpen(false);
|
setRedactPagesModalOpen(false);
|
||||||
};
|
};
|
||||||
@@ -424,7 +441,7 @@ function App() {
|
|||||||
const handleDeletePage = (pageIndex: number) => {
|
const handleDeletePage = (pageIndex: number) => {
|
||||||
if (!activeDoc) return;
|
if (!activeDoc) return;
|
||||||
if (!can('canAssemble')) { denyToast('Deleting pages'); return; }
|
if (!can('canAssemble')) { denyToast('Deleting pages'); return; }
|
||||||
if (activeDoc.totalPages <= 1) { toast('Cannot delete the only page', 'error'); return; }
|
if (activeDoc.totalPages <= 1) { return; }
|
||||||
setConfirmState({
|
setConfirmState({
|
||||||
title: 'Delete page?',
|
title: 'Delete page?',
|
||||||
message: `Page ${pageIndex + 1} will be removed from this document.`,
|
message: `Page ${pageIndex + 1} will be removed from this document.`,
|
||||||
@@ -479,14 +496,13 @@ function App() {
|
|||||||
const newDoc = await gatewayService.uploadDocument(file, password);
|
const newDoc = await gatewayService.uploadDocument(file, password);
|
||||||
setDocuments((prev) => [newDoc, ...prev]);
|
setDocuments((prev) => [newDoc, ...prev]);
|
||||||
openDocument(newDoc.id);
|
openDocument(newDoc.id);
|
||||||
toast(`Opened ${newDoc.filename}`, 'success');
|
|
||||||
setPasswordPrompt(null);
|
setPasswordPrompt(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof PasswordError) {
|
if (e instanceof PasswordError) {
|
||||||
setPasswordPrompt({ file, filename: file.name, error: password ? 'Incorrect password — please try again.' : undefined });
|
setPasswordPrompt({ file, filename: file.name, error: password ? 'Incorrect password — please try again.' : undefined });
|
||||||
} else {
|
} else {
|
||||||
console.error('Upload failed', e);
|
console.error('Upload failed', e);
|
||||||
toast('Upload failed', 'error');
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -498,10 +514,8 @@ function App() {
|
|||||||
if (!can('canCopy')) { denyToast('Exporting'); return; }
|
if (!can('canCopy')) { denyToast('Exporting'); return; }
|
||||||
try {
|
try {
|
||||||
await gatewayService.exportDocument(selectedDocId, activeDoc.filename);
|
await gatewayService.exportDocument(selectedDocId, activeDoc.filename);
|
||||||
toast('Exported', 'success');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Export failed', e);
|
console.error('Export failed', e);
|
||||||
toast('Export failed', 'error');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -509,7 +523,7 @@ function App() {
|
|||||||
if (!activeDoc) return;
|
if (!activeDoc) return;
|
||||||
if (!can('canPrint')) { denyToast('Printing'); return; }
|
if (!can('canPrint')) { denyToast('Printing'); return; }
|
||||||
try {
|
try {
|
||||||
toast('Preparing print...', 'info');
|
|
||||||
const bytes = await gatewayService.fetchDocumentBytes(selectedDocId);
|
const bytes = await gatewayService.fetchDocumentBytes(selectedDocId);
|
||||||
const blob = new Blob([bytes], { type: 'application/pdf' });
|
const blob = new Blob([bytes], { type: 'application/pdf' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -527,7 +541,6 @@ function App() {
|
|||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Print failed', e);
|
console.error('Print failed', e);
|
||||||
toast('Print failed', 'error');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -752,7 +765,6 @@ function App() {
|
|||||||
setPendingSignature({ url, aspect });
|
setPendingSignature({ url, aspect });
|
||||||
setSignatureModalOpen(false);
|
setSignatureModalOpen(false);
|
||||||
setActiveTool('signature');
|
setActiveTool('signature');
|
||||||
toast('Signature ready — click on the page to place it', 'info');
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -774,7 +786,6 @@ function App() {
|
|||||||
onSubmit={(pw) => { if (passwordPrompt) handleUpload(passwordPrompt.file, pw); }}
|
onSubmit={(pw) => { if (passwordPrompt) handleUpload(passwordPrompt.file, pw); }}
|
||||||
onClose={() => setPasswordPrompt(null)}
|
onClose={() => setPasswordPrompt(null)}
|
||||||
/>
|
/>
|
||||||
<ToastViewport />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CustomButton } from './custom/CustomButton';
|
import { CustomButton } from './custom/CustomButton';
|
||||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||||
import { toast } from '../lib/toast';
|
|
||||||
|
|
||||||
/* ─── Types ─────────────────────────────────────────────── */
|
/* ─── Types ─────────────────────────────────────────────── */
|
||||||
interface SignatureModalProps {
|
interface SignatureModalProps {
|
||||||
@@ -224,19 +224,19 @@ export const SignatureModal: React.FC<SignatureModalProps> = ({ open, onClose, o
|
|||||||
let aspect = 3;
|
let aspect = 3;
|
||||||
|
|
||||||
if (mode === 'draw') {
|
if (mode === 'draw') {
|
||||||
if (!hasInk.current) { toast('Draw your signature first', 'error'); return; }
|
if (!hasInk.current) return;
|
||||||
const c = canvasRef.current!;
|
const c = canvasRef.current!;
|
||||||
dataUrl = c.toDataURL('image/png');
|
dataUrl = c.toDataURL('image/png');
|
||||||
aspect = c.width / c.height;
|
aspect = c.width / c.height;
|
||||||
} else if (mode === 'type') {
|
} else if (mode === 'type') {
|
||||||
if (!typed.trim()) { toast('Type your name first', 'error'); return; }
|
if (!typed.trim()) return;
|
||||||
const res = buildTypedCanvas();
|
const res = buildTypedCanvas();
|
||||||
dataUrl = res.dataUrl; aspect = res.aspect;
|
dataUrl = res.dataUrl; aspect = res.aspect;
|
||||||
} else if (mode === 'upload') {
|
} else if (mode === 'upload') {
|
||||||
if (!uploaded) { toast('Upload a signature image first', 'error'); return; }
|
if (!uploaded) return;
|
||||||
dataUrl = uploaded.url; aspect = uploaded.aspect;
|
dataUrl = uploaded.url; aspect = uploaded.aspect;
|
||||||
} else if (mode === 'saved') {
|
} else if (mode === 'saved') {
|
||||||
toast('Click "Use" on a saved signature', 'error'); return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveToStorage(dataUrl, aspect);
|
saveToStorage(dataUrl, aspect);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CustomButton } from './custom/CustomButton';
|
import { CustomButton } from './custom/CustomButton';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { ToolId } from '../lib/tools';
|
import type { ToolId } from '../lib/tools';
|
||||||
import { toast } from '../lib/toast';
|
|
||||||
import { Popover } from './ui';
|
import { Popover } from './ui';
|
||||||
import {
|
import {
|
||||||
SelectIcon, PanIcon, HighlightIcon, DrawIcon, CommentIcon, TextBoxIcon,
|
SelectIcon, PanIcon, HighlightIcon, DrawIcon, CommentIcon, TextBoxIcon,
|
||||||
@@ -86,7 +86,6 @@ const RailButton: React.FC<{ t: ToolDef; active: boolean; disabled?: boolean; on
|
|||||||
export const ToolRail: React.FC<ToolRailProps> = ({ activeTool, onToolChange, hasSignature, onOpenSignature, onOpenAbout, disabledTools }) => {
|
export const ToolRail: React.FC<ToolRailProps> = ({ activeTool, onToolChange, hasSignature, onOpenSignature, onOpenAbout, disabledTools }) => {
|
||||||
const pickTool = (id: ToolId) => {
|
const pickTool = (id: ToolId) => {
|
||||||
if (disabledTools?.has(id)) {
|
if (disabledTools?.has(id)) {
|
||||||
toast("This tool is not permitted by this document's restrictions", 'error');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onToolChange(id);
|
onToolChange(id);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { subscribeToasts, dismissToast } from '../lib/toast';
|
import { XIcon } from './icons';
|
||||||
import type { ToastItem } from '../lib/toast';
|
|
||||||
import { XIcon, CheckIcon, InfoIcon } from './icons';
|
|
||||||
import { CustomButton } from './custom/CustomButton';
|
import { CustomButton } from './custom/CustomButton';
|
||||||
|
|
||||||
interface PopoverProps {
|
interface PopoverProps {
|
||||||
@@ -190,29 +188,3 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ state, onClose })
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
␍
|
␍
|
||||||
const toastStyles: Record<ToastItem['kind'], string> = {
|
|
||||||
info: 'border-[#ebedf0] bg-[#18212e] text-white',
|
|
||||||
success: 'border-transparent bg-[#16a34a] text-white',
|
|
||||||
error: 'border-transparent bg-[#dc2626] text-white',
|
|
||||||
};
|
|
||||||
export const ToastViewport: React.FC = () => {
|
|
||||||
const [items, setItems] = useState<ToastItem[]>([]);
|
|
||||||
useEffect(() => subscribeToasts(setItems), []);
|
|
||||||
return (
|
|
||||||
<div className="pointer-events-none fixed bottom-5 left-1/2 z-[300] flex -translate-x-1/2 flex-col items-center gap-2">
|
|
||||||
{items.map((t) => (
|
|
||||||
<div
|
|
||||||
key={t.id}
|
|
||||||
className={`pointer-events-auto flex items-center gap-2 rounded-full border px-4 py-2 text-[12.5px] font-semibold shadow-[0_12px_32px_rgba(16,24,40,0.16)] ${toastStyles[t.kind]}`}
|
|
||||||
style={{ animation: 'toastIn 0.18s ease-out' }}
|
|
||||||
>
|
|
||||||
{t.kind === 'success' && <CheckIcon size={15} />}
|
|
||||||
{t.kind === 'error' && <XIcon size={15} />}
|
|
||||||
{t.kind === 'info' && <InfoIcon size={15} />}
|
|
||||||
<span>{t.message}</span>
|
|
||||||
<CustomButton variant="unstyled" className="ml-1 opacity-60 hover:opacity-100" onClick={() => dismissToast(t.id)}><XIcon size={13} /></CustomButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -12,7 +12,7 @@ import type { Rect } from '../lib/coordinateMapping';
|
|||||||
import { gatewayService } from '../lib/gatewayService';
|
import { gatewayService } from '../lib/gatewayService';
|
||||||
import type { SearchResult, PageInfo, Glyph } from '../lib/gatewayService';
|
import type { SearchResult, PageInfo, Glyph } from '../lib/gatewayService';
|
||||||
import type { ToolSettings, ToolId, StampPreset } from '../lib/tools';
|
import type { ToolSettings, ToolId, StampPreset } from '../lib/tools';
|
||||||
import { toast } from '../lib/toast';
|
|
||||||
import { RedactionLayer } from './RedactionLayer';
|
import { RedactionLayer } from './RedactionLayer';
|
||||||
import { StreamEditLayer } from './StreamEditLayer';
|
import { StreamEditLayer } from './StreamEditLayer';
|
||||||
import { wasmFreeDocument } from '../lib/pdfiumEngine';
|
import { wasmFreeDocument } from '../lib/pdfiumEngine';
|
||||||
@@ -325,17 +325,9 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
|||||||
const handleTextSelection = (text: string, bbox: Rect, lines: Rect[], pageIndex: number) => {
|
const handleTextSelection = (text: string, bbox: Rect, lines: Rect[], pageIndex: number) => {
|
||||||
// This is still called on Ctrl+C for select mode
|
// This is still called on Ctrl+C for select mode
|
||||||
if (activeTool === 'select') {
|
if (activeTool === 'select') {
|
||||||
if (!canCopy) {
|
if (!canCopy) return;
|
||||||
toast("Copying is not permitted by this document's restrictions", 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
navigator.clipboard?.writeText(text).then(
|
navigator.clipboard?.writeText(text).catch(() => {});
|
||||||
() => toast(`Copied ${text.length} character${text.length > 1 ? 's' : ''}`, 'success'),
|
|
||||||
() => toast('Copy failed — clipboard unavailable', 'error'),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toast('No selectable text in that area', 'info');
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -386,14 +378,8 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
|||||||
const { pageIndex, text, bbox, lines } = textSelection;
|
const { pageIndex, text, bbox, lines } = textSelection;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'copy':
|
case 'copy':
|
||||||
if (!canCopy) {
|
if (!canCopy) break;
|
||||||
toast("Copying is not permitted by this document's restrictions", 'error');
|
navigator.clipboard?.writeText(text).catch(() => {});
|
||||||
break;
|
|
||||||
}
|
|
||||||
navigator.clipboard?.writeText(text).then(
|
|
||||||
() => toast(`Copied ${text.length} character${text.length > 1 ? 's' : ''}`, 'success'),
|
|
||||||
() => toast('Copy failed — clipboard unavailable', 'error')
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'highlight': {
|
case 'highlight': {
|
||||||
const newAnno: Annotation = {
|
const newAnno: Annotation = {
|
||||||
@@ -432,7 +418,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'edit':
|
case 'edit':
|
||||||
toast('Please select the Edit Text tool from the toolbar to edit text content', 'info');
|
|
||||||
break;
|
break;
|
||||||
case 'comment': {
|
case 'comment': {
|
||||||
const newAnno: Annotation = {
|
const newAnno: Annotation = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { gatewayService } from '../lib/gatewayService';
|
import { gatewayService } from '../lib/gatewayService';
|
||||||
import type { TextObjectResponse } from '../lib/gatewayService';
|
import type { TextObjectResponse } from '../lib/gatewayService';
|
||||||
import { toast } from '../lib/toast';
|
|
||||||
|
|
||||||
import { loadPdfFont } from '../lib/fontFaceLoader';
|
import { loadPdfFont } from '../lib/fontFaceLoader';
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
|||||||
if (newText === obj.text) return;
|
if (newText === obj.text) return;
|
||||||
try {
|
try {
|
||||||
const res = await gatewayService.updateTextObject(documentId, pageIndex, idx, newText);
|
const res = await gatewayService.updateTextObject(documentId, pageIndex, idx, newText);
|
||||||
toast('Text updated successfully', 'success');
|
|
||||||
if (res.newDocumentId && onDocumentChanged) {
|
if (res.newDocumentId && onDocumentChanged) {
|
||||||
onDocumentChanged(res.newDocumentId);
|
onDocumentChanged(res.newDocumentId);
|
||||||
} else {
|
} else {
|
||||||
@@ -102,7 +102,7 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
|||||||
onEditSuccess();
|
onEditSuccess();
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
toast(`Failed to update text: ${e.message}`, 'error');
|
console.error('Failed to update text:', e.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user