mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Fix any type usages in frontend (#7617)
# Description of Changes Follow-on from #7334. Fix more `any` type usages and ban them in the linter. We're starting to get down to only difficult folders left now, so some of these fixes replace an excluded folder with a couple of individual files to reduce scope to manageable levels. There are two real behaviour changes in this PR because of bugs that were never caught due to the lack of proper typing: - In the Google Drive service, `lastModified` was always `undefined` because it should have been read via `lastModifiedUtc`, which it now is. This means that files being read from Google Drive should now accurately retain their last modified date from Drive. - In the error toasts, there was translation logic to try and make friendlier error messages, but it'd never actually fire since it relied on `i18n` being written to `globalThis`, which it never was. It now imports the singleton instead so that translation should start working. I also had to tweak the way that FitText works because it was relying on `any` typing to mix refs between different places where they weren't technically compatible but I've changed it to go via a function and the behaviour doesn't change.
This commit is contained in:
@@ -153,7 +153,7 @@ const BulkShareModal: React.FC<BulkShareModalProps> = ({
|
||||
if (onShared) {
|
||||
await onShared();
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to generate share link:", error);
|
||||
setErrorMessage(
|
||||
t(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Stack, Card, Text, Flex } from "@mantine/core";
|
||||
import { Tooltip } from "@app/components/shared/Tooltip";
|
||||
import { TooltipTip } from "@app/types/tips";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface CardOption<T = string> {
|
||||
@@ -7,14 +8,14 @@ export interface CardOption<T = string> {
|
||||
prefixKey: string;
|
||||
nameKey: string;
|
||||
tooltipKey?: string;
|
||||
tooltipContent?: any[];
|
||||
tooltipContent?: TooltipTip[];
|
||||
}
|
||||
|
||||
export interface CardSelectorProps<T, K extends CardOption<T>> {
|
||||
options: K[];
|
||||
onSelect: (value: T) => void;
|
||||
disabled?: boolean;
|
||||
getTooltipContent?: (option: K) => any[];
|
||||
getTooltipContent?: (option: K) => TooltipTip[];
|
||||
}
|
||||
|
||||
const CardSelector = <T, K extends CardOption<T>>({
|
||||
|
||||
@@ -269,7 +269,7 @@ export function FileSelectorPicker({
|
||||
responseType: "blob",
|
||||
suppressErrorToast: true,
|
||||
skipAuthRedirect: true,
|
||||
} as any,
|
||||
},
|
||||
);
|
||||
const ct = readResponseHeader(res.headers, "content-type");
|
||||
const disp = readResponseHeader(res.headers, "content-disposition");
|
||||
@@ -287,7 +287,7 @@ export function FileSelectorPicker({
|
||||
responseType: "blob",
|
||||
suppressErrorToast: true,
|
||||
skipAuthRedirect: true,
|
||||
} as any,
|
||||
},
|
||||
);
|
||||
const ct = readResponseHeader(res.headers, "content-type");
|
||||
const disp = readResponseHeader(res.headers, "content-disposition");
|
||||
|
||||
@@ -208,7 +208,7 @@ const FileSidebar = forwardRef<HTMLDivElement, FileSidebarProps>(
|
||||
const openWatchedFolders = useCallback(() => {
|
||||
if (collapsed && onToggleCollapse) onToggleCollapse();
|
||||
setCustomWorkbenchViewData(WATCHED_FOLDER_VIEW_ID, { folderId: null });
|
||||
navActions.setWorkbench(WATCHED_FOLDER_WORKBENCH_ID as any);
|
||||
navActions.setWorkbench(WATCHED_FOLDER_WORKBENCH_ID);
|
||||
}, [collapsed, onToggleCollapse, setCustomWorkbenchViewData, navActions]);
|
||||
|
||||
// Clicking a file's membership dot jumps straight into that folder.
|
||||
@@ -216,7 +216,7 @@ const FileSidebar = forwardRef<HTMLDivElement, FileSidebarProps>(
|
||||
(folderId: string) => {
|
||||
if (collapsed && onToggleCollapse) onToggleCollapse();
|
||||
setCustomWorkbenchViewData(WATCHED_FOLDER_VIEW_ID, { folderId });
|
||||
navActions.setWorkbench(WATCHED_FOLDER_WORKBENCH_ID as any);
|
||||
navActions.setWorkbench(WATCHED_FOLDER_WORKBENCH_ID);
|
||||
},
|
||||
[collapsed, onToggleCollapse, setCustomWorkbenchViewData, navActions],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Modal, Stack, Text, PasswordInput, Alert } from "@mantine/core";
|
||||
import { Button } from "@app/ui/Button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -95,10 +96,13 @@ export default function FirstLoginModal({
|
||||
setTimeout(() => {
|
||||
onPasswordChanged();
|
||||
}, 1500);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
console.error("Failed to change password:", err);
|
||||
const message = axios.isAxiosError<{ message?: string }>(err)
|
||||
? err.response?.data?.message
|
||||
: undefined;
|
||||
setError(
|
||||
err.response?.data?.message ||
|
||||
message ||
|
||||
t(
|
||||
"firstLogin.passwordChangeFailed",
|
||||
"Failed to change password. Please check your current password.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { CSSProperties, useMemo, useRef } from "react";
|
||||
import React, { CSSProperties, useCallback, useMemo, useRef } from "react";
|
||||
import { useAdjustFontSizeToFit } from "@app/components/shared/fitText/textFit";
|
||||
|
||||
type FitTextProps = {
|
||||
@@ -28,8 +28,14 @@ const FitText: React.FC<FitTextProps> = ({
|
||||
}) => {
|
||||
const ref = useRef<HTMLElement | null>(null);
|
||||
|
||||
// Callback ref: an HTMLElement handler satisfies span's/div's differing ref
|
||||
// types (callback refs are contravariant), so the tag can stay polymorphic.
|
||||
const setRef = useCallback((node: HTMLElement | null) => {
|
||||
ref.current = node;
|
||||
}, []);
|
||||
|
||||
// Hook runs after mount and on size/text changes; uses observers internally
|
||||
useAdjustFontSizeToFit(ref as any, {
|
||||
useAdjustFontSizeToFit(ref, {
|
||||
maxFontSizePx: fontSize,
|
||||
minFontScale: minimumFontScale,
|
||||
maxLines: lines,
|
||||
@@ -38,7 +44,7 @@ const FitText: React.FC<FitTextProps> = ({
|
||||
|
||||
// Memoize the HTML tag to render (span/div) from the `as` prop so
|
||||
// React doesn't create a new component function on each render.
|
||||
const ElementTag: any = useMemo(() => as, [as]);
|
||||
const ElementTag: React.ElementType = useMemo(() => as, [as]);
|
||||
|
||||
// For the / character, insert zero-width soft breaks to prefer wrapping at them
|
||||
const displayText = useMemo(() => {
|
||||
@@ -70,7 +76,7 @@ const FitText: React.FC<FitTextProps> = ({
|
||||
|
||||
return (
|
||||
<ElementTag
|
||||
ref={ref}
|
||||
ref={setRef}
|
||||
className={className}
|
||||
style={{ ...clampStyles, ...style }}
|
||||
>
|
||||
|
||||
@@ -165,7 +165,7 @@ const ShareFileModal: React.FC<ShareFileModalProps> = ({
|
||||
if (onUploaded) {
|
||||
await onUploaded();
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to generate share link:", error);
|
||||
setErrorMessage(
|
||||
t(
|
||||
|
||||
@@ -226,7 +226,7 @@ const ShareManagementModal: React.FC<ShareManagementModalProps> = ({
|
||||
durationMs: 2500,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to create share link:", error);
|
||||
setErrorMessage(
|
||||
t(
|
||||
|
||||
@@ -18,6 +18,18 @@ import { useLogoAssets } from "@app/hooks/useLogoAssets";
|
||||
import styles from "@app/components/shared/tooltip/Tooltip.module.css";
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex";
|
||||
|
||||
// The wrapped child's own event handlers, which Tooltip forwards to after
|
||||
// running its own trigger logic. Kept partial since any given child may set none.
|
||||
interface ForwardedHandlers {
|
||||
onPointerEnter?: React.PointerEventHandler;
|
||||
onPointerLeave?: React.PointerEventHandler;
|
||||
onMouseDown?: React.MouseEventHandler;
|
||||
onMouseUp?: React.MouseEventHandler;
|
||||
onClick?: React.MouseEventHandler;
|
||||
onFocus?: React.FocusEventHandler;
|
||||
onBlur?: React.FocusEventHandler;
|
||||
}
|
||||
|
||||
export interface TooltipProps {
|
||||
sidebarTooltip?: boolean;
|
||||
position?: "right" | "left" | "top" | "bottom";
|
||||
@@ -218,7 +230,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
const handlePointerEnter = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!isPinned && !disabled) openWithDelay();
|
||||
(children.props as any)?.onPointerEnter?.(e);
|
||||
(children.props as ForwardedHandlers).onPointerEnter?.(e);
|
||||
},
|
||||
[isPinned, openWithDelay, children.props, disabled],
|
||||
);
|
||||
@@ -233,19 +245,19 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
tooltipRef.current &&
|
||||
tooltipRef.current.contains(related)
|
||||
) {
|
||||
(children.props as any)?.onPointerLeave?.(e);
|
||||
(children.props as ForwardedHandlers).onPointerLeave?.(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore transient leave between mousedown and click
|
||||
if (clickPendingRef.current) {
|
||||
(children.props as any)?.onPointerLeave?.(e);
|
||||
(children.props as ForwardedHandlers).onPointerLeave?.(e);
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimers();
|
||||
if (allowAutoClose && !isPinned) setOpen(false);
|
||||
(children.props as any)?.onPointerLeave?.(e);
|
||||
(children.props as ForwardedHandlers).onPointerLeave?.(e);
|
||||
},
|
||||
[clearTimers, isPinned, setOpen, children.props, allowAutoClose],
|
||||
);
|
||||
@@ -253,7 +265,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
clickPendingRef.current = true;
|
||||
(children.props as any)?.onMouseDown?.(e);
|
||||
(children.props as ForwardedHandlers).onMouseDown?.(e);
|
||||
},
|
||||
[children.props],
|
||||
);
|
||||
@@ -262,7 +274,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
(e: React.MouseEvent) => {
|
||||
// allow microtask turn so click can see this false
|
||||
queueMicrotask(() => (clickPendingRef.current = false));
|
||||
(children.props as any)?.onMouseUp?.(e);
|
||||
(children.props as ForwardedHandlers).onMouseUp?.(e);
|
||||
},
|
||||
[children.props],
|
||||
);
|
||||
@@ -279,7 +291,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
return;
|
||||
}
|
||||
clickPendingRef.current = false;
|
||||
(children.props as any)?.onClick?.(e);
|
||||
(children.props as ForwardedHandlers).onClick?.(e);
|
||||
},
|
||||
[clearTimers, pinOnClick, open, setOpen, children.props],
|
||||
);
|
||||
@@ -288,7 +300,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
const handleFocus = useCallback(
|
||||
(e: React.FocusEvent) => {
|
||||
if (!isPinned && !disabled && openOnFocus) openWithDelay();
|
||||
(children.props as any)?.onFocus?.(e);
|
||||
(children.props as ForwardedHandlers).onFocus?.(e);
|
||||
},
|
||||
[isPinned, openWithDelay, children.props, disabled, openOnFocus],
|
||||
);
|
||||
@@ -301,12 +313,12 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
tooltipRef.current &&
|
||||
tooltipRef.current.contains(related)
|
||||
) {
|
||||
(children.props as any)?.onBlur?.(e);
|
||||
(children.props as ForwardedHandlers).onBlur?.(e);
|
||||
return;
|
||||
}
|
||||
clearTimers();
|
||||
if (allowAutoClose && !isPinned) setOpen(false);
|
||||
(children.props as any)?.onBlur?.(e);
|
||||
(children.props as ForwardedHandlers).onBlur?.(e);
|
||||
},
|
||||
[isPinned, setOpen, children.props, allowAutoClose, clearTimers],
|
||||
);
|
||||
@@ -339,24 +351,30 @@ 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;
|
||||
const originalRef = (children as any).ref;
|
||||
if (typeof originalRef === "function") originalRef(node);
|
||||
else if (originalRef && typeof originalRef === "object")
|
||||
(originalRef as any).current = node;
|
||||
const childWithHandlers = React.cloneElement(
|
||||
children as React.ReactElement<Record<string, unknown>>,
|
||||
{
|
||||
ref: (node: HTMLElement | null) => {
|
||||
triggerRef.current = node || null;
|
||||
const originalRef = (
|
||||
children as React.ReactElement & { ref?: React.Ref<HTMLElement> }
|
||||
).ref;
|
||||
if (typeof originalRef === "function") originalRef(node);
|
||||
else if (originalRef && typeof originalRef === "object")
|
||||
(originalRef as React.MutableRefObject<HTMLElement | null>).current =
|
||||
node;
|
||||
},
|
||||
"aria-describedby": open ? tooltipIdRef.current : undefined,
|
||||
onPointerEnter: handlePointerEnter,
|
||||
onPointerLeave: handlePointerLeave,
|
||||
onMouseDown: handleMouseDown,
|
||||
onMouseUp: handleMouseUp,
|
||||
onClick: handleClick,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
onKeyDown: handleKeyDown,
|
||||
},
|
||||
"aria-describedby": open ? tooltipIdRef.current : undefined,
|
||||
onPointerEnter: handlePointerEnter,
|
||||
onPointerLeave: handlePointerLeave,
|
||||
onMouseDown: handleMouseDown,
|
||||
onMouseUp: handleMouseUp,
|
||||
onClick: handleClick,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
onKeyDown: handleKeyDown,
|
||||
});
|
||||
);
|
||||
|
||||
const shouldShowTooltip = open;
|
||||
const shouldShowCloseButton = showCloseButton || isPinned;
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function AutomationCreation({
|
||||
setConfigModalOpen(true);
|
||||
};
|
||||
|
||||
const handleToolConfigSave = (parameters: Record<string, any>) => {
|
||||
const handleToolConfigSave = (parameters: Record<string, unknown>) => {
|
||||
if (configuraingToolIndex >= 0) {
|
||||
updateTool(configuraingToolIndex, {
|
||||
configured: true,
|
||||
|
||||
@@ -19,7 +19,7 @@ interface AutomationEntryProps {
|
||||
/** Optional description for tooltip */
|
||||
description?: string;
|
||||
/** MUI Icon component for the badge */
|
||||
badgeIcon?: React.ComponentType<any>;
|
||||
badgeIcon?: React.ComponentType;
|
||||
/** Array of tool operation names in the workflow */
|
||||
operations: string[];
|
||||
/** Click handler */
|
||||
|
||||
@@ -9,11 +9,12 @@ import { useToolRegistry } from "@app/contexts/ToolRegistryContext";
|
||||
import { AutomationConfig, ExecutionStep } from "@app/types/automation";
|
||||
import { EXECUTION_STATUS } from "@app/constants/automation";
|
||||
import { useResourceCleanup } from "@app/utils/resourceManager";
|
||||
import type { useAutomateOperation } from "@app/hooks/tools/automate/useAutomateOperation";
|
||||
|
||||
interface AutomationRunProps {
|
||||
automation: AutomationConfig;
|
||||
onComplete: () => void;
|
||||
automateOperation?: any; // TODO: Type this properly when available
|
||||
automateOperation?: ReturnType<typeof useAutomateOperation>;
|
||||
}
|
||||
|
||||
export default function AutomationRun({
|
||||
@@ -34,13 +35,13 @@ export default function AutomationRun({
|
||||
// Use the operation hook's loading state
|
||||
const isExecuting = automateOperation?.isLoading || false;
|
||||
const hasResults =
|
||||
automateOperation?.files.length > 0 ||
|
||||
(automateOperation?.files.length ?? 0) > 0 ||
|
||||
automateOperation?.downloadUrl !== null;
|
||||
|
||||
// Initialize execution steps from automation
|
||||
useEffect(() => {
|
||||
if (automation?.operations) {
|
||||
const steps = automation.operations.map((op: any, index: number) => {
|
||||
const steps = automation.operations.map((op, index) => {
|
||||
const tool = toolRegistry[op.operation as keyof typeof toolRegistry];
|
||||
return {
|
||||
id: `${op.operation}-${index}`,
|
||||
@@ -125,7 +126,7 @@ export default function AutomationRun({
|
||||
// Mark all as completed and reset current step
|
||||
setCurrentStepIndex(-1);
|
||||
console.log(`✅ Automation completed successfully`);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Automation execution failed:", error);
|
||||
setCurrentStepIndex(-1);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Stack, Text, ScrollArea } from "@mantine/core";
|
||||
import {
|
||||
ToolRegistryEntry,
|
||||
ToolRegistry,
|
||||
SubcategoryId,
|
||||
getToolSupportsAutomate,
|
||||
} from "@app/data/toolsTaxonomy";
|
||||
import { useToolSections } from "@app/hooks/useToolSections";
|
||||
@@ -81,9 +82,7 @@ export default function ToolSelector({
|
||||
}, [filteredTools]);
|
||||
|
||||
// Use the same tool sections logic as the main ToolPicker
|
||||
const { sections, searchGroups } = useToolSections(
|
||||
transformedFilteredTools as any /* FIX ME */,
|
||||
);
|
||||
const { sections, searchGroups } = useToolSections(transformedFilteredTools);
|
||||
|
||||
// Determine what to display: search results or organized sections
|
||||
const isSearching = searchTerm.trim().length > 0;
|
||||
@@ -98,7 +97,9 @@ export default function ToolSelector({
|
||||
return [
|
||||
{
|
||||
name: "Tools",
|
||||
subcategoryId: "all" as any,
|
||||
// Synthetic "all tools" group used only as a fallback when the
|
||||
// taxonomy produces no sections; "all" is not a real SubcategoryId.
|
||||
subcategoryId: "all" as unknown as SubcategoryId,
|
||||
tools: baseFilteredTools.map(([key, tool]) => ({ id: key, tool })),
|
||||
},
|
||||
];
|
||||
@@ -125,7 +126,7 @@ export default function ToolSelector({
|
||||
displayGroups.map((subcategory) =>
|
||||
renderToolButtons(
|
||||
t,
|
||||
subcategory as any,
|
||||
subcategory,
|
||||
null,
|
||||
handleToolSelect,
|
||||
!isSearching,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
useFileActions,
|
||||
} from "@app/contexts/FileContext";
|
||||
import { useFileWithUrl } from "@app/hooks/useFileWithUrl";
|
||||
import { ZoomMode } from "@embedpdf/plugin-zoom/react";
|
||||
import { useViewer } from "@app/contexts/ViewerContext";
|
||||
import { LocalEmbedPDF } from "@app/components/viewer/LocalEmbedPDF";
|
||||
import { PdfViewerToolbar } from "@app/components/viewer/PdfViewerToolbar";
|
||||
@@ -418,7 +419,7 @@ const EmbedPdfViewerContent = ({
|
||||
return;
|
||||
case "0":
|
||||
event.preventDefault();
|
||||
zoomActions.requestZoom("fit-width");
|
||||
zoomActions.requestZoom(ZoomMode.FitWidth);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,6 @@ export function SelectionAPIBridge() {
|
||||
|
||||
const buildApi = () => ({
|
||||
copyToClipboard: () => selection.copyToClipboard(),
|
||||
getSelectedText: () => selection.getSelectedText(),
|
||||
getFormattedSelection: () => selection.getFormattedSelection(),
|
||||
selectAll: async (totalPages: number) => {
|
||||
const docId = activeDocumentId;
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
ZoomState,
|
||||
} from "@app/contexts/viewer/viewerBridges";
|
||||
import { PdfBookmarkObject, PdfAttachmentObject } from "@embedpdf/models";
|
||||
import { ZoomLevel, Point } from "@embedpdf/plugin-zoom";
|
||||
import { FormattedSelection } from "@embedpdf/plugin-selection";
|
||||
|
||||
export interface ScrollActions {
|
||||
scrollToPage: (page: number, behavior?: "smooth" | "instant") => void;
|
||||
@@ -19,7 +21,7 @@ export interface ZoomActions {
|
||||
zoomIn: () => void;
|
||||
zoomOut: () => void;
|
||||
toggleMarqueeZoom: () => void;
|
||||
requestZoom: (level: any, center?: any) => void;
|
||||
requestZoom: (level: ZoomLevel, center?: Point) => void;
|
||||
setZoomLevel: (factor: number) => void;
|
||||
}
|
||||
|
||||
@@ -31,8 +33,7 @@ export interface PanActions {
|
||||
|
||||
export interface SelectionActions {
|
||||
copyToClipboard: () => void;
|
||||
getSelectedText: () => string;
|
||||
getFormattedSelection: () => any;
|
||||
getFormattedSelection: () => FormattedSelection[] | null;
|
||||
selectAll: (totalPages: number) => Promise<boolean>;
|
||||
selectWordAt: (pageIndex: number, x: number, y: number) => boolean;
|
||||
}
|
||||
@@ -51,7 +52,7 @@ export interface RotationActions {
|
||||
}
|
||||
|
||||
export interface SearchActions {
|
||||
search: (query: string) => Promise<any> | undefined;
|
||||
search: (query: string) => Promise<unknown> | undefined;
|
||||
next: () => void;
|
||||
previous: () => void;
|
||||
clear: () => void;
|
||||
@@ -214,7 +215,7 @@ export function createViewerActions({
|
||||
api.toggleMarqueeZoom();
|
||||
}
|
||||
},
|
||||
requestZoom: (level: any, center?: any) => {
|
||||
requestZoom: (level: ZoomLevel, center?: Point) => {
|
||||
const api = registry.current.zoom?.api;
|
||||
if (api?.requestZoom) {
|
||||
api.requestZoom(level, center);
|
||||
@@ -257,13 +258,6 @@ export function createViewerActions({
|
||||
api.copyToClipboard();
|
||||
}
|
||||
},
|
||||
getSelectedText: () => {
|
||||
const api = registry.current.selection?.api;
|
||||
if (api?.getSelectedText) {
|
||||
return api.getSelectedText() ?? "";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
getFormattedSelection: () => {
|
||||
const api = registry.current.selection?.api;
|
||||
if (api?.getFormattedSelection) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { SpreadMode } from "@embedpdf/plugin-spread/react";
|
||||
import { PdfBookmarkObject, PdfAttachmentObject } from "@embedpdf/models";
|
||||
import { ZoomLevel, Point } from "@embedpdf/plugin-zoom";
|
||||
import { FormattedSelection } from "@embedpdf/plugin-selection";
|
||||
|
||||
export enum PdfPermissionFlag {
|
||||
Print = 0x0004,
|
||||
@@ -46,7 +48,7 @@ export interface ZoomAPIWrapper {
|
||||
zoomIn: () => void;
|
||||
zoomOut: () => void;
|
||||
toggleMarqueeZoom: () => void;
|
||||
requestZoom: (level: any, center?: any) => void;
|
||||
requestZoom: (level: ZoomLevel, center?: Point) => void;
|
||||
}
|
||||
|
||||
export interface PanAPIWrapper {
|
||||
@@ -58,8 +60,7 @@ export interface PanAPIWrapper {
|
||||
|
||||
export interface SelectionAPIWrapper {
|
||||
copyToClipboard: () => void;
|
||||
getSelectedText: () => string | any;
|
||||
getFormattedSelection: () => any;
|
||||
getFormattedSelection: () => FormattedSelection[];
|
||||
selectAll: (totalPages: number) => Promise<boolean>;
|
||||
selectWordAt: (pageIndex: number, x: number, y: number) => boolean;
|
||||
}
|
||||
@@ -79,7 +80,7 @@ export interface RotationAPIWrapper {
|
||||
}
|
||||
|
||||
export interface SearchAPIWrapper {
|
||||
search: (query: string) => Promise<any>;
|
||||
search: (query: string) => Promise<unknown>;
|
||||
clear: () => void;
|
||||
next: () => void;
|
||||
previous: () => void;
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
*/
|
||||
export function getApiBaseUrl(): string {
|
||||
// Runtime override to fix hardcoded localhost in builds
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
(window as any).STIRLING_PDF_API_BASE_URL
|
||||
) {
|
||||
return (window as any).STIRLING_PDF_API_BASE_URL;
|
||||
if (typeof window !== "undefined") {
|
||||
const override = window.STIRLING_PDF_API_BASE_URL;
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
}
|
||||
|
||||
return import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface AuditEvent {
|
||||
eventType: string;
|
||||
username: string;
|
||||
ipAddress: string;
|
||||
details: Record<string, any>;
|
||||
details: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AuditEventsResponse {
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface AutomationConfig {
|
||||
description?: string;
|
||||
operations: Array<{
|
||||
operation: string;
|
||||
parameters: any;
|
||||
parameters: Record<string, unknown>;
|
||||
}>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ProcessingConfig,
|
||||
ProcessingMetrics,
|
||||
} from "@app/types/processing";
|
||||
import type { PDFPageProxy } from "pdfjs-dist";
|
||||
import { ProcessingCache } from "@app/services/processingCache";
|
||||
import { FileHasher } from "@app/utils/fileHash";
|
||||
import { FileAnalyzer } from "@app/services/fileAnalyzer";
|
||||
@@ -415,7 +416,7 @@ export class EnhancedPDFProcessingService {
|
||||
* Render a page thumbnail with specified quality
|
||||
*/
|
||||
private async renderPageThumbnail(
|
||||
page: any,
|
||||
page: PDFPageProxy,
|
||||
quality: "low" | "medium" | "high",
|
||||
): Promise<string> {
|
||||
const scales = { low: 0.2, medium: 0.5, high: 0.8 }; // Reduced low quality for page editor
|
||||
@@ -431,7 +432,7 @@ export class EnhancedPDFProcessingService {
|
||||
throw new Error("Could not get canvas context");
|
||||
}
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||
return canvas.toDataURL("image/jpeg", 0.8); // Use JPEG for better compression
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export const FILE_EVENTS = {
|
||||
const UUID_REGEX =
|
||||
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
|
||||
|
||||
export function tryParseJson<T = any>(input: unknown): T | undefined {
|
||||
export function tryParseJson<T = unknown>(input: unknown): T | undefined {
|
||||
if (typeof input !== "string") return input as T | undefined;
|
||||
try {
|
||||
return JSON.parse(input) as T;
|
||||
@@ -14,19 +14,20 @@ export function tryParseJson<T = any>(input: unknown): T | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
export async function normalizeAxiosErrorData(data: any): Promise<any> {
|
||||
export async function normalizeAxiosErrorData(data: unknown): Promise<unknown> {
|
||||
if (!data) return undefined;
|
||||
if (typeof data?.text === "function") {
|
||||
const text = await data.text();
|
||||
const blobLike = data as { text?: () => Promise<string> };
|
||||
if (typeof blobLike.text === "function") {
|
||||
const text = await blobLike.text();
|
||||
return tryParseJson(text) ?? text;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function extractErrorFileIds(payload: any): string[] | undefined {
|
||||
export function extractErrorFileIds(payload: unknown): string[] | undefined {
|
||||
if (!payload) return undefined;
|
||||
if (Array.isArray(payload?.errorFileIds))
|
||||
return payload.errorFileIds as string[];
|
||||
const errorFileIds = (payload as { errorFileIds?: unknown }).errorFileIds;
|
||||
if (Array.isArray(errorFileIds)) return errorFileIds as string[];
|
||||
if (typeof payload === "string") {
|
||||
const matches = payload.match(UUID_REGEX);
|
||||
if (matches && matches.length > 0) return Array.from(new Set(matches));
|
||||
@@ -45,11 +46,11 @@ export function isZeroByte(
|
||||
file: File | { size?: number } | null | undefined,
|
||||
): boolean {
|
||||
if (!file) return true;
|
||||
const size = (file as any).size;
|
||||
const size = file.size;
|
||||
return typeof size === "number" ? size <= 0 : true;
|
||||
}
|
||||
|
||||
export function isEmptyOutput(files: File[] | null | undefined): boolean {
|
||||
if (!files || files.length === 0) return true;
|
||||
return files.every((f) => (f as any)?.size === 0);
|
||||
return files.every((f) => f?.size === 0);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("legacyDerivedFromTool — IndexedDB backfill for pre-upgrade files", (
|
||||
it("flags a legacy versioned edit (has tool history)", () => {
|
||||
expect(
|
||||
legacyDerivedFromTool(
|
||||
record({ toolHistory: [{ toolId: "compress" as any, timestamp: 0 }] }),
|
||||
record({ toolHistory: [{ toolId: "compress", timestamp: 0 }] }),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ export async function reconcileServerFiles(
|
||||
{
|
||||
suppressErrorToast: true,
|
||||
skipAuthRedirect: true,
|
||||
} as any,
|
||||
},
|
||||
);
|
||||
const serverFiles = Array.isArray(response.data) ? response.data : [];
|
||||
const serverMap = new Map<number, StoredFileResponse>();
|
||||
@@ -280,7 +280,7 @@ export async function reconcileServerFiles(
|
||||
try {
|
||||
const response = await apiClient.get<AccessedShareLinkResponse[]>(
|
||||
"/api/v1/storage/share-links/accessed",
|
||||
{ suppressErrorToast: true, skipAuthRedirect: true } as any,
|
||||
{ suppressErrorToast: true, skipAuthRedirect: true },
|
||||
);
|
||||
const sharedLinks = Array.isArray(response.data) ? response.data : [];
|
||||
const allowed = new Set(
|
||||
@@ -426,7 +426,7 @@ export async function materializeServerStubs(
|
||||
responseType: "blob",
|
||||
suppressErrorToast: true,
|
||||
skipAuthRedirect: true,
|
||||
} as any);
|
||||
});
|
||||
const rawHeaders = (response.headers ?? {}) as Record<string, unknown> & {
|
||||
get?: (name: string) => string | null;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
import { loadScript } from "@app/utils/scriptLoader";
|
||||
import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex";
|
||||
import { AppConfig } from "@app/types/appConfig";
|
||||
|
||||
// The GIS token client lets you reassign `callback` after init (to resolve the
|
||||
// per-request promise), but @types/google.accounts only models it on the config.
|
||||
type TokenClientWithCallback = google.accounts.oauth2.TokenClient & {
|
||||
callback: (response: google.accounts.oauth2.TokenResponse) => void;
|
||||
};
|
||||
|
||||
const SCOPES = "https://www.googleapis.com/auth/drive.readonly";
|
||||
const SESSION_STORAGE_ID = "googleDrivePickerAccessToken";
|
||||
@@ -59,7 +66,7 @@ function fileInputToGooglePickerMimeTypes(accept?: string): string | null {
|
||||
|
||||
class GoogleDrivePickerService {
|
||||
private config: GoogleDriveConfig | null = null;
|
||||
private tokenClient: any = null;
|
||||
private tokenClient: TokenClientWithCallback | null = null;
|
||||
private accessToken: string | null = null;
|
||||
private gapiLoaded = false;
|
||||
private gisLoaded = false;
|
||||
@@ -119,7 +126,7 @@ class GoogleDrivePickerService {
|
||||
client_id: this.config.clientId,
|
||||
scope: SCOPES,
|
||||
callback: () => {}, // Will be overridden during picker creation
|
||||
});
|
||||
}) as TokenClientWithCallback;
|
||||
|
||||
this.gisLoaded = true;
|
||||
}
|
||||
@@ -149,7 +156,9 @@ class GoogleDrivePickerService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tokenClient.callback = (response: any) => {
|
||||
this.tokenClient.callback = (
|
||||
response: google.accounts.oauth2.TokenResponse,
|
||||
) => {
|
||||
if (response.error !== undefined) {
|
||||
reject(new Error(response.error));
|
||||
return;
|
||||
@@ -201,7 +210,9 @@ class GoogleDrivePickerService {
|
||||
.setOAuthToken(this.accessToken)
|
||||
.addView(view1)
|
||||
.addView(view2)
|
||||
.setCallback((data: any) => this.pickerCallback(data, resolve, reject));
|
||||
.setCallback((data: google.picker.ResponseObject) =>
|
||||
this.pickerCallback(data, resolve, reject),
|
||||
);
|
||||
|
||||
(builder as unknown as { setZIndex(z: number): void }).setZIndex(
|
||||
Z_INDEX_OVER_FILE_MANAGER_MODAL,
|
||||
@@ -220,37 +231,39 @@ class GoogleDrivePickerService {
|
||||
* Handle picker selection callback
|
||||
*/
|
||||
private async pickerCallback(
|
||||
data: any,
|
||||
data: google.picker.ResponseObject,
|
||||
resolve: (files: File[]) => void,
|
||||
reject: (error: Error) => void,
|
||||
): Promise<void> {
|
||||
if (data.action === window.google.picker.Action.PICKED) {
|
||||
const action = data[window.google.picker.Response.ACTION];
|
||||
if (action === window.google.picker.Action.PICKED) {
|
||||
try {
|
||||
const documents = data[window.google.picker.Response.DOCUMENTS] ?? [];
|
||||
const files = await Promise.all(
|
||||
data[window.google.picker.Response.DOCUMENTS].map(
|
||||
async (pickedFile: any) => {
|
||||
const fileId = pickedFile[window.google.picker.Document.ID];
|
||||
const res = await window.gapi.client.drive.files.get({
|
||||
fileId: fileId,
|
||||
alt: "media",
|
||||
});
|
||||
documents.map(async (pickedFile) => {
|
||||
const fileId = pickedFile[window.google.picker.Document.ID];
|
||||
const res = await window.gapi.client.drive.files.get({
|
||||
fileId: fileId,
|
||||
alt: "media",
|
||||
});
|
||||
|
||||
// Convert response body to File object
|
||||
const file = new File(
|
||||
[
|
||||
new Uint8Array(res.body.length).map((_: any, i: number) =>
|
||||
res.body.charCodeAt(i),
|
||||
),
|
||||
],
|
||||
pickedFile.name,
|
||||
{
|
||||
type: pickedFile.mimeType,
|
||||
lastModified: pickedFile.lastModified,
|
||||
},
|
||||
);
|
||||
return file;
|
||||
},
|
||||
),
|
||||
// Convert response body to File object
|
||||
const file = new File(
|
||||
[
|
||||
new Uint8Array(res.body.length).map((_, i) =>
|
||||
res.body.charCodeAt(i),
|
||||
),
|
||||
],
|
||||
pickedFile[window.google.picker.Document.NAME] ?? "",
|
||||
{
|
||||
type: pickedFile[window.google.picker.Document.MIME_TYPE],
|
||||
lastModified:
|
||||
pickedFile[window.google.picker.Document.LAST_EDITED_UTC] ??
|
||||
Date.now(),
|
||||
},
|
||||
);
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
|
||||
resolve(files);
|
||||
@@ -261,7 +274,7 @@ class GoogleDrivePickerService {
|
||||
: new Error("Failed to download files"),
|
||||
);
|
||||
}
|
||||
} else if (data.action === window.google.picker.Action.CANCEL) {
|
||||
} else if (action === window.google.picker.Action.CANCEL) {
|
||||
resolve([]); // User cancelled, return empty array
|
||||
}
|
||||
}
|
||||
@@ -369,7 +382,7 @@ export function getGoogleDriveConfig(
|
||||
* Eliminates duplicated config construction pattern
|
||||
*/
|
||||
export function extractGoogleDriveBackendConfig(
|
||||
appConfig: any,
|
||||
appConfig: AppConfig | null,
|
||||
): BackendGoogleDriveConfig {
|
||||
return {
|
||||
enabled: appConfig?.googleDriveEnabled,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
normalizeAxiosErrorData,
|
||||
} from "@app/services/errorUtils";
|
||||
import { showSpecialErrorToast } from "@app/services/specialErrorToasts";
|
||||
import axios from "axios";
|
||||
import { handleSaaSError } from "@app/services/saasErrorInterceptor";
|
||||
import {
|
||||
clampText,
|
||||
@@ -95,15 +96,16 @@ if (typeof window !== "undefined") {
|
||||
* Handles HTTP errors with toast notifications and file error broadcasting
|
||||
* Returns true if the error should be suppressed (deduplicated), false otherwise
|
||||
*/
|
||||
export async function handleHttpError(error: any): Promise<boolean> {
|
||||
const skipAuthRedirect = error?.config?.skipAuthRedirect === true;
|
||||
export async function handleHttpError(error: unknown): Promise<boolean> {
|
||||
const axiosError = axios.isAxiosError(error) ? error : undefined;
|
||||
const skipAuthRedirect = axiosError?.config?.skipAuthRedirect === true;
|
||||
// Check if this error should skip the global toast (component will handle it)
|
||||
if (error?.config?.suppressErrorToast === true) {
|
||||
if (axiosError?.config?.suppressErrorToast === true) {
|
||||
return false; // Don't show global toast, but continue rejection
|
||||
}
|
||||
|
||||
// Handle 401 authentication errors
|
||||
const status: number | undefined = error?.response?.status;
|
||||
const status: number | undefined = axiosError?.response?.status;
|
||||
if (status === 401) {
|
||||
const pathname = window.location.pathname;
|
||||
|
||||
@@ -119,7 +121,7 @@ export async function handleHttpError(error: any): Promise<boolean> {
|
||||
if (loginRedirectRecentlyFired()) {
|
||||
console.warn(
|
||||
"[httpErrorHandler] 401 redirect already fired moments ago — suppressing repeat to avoid a login loop:",
|
||||
error?.config?.url,
|
||||
axiosError?.config?.url,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -151,7 +153,7 @@ export async function handleHttpError(error: any): Promise<boolean> {
|
||||
const { title, body } = extractAxiosErrorMessage(error);
|
||||
|
||||
// Normalize response data ONCE, reuse for both ID extraction and special-toast matching
|
||||
const raw = error?.response?.data as any;
|
||||
const raw = axiosError?.response?.data;
|
||||
let normalized: unknown = raw;
|
||||
try {
|
||||
normalized = await normalizeAxiosErrorData(raw);
|
||||
@@ -170,7 +172,7 @@ export async function handleHttpError(error: any): Promise<boolean> {
|
||||
}
|
||||
|
||||
// 2) Generic-vs-special dedupe by endpoint
|
||||
const url: string | undefined = error?.config?.url;
|
||||
const url: string | undefined = axiosError?.config?.url;
|
||||
const now = Date.now();
|
||||
const isSpecial =
|
||||
status === 422 ||
|
||||
|
||||
@@ -25,14 +25,14 @@ function titleForStatus(status?: number): string {
|
||||
return "Request failed";
|
||||
}
|
||||
|
||||
export function extractAxiosErrorMessage(error: any): {
|
||||
export function extractAxiosErrorMessage(error: unknown): {
|
||||
title: string;
|
||||
body: string;
|
||||
} {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
const _statusText = error.response?.statusText || "";
|
||||
let parsed: any = undefined;
|
||||
let parsed: unknown = undefined;
|
||||
const raw = error.response?.data;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
@@ -44,8 +44,8 @@ export function extractAxiosErrorMessage(error: any): {
|
||||
parsed = raw;
|
||||
}
|
||||
const extractIds = (): string[] | undefined => {
|
||||
if (Array.isArray(parsed?.errorFileIds))
|
||||
return parsed.errorFileIds as string[];
|
||||
const errorFileIds = (parsed as { errorFileIds?: unknown })?.errorFileIds;
|
||||
if (Array.isArray(errorFileIds)) return errorFileIds as string[];
|
||||
const rawText = typeof raw === "string" ? raw : "";
|
||||
const uuidMatches = rawText.match(
|
||||
/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g,
|
||||
@@ -60,7 +60,8 @@ export function extractAxiosErrorMessage(error: any): {
|
||||
if (!data) return typeof raw === "string" ? raw : "";
|
||||
const ids = extractIds();
|
||||
if (ids && ids.length > 0) return `Failed files: ${ids.join(", ")}`;
|
||||
if (data?.message) return data.message as string;
|
||||
const message = (data as { message?: unknown })?.message;
|
||||
if (message) return message as string;
|
||||
if (typeof raw === "string") return raw;
|
||||
try {
|
||||
return JSON.stringify(data);
|
||||
@@ -82,7 +83,8 @@ export function extractAxiosErrorMessage(error: any): {
|
||||
return { title, body: bodyMsg };
|
||||
}
|
||||
try {
|
||||
const msg = (error?.message || String(error)) as string;
|
||||
const msg = ((error as { message?: unknown })?.message ||
|
||||
String(error)) as string;
|
||||
return {
|
||||
title: "Network error",
|
||||
body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg,
|
||||
|
||||
@@ -38,7 +38,7 @@ class PDFWorkerManager {
|
||||
"pdfjs-dist/legacy/build/pdf.worker.min.mjs",
|
||||
import.meta.url,
|
||||
).toString();
|
||||
(GlobalWorkerOptions as any).docBaseUrl = undefined;
|
||||
(GlobalWorkerOptions as { docBaseUrl?: string }).docBaseUrl = undefined;
|
||||
this.isInitialized = true;
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class PDFWorkerManager {
|
||||
}
|
||||
|
||||
// Normalize input data to PDF.js format
|
||||
let pdfData: any;
|
||||
let pdfData: string | { data: ArrayBuffer | Uint8Array };
|
||||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
||||
pdfData = { data };
|
||||
} else if (typeof data === "string") {
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
* without needing to make API calls
|
||||
*/
|
||||
|
||||
// PDF.js types (simplified)
|
||||
declare global {
|
||||
interface Window {
|
||||
pdfjsLib?: any;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SignatureDetectionResult {
|
||||
hasSignatures: boolean;
|
||||
signatureCount?: number;
|
||||
@@ -53,7 +46,7 @@ const detectSignaturesInFile = async (
|
||||
|
||||
// Count signature annotations (Type: /Sig)
|
||||
const signatureAnnotations = annotations.filter(
|
||||
(annotation: any) =>
|
||||
(annotation: { subtype?: string; fieldType?: string }) =>
|
||||
annotation.subtype === "Widget" && annotation.fieldType === "Sig",
|
||||
);
|
||||
|
||||
@@ -62,7 +55,11 @@ const detectSignaturesInFile = async (
|
||||
|
||||
// Also check for document-level signatures in AcroForm
|
||||
const metadata = await pdf.getMetadata();
|
||||
if (metadata?.info?.Signature || metadata?.metadata?.has("dc:signature")) {
|
||||
const info = metadata?.info as { Signature?: unknown } | undefined;
|
||||
const xmpMetadata = metadata?.metadata as
|
||||
| { has?: (name: string) => boolean }
|
||||
| undefined;
|
||||
if (info?.Signature || xmpMetadata?.has?.("dc:signature")) {
|
||||
totalSignatures = Math.max(totalSignatures, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import axios from "axios";
|
||||
import apiClient from "@app/services/apiClient";
|
||||
import type { SavedSignature } from "@app/types/signature";
|
||||
import { readResponseHeader } from "@app/services/shareBundleUtils";
|
||||
@@ -54,14 +55,17 @@ class SignatureStorageService {
|
||||
supportsBackend: true,
|
||||
storageType: "backend",
|
||||
};
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const status = axios.isAxiosError(error)
|
||||
? error.response?.status
|
||||
: undefined;
|
||||
// Check if it's an HTTP error with status code
|
||||
if (error?.response?.status === 401 || error?.response?.status === 403) {
|
||||
if (status === 401 || status === 403) {
|
||||
// Backend exists but needs auth - gracefully fall back to localStorage
|
||||
console.log(
|
||||
"[SignatureStorage] Backend signature API requires authentication, using localStorage",
|
||||
);
|
||||
} else if (error?.response?.status === 404) {
|
||||
} else if (status === 404) {
|
||||
// Endpoint doesn't exist (not running proprietary mode)
|
||||
console.log(
|
||||
"[SignatureStorage] Backend signature API not available (not in proprietary mode), using localStorage",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18n from "i18next";
|
||||
import { alert } from "@app/components/toast";
|
||||
|
||||
interface ErrorToastMapping {
|
||||
@@ -42,18 +43,13 @@ export function showSpecialErrorToast(
|
||||
|
||||
for (const mapping of MAPPINGS) {
|
||||
if (mapping.regex.test(message)) {
|
||||
// Best-effort translation without hard dependency on i18n config
|
||||
let body = mapping.defaultMessage;
|
||||
try {
|
||||
const anyGlobal: any = globalThis as any;
|
||||
const i18next = anyGlobal?.i18next;
|
||||
if (i18next && typeof i18next.t === "function") {
|
||||
body = i18next.t(mapping.i18nKey, {
|
||||
defaultValue: mapping.defaultMessage,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* ignore translation errors */
|
||||
// The app bootstraps this shared i18next singleton at startup; guard in
|
||||
// case a toast fires before that (e.g. tests) so we keep the default copy.
|
||||
if (i18n.isInitialized) {
|
||||
body = i18n.t(mapping.i18nKey, {
|
||||
defaultValue: mapping.defaultMessage,
|
||||
});
|
||||
}
|
||||
const title = titleForStatus(options?.status);
|
||||
alert({
|
||||
|
||||
@@ -25,7 +25,7 @@ const usageAnalyticsService = {
|
||||
limit?: number,
|
||||
dataType: "all" | "api" | "ui" = "all",
|
||||
): Promise<EndpointStatisticsResponse> {
|
||||
const params: Record<string, any> = {};
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
if (limit !== undefined) {
|
||||
params.limit = limit;
|
||||
|
||||
Vendored
+1
@@ -22,6 +22,7 @@ declare global {
|
||||
__STIRLING_PDF_BASE_URL__?: string;
|
||||
STIRLING_PDF_API_BASE_URL?: string;
|
||||
endpointAvailabilityService?: unknown;
|
||||
pdfjsLib?: typeof import("pdfjs-dist");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,15 +74,14 @@ const modernGlobals: OxlintGlobals = {
|
||||
|
||||
// Folders not yet conformant to the stricter no-explicit-any rule
|
||||
const noExplicitAnyExcludes = [
|
||||
"editor/src/core/components/shared/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/components/shared/FilePickerModal.tsx",
|
||||
"editor/src/core/components/shared/config/configSections/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/components/tools/addStamp/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/components/tools/automate/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/components/viewer/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/contexts/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/contexts/viewer/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/hooks/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/services/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/services/pdfProcessingService.ts",
|
||||
"editor/src/core/services/zipFileService.ts",
|
||||
"editor/src/core/tools/annotate/useAnnotationSelection.ts",
|
||||
"editor/src/core/types/*.{js,mjs,jsx,ts,tsx}",
|
||||
"editor/src/core/utils/*.{js,mjs,jsx,ts,tsx}",
|
||||
|
||||
Reference in New Issue
Block a user