Prefer using nullish coalescing operator (??) instead of a logical or (||), as it is a safer operator

This commit is contained in:
Ludy87 2025-09-26 09:16:42 +02:00
parent 4e2beab35b
commit e75d997ec8
No known key found for this signature in database
GPG Key ID: 92696155E0220F94
36 changed files with 130 additions and 120 deletions

View File

@ -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);

View File

@ -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 {

View File

@ -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 (

View File

@ -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");

View File

@ -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';

View File

@ -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">

View File

@ -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);

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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>
)}

View File

@ -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}`;

View File

@ -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';

View File

@ -1,3 +1,4 @@
import React from "react";
import { Button, Group, Stack, Text } from "@mantine/core";
import FitText from "./FitText";

View File

@ -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';

View File

@ -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;

View File

@ -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',

View File

@ -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";

View File

@ -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>

View File

@ -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>

View File

@ -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);
}

View File

@ -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' : ''}

View File

@ -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" />

View File

@ -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"

View File

@ -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';
};

View File

@ -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,

View File

@ -22,7 +22,7 @@ function createImperativeApi() {
};
}
if (!_api) _api = createImperativeApi();
_api ??= createImperativeApi();
// Hook helper to wire context API back to singleton
export function ToastPortalBinder() {

View File

@ -1,3 +1,4 @@
import React from "react";
import { Center, Stack, Loader, Text } from "@mantine/core";
export default function ToolLoadingFallback({ toolName }: { toolName?: string }) {

View File

@ -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";

View File

@ -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]);

View File

@ -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());

View File

@ -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 {

View File

@ -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';

View File

@ -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) {

View File

@ -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);