mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Prefer using nullish coalescing operator (??
) instead of a logical or (||
), as it is a safer operator
This commit is contained in:
parent
4e2beab35b
commit
e75d997ec8
@ -34,7 +34,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
setRecentFiles(files);
|
setRecentFiles(files);
|
||||||
}, [loadRecentFiles]);
|
}, [loadRecentFiles]);
|
||||||
|
|
||||||
const handleRecentFilesSelected = useCallback(async (files: StirlingFileStub[]) => {
|
const handleRecentFilesSelected = useCallback((files: StirlingFileStub[]) => {
|
||||||
try {
|
try {
|
||||||
// Use StirlingFileStubs directly - preserves all metadata!
|
// Use StirlingFileStubs directly - preserves all metadata!
|
||||||
onRecentFileSelect(files);
|
onRecentFileSelect(files);
|
||||||
@ -68,7 +68,9 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFilesModalOpen) {
|
if (isFilesModalOpen) {
|
||||||
refreshRecentFiles();
|
refreshRecentFiles().catch((error) => {
|
||||||
|
console.error('Failed to refresh recent files:', error);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Reset state when modal is closed
|
// Reset state when modal is closed
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
@ -317,7 +317,7 @@ const FileEditor = ({
|
|||||||
}
|
}
|
||||||
}, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]);
|
}, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]);
|
||||||
|
|
||||||
const handleLoadFromStorage = useCallback(async (selectedFiles: File[]) => {
|
const handleLoadFromStorage = useCallback((selectedFiles: File[]) => {
|
||||||
if (selectedFiles.length === 0) return;
|
if (selectedFiles.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -56,7 +56,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
filteredFiles.map((file, index) => {
|
filteredFiles.map((file, index) => {
|
||||||
// All files in filteredFiles are now leaf files only
|
// All files in filteredFiles are now leaf files only
|
||||||
const historyFiles = loadedHistoryFiles.get(file.id) || [];
|
const historyFiles = loadedHistoryFiles.get(file.id) ?? [];
|
||||||
const isExpanded = expandedFileIds.has(file.id);
|
const isExpanded = expandedFileIds.has(file.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||||
@ -75,7 +76,7 @@ export default function Workbench() {
|
|||||||
return (
|
return (
|
||||||
<FileEditor
|
<FileEditor
|
||||||
toolMode={!!selectedToolId}
|
toolMode={!!selectedToolId}
|
||||||
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
|
supportedExtensions={selectedTool?.supportedFormats ?? ["pdf"]}
|
||||||
{...(!selectedToolId && {
|
{...(!selectedToolId && {
|
||||||
onOpenPageEditor: () => {
|
onOpenPageEditor: () => {
|
||||||
setCurrentView("pageEditor");
|
setCurrentView("pageEditor");
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import classes from './bulkSelectionPanel/BulkSelectionPanel.module.css';
|
import classes from './bulkSelectionPanel/BulkSelectionPanel.module.css';
|
||||||
import { parseSelectionWithDiagnostics } from '../../utils/bulkselection/parseSelection';
|
import { parseSelectionWithDiagnostics } from '../../utils/bulkselection/parseSelection';
|
||||||
import PageSelectionInput from './bulkSelectionPanel/PageSelectionInput';
|
import PageSelectionInput from './bulkSelectionPanel/PageSelectionInput';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useRef, useEffect } from "react";
|
import React, { useState, useCallback, useRef, useEffect } from "react";
|
||||||
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||||
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
||||||
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
||||||
@ -105,14 +105,14 @@ const PageEditor = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Interface functions for parent component
|
// Interface functions for parent component
|
||||||
const displayDocument = editedDocument || mergedPdfDocument;
|
const displayDocument = editedDocument ?? mergedPdfDocument;
|
||||||
|
|
||||||
// Utility functions to convert between page IDs and page numbers
|
// Utility functions to convert between page IDs and page numbers
|
||||||
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
|
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
|
||||||
if (!displayDocument) return [];
|
if (!displayDocument) return [];
|
||||||
return pageIds.map(id => {
|
return pageIds.map(id => {
|
||||||
const page = displayDocument.pages.find(p => p.id === id);
|
const page = displayDocument.pages.find(p => p.id === id);
|
||||||
return page?.pageNumber || 0;
|
return page?.pageNumber ?? 0;
|
||||||
}).filter(num => num > 0);
|
}).filter(num => num > 0);
|
||||||
}, [displayDocument]);
|
}, [displayDocument]);
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ const PageEditor = ({
|
|||||||
if (!displayDocument) return [];
|
if (!displayDocument) return [];
|
||||||
return pageNumbers.map(num => {
|
return pageNumbers.map(num => {
|
||||||
const page = displayDocument.pages.find(p => p.pageNumber === num);
|
const page = displayDocument.pages.find(p => p.pageNumber === num);
|
||||||
return page?.id || '';
|
return page?.id ?? '';
|
||||||
}).filter(id => id !== '');
|
}).filter(id => id !== '');
|
||||||
}, [displayDocument]);
|
}, [displayDocument]);
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ const PageEditor = ({
|
|||||||
const pagesToDelete = pageIds.map(pageId => {
|
const pagesToDelete = pageIds.map(pageId => {
|
||||||
|
|
||||||
const page = displayDocument.pages.find(p => p.id === pageId);
|
const page = displayDocument.pages.find(p => p.id === pageId);
|
||||||
return page?.pageNumber || 0;
|
return page?.pageNumber ?? 0;
|
||||||
}).filter(num => num > 0);
|
}).filter(num => num > 0);
|
||||||
|
|
||||||
if (pagesToDelete.length > 0) {
|
if (pagesToDelete.length > 0) {
|
||||||
@ -446,7 +446,7 @@ const PageEditor = ({
|
|||||||
const getExportFilename = useCallback((): string => {
|
const getExportFilename = useCallback((): string => {
|
||||||
if (activeFileIds.length <= 1) {
|
if (activeFileIds.length <= 1) {
|
||||||
// Single file - use original name
|
// Single file - use original name
|
||||||
return displayDocument?.name || 'document.pdf';
|
return displayDocument?.name ?? 'document.pdf';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiple files - use first file name with " (merged)" suffix
|
// Multiple files - use first file name with " (merged)" suffix
|
||||||
@ -466,7 +466,7 @@ const PageEditor = ({
|
|||||||
try {
|
try {
|
||||||
// Step 1: Apply DOM changes to document state first
|
// Step 1: Apply DOM changes to document state first
|
||||||
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||||
mergedPdfDocument || displayDocument, // Original order
|
mergedPdfDocument ?? displayDocument, // Original order
|
||||||
displayDocument, // Current display order (includes reordering)
|
displayDocument, // Current display order (includes reordering)
|
||||||
splitPositions // Position-based splits
|
splitPositions // Position-based splits
|
||||||
);
|
);
|
||||||
@ -514,7 +514,7 @@ const PageEditor = ({
|
|||||||
try {
|
try {
|
||||||
// Step 1: Apply DOM changes to document state first
|
// Step 1: Apply DOM changes to document state first
|
||||||
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||||
mergedPdfDocument || displayDocument, // Original order
|
mergedPdfDocument ?? displayDocument, // Original order
|
||||||
displayDocument, // Current display order (includes reordering)
|
displayDocument, // Current display order (includes reordering)
|
||||||
splitPositions // Position-based splits
|
splitPositions // Position-based splits
|
||||||
);
|
);
|
||||||
@ -585,7 +585,7 @@ const PageEditor = ({
|
|||||||
|
|
||||||
// Pass current display document (which includes reordering) to get both reordering AND DOM changes
|
// Pass current display document (which includes reordering) to get both reordering AND DOM changes
|
||||||
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||||
mergedPdfDocument || displayDocument, // Original order
|
mergedPdfDocument ?? displayDocument, // Original order
|
||||||
displayDocument, // Current display order (includes reordering)
|
displayDocument, // Current display order (includes reordering)
|
||||||
splitPositions // Position-based splits
|
splitPositions // Position-based splits
|
||||||
);
|
);
|
||||||
@ -642,9 +642,9 @@ const PageEditor = ({
|
|||||||
exportLoading,
|
exportLoading,
|
||||||
selectionMode,
|
selectionMode,
|
||||||
selectedPageIds,
|
selectedPageIds,
|
||||||
displayDocument: displayDocument || undefined,
|
displayDocument: displayDocument ?? undefined,
|
||||||
splitPositions,
|
splitPositions,
|
||||||
totalPages: displayDocument?.pages.length || 0,
|
totalPages: displayDocument?.pages.length ?? 0,
|
||||||
closePdf,
|
closePdf,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -655,7 +655,7 @@ const PageEditor = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Display all pages - use edited or original document
|
// Display all pages - use edited or original document
|
||||||
const displayedPages = displayDocument?.pages || [];
|
const displayedPages = displayDocument?.pages ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pos="relative" h='100%' pt={40} style={{ overflow: 'auto' }} data-scrolling-container="true">
|
<Box pos="relative" h='100%' pt={40} style={{ overflow: 'auto' }} data-scrolling-container="true">
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
@ -65,7 +66,7 @@ const PageEditorControls = ({
|
|||||||
// Convert selected pages to split positions (same logic as handleSplit)
|
// Convert selected pages to split positions (same logic as handleSplit)
|
||||||
const selectedPageNumbers = displayDocument ? selectedPageIds.map(id => {
|
const selectedPageNumbers = displayDocument ? selectedPageIds.map(id => {
|
||||||
const page = displayDocument.pages.find(p => p.id === id);
|
const page = displayDocument.pages.find(p => p.id === id);
|
||||||
return page?.pageNumber || 0;
|
return page?.pageNumber ?? 0;
|
||||||
}).filter(num => num > 0) : [];
|
}).filter(num => num > 0) : [];
|
||||||
const selectedSplitPositions = selectedPageNumbers.map(pageNum => pageNum - 1).filter(pos => pos < totalPages - 1);
|
const selectedSplitPositions = selectedPageNumbers.map(pageNum => pageNum - 1).filter(pos => pos < totalPages - 1);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Flex } from '@mantine/core';
|
import { Flex } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
import classes from './BulkSelectionPanel.module.css';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Button, Text, Group, Divider } from '@mantine/core';
|
import { Button, Text, Group, Divider } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
import classes from './BulkSelectionPanel.module.css';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { TextInput, Button, Text, Flex, Switch } from '@mantine/core';
|
import { TextInput, Button, Text, Flex, Switch } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import LocalIcon from '../../shared/LocalIcon';
|
import LocalIcon from '../../shared/LocalIcon';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Text, NumberInput, Group } from '@mantine/core';
|
import { Button, Text, NumberInput, Group } from '@mantine/core';
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
import classes from './BulkSelectionPanel.module.css';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Text } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
import classes from './BulkSelectionPanel.module.css';
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ const SelectedPagesDisplay = ({
|
|||||||
<Text size="sm" c="dimmed" className={classes.selectedText}>
|
<Text size="sm" c="dimmed" className={classes.selectedText}>
|
||||||
Selected: {selectedPageIds.length} pages ({displayDocument ? selectedPageIds.map(id => {
|
Selected: {selectedPageIds.length} pages ({displayDocument ? selectedPageIds.map(id => {
|
||||||
const page = displayDocument.pages.find(p => p.id === id);
|
const page = displayDocument.pages.find(p => p.id === id);
|
||||||
return page?.pageNumber || 0;
|
return page?.pageNumber ?? 0;
|
||||||
}).filter(n => n > 0).join(', ') : ''})
|
}).filter(n => n > 0).join(', ') : ''})
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
@ -91,7 +91,7 @@ export class DeletePagesCommand extends DOMCommand {
|
|||||||
// Convert page numbers to page IDs for stable identification
|
// Convert page numbers to page IDs for stable identification
|
||||||
this.pageIdsToDelete = this.pagesToDelete.map(pageNum => {
|
this.pageIdsToDelete = this.pagesToDelete.map(pageNum => {
|
||||||
const page = currentDoc.pages.find(p => p.pageNumber === pageNum);
|
const page = currentDoc.pages.find(p => p.pageNumber === pageNum);
|
||||||
return page?.id || '';
|
return page?.id ?? '';
|
||||||
}).filter(id => id);
|
}).filter(id => id);
|
||||||
|
|
||||||
this.hasExecuted = true;
|
this.hasExecuted = true;
|
||||||
@ -224,9 +224,7 @@ export class ReorderPagesCommand extends DOMCommand {
|
|||||||
this.setDocument(restoredDocument);
|
this.setDocument(restoredDocument);
|
||||||
}
|
}
|
||||||
|
|
||||||
get description(): string {
|
readonly description = `Reorder page(s)`;
|
||||||
return `Reorder page(s)`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SplitCommand extends DOMCommand {
|
export class SplitCommand extends DOMCommand {
|
||||||
@ -560,9 +558,7 @@ export class BulkPageBreakCommand extends DOMCommand {
|
|||||||
this.setDocument(this.originalDocument);
|
this.setDocument(this.originalDocument);
|
||||||
}
|
}
|
||||||
|
|
||||||
get description(): string {
|
readonly description = `Insert page breaks after all pages`;
|
||||||
return `Insert page breaks after all pages`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InsertFilesCommand extends DOMCommand {
|
export class InsertFilesCommand extends DOMCommand {
|
||||||
@ -711,7 +707,7 @@ export class InsertFilesCommand extends DOMCommand {
|
|||||||
|
|
||||||
console.log('Generating thumbnails for file:', fileId);
|
console.log('Generating thumbnails for file:', fileId);
|
||||||
console.log('Pages:', pages.length);
|
console.log('Pages:', pages.length);
|
||||||
console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
|
console.log('ArrayBuffer size:', arrayBuffer?.byteLength ?? 'undefined');
|
||||||
|
|
||||||
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
if (arrayBuffer && arrayBuffer.byteLength > 0) {
|
||||||
// Extract page numbers for all pages from this file
|
// Extract page numbers for all pages from this file
|
||||||
@ -788,7 +784,7 @@ export class InsertFilesCommand extends DOMCommand {
|
|||||||
this.fileDataMap.set(fileId, arrayBuffer);
|
this.fileDataMap.set(fileId, arrayBuffer);
|
||||||
|
|
||||||
console.log('After storing - fileDataMap size:', this.fileDataMap.size);
|
console.log('After storing - fileDataMap size:', this.fileDataMap.size);
|
||||||
console.log('Stored value size:', this.fileDataMap.get(fileId)?.byteLength || 'undefined');
|
console.log('Stored value size:', this.fileDataMap.get(fileId)?.byteLength ?? 'undefined');
|
||||||
|
|
||||||
for (let i = 1; i <= pageCount; i++) {
|
for (let i = 1; i <= pageCount; i++) {
|
||||||
const pageId = `${fileId}-page-${i}`;
|
const pageId = `${fileId}-page-${i}`;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { Button, Group, Stack, Text } from "@mantine/core";
|
import { Button, Group, Stack, Text } from "@mantine/core";
|
||||||
import FitText from "./FitText";
|
import FitText from "./FitText";
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Stack, Card, Text, Flex } from '@mantine/core';
|
import { Stack, Card, Text, Flex } from '@mantine/core';
|
||||||
import { Tooltip } from './Tooltip';
|
import { Tooltip } from './Tooltip';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -14,26 +14,26 @@ export interface DropdownListWithFooterProps {
|
|||||||
// Value and onChange - support both single and multi-select
|
// Value and onChange - support both single and multi-select
|
||||||
value: string | string[];
|
value: string | string[];
|
||||||
onChange: (value: string | string[]) => void;
|
onChange: (value: string | string[]) => void;
|
||||||
|
|
||||||
// Items and display
|
// Items and display
|
||||||
items: DropdownItem[];
|
items: DropdownItem[];
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
// Labels and headers
|
// Labels and headers
|
||||||
label?: string;
|
label?: string;
|
||||||
header?: ReactNode;
|
header?: ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
|
|
||||||
// Behavior
|
// Behavior
|
||||||
multiSelect?: boolean;
|
multiSelect?: boolean;
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
|
|
||||||
// Styling
|
// Styling
|
||||||
className?: string;
|
className?: string;
|
||||||
dropdownClassName?: string;
|
dropdownClassName?: string;
|
||||||
|
|
||||||
// Popover props
|
// Popover props
|
||||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
withArrow?: boolean;
|
withArrow?: boolean;
|
||||||
@ -58,9 +58,9 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
withArrow = false,
|
withArrow = false,
|
||||||
width = 'target'
|
width = 'target'
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
const isMultiValue = Array.isArray(value);
|
const isMultiValue = Array.isArray(value);
|
||||||
const selectedValues = isMultiValue ? value : (value ? [value] : []);
|
const selectedValues = isMultiValue ? value : (value ? [value] : []);
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
if (!searchable || !searchTerm.trim()) {
|
if (!searchable || !searchTerm.trim()) {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return items.filter(item =>
|
return items.filter(item =>
|
||||||
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
}, [items, searchTerm, searchable]);
|
}, [items, searchTerm, searchable]);
|
||||||
@ -90,7 +90,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
return placeholder;
|
return placeholder;
|
||||||
} else if (selectedValues.length === 1) {
|
} else if (selectedValues.length === 1) {
|
||||||
const selectedItem = items.find(item => item.value === selectedValues[0]);
|
const selectedItem = items.find(item => item.value === selectedValues[0]);
|
||||||
return selectedItem?.name || selectedValues[0];
|
return selectedItem?.name ?? selectedValues[0];
|
||||||
} else {
|
} else {
|
||||||
return `${selectedValues.length} selected`;
|
return `${selectedValues.length} selected`;
|
||||||
}
|
}
|
||||||
@ -107,11 +107,11 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
width={width}
|
width={width}
|
||||||
position={position}
|
position={position}
|
||||||
withArrow={withArrow}
|
withArrow={withArrow}
|
||||||
shadow="md"
|
shadow="md"
|
||||||
onClose={() => searchable && setSearchTerm('')}
|
onClose={() => searchable && setSearchTerm('')}
|
||||||
>
|
>
|
||||||
@ -133,28 +133,28 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
<Text size="sm" style={{ flex: 1 }}>
|
<Text size="sm" style={{ flex: 1 }}>
|
||||||
{getDisplayText()}
|
{getDisplayText()}
|
||||||
</Text>
|
</Text>
|
||||||
<UnfoldMoreIcon style={{
|
<UnfoldMoreIcon style={{
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
color: 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-2))'
|
color: 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-2))'
|
||||||
}} />
|
}} />
|
||||||
</Box>
|
</Box>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
|
|
||||||
<Popover.Dropdown className={dropdownClassName}>
|
<Popover.Dropdown className={dropdownClassName}>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{header && (
|
{header && (
|
||||||
<Box style={{
|
<Box style={{
|
||||||
borderBottom: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
borderBottom: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
||||||
paddingBottom: '8px'
|
paddingBottom: '8px'
|
||||||
}}>
|
}}>
|
||||||
{header}
|
{header}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{searchable && (
|
{searchable && (
|
||||||
<Box style={{
|
<Box style={{
|
||||||
borderBottom: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
borderBottom: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
||||||
paddingBottom: '8px'
|
paddingBottom: '8px'
|
||||||
}}>
|
}}>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
@ -166,7 +166,7 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box style={{ maxHeight, overflowY: 'auto' }}>
|
<Box style={{ maxHeight, overflowY: 'auto' }}>
|
||||||
{filteredItems.length === 0 ? (
|
{filteredItems.length === 0 ? (
|
||||||
<Box style={{ padding: '12px', textAlign: 'center' }}>
|
<Box style={{ padding: '12px', textAlign: 'center' }}>
|
||||||
@ -205,11 +205,11 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
)}
|
)}
|
||||||
<Text size="sm">{item.name}</Text>
|
<Text size="sm">{item.name}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{multiSelect && (
|
{multiSelect && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedValues.includes(item.value)}
|
checked={selectedValues.includes(item.value)}
|
||||||
onChange={() => {}} // Handled by parent onClick
|
onChange={() => { /* empty */ }} // Handled by parent onClick
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
/>
|
/>
|
||||||
@ -218,11 +218,11 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{footer && (
|
{footer && (
|
||||||
<Box style={{
|
<Box style={{
|
||||||
borderTop: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
borderTop: 'light-dark(1px solid var(--mantine-color-gray-2), 1px solid var(--mantine-color-dark-4))',
|
||||||
paddingTop: '8px'
|
paddingTop: '8px'
|
||||||
}}>
|
}}>
|
||||||
{footer}
|
{footer}
|
||||||
</Box>
|
</Box>
|
||||||
@ -234,4 +234,4 @@ const DropdownListWithFooter: React.FC<DropdownListWithFooterProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DropdownListWithFooter;
|
export default DropdownListWithFooter;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Card, Stack, Text, Group, Badge, Button, Box, Image, ThemeIcon, ActionIcon, Tooltip } from "@mantine/core";
|
import { Card, Stack, Text, Group, Badge, Button, Box, Image, ThemeIcon, ActionIcon, Tooltip } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||||
@ -26,7 +26,7 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// Use record thumbnail if available, otherwise fall back to IndexedDB lookup
|
// Use record thumbnail if available, otherwise fall back to IndexedDB lookup
|
||||||
const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileStub);
|
const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileStub);
|
||||||
const thumb = fileStub?.thumbnailUrl || indexedDBThumb;
|
const thumb = fileStub?.thumbnailUrl ?? indexedDBThumb;
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -68,7 +68,7 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Hover action buttons */}
|
{/* Hover action buttons */}
|
||||||
{isHovered && (onView || onEdit) && (
|
{isHovered && (onView ?? onEdit) && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Box, Flex, Group, Text, Button, TextInput, Select } from "@mantine/core";
|
import { Box, Flex, Group, Text, Button, TextInput, Select } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -78,19 +78,19 @@ const FilePickerModal = ({
|
|||||||
// If it's from IndexedDB storage, reconstruct the File
|
// If it's from IndexedDB storage, reconstruct the File
|
||||||
if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') {
|
if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') {
|
||||||
const arrayBuffer = await fileItem.arrayBuffer();
|
const arrayBuffer = await fileItem.arrayBuffer();
|
||||||
const blob = new Blob([arrayBuffer], { type: fileItem.type || 'application/pdf' });
|
const blob = new Blob([arrayBuffer], { type: fileItem.type ?? 'application/pdf' });
|
||||||
return new File([blob], fileItem.name, {
|
return new File([blob], fileItem.name, {
|
||||||
type: fileItem.type || 'application/pdf',
|
type: fileItem.type ?? 'application/pdf',
|
||||||
lastModified: fileItem.lastModified || Date.now()
|
lastModified: fileItem.lastModified ?? Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it has data property, reconstruct the File
|
// If it has data property, reconstruct the File
|
||||||
if (fileItem.data) {
|
if (fileItem.data) {
|
||||||
const blob = new Blob([fileItem.data], { type: fileItem.type || 'application/pdf' });
|
const blob = new Blob([fileItem.data], { type: fileItem.type ?? 'application/pdf' });
|
||||||
return new File([blob], fileItem.name, {
|
return new File([blob], fileItem.name, {
|
||||||
type: fileItem.type || 'application/pdf',
|
type: fileItem.type ?? 'application/pdf',
|
||||||
lastModified: fileItem.lastModified || Date.now()
|
lastModified: fileItem.lastModified ?? Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ const FilePickerModal = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Badge size="xs" variant="light" color="gray">
|
<Badge size="xs" variant="light" color="gray">
|
||||||
{formatFileSize(file.size || (file.file?.size || 0))}
|
{formatFileSize(file.size ?? ((file.file?.size ?? 0)))}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -36,7 +36,7 @@ const FileUploadButton = ({
|
|||||||
>
|
>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Button {...props} variant={variant} fullWidth={fullWidth} color="blue">
|
<Button {...props} variant={variant} fullWidth={fullWidth} color="blue">
|
||||||
{file ? file.name : (placeholder || defaultPlaceholder)}
|
{file ? file.name : (placeholder ?? defaultPlaceholder)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</FileButton>
|
</FileButton>
|
||||||
|
@ -28,7 +28,7 @@ const LandingPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = Array.from(event.target.files || []);
|
const files = Array.from(event.target.files ?? []);
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
await addFiles(files);
|
await addFiles(files);
|
||||||
}
|
}
|
||||||
|
@ -41,10 +41,10 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
// Helper function to render navigation buttons with URL support
|
// Helper function to render navigation buttons with URL support
|
||||||
const renderNavButton = (config: ButtonConfig, index: number) => {
|
const renderNavButton = (config: ButtonConfig, index: number) => {
|
||||||
const isActive = isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView);
|
const isActive = isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView);
|
||||||
|
|
||||||
// Check if this button has URL navigation support
|
// Check if this button has URL navigation support
|
||||||
const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate')
|
const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate')
|
||||||
? getToolNavigation(config.id)
|
? getToolNavigation(config.id)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const handleClick = (e?: React.MouseEvent) => {
|
const handleClick = (e?: React.MouseEvent) => {
|
||||||
@ -59,7 +59,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
return (
|
return (
|
||||||
<div key={config.id} className="flex flex-col items-center gap-1" style={{ marginTop: index === 0 ? '0.5rem' : "0rem" }}>
|
<div key={config.id} className="flex flex-col items-center gap-1" style={{ marginTop: index === 0 ? '0.5rem' : "0rem" }}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
{...(navProps ? {
|
{...(navProps ? {
|
||||||
component: "a" as const,
|
component: "a" as const,
|
||||||
href: navProps.href,
|
href: navProps.href,
|
||||||
onClick: (e: React.MouseEvent) => handleClick(e),
|
onClick: (e: React.MouseEvent) => handleClick(e),
|
||||||
@ -67,7 +67,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
} : {
|
} : {
|
||||||
onClick: () => handleClick()
|
onClick: () => handleClick()
|
||||||
})}
|
})}
|
||||||
size={isActive ? (config.size || 'lg') : 'lg'}
|
size={isActive ? (config.size ?? 'lg') : 'lg'}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)}
|
style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)}
|
||||||
className={isActive ? 'activeIconScale' : ''}
|
className={isActive ? 'activeIconScale' : ''}
|
||||||
|
@ -26,7 +26,7 @@ export default function RightRail() {
|
|||||||
const viewerContext = React.useContext(ViewerContext);
|
const viewerContext = React.useContext(ViewerContext);
|
||||||
const { toggleTheme } = useRainbowThemeContext();
|
const { toggleTheme } = useRainbowThemeContext();
|
||||||
const { buttons, actions } = useRightRail();
|
const { buttons, actions } = useRightRail();
|
||||||
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
const topButtons = useMemo(() => buttons.filter(b => (b.section ?? 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
||||||
|
|
||||||
// Access PageEditor functions for page-editor-specific actions
|
// Access PageEditor functions for page-editor-specific actions
|
||||||
const { pageEditorFunctions } = useToolWorkflow();
|
const { pageEditorFunctions } = useToolWorkflow();
|
||||||
@ -56,8 +56,8 @@ export default function RightRail() {
|
|||||||
|
|
||||||
if (currentView === 'pageEditor') {
|
if (currentView === 'pageEditor') {
|
||||||
// Use PageEditor's own state
|
// Use PageEditor's own state
|
||||||
const totalItems = pageEditorFunctions?.totalPages || 0;
|
const totalItems = pageEditorFunctions?.totalPages ?? 0;
|
||||||
const selectedCount = pageEditorFunctions?.selectedPageIds?.length || 0;
|
const selectedCount = pageEditorFunctions?.selectedPageIds?.length ?? 0;
|
||||||
return { totalItems, selectedCount };
|
return { totalItems, selectedCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ export default function RightRail() {
|
|||||||
}, [currentView, selectedFileIds, removeFiles, setSelectedFiles]);
|
}, [currentView, selectedFileIds, removeFiles, setSelectedFiles]);
|
||||||
|
|
||||||
const updatePagesFromCSV = useCallback((override?: string) => {
|
const updatePagesFromCSV = useCallback((override?: string) => {
|
||||||
const maxPages = pageEditorFunctions?.totalPages || 0;
|
const maxPages = pageEditorFunctions?.totalPages ?? 0;
|
||||||
const normalized = parseSelection(override ?? csvInput, maxPages);
|
const normalized = parseSelection(override ?? csvInput, maxPages);
|
||||||
pageEditorFunctions?.handleSetSelectedPages?.(normalized);
|
pageEditorFunctions?.handleSetSelectedPages?.(normalized);
|
||||||
}, [csvInput, pageEditorFunctions]);
|
}, [csvInput, pageEditorFunctions]);
|
||||||
@ -365,7 +365,7 @@ export default function RightRail() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => { pageEditorFunctions?.handleDelete?.(); }}
|
onClick={() => { pageEditorFunctions?.handleDelete?.(); }}
|
||||||
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length || 0) === 0}
|
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length ?? 0) === 0}
|
||||||
aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'}
|
aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />
|
||||||
@ -386,7 +386,7 @@ export default function RightRail() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => { pageEditorFunctions?.onExportSelected?.(); }}
|
onClick={() => { pageEditorFunctions?.onExportSelected?.(); }}
|
||||||
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length || 0) === 0 || pageEditorFunctions?.exportLoading}
|
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length ?? 0) === 0 || pageEditorFunctions?.exportLoading}
|
||||||
aria-label={typeof t === 'function' ? t('rightRail.exportSelected', 'Export Selected Pages') : 'Export Selected Pages'}
|
aria-label={typeof t === 'function' ? t('rightRail.exportSelected', 'Export Selected Pages') : 'Export Selected Pages'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
||||||
|
@ -266,7 +266,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
// Enhance child with handlers and ref
|
// Enhance child with handlers and ref
|
||||||
const childWithHandlers = React.cloneElement(children as any, {
|
const childWithHandlers = React.cloneElement(children as any, {
|
||||||
ref: (node: HTMLElement | null) => {
|
ref: (node: HTMLElement | null) => {
|
||||||
triggerRef.current = node || null;
|
triggerRef.current = node ?? null;
|
||||||
const originalRef = (children as any).ref;
|
const originalRef = (children as any).ref;
|
||||||
if (typeof originalRef === 'function') originalRef(node);
|
if (typeof originalRef === 'function') originalRef(node);
|
||||||
else if (originalRef && typeof originalRef === 'object') (originalRef).current = node;
|
else if (originalRef && typeof originalRef === 'object') (originalRef).current = node;
|
||||||
@ -296,7 +296,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: coords.top,
|
top: coords.top,
|
||||||
left: coords.left,
|
left: coords.left,
|
||||||
width: maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' as const : undefined),
|
width: maxWidth ?? (sidebarTooltip ? '25rem' as const : undefined),
|
||||||
minWidth,
|
minWidth,
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
visibility: positionReady ? 'visible' : 'hidden',
|
visibility: positionReady ? 'visible' : 'hidden',
|
||||||
@ -334,7 +334,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
{header && (
|
{header && (
|
||||||
<div className={styles['tooltip-header']}>
|
<div className={styles['tooltip-header']}>
|
||||||
<div className={styles['tooltip-logo']}>
|
<div className={styles['tooltip-logo']}>
|
||||||
{header.logo || (
|
{header.logo ?? (
|
||||||
<img
|
<img
|
||||||
src={`${BASE_PATH}/logo-tooltip.svg`}
|
src={`${BASE_PATH}/logo-tooltip.svg`}
|
||||||
alt="Stirling PDF"
|
alt="Stirling PDF"
|
||||||
|
@ -79,5 +79,5 @@ export const getActiveNavButton = (
|
|||||||
// If a tool is selected, highlight it immediately even if the panel view
|
// If a tool is selected, highlight it immediately even if the panel view
|
||||||
// transition to 'toolContent' has not completed yet. This prevents a brief
|
// transition to 'toolContent' has not completed yet. This prevents a brief
|
||||||
// period of no-highlight during rapid navigation.
|
// period of no-highlight during rapid navigation.
|
||||||
return selectedToolKey ? selectedToolKey : 'tools';
|
return selectedToolKey ?? 'tools';
|
||||||
};
|
};
|
||||||
|
@ -48,7 +48,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const show = useCallback<ToastApi['show']>((options) => {
|
const show = useCallback<ToastApi['show']>((options) => {
|
||||||
const id = options.id || generateId();
|
const id = options.id ?? generateId();
|
||||||
const hasButton = !!(options.buttonText && options.buttonCallback);
|
const hasButton = !!(options.buttonText && options.buttonCallback);
|
||||||
const merged: ToastInstance = {
|
const merged: ToastInstance = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
|
@ -22,7 +22,7 @@ function createImperativeApi() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_api) _api = createImperativeApi();
|
_api ??= createImperativeApi();
|
||||||
|
|
||||||
// Hook helper to wire context API back to singleton
|
// Hook helper to wire context API back to singleton
|
||||||
export function ToastPortalBinder() {
|
export function ToastPortalBinder() {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { Center, Stack, Loader, Text } from "@mantine/core";
|
import { Center, Stack, Loader, Text } from "@mantine/core";
|
||||||
|
|
||||||
export default function ToolLoadingFallback({ toolName }: { toolName?: string }) {
|
export default function ToolLoadingFallback({ toolName }: { toolName?: string }) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { Stack, PasswordInput, Select } from "@mantine/core";
|
import { Stack, PasswordInput, Select } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPasswordParameters";
|
import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPasswordParameters";
|
||||||
|
@ -44,7 +44,7 @@ const ToolSearch = ({
|
|||||||
([key]) => idToWords(key),
|
([key]) => idToWords(key),
|
||||||
([, v]) => v.name,
|
([, v]) => v.name,
|
||||||
([, v]) => v.description,
|
([, v]) => v.description,
|
||||||
([, v]) => v.synonyms?.join(' ') || '',
|
([, v]) => v.synonyms?.join(' ') ?? '',
|
||||||
]).slice(0, 6);
|
]).slice(0, 6);
|
||||||
return ranked.map(({ item: [id, tool] }) => ({ id, tool }));
|
return ranked.map(({ item: [id, tool] }) => ({ id, tool }));
|
||||||
}, [value, toolRegistry, mode, selectedToolKey]);
|
}, [value, toolRegistry, mode, selectedToolKey]);
|
||||||
|
@ -10,7 +10,7 @@ const buildFormData = (parameters: MergeParameters, files: File[]): FormData =>
|
|||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
});
|
});
|
||||||
// Provide stable client file IDs (align with files order)
|
// Provide stable client file IDs (align with files order)
|
||||||
const clientIds: string[] = files.map((f: any) => String((f).fileId || f.name));
|
const clientIds: string[] = files.map((f: any) => String((f).fileId ?? f.name));
|
||||||
formData.append('clientFileIds', JSON.stringify(clientIds));
|
formData.append('clientFileIds', JSON.stringify(clientIds));
|
||||||
formData.append("sortType", "orderProvided"); // Always use orderProvided since UI handles sorting
|
formData.append("sortType", "orderProvided"); // Always use orderProvided since UI handles sorting
|
||||||
formData.append("removeCertSign", parameters.removeDigitalSignature.toString());
|
formData.append("removeCertSign", parameters.removeDigitalSignature.toString());
|
||||||
|
@ -177,8 +177,8 @@ export const useToolOperation = <TParams>(
|
|||||||
for (const f of zeroByteFiles) {
|
for (const f of zeroByteFiles) {
|
||||||
(fileActions.markFileError as any)((f as any).fileId);
|
(fileActions.markFileError as any)((f as any).fileId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('markFileError', e);
|
console.log('markFileError', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const validFiles = selectedFiles.filter(file => (file as any)?.size > 0);
|
const validFiles = selectedFiles.filter(file => (file as any)?.size > 0);
|
||||||
@ -437,7 +437,7 @@ export const useToolOperation = <TParams>(
|
|||||||
}
|
}
|
||||||
} catch (_e) { void _e; }
|
} catch (_e) { void _e; }
|
||||||
|
|
||||||
const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error);
|
const errorMessage = config.getErrorMessage?.(error) ?? extractErrorMessage(error);
|
||||||
actions.setError(errorMessage);
|
actions.setError(errorMessage);
|
||||||
actions.setStatus('');
|
actions.setStatus('');
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -13,7 +13,7 @@ function clampText(s: string, max = MAX_TOAST_BODY_CHARS): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isUnhelpfulMessage(msg: string | null | undefined): boolean {
|
function isUnhelpfulMessage(msg: string | null | undefined): boolean {
|
||||||
const s = (msg || '').trim();
|
const s = (msg ?? '').trim();
|
||||||
if (!s) return true;
|
if (!s) return true;
|
||||||
// Common unhelpful payloads we see
|
// Common unhelpful payloads we see
|
||||||
if (s === '{}' || s === '[]') return true;
|
if (s === '{}' || s === '[]') return true;
|
||||||
@ -33,7 +33,7 @@ function titleForStatus(status?: number): string {
|
|||||||
function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const status = error.response?.status;
|
const status = error.response?.status;
|
||||||
const _statusText = error.response?.statusText || '';
|
const _statusText = error.response?.statusText ?? '';
|
||||||
let parsed: any = undefined;
|
let parsed: any = undefined;
|
||||||
const raw = error.response?.data;
|
const raw = error.response?.data;
|
||||||
if (typeof raw === 'string') {
|
if (typeof raw === 'string') {
|
||||||
@ -71,7 +71,7 @@ function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
|||||||
return { title, body: bodyMsg };
|
return { title, body: bodyMsg };
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const msg = (error?.message || String(error)) as string;
|
const msg = (error?.message ?? String(error)) as string;
|
||||||
return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg };
|
return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore extraction errors
|
// ignore extraction errors
|
||||||
@ -81,7 +81,7 @@ function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Axios instance creation ----------
|
// ---------- Axios instance creation ----------
|
||||||
const __globalAny = (typeof window !== 'undefined' ? (window as any) : undefined);
|
const __globalAny: Window | undefined = (typeof window !== 'undefined' ? window : undefined);
|
||||||
|
|
||||||
type ExtendedAxiosInstance = AxiosInstance & {
|
type ExtendedAxiosInstance = AxiosInstance & {
|
||||||
CancelToken: typeof axios.CancelToken;
|
CancelToken: typeof axios.CancelToken;
|
||||||
@ -105,9 +105,9 @@ if (__PREV_CLIENT) {
|
|||||||
__createdClient = axios as any;
|
__createdClient = axios as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiClient: ExtendedAxiosInstance = (__createdClient || (axios as any)) as ExtendedAxiosInstance;
|
const apiClient: ExtendedAxiosInstance = (__createdClient ?? (axios as any)) as ExtendedAxiosInstance;
|
||||||
|
|
||||||
// Augment instance with axios static helpers for backwards compatibility
|
// Augment instance with axios static helpers for backwards compatibility
|
||||||
if (apiClient) {
|
if (apiClient) {
|
||||||
try { (apiClient as any).CancelToken = (axios as any).CancelToken; } catch (e) { console.debug('setCancelToken', e); }
|
try { (apiClient as any).CancelToken = (axios as any).CancelToken; } catch (e) { console.debug('setCancelToken', e); }
|
||||||
try { (apiClient as any).isCancel = (axios as any).isCancel; } catch (e) { console.debug('setIsCancel', e); }
|
try { (apiClient as any).isCancel = (axios as any).isCancel; } catch (e) { console.debug('setIsCancel', e); }
|
||||||
@ -115,7 +115,7 @@ if (apiClient) {
|
|||||||
|
|
||||||
// ---------- Base defaults ----------
|
// ---------- Base defaults ----------
|
||||||
try {
|
try {
|
||||||
const env = (import.meta as any)?.env || {};
|
const env = (import.meta as any)?.env ?? {};
|
||||||
apiClient.defaults.baseURL = env?.VITE_API_BASE_URL ?? '/';
|
apiClient.defaults.baseURL = env?.VITE_API_BASE_URL ?? '/';
|
||||||
apiClient.defaults.responseType = 'json';
|
apiClient.defaults.responseType = 'json';
|
||||||
// If OSS relies on cookies, uncomment:
|
// If OSS relies on cookies, uncomment:
|
||||||
@ -124,9 +124,9 @@ try {
|
|||||||
apiClient.defaults.timeout = 20000;
|
apiClient.defaults.timeout = 20000;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.debug('setDefaults', e);
|
console.debug('setDefaults', e);
|
||||||
apiClient.defaults.baseURL = apiClient.defaults.baseURL || '/';
|
apiClient.defaults.baseURL = apiClient.defaults.baseURL ?? '/';
|
||||||
apiClient.defaults.responseType = apiClient.defaults.responseType || 'json';
|
apiClient.defaults.responseType = apiClient.defaults.responseType ?? 'json';
|
||||||
apiClient.defaults.timeout = apiClient.defaults.timeout || 20000;
|
apiClient.defaults.timeout = apiClient.defaults.timeout ?? 20000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Install a single response error interceptor (dedup + UX) ----------
|
// ---------- Install a single response error interceptor (dedup + UX) ----------
|
||||||
@ -138,7 +138,7 @@ if (__globalAny?.__SPDF_HTTP_ERR_INTERCEPTOR_ID !== undefined && __PREV_CLIENT)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const __recentSpecialByEndpoint: Record<string, number> = (__globalAny?.__SPDF_RECENT_SPECIAL || {});
|
const __recentSpecialByEndpoint: Record<string, number> = (__globalAny?.__SPDF_RECENT_SPECIAL ?? {});
|
||||||
const __SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate after special toast
|
const __SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate after special toast
|
||||||
|
|
||||||
const __INTERCEPTOR_ID__ = apiClient?.interceptors?.response?.use
|
const __INTERCEPTOR_ID__ = apiClient?.interceptors?.response?.use
|
||||||
@ -219,10 +219,10 @@ export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Pr
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
let detail = '';
|
let detail = '';
|
||||||
try {
|
try {
|
||||||
const ct = res.headers.get('content-type') || '';
|
const ct = res.headers.get('content-type') ?? '';
|
||||||
if (ct.includes('application/json')) {
|
if (ct.includes('application/json')) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
|
detail = typeof data === 'string' ? data : (data?.message ?? JSON.stringify(data));
|
||||||
} else {
|
} else {
|
||||||
detail = await res.text();
|
detail = await res.text();
|
||||||
}
|
}
|
||||||
@ -243,13 +243,13 @@ export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Pr
|
|||||||
|
|
||||||
// ---------- Convenience API surface and exports ----------
|
// ---------- Convenience API surface and exports ----------
|
||||||
export const api = {
|
export const api = {
|
||||||
get: apiClient.get,
|
get: apiClient.get.bind(apiClient),
|
||||||
post: apiClient.post,
|
post: apiClient.post.bind(apiClient),
|
||||||
put: apiClient.put,
|
put: apiClient.put.bind(apiClient),
|
||||||
patch: apiClient.patch,
|
patch: apiClient.patch.bind(apiClient),
|
||||||
delete: apiClient.delete,
|
delete: apiClient.delete.bind(apiClient),
|
||||||
request: apiClient.request,
|
request: apiClient.request.bind(apiClient),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default apiClient;
|
export default apiClient;
|
||||||
export type { CancelTokenSource } from 'axios';
|
export type { CancelTokenSource } from 'axios';
|
||||||
|
@ -32,7 +32,7 @@ function titleForStatus(status?: number): string {
|
|||||||
* Returns true if a special toast was shown, false otherwise.
|
* Returns true if a special toast was shown, false otherwise.
|
||||||
*/
|
*/
|
||||||
export function showSpecialErrorToast(rawError: string | undefined, options?: { status?: number }): boolean {
|
export function showSpecialErrorToast(rawError: string | undefined, options?: { status?: number }): boolean {
|
||||||
const message = (rawError || '').toString();
|
const message = (rawError ?? '').toString();
|
||||||
if (!message) return false;
|
if (!message) return false;
|
||||||
|
|
||||||
for (const mapping of MAPPINGS) {
|
for (const mapping of MAPPINGS) {
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
type ConversionEndpoint
|
type ConversionEndpoint
|
||||||
} from '../helpers/conversionEndpointDiscovery';
|
} from '../helpers/conversionEndpointDiscovery';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
// Test configuration
|
// Test configuration
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
||||||
@ -238,7 +238,7 @@ async function testConversion(page: Page, conversion: ConversionEndpoint) {
|
|||||||
// Save and verify file is not empty
|
// Save and verify file is not empty
|
||||||
const path = await download.path();
|
const path = await download.path();
|
||||||
if (path) {
|
if (path) {
|
||||||
const fs = require('fs');
|
// fs is already imported at the top of the file
|
||||||
const stats = fs.statSync(path);
|
const stats = fs.statSync(path);
|
||||||
expect(stats.size).toBeGreaterThan(0);
|
expect(stats.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user