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