Remove a bunch of unnecessary casts from the frontend (#7662)

# Description of Changes
Originally, I wanted to re-enable typed linting on our repo but using
Oxlint this time to avoid the memory and speed issues that ESLint was
causing. Unfortunately, it's not stable enough yet to actually use on
our repo (although it is close, I suspect it'll be stable enough fairly
soon). I was able to remove many of the unnecessary casts that it found
though, so even though this won't be enforced, it's still worth cleaning
up what I've found.
This commit is contained in:
James Brunton
2026-08-26 14:09:55 +00:00
committed by GitHub
parent f945cc7dc6
commit c93feb5dfc
100 changed files with 217 additions and 345 deletions
@@ -360,11 +360,9 @@ const TeamSection: React.FC = () => {
verticalSpacing="sm" verticalSpacing="sm"
withRowBorders withRowBorders
highlightOnHover highlightOnHover
style={ style={{
{ "--table-border-color": "var(--mantine-color-gray-3)",
"--table-border-color": "var(--mantine-color-gray-3)", }}
} as React.CSSProperties
}
> >
<Table.Thead> <Table.Thead>
<Table.Tr <Table.Tr
@@ -336,7 +336,7 @@ const FileEditor = ({
(fileId: FileId) => { (fileId: FileId) => {
const index = stubsRef.current.findIndex((r) => r.id === fileId); const index = stubsRef.current.findIndex((r) => r.id === fileId);
if (index !== -1) { if (index !== -1) {
setActiveFileId(fileId as string); setActiveFileId(fileId);
setActiveFileIndex(index); setActiveFileIndex(index);
navActions.setWorkbench("viewer"); navActions.setWorkbench("viewer");
} }
@@ -410,10 +410,7 @@ const FileEditor = ({
onUnzipFile={handleUnzipFile} onUnzipFile={handleUnzipFile}
toolMode={toolMode} toolMode={toolMode}
isSupported={isFileSupported(record.name)} isSupported={isFileSupported(record.name)}
policies={ policies={policyFileBadges.get(record.id) ?? EMPTY_POLICIES}
policyFileBadges.get(record.id as string) ??
EMPTY_POLICIES
}
/> />
); );
})} })}
@@ -140,7 +140,7 @@ export function FileDetailsPanel({
return null; return null;
} }
const single = files.length === 1 ? files[0]! : null; const single = files.length === 1 ? files[0] : null;
const totalSize = files.reduce((sum, f) => sum + f.size, 0); const totalSize = files.reduce((sum, f) => sum + f.size, 0);
const ext = single ? (single.name.split(".").pop() ?? "").toUpperCase() : ""; const ext = single ? (single.name.split(".").pop() ?? "").toUpperCase() : "";
// Files still needing a server upload; drives Save-to-server visibility. // Files still needing a server upload; drives Save-to-server visibility.
@@ -422,7 +422,7 @@ function GridView(props: FileGridProps) {
parentPath={entry.parentPath} parentPath={entry.parentPath}
isSelected={selectedFileIds.has(entry.file.id)} isSelected={selectedFileIds.has(entry.file.id)}
isInWorkspace={ isInWorkspace={
activeWorkspaceFileIds?.has(entry.file.id as string) ?? false activeWorkspaceFileIds?.has(entry.file.id) ?? false
} }
selectedFileIds={selectedFileIds} selectedFileIds={selectedFileIds}
multiSelectActive={selectedFileIds.size >= 2} multiSelectActive={selectedFileIds.size >= 2}
@@ -938,7 +938,7 @@ function FileCard({
shiftKey: false, shiftKey: false,
ctrlKey: true, ctrlKey: true,
metaKey: true, metaKey: true,
} as unknown as React.MouseEvent); });
}} }}
onChange={() => { onChange={() => {
/* handled by onClick */ /* handled by onClick */
@@ -982,7 +982,7 @@ function FileCard({
· ·
</span> </span>
<span>{fileDate}</span> <span>{fileDate}</span>
<PolicyBadges fileId={file.id as string} /> <PolicyBadges fileId={file.id} />
</div> </div>
</div> </div>
<div className="files-page-card-actions"> <div className="files-page-card-actions">
@@ -1137,7 +1137,7 @@ function ListView(
parentPath={entry.parentPath} parentPath={entry.parentPath}
isSelected={selectedFileIds.has(entry.file.id)} isSelected={selectedFileIds.has(entry.file.id)}
isInWorkspace={ isInWorkspace={
activeWorkspaceFileIds?.has(entry.file.id as string) ?? false activeWorkspaceFileIds?.has(entry.file.id) ?? false
} }
selectedFileIds={selectedFileIds} selectedFileIds={selectedFileIds}
multiSelectActive={selectedFileIds.size >= 2} multiSelectActive={selectedFileIds.size >= 2}
@@ -1424,7 +1424,7 @@ function FileRow({
shiftKey: false, shiftKey: false,
ctrlKey: true, ctrlKey: true,
metaKey: true, metaKey: true,
} as unknown as React.MouseEvent); });
}} }}
onChange={() => { onChange={() => {
/* handled by onClick */ /* handled by onClick */
@@ -1491,7 +1491,7 @@ function FileRow({
)} )}
</span> </span>
<FileOriginBadge origin={getFileOrigin(file)} compact /> <FileOriginBadge origin={getFileOrigin(file)} compact />
<PolicyBadges fileId={file.id as string} /> <PolicyBadges fileId={file.id} />
{isInWorkspace && ( {isInWorkspace && (
<span className="files-page-row-open-pill"> <span className="files-page-row-open-pill">
<span className="files-page-card-open-dot" /> <span className="files-page-card-open-dot" />
@@ -474,7 +474,7 @@ export default function FileManagerView() {
if (idx >= 0 && lastIdx >= 0) { if (idx >= 0 && lastIdx >= 0) {
const [a, b] = idx < lastIdx ? [idx, lastIdx] : [lastIdx, idx]; const [a, b] = idx < lastIdx ? [idx, lastIdx] : [lastIdx, idx];
for (let i = a; i <= b; i += 1) { for (let i = a; i <= b; i += 1) {
next.add(visibleFiles[i]!.id); next.add(visibleFiles[i].id);
} }
return next; return next;
} }
@@ -593,7 +593,7 @@ export default function FileManagerView() {
}); });
// Branch on requested stubs so already-active files still activate. // Branch on requested stubs so already-active files still activate.
if (materialized.length === 1) { if (materialized.length === 1) {
setActiveFileId(materialized[0]!.id); setActiveFileId(materialized[0].id);
navActions.setWorkbench("viewer"); navActions.setWorkbench("viewer");
} else if (materialized.length > 1) { } else if (materialized.length > 1) {
navActions.setWorkbench("fileEditor"); navActions.setWorkbench("fileEditor");
@@ -1172,7 +1172,7 @@ export default function FileManagerView() {
else if (e.key === "End") next = TAB_DEFS.length - 1; else if (e.key === "End") next = TAB_DEFS.length - 1;
else return; else return;
e.preventDefault(); e.preventDefault();
const target = TAB_DEFS[next]!; const target = TAB_DEFS[next];
setCurrentTab(target.id); setCurrentTab(target.id);
focusTab(target.id); focusTab(target.id);
}} }}
@@ -1602,7 +1602,7 @@ export default function FileManagerView() {
) )
) )
return; return;
setViewMode(v as (typeof FILES_PAGE_VIEW_MODES)[number]); setViewMode(v);
}} }}
aria-label={t("filesPage.viewMode.label", "View mode")} aria-label={t("filesPage.viewMode.label", "View mode")}
options={[ options={[
@@ -120,14 +120,14 @@ export function VersionTimeline({
}; };
const rows: Row[] = useMemo(() => { const rows: Row[] = useMemo(() => {
if (!collapsible || showAllCollapsed) { if (!collapsible || showAllCollapsed) {
return ordered.map((v) => ({ kind: "version", version: v }) as Row); return ordered.map<Row>((v) => ({ kind: "version", version: v }));
} }
const head = ordered const head = ordered
.slice(0, 3) .slice(0, 3)
.map((v) => ({ kind: "version", version: v }) as Row); .map<Row>((v) => ({ kind: "version", version: v }));
const tail = ordered const tail = ordered
.slice(-2) .slice(-2)
.map((v) => ({ kind: "version", version: v }) as Row); .map<Row>((v) => ({ kind: "version", version: v }));
const hidden = ordered.length - 5; const hidden = ordered.length - 5;
return [...head, { kind: "ellipsis", hidden }, ...tail]; return [...head, { kind: "ellipsis", hidden }, ...tail];
}, [collapsible, showAllCollapsed, ordered]); }, [collapsible, showAllCollapsed, ordered]);
@@ -27,7 +27,7 @@ function depthOf(
let cursor: FolderRecord | undefined = folder; let cursor: FolderRecord | undefined = folder;
while (cursor && cursor.parentFolderId) { while (cursor && cursor.parentFolderId) {
depth += 1; depth += 1;
cursor = byId.get(cursor.parentFolderId as string); cursor = byId.get(cursor.parentFolderId);
if (depth > 50) break; if (depth > 50) break;
} }
return depth; return depth;
@@ -139,9 +139,9 @@ export const MobileDrawCanvas = forwardRef<
// where the per-frame synthetic event alone would drop curvature. // where the per-frame synthetic event alone would drop curvature.
const events = const events =
"getCoalescedEvents" in e.nativeEvent "getCoalescedEvents" in e.nativeEvent
? (e.nativeEvent as PointerEvent).getCoalescedEvents() ? e.nativeEvent.getCoalescedEvents()
: [e.nativeEvent as PointerEvent]; : [e.nativeEvent as PointerEvent];
const rect = (e.currentTarget as HTMLCanvasElement).getBoundingClientRect(); const rect = e.currentTarget.getBoundingClientRect();
for (const ev of events) { for (const ev of events) {
stroke.points.push({ stroke.points.push({
x: ev.clientX - rect.left, x: ev.clientX - rect.left,
@@ -20,7 +20,6 @@ import {
import { useOnboardingDownload } from "@app/components/onboarding/useOnboardingDownload"; import { useOnboardingDownload } from "@app/components/onboarding/useOnboardingDownload";
import { import {
SLIDE_DEFINITIONS, SLIDE_DEFINITIONS,
type SlideId,
type ButtonAction, type ButtonAction,
} from "@app/components/onboarding/onboardingFlowConfig"; } from "@app/components/onboarding/onboardingFlowConfig";
import ToolPanelModePrompt from "@app/components/tools/ToolPanelModePrompt"; import ToolPanelModePrompt from "@app/components/tools/ToolPanelModePrompt";
@@ -322,7 +321,7 @@ export default function Onboarding() {
) { ) {
return null; return null;
} }
return SLIDE_DEFINITIONS[currentStep.slideId as SlideId]; return SLIDE_DEFINITIONS[currentStep.slideId];
}, [currentStep]); }, [currentStep]);
const currentSlideContent = useMemo(() => { const currentSlideContent = useMemo(() => {
@@ -244,7 +244,7 @@ export class ReorderPagesCommand extends DOMCommand {
.map((pageNum) => .map((pageNum) =>
currentDoc.pages.find((p) => p.pageNumber === pageNum), currentDoc.pages.find((p) => p.pageNumber === pageNum),
) )
.filter((page) => page !== undefined) as PDFPage[]; .filter((page) => page !== undefined);
const remainingPages = currentDoc.pages.filter( const remainingPages = currentDoc.pages.filter(
(page) => !this.selectedPages!.includes(page.pageNumber), (page) => !this.selectedPages!.includes(page.pageNumber),
@@ -84,7 +84,7 @@ function renderSearch(
<MantineProvider> <MantineProvider>
<SuperSearch <SuperSearch
inputId="test-super-search" inputId="test-super-search"
useResults={useResults as TestUseResultsHook | undefined} useResults={useResults}
scopes={scopes} scopes={scopes}
/> />
</MantineProvider>, </MantineProvider>,
@@ -103,7 +103,7 @@ describe("SuperSearch", () => {
width: 320, width: 320,
height: 40, height: 40,
toJSON: () => "", toJSON: () => "",
} as DOMRect); });
Object.defineProperty(Element.prototype, "scrollIntoView", { Object.defineProperty(Element.prototype, "scrollIntoView", {
value: vi.fn(), value: vi.fn(),
@@ -19,7 +19,7 @@ export const SignatureTypeSelector: React.FC<SignatureTypeSelectorProps> = ({
return ( return (
<SegmentedControl <SegmentedControl
value={value} value={value}
onChange={(val) => onChange(val as SignatureType)} onChange={(val) => onChange(val)}
options={[ options={[
{ {
value: "draw", value: "draw",
@@ -93,7 +93,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
? true ? true
: false, : false,
createdAt: Date.now(), createdAt: Date.now(),
} as ToastInstance; };
setToasts((prev) => { setToasts((prev) => {
// Coalesce duplicates by alertType + title + body text if no explicit id was provided // Coalesce duplicates by alertType + title + body text if no explicit id was provided
if (!options.id) { if (!options.id) {
@@ -138,7 +138,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
...t, ...t,
...updates, ...updates,
progress, progress,
} as ToastInstance; };
// Detect completion but do not auto-flip to success. // Detect completion but do not auto-flip to success.
// Callers (e.g., compare workbench) explicitly set alertType when done. // Callers (e.g., compare workbench) explicitly set alertType when done.
@@ -197,9 +197,8 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
), ),
); );
}; };
window.addEventListener("toast:toggle", handler as EventListener); window.addEventListener("toast:toggle", handler);
return () => return () => window.removeEventListener("toast:toggle", handler);
window.removeEventListener("toast:toggle", handler as EventListener);
}, []); }, []);
return ( return (
@@ -108,7 +108,7 @@ const FullscreenToolList = ({
window.open(tool.link, "_blank", "noopener,noreferrer"); window.open(tool.link, "_blank", "noopener,noreferrer");
return; return;
} }
onSelect(id as ToolId); onSelect(id);
}; };
if (showDescriptions) { if (showDescriptions) {
@@ -274,15 +274,11 @@ const FullscreenToolList = ({
{showDescriptions ? ( {showDescriptions ? (
<div className="tool-panel__fullscreen-grid tool-panel__fullscreen-grid--detailed"> <div className="tool-panel__fullscreen-grid tool-panel__fullscreen-grid--detailed">
{tools.map(({ id, tool }) => {tools.map(({ id, tool }) => renderToolItem(id, tool))}
renderToolItem(id as ToolId, tool),
)}
</div> </div>
) : ( ) : (
<div className="tool-panel__fullscreen-list"> <div className="tool-panel__fullscreen-list">
{tools.map(({ id, tool }) => {tools.map(({ id, tool }) => renderToolItem(id, tool))}
renderToolItem(id as ToolId, tool),
)}
</div> </div>
)} )}
</section> </section>
@@ -108,7 +108,7 @@ export default function RightSidebar() {
const activeTool: ToolRegistryEntry | null = const activeTool: ToolRegistryEntry | null =
inToolView && selectedToolKey inToolView && selectedToolKey
? (toolRegistry[selectedToolKey as ToolId] ?? null) ? (toolRegistry[selectedToolKey] ?? null)
: null; : null;
const expandedWidth = "18.5rem"; const expandedWidth = "18.5rem";
@@ -131,7 +131,7 @@ export default function RightSidebar() {
const items: Array<{ id: ToolId; tool: ToolRegistryEntry }> = []; const items: Array<{ id: ToolId; tool: ToolRegistryEntry }> = [];
collapsedQuickSection.subcategories.forEach((sc: SubcategoryGroup) => collapsedQuickSection.subcategories.forEach((sc: SubcategoryGroup) =>
sc.tools.forEach((entry) => sc.tools.forEach((entry) =>
items.push({ id: entry.id as ToolId, tool: entry.tool }), items.push({ id: entry.id, tool: entry.tool }),
), ),
); );
return items; return items;
@@ -19,9 +19,7 @@ const ToolRenderer = ({
// Get the tool from context (instead of direct hook call) // Get the tool from context (instead of direct hook call)
const { toolRegistry } = useToolWorkflow(); const { toolRegistry } = useToolWorkflow();
const selectedTool = const selectedTool =
selectedToolKey in toolRegistry selectedToolKey in toolRegistry ? toolRegistry[selectedToolKey] : undefined;
? toolRegistry[selectedToolKey as ToolId]
: undefined;
// Handle tools that only work in workbenches (read, multiTool) // Handle tools that only work in workbenches (read, multiTool)
if (selectedTool && !selectedTool.component && selectedTool.workbench) { if (selectedTool && !selectedTool.component && selectedTool.workbench) {
@@ -261,12 +261,7 @@ export default function PageNumberPreview({
variant="tertiary" variant="tertiary"
key={idx} key={idx}
className={`${styles.gridTile} ${selected || hoverTile === idx ? styles.gridTileSelected : ""} ${hoverTile === idx ? styles.gridTileHovered : ""}`} className={`${styles.gridTile} ${selected || hoverTile === idx ? styles.gridTileSelected : ""} ${hoverTile === idx ? styles.gridTileHovered : ""}`}
onClick={() => onClick={() => onParameterChange("position", idx)}
onParameterChange(
"position",
idx as AddPageNumbersParameters["position"],
)
}
onMouseEnter={() => setHoverTile(idx)} onMouseEnter={() => setHoverTile(idx)}
onMouseLeave={() => setHoverTile(null)} onMouseLeave={() => setHoverTile(null)}
style={{ style={{
@@ -36,9 +36,7 @@ const WatermarkStyleSettings = ({
onChange={(value) => onChange={(value) =>
onParameterChange( onParameterChange(
"rotation", "rotation",
typeof value === "number" typeof value === "number" ? value : parseInt(value, 10) || 0,
? value
: parseInt(value as string, 10) || 0,
) )
} }
min={-360} min={-360}
@@ -55,9 +53,7 @@ const WatermarkStyleSettings = ({
onChange={(value) => onChange={(value) =>
onParameterChange( onParameterChange(
"opacity", "opacity",
typeof value === "number" typeof value === "number" ? value : parseInt(value, 10) || 50,
? value
: parseInt(value as string, 10) || 50,
) )
} }
min={0} min={0}
@@ -77,9 +73,7 @@ const WatermarkStyleSettings = ({
onChange={(value) => onChange={(value) =>
onParameterChange( onParameterChange(
"widthSpacer", "widthSpacer",
typeof value === "number" typeof value === "number" ? value : parseInt(value, 10) || 50,
? value
: parseInt(value as string, 10) || 50,
) )
} }
min={0} min={0}
@@ -96,9 +90,7 @@ const WatermarkStyleSettings = ({
onChange={(value) => onChange={(value) =>
onParameterChange( onParameterChange(
"heightSpacer", "heightSpacer",
typeof value === "number" typeof value === "number" ? value : parseInt(value, 10) || 50,
? value
: parseInt(value as string, 10) || 50,
) )
} }
min={0} min={0}
@@ -103,9 +103,7 @@ const AdjustPageScaleSettings = ({
<SegmentedControl <SegmentedControl
aria-label={t("adjustPageScale.orientation.label", "Page orientation")} aria-label={t("adjustPageScale.orientation.label", "Page orientation")}
value={parameters.orientation} value={parameters.orientation}
onChange={(value) => onChange={(value) => onParameterChange("orientation", value)}
onParameterChange("orientation", value as Orientation)
}
options={orientationOptions} options={orientationOptions}
fullWidth fullWidth
/> />
@@ -238,9 +238,7 @@ const WetSignatureInput = ({
<SegmentedControl <SegmentedControl
value={signatureType} value={signatureType}
fullWidth fullWidth
onChange={(value) => onChange={(value) => handleSignatureTypeChange(value)}
handleSignatureTypeChange(value as SignatureType)
}
options={[ options={[
{ label: t("sign.type.canvas", "Draw"), value: "canvas", disabled }, { label: t("sign.type.canvas", "Draw"), value: "canvas", disabled },
{ label: t("sign.type.image", "Upload"), value: "image", disabled }, { label: t("sign.type.image", "Upload"), value: "image", disabled },
@@ -170,7 +170,7 @@ const ComparePixelWorkbenchView = ({
<SegmentedControl <SegmentedControl
size="sm" size="sm"
value={viewMode} value={viewMode}
onChange={(value) => setViewMode(value as PixelViewMode)} onChange={(value) => setViewMode(value)}
options={[ options={[
{ {
value: "side-by-side", value: "side-by-side",
@@ -28,7 +28,6 @@ import {
updateToastProgress, updateToastProgress,
dismissToast, dismissToast,
} from "@app/components/toast"; } from "@app/components/toast";
import type { ToastLocation } from "@app/components/toast/types";
interface CompareWorkbenchViewProps { interface CompareWorkbenchViewProps {
data: CompareWorkbenchData | null; data: CompareWorkbenchData | null;
@@ -323,7 +322,7 @@ const CompareTextWorkbenchView = ({ data }: CompareTextWorkbenchViewProps) => {
"At least one of these PDFs are very large, scrolling won't be smooth until the rendering is complete", "At least one of these PDFs are very large, scrolling won't be smooth until the rendering is complete",
), ),
body: `${countsText} ${t("compare.rendering.pagesRendered", "pages rendered")}`, body: `${countsText} ${t("compare.rendering.pagesRendered", "pages rendered")}`,
location: "bottom-right" as ToastLocation, location: "bottom-right",
isPersistentPopup: true, isPersistentPopup: true,
durationMs: 0, durationMs: 0,
expandable: false, expandable: false,
@@ -337,7 +336,7 @@ const CompareTextWorkbenchView = ({ data }: CompareTextWorkbenchViewProps) => {
"At least one of these PDFs are very large, scrolling won't be smooth until the rendering is complete", "At least one of these PDFs are very large, scrolling won't be smooth until the rendering is complete",
), ),
body: `${countsText} ${t("compare.rendering.pagesRendered", "pages rendered")}`, body: `${countsText} ${t("compare.rendering.pagesRendered", "pages rendered")}`,
location: "bottom-right" as ToastLocation, location: "bottom-right",
isPersistentPopup: true, isPersistentPopup: true,
alertType: "neutral", // ensure it stays neutral until completion alertType: "neutral", // ensure it stays neutral until completion
}); });
@@ -452,7 +451,7 @@ const CompareTextWorkbenchView = ({ data }: CompareTextWorkbenchViewProps) => {
"compare.rendering.pageNotReadyBody", "compare.rendering.pageNotReadyBody",
"Some pages are still rendering. Navigation will snap once they are ready.", "Some pages are still rendering. Navigation will snap once they are ready.",
), ),
location: "bottom-right" as ToastLocation, location: "bottom-right",
isPersistentPopup: false, isPersistentPopup: false,
durationMs: 2500, durationMs: 2500,
}); });
@@ -186,7 +186,7 @@ export const getFileFromSelection = (
): StirlingFile | null => { ): StirlingFile | null => {
if (explicit) return explicit; if (explicit) return explicit;
if (!fileId) return null; if (!fileId) return null;
return (selectors.getFile(fileId) as StirlingFile | undefined | null) ?? null; return selectors.getFile(fileId) ?? null;
}; };
export const getStubFromSelection = ( export const getStubFromSelection = (
@@ -79,7 +79,7 @@ export const useCompareChangeNavigation = (
const inner = anchor.closest( const inner = anchor.closest(
".compare-diff-page__inner", ".compare-diff-page__inner",
) as HTMLElement | null; ) as HTMLElement | null;
const topPercent = parseFloat((anchor as HTMLElement).style.top || "0"); const topPercent = parseFloat(anchor.style.top || "0");
if (pageEl && inner && !Number.isNaN(topPercent)) { if (pageEl && inner && !Number.isNaN(topPercent)) {
const innerRect = inner.getBoundingClientRect(); const innerRect = inner.getBoundingClientRect();
const innerHeight = Math.max(1, innerRect.height); const innerHeight = Math.max(1, innerRect.height);
@@ -156,9 +156,7 @@ export const useCompareChangeNavigation = (
".compare-diff-page", ".compare-diff-page",
) as HTMLElement | null; ) as HTMLElement | null;
const pageNumAttr = pageEl?.getAttribute("data-page-number"); const pageNumAttr = pageEl?.getAttribute("data-page-number");
const topPercent = parseFloat( const topPercent = parseFloat(anchor.style.top || "0");
(anchor as HTMLElement).style.top || "0",
);
if (pageNumAttr) { if (pageNumAttr) {
const peerPageEl = peer.querySelector( const peerPageEl = peer.querySelector(
`.compare-diff-page[data-page-number="${pageNumAttr}"]`, `.compare-diff-page[data-page-number="${pageNumAttr}"]`,
@@ -323,7 +323,7 @@ export const useComparePanZoom = ({
const pages = getPagesForPane(pane); const pages = getPagesForPane(pane);
const rotation = pages[0]?.rotation ?? 0; const rotation = pages[0]?.rotation ?? 0;
const normalized = ((rotation % 360) + 360) % 360; const normalized = ((rotation % 360) + 360) % 360;
return normalized as 0 | 90 | 180 | 270 | number; return normalized;
}, },
[getPagesForPane], [getPagesForPane],
); );
@@ -656,7 +656,7 @@ export const useComparePanZoom = ({
}; };
edgeOverscrollRef.current[pane] = 0; edgeOverscrollRef.current[pane] = 0;
lastActivePaneRef.current = pane; lastActivePaneRef.current = pane;
(container as HTMLDivElement).style.cursor = "grabbing"; container.style.cursor = "grabbing";
}, },
[isPanMode, baseZoom, comparisonZoom, basePan, comparisonPan], [isPanMode, baseZoom, comparisonZoom, basePan, comparisonPan],
); );
@@ -700,11 +700,7 @@ export const useComparePanZoom = ({
: comparisonScrollRef.current; : comparisonScrollRef.current;
if (sourceEl) { if (sourceEl) {
const zoom = drag.source === "base" ? baseZoom : comparisonZoom; const zoom = drag.source === "base" ? baseZoom : comparisonZoom;
(sourceEl as HTMLDivElement).style.cursor = isPanMode sourceEl.style.cursor = isPanMode ? (zoom > 1 ? "grab" : "auto") : "";
? zoom > 1
? "grab"
: "auto"
: "";
} }
panDragRef.current.active = false; panDragRef.current.active = false;
panDragRef.current.source = null; panDragRef.current.source = null;
@@ -3,7 +3,6 @@ import type React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon"; import LocalIcon from "@app/components/shared/LocalIcon";
import { alert } from "@app/components/toast"; import { alert } from "@app/components/toast";
import type { ToastLocation } from "@app/components/toast/types";
import type { WorkbenchBarButtonWithAction } from "@app/hooks/useWorkbenchBarButtons"; import type { WorkbenchBarButtonWithAction } from "@app/hooks/useWorkbenchBarButtons";
import { useIsMobile } from "@app/hooks/useIsMobile"; import { useIsMobile } from "@app/hooks/useIsMobile";
@@ -179,7 +178,7 @@ export const useCompareWorkbenchBarButtons = ({
"Tip: Arrow Up/Down scroll both panes when unlinked is off.", "Tip: Arrow Up/Down scroll both panes when unlinked is off.",
), ),
durationMs: 5000, durationMs: 5000,
location: "bottom-center" as ToastLocation, location: "bottom-center",
expandable: false, expandable: false,
}); });
} }
@@ -94,10 +94,10 @@ type Story = StoryObj<typeof meta>;
/** An available tool rendered in its default, unselected state. */ /** An available tool rendered in its default, unselected state. */
export const Default: Story = { export const Default: Story = {
render: () => <ToolItemDemo toolId={"split" as ToolId} />, render: () => <ToolItemDemo toolId={"split"} />,
}; };
/** The active tool in the panel — highlighted selected state. */ /** The active tool in the panel — highlighted selected state. */
export const Selected: Story = { export const Selected: Story = {
render: () => <ToolItemDemo toolId={"split" as ToolId} isSelected />, render: () => <ToolItemDemo toolId={"split"} isSelected />,
}; };
@@ -137,7 +137,7 @@ export default function OverlayPdfsSettings({
<SegmentedControl <SegmentedControl
value={String(parameters.overlayPosition)} value={String(parameters.overlayPosition)}
onChange={(v) => onChange={(v) =>
onParameterChange("overlayPosition", (v === "1" ? 1 : 0) as 0 | 1) onParameterChange("overlayPosition", v === "1" ? 1 : 0)
} }
options={[ options={[
{ {
@@ -279,9 +279,7 @@ const PdfTextEditorSidebar = ({ data }: PdfTextEditorSidebarProps) => {
</Text> </Text>
<SegmentedControl <SegmentedControl
value={externalGroupingMode} value={externalGroupingMode}
onChange={(value) => onChange={(value) => handleModeChangeRequest(value)}
handleModeChangeRequest(value as GroupingMode)
}
options={[ options={[
{ {
label: t("pdfTextEditor.groupingMode.auto", "Auto"), label: t("pdfTextEditor.groupingMode.auto", "Auto"),
@@ -12,7 +12,6 @@ import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology";
import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; import { useFileActionIcons } from "@app/hooks/useFileActionIcons";
import { saveOperationResults } from "@app/services/operationResultsSaveService"; import { saveOperationResults } from "@app/services/operationResultsSaveService";
import { useFileActions, useFileSelectors } from "@app/contexts/FileContext"; import { useFileActions, useFileSelectors } from "@app/contexts/FileContext";
import { FileId } from "@app/types/fileContext";
import i18n from "@app/i18n"; import i18n from "@app/i18n";
export interface ReviewToolStepProps<TParams = unknown> { export interface ReviewToolStepProps<TParams = unknown> {
@@ -65,11 +64,11 @@ function ReviewStepContent<TParams = unknown>({
downloadFilename: operation.downloadFilename || "download", downloadFilename: operation.downloadFilename || "download",
downloadLocalPath: operation.downloadLocalPath, downloadLocalPath: operation.downloadLocalPath,
outputFileIds: operation.outputFileIds, outputFileIds: operation.outputFileIds,
getFile: (fileId) => selectors.getFile(fileId as FileId), getFile: (fileId) => selectors.getFile(fileId),
getStub: (fileId) => selectors.getStirlingFileStub(fileId as FileId), getStub: (fileId) => selectors.getStirlingFileStub(fileId),
markSaved: (fileId, savedPath) => { markSaved: (fileId, savedPath) => {
const stub = selectors.getStirlingFileStub(fileId as FileId); const stub = selectors.getStirlingFileStub(fileId);
fileActions.updateStirlingFileStub(fileId as FileId, { fileActions.updateStirlingFileStub(fileId, {
localFilePath: stub?.localFilePath ?? savedPath, localFilePath: stub?.localFilePath ?? savedPath,
isDirty: false, isDirty: false,
}); });
@@ -186,7 +186,7 @@ export function tokenizeToLines(
} }
if (isStringDelimiter) { if (isStringDelimiter) {
startString(ch as '"' | "'" | "`"); startString(ch);
continue; continue;
} }
@@ -312,7 +312,7 @@ export function computeBlocks(
continue; continue;
} }
if (isStringDelimiter) { if (isStringDelimiter) {
startString(ch as '"' | "'" | "`"); startString(ch);
continue; continue;
} }
if (isOpenBrace) { if (isOpenBrace) {
@@ -52,9 +52,9 @@ function primeSession(
mockedApi.post.mockResolvedValue({ mockedApi.post.mockResolvedValue({
status: 200, status: 200,
data: SESSION_INFO, data: SESSION_INFO,
} as never); });
mockedApi.delete.mockResolvedValue({ status: 200 } as never); mockedApi.delete.mockResolvedValue({ status: 200 });
mockedApi.get.mockImplementation(((url: string, config?: unknown) => { mockedApi.get.mockImplementation((url: string, config?: unknown) => {
if (url.includes("/files/")) { if (url.includes("/files/")) {
return Promise.resolve({ status: 200, data: { files } } as never); return Promise.resolve({ status: 200, data: { files } } as never);
} }
@@ -70,7 +70,7 @@ function primeSession(
} as never); } as never);
} }
return Promise.reject(new Error(`unexpected GET ${url}`)); return Promise.reject(new Error(`unexpected GET ${url}`));
}) as never); });
} }
function renderModal( function renderModal(
@@ -519,7 +519,7 @@ const SignSettings = ({
return; return;
} }
const nextSource = allowedSignatureSources.includes( const nextSource = allowedSignatureSources.includes(
parameters.signatureType as SignatureSource, parameters.signatureType,
) )
? (parameters.signatureType as SignatureSource) ? (parameters.signatureType as SignatureSource)
: effectiveDefaultSource; : effectiveDefaultSource;
@@ -1282,9 +1282,7 @@ const SignSettings = ({
<SegmentedControl <SegmentedControl
value={signatureSource} value={signatureSource}
fullWidth fullWidth
onChange={(value) => onChange={(value) => handleSignatureSourceChange(value)}
handleSignatureSourceChange(value as SignatureSource)
}
options={sourceOptions} options={sourceOptions}
/> />
)} )}
@@ -74,7 +74,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
const { hotkeys } = useHotkeys(); const { hotkeys } = useHotkeys();
const binding = hotkeys[id]; const binding = hotkeys[id];
const { getToolNavigation } = useToolNavigation(); const { getToolNavigation } = useToolNavigation();
const fav = isFavorite(id as ToolId); const fav = isFavorite(id);
// Check if this tool will route to SaaS backend (desktop only) // Check if this tool will route to SaaS backend (desktop only)
const rawEndpoint = tool.operationConfig?.endpoint; const rawEndpoint = tool.operationConfig?.endpoint;
@@ -308,7 +308,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
hasStars && !visuallyUnavailable ? ( hasStars && !visuallyUnavailable ? (
<FavoriteStar <FavoriteStar
isFavorite={fav} isFavorite={fav}
onToggle={() => toggleFavorite(id as ToolId)} onToggle={() => toggleFavorite(id)}
className="tool-button-star" className="tool-button-star"
size="xs" size="xs"
/> />
@@ -294,7 +294,7 @@ const ValidateSignatureResults = ({
</Text> </Text>
<SegmentedControl <SegmentedControl
value={selectedType} value={selectedType}
onChange={(v) => setSelectedType(v as "pdf" | "csv" | "json")} onChange={(v) => setSelectedType(v)}
options={downloadTypeOptions} options={downloadTypeOptions}
/> />
<Button <Button
@@ -35,10 +35,7 @@ function stateWith(...stubs: StirlingFileStub[]): FileContextState {
return { return {
files: { files: {
ids: stubs.map((s) => s.id), ids: stubs.map((s) => s.id),
byId: Object.fromEntries(stubs.map((s) => [s.id, s])) as Record< byId: Object.fromEntries(stubs.map((s) => [s.id, s])),
FileId,
StirlingFileStub
>,
}, },
pinnedFiles: new Set<FileId>(), pinnedFiles: new Set<FileId>(),
ui: { ui: {
@@ -153,7 +150,7 @@ describe("classification landing vs a manually-run tool", () => {
versionNumber: 7, versionNumber: 7,
thumbnailUrl: "blob:thumb", thumbnailUrl: "blob:thumb",
isPinned: true, isPinned: true,
} as Partial<StirlingFileStub>), }),
); );
s = fileContextReducer(s, classify("out")); s = fileContextReducer(s, classify("out"));
@@ -11,5 +11,5 @@ import { type PrototypeToolRegistry } from "@app/data/toolsTaxonomy";
// Empty hook that returns an empty registry (overridden in the prototypes overlay). // Empty hook that returns an empty registry (overridden in the prototypes overlay).
export function usePrototypeToolRegistry(): PrototypeToolRegistry { export function usePrototypeToolRegistry(): PrototypeToolRegistry {
return useMemo(() => ({}) as PrototypeToolRegistry, []); return useMemo(() => ({}), []);
} }
@@ -152,7 +152,7 @@ describe("addPassword mappers", () => {
// undefined: the settings UI calls keyLength.toString() on it. // undefined: the settings UI calls keyLength.toString() on it.
const restored = addPasswordFromApiParams({ const restored = addPasswordFromApiParams({
password: "user-pw", password: "user-pw",
} as never); });
expect(restored.keyLength).toBe(128); expect(restored.keyLength).toBe(128);
}); });
@@ -22,7 +22,6 @@ import type {
} from "@app/hooks/tools/shared/toolApiMapping"; } from "@app/hooks/tools/shared/toolApiMapping";
import { import {
AutoRotateParameters, AutoRotateParameters,
AutoRotateDetectionMode,
defaultParameters, defaultParameters,
validateAutoRotateParameters, validateAutoRotateParameters,
} from "@app/hooks/tools/autoRotate/useAutoRotateParameters"; } from "@app/hooks/tools/autoRotate/useAutoRotateParameters";
@@ -50,7 +49,7 @@ export const autoRotateToApiParams = (
export const autoRotateFromApiParams = ( export const autoRotateFromApiParams = (
apiParams: AutoRotateApiParams, apiParams: AutoRotateApiParams,
): Partial<AutoRotateParameters> => ({ ): Partial<AutoRotateParameters> => ({
detectionMode: apiParams.detectionMode as AutoRotateDetectionMode, detectionMode: apiParams.detectionMode,
confidenceThreshold: apiParams.confidenceThreshold, confidenceThreshold: apiParams.confidenceThreshold,
inferUndetected: apiParams.inferUndetected, inferUndetected: apiParams.inferUndetected,
}); });
@@ -24,7 +24,7 @@ export function useAutomateOperation() {
// Execute the automation sequence and return the final results // Execute the automation sequence and return the final results
const finalResults = await executeAutomationSequence( const finalResults = await executeAutomationSequence(
params.automationConfig!, params.automationConfig,
files, files,
toolRegistry, toolRegistry,
(stepIndex: number, operationName: string) => { (stepIndex: number, operationName: string) => {
@@ -358,7 +358,7 @@ export const extractContentFromPdf = async (
.trim(); .trim();
const isParagraphBreak = (curr: TextItem, prev: TextItem | null) => { const isParagraphBreak = (curr: TextItem, prev: TextItem | null) => {
const hasHardBreak = "hasEOL" in curr && (curr as TextItem).hasEOL; const hasHardBreak = "hasEOL" in curr && curr.hasEOL;
if (hasHardBreak) return true; if (hasHardBreak) return true;
if (!prev) return false; if (!prev) return false;
const prevY = prev.transform[5]; const prevY = prev.transform[5];
@@ -624,7 +624,7 @@ export const extractContentFromPdf = async (
paragraphBuffer = appendWord(paragraphBuffer, normalizedWord); paragraphBuffer = appendWord(paragraphBuffer, normalizedWord);
} }
if (isParagraphBreak(item as TextItem, prevItem)) { if (isParagraphBreak(item, prevItem)) {
if (paragraphBuffer.trim().length > 0) { if (paragraphBuffer.trim().length > 0) {
paragraphs.push({ paragraphs.push({
page: pageIndex, page: pageIndex,
@@ -641,7 +641,7 @@ export const extractContentFromPdf = async (
}); });
paragraphIndex += 1; paragraphIndex += 1;
} }
prevItem = item as TextItem; prevItem = item;
} }
if (paragraphBuffer.trim().length > 0) { if (paragraphBuffer.trim().length > 0) {
@@ -28,7 +28,6 @@ import {
filterTokensForDiff, filterTokensForDiff,
} from "@app/hooks/tools/compare/operationUtils"; } from "@app/hooks/tools/compare/operationUtils";
import { alert, dismissToast } from "@app/components/toast"; import { alert, dismissToast } from "@app/components/toast";
import type { ToastLocation } from "@app/components/toast/types";
import CompareWorkerCtor from "@app/workers/compareWorker?worker"; import CompareWorkerCtor from "@app/workers/compareWorker?worker";
const LONG_RUNNING_PAGE_THRESHOLD = 2000; const LONG_RUNNING_PAGE_THRESHOLD = 2000;
@@ -406,7 +405,7 @@ export const useCompareOperation = (): CompareOperationHook => {
"compare.longJob.body", "compare.longJob.body",
"These PDFs together exceed 2,000 pages. Processing can take several minutes.", "These PDFs together exceed 2,000 pages. Processing can take several minutes.",
), ),
location: "bottom-right" as ToastLocation, location: "bottom-right",
isPersistentPopup: true, isPersistentPopup: true,
expandable: false, expandable: false,
}); });
@@ -435,7 +434,7 @@ export const useCompareOperation = (): CompareOperationHook => {
"compare.earlyDissimilarity.body", "compare.earlyDissimilarity.body",
"We're seeing very few similarities so far. You can stop the comparison if these aren't related documents.", "We're seeing very few similarities so far. You can stop the comparison if these aren't related documents.",
), ),
location: "bottom-right" as ToastLocation, location: "bottom-right",
isPersistentPopup: true, isPersistentPopup: true,
expandable: false, expandable: false,
buttonText: t( buttonText: t(
@@ -635,7 +634,7 @@ export const useCompareOperation = (): CompareOperationHook => {
alertType: "warning", alertType: "warning",
title: t("compare.error.title", "Comparison failed"), title: t("compare.error.title", "Comparison failed"),
body: resolvedMessage, body: resolvedMessage,
location: "bottom-right" as ToastLocation, location: "bottom-right",
}); });
} finally { } finally {
const duration = performance.now() - operationStart; const duration = performance.now() - operationStart;
@@ -383,7 +383,7 @@ export const convertToApiParams = (
formData.forEach((value, key) => { formData.forEach((value, key) => {
if (typeof value === "string") body[key] = value; if (typeof value === "string") body[key] = value;
}); });
return body as unknown as ToolApiParams[ToolEndpoint]; return body;
}; };
/** /**
@@ -406,12 +406,6 @@ type ConvertOptionReaders = {
) => Partial<ConvertParameters>; ) => Partial<ConvertParameters>;
}; };
/** The reader shape collapsed for the runtime dispatch, where the endpoint is only a string. */
type ConvertOptionReader = (
body: Record<string, string | undefined>,
toExtension: string,
) => Partial<ConvertParameters>;
// The stored values are strings; these read one back into the typed shape ConvertParameters expects, // The stored values are strings; these read one back into the typed shape ConvertParameters expects,
// validating an enum against its allowed set (asEnum) rather than blind-casting an arbitrary string. // validating an enum against its allowed set (asEnum) rather than blind-casting an arbitrary string.
const asFlag = (value: string | undefined): boolean => value === "true"; const asFlag = (value: string | undefined): boolean => value === "true";
@@ -583,12 +577,9 @@ export const convertFromApiParams = (
const fromExtension = body.fromExtension ?? ""; const fromExtension = body.fromExtension ?? "";
const toExtension = body.toExtension ?? ""; const toExtension = body.toExtension ?? "";
const endpoint = convertEndpointFor(fromExtension, toExtension); const endpoint = convertEndpointFor(fromExtension, toExtension);
// Each reader reads only its own endpoint's fields; the runtime endpoint is just a string, so the // Each reader reads only its own endpoint's fields; the runtime endpoint is just a string, so
// precise per-endpoint type is recovered with one widening cast here (every reader accepts a // readOptions is looked up per-endpoint (undefined when the pair has no reader).
// superset string record). const readOptions = endpoint ? CONVERT_OPTION_READERS[endpoint] : undefined;
const readOptions = endpoint
? (CONVERT_OPTION_READERS[endpoint] as ConvertOptionReader | undefined)
: undefined;
return { return {
fromExtension, fromExtension,
toExtension, toExtension,
@@ -534,7 +534,7 @@ export function deserializeToolStep(
const params: ErasedToolParams = { const params: ErasedToolParams = {
...(config?.defaultParameters ?? {}), ...(config?.defaultParameters ?? {}),
...mapped, ...mapped,
} as ErasedToolParams; };
// Validate against the generated endpoint set instead of casting the matched string. // Validate against the generated endpoint set instead of casting the matched string.
const operation = const operation =
resolveEndpoint(config, params) ?? resolveEndpoint(config, params) ??
@@ -50,10 +50,9 @@ export function describeToolOperation<
// A dynamic tool's mapper is typed against the union; narrow to this endpoint (sound - the // A dynamic tool's mapper is typed against the union; narrow to this endpoint (sound - the
// runtime mapper produces this endpoint's model). // runtime mapper produces this endpoint's model).
toApi: (params) => toApiParams(params) as ToolApiParams[E], toApi: (params) => toApiParams(params) as ToolApiParams[E],
fromApi: (api) => fromApi: (api) => ({
({ ...defaultParameters,
...defaultParameters, ...fromApiParams(api),
...fromApiParams(api as ToolApiParams[CE]), }),
}) as TParams,
}; };
} }
@@ -284,7 +284,7 @@ export function defineSingleFileTool<
return { return {
...config, ...config,
toolType: ToolType.singleFile, toolType: ToolType.singleFile,
} as SingleFileToolOperationConfig<TParams, TEndpoint>; };
} }
/** Multi-file counterpart of {@link defineSingleFileTool}. */ /** Multi-file counterpart of {@link defineSingleFileTool}. */
@@ -300,7 +300,7 @@ export function defineMultiFileTool<
return { return {
...config, ...config,
toolType: ToolType.multiFile, toolType: ToolType.multiFile,
} as MultiFileToolOperationConfig<TParams, TEndpoint>; };
} }
/** /**
@@ -227,10 +227,7 @@ export const useToolOperation = <TParams>(
: []; : [];
} }
}; };
window.addEventListener( window.addEventListener(FILE_EVENTS.markError, errorListener);
FILE_EVENTS.markError,
errorListener as EventListener,
);
try { try {
let processedFiles: File[]; let processedFiles: File[];
@@ -619,10 +616,7 @@ export const useToolOperation = <TParams>(
actions.setError(errorMessage); actions.setError(errorMessage);
actions.setStatus(""); actions.setStatus("");
} finally { } finally {
window.removeEventListener( window.removeEventListener(FILE_EVENTS.markError, errorListener);
FILE_EVENTS.markError,
errorListener as EventListener,
);
actions.setLoading(false); actions.setLoading(false);
actions.setProgress(null); actions.setProgress(null);
} }
@@ -9,8 +9,8 @@ export function useFavoriteToolItems(
return useMemo(() => { return useMemo(() => {
return favoriteTools return favoriteTools
.map((toolId) => { .map((toolId) => {
const tool = toolRegistry[toolId as ToolId]; const tool = toolRegistry[toolId];
return tool ? { id: toolId as ToolId, tool } : null; return tool ? { id: toolId, tool } : null;
}) })
.filter((x): x is { id: ToolId; tool: ToolRegistryEntry } => x !== null) .filter((x): x is { id: ToolId; tool: ToolRegistryEntry } => x !== null)
.filter( .filter(
@@ -95,10 +95,7 @@ describe("computeSignatureStatus - trust surfacing", () => {
}); });
test("backend error message -> Invalid", () => { test("backend error message -> Invalid", () => {
const status = computeSignatureStatus( const status = computeSignatureStatus(sig({ errorMessage: "boom" }), t);
sig({ errorMessage: "boom" } as Partial<SignatureValidationSignature>),
t,
);
expect(status.kind).toBe("invalid"); expect(status.kind).toBe("invalid");
expect(status.details).toContain("boom"); expect(status.details).toContain("boom");
}); });
+1 -1
View File
@@ -102,7 +102,7 @@ function getCurrentSourcePriority(): LanguageSource {
const sourceStr = localStorage.getItem(I18N_STORAGE_KEYS.LANGUAGE_SOURCE); const sourceStr = localStorage.getItem(I18N_STORAGE_KEYS.LANGUAGE_SOURCE);
const sourceNum = sourceStr ? parseInt(sourceStr, 10) : null; const sourceNum = sourceStr ? parseInt(sourceStr, 10) : null;
return sourceNum !== null && !isNaN(sourceNum) return sourceNum !== null && !isNaN(sourceNum)
? (sourceNum as LanguageSource) ? sourceNum
: LanguageSource.Fallback; : LanguageSource.Fallback;
} }
+1 -1
View File
@@ -118,7 +118,7 @@ Object.defineProperty(globalThis, "crypto", {
} }
return array; return array;
}), }),
} as unknown as Crypto, },
writable: true, writable: true,
configurable: true, configurable: true,
}); });
@@ -144,15 +144,11 @@ test.describe("Settings dialog", () => {
const origReplace = window.history.replaceState.bind(window.history); const origReplace = window.history.replaceState.bind(window.history);
window.history.pushState = function (...args) { window.history.pushState = function (...args) {
w.__historyOps.push++; w.__historyOps.push++;
return origPush( return origPush(...args);
...(args as Parameters<typeof window.history.pushState>),
);
}; };
window.history.replaceState = function (...args) { window.history.replaceState = function (...args) {
w.__historyOps.replace++; w.__historyOps.replace++;
return origReplace( return origReplace(...args);
...(args as Parameters<typeof window.history.replaceState>),
);
}; };
}); });
+3 -5
View File
@@ -175,8 +175,8 @@ const Compare = (props: BaseToolProps) => {
); );
useEffect(() => { useEffect(() => {
const baseFileId = params.baseFileId as FileId | null; const baseFileId = params.baseFileId;
const comparisonFileId = params.comparisonFileId as FileId | null; const comparisonFileId = params.comparisonFileId;
if (!baseFileId || !comparisonFileId) { if (!baseFileId || !comparisonFileId) {
lastProcessedAtRef.current = null; lastProcessedAtRef.current = null;
@@ -437,9 +437,7 @@ const Compare = (props: BaseToolProps) => {
: "Select the edited PDF", : "Select the edited PDF",
) )
} }
excludeIds={ excludeIds={otherSlot ? [otherSlot.stirlingFile.fileId] : []}
otherSlot ? [otherSlot.stirlingFile.fileId as string] : []
}
disabled={isDisabled} disabled={isDisabled}
onSelect={(result: FileSelectorResult) => { onSelect={(result: FileSelectorResult) => {
if (role === "base") setBaseSlot(result); if (role === "base") setBaseSlot(result);
@@ -129,9 +129,9 @@ const SharedSign = (_props: BaseToolProps) => {
const onItemClick = (item: SessionItem) => { const onItemClick = (item: SessionItem) => {
if (item.itemType === "signRequest") { if (item.itemType === "signRequest") {
void controller.openSignRequest(item as SignRequestSummary); void controller.openSignRequest(item);
} else { } else {
void controller.openSession(item as SessionSummary); void controller.openSession(item);
} }
}; };
@@ -247,7 +247,7 @@ const SharedSign = (_props: BaseToolProps) => {
<SegmentedControl <SegmentedControl
fullWidth fullWidth
value={tab} value={tab}
onChange={(value) => changeTab(value as Tab)} onChange={(value) => changeTab(value)}
options={[ options={[
{ label: t("sharedSign.tab.active", "Active"), value: "active" }, { label: t("sharedSign.tab.active", "Active"), value: "active" },
{ {
@@ -891,7 +891,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
) { ) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
opacity: opacity / 100, opacity: opacity / 100,
}, },
@@ -906,7 +906,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
if (selectedAnn?.object?.id && selectedAnn.object?.type === 10) { if (selectedAnn?.object?.id && selectedAnn.object?.type === 10) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
opacity: opacity / 100, opacity: opacity / 100,
}, },
@@ -921,7 +921,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
if (selectedAnn?.object?.id && selectedAnn.object?.type === 12) { if (selectedAnn?.object?.id && selectedAnn.object?.type === 12) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
opacity: opacity / 100, opacity: opacity / 100,
}, },
@@ -936,7 +936,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
if (selectedAnn?.object?.id && selectedAnn.object?.type === 11) { if (selectedAnn?.object?.id && selectedAnn.object?.type === 11) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
opacity: opacity / 100, opacity: opacity / 100,
}, },
@@ -1017,7 +1017,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
if (selectedAnn?.object?.id) { if (selectedAnn?.object?.id) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
color, color,
}, },
@@ -1032,7 +1032,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
if (selectedAnn?.object?.id) { if (selectedAnn?.object?.id) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
color, color,
}, },
@@ -1047,7 +1047,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
if (selectedAnn?.object?.id) { if (selectedAnn?.object?.id) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
color, color,
}, },
@@ -1140,7 +1140,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
if (selectedAnn?.object?.id) { if (selectedAnn?.object?.id) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
strokeColor: color, strokeColor: color,
color: selectedAnn.object?.color ?? shapeFillColor, color: selectedAnn.object?.color ?? shapeFillColor,
@@ -1163,7 +1163,7 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
if (selectedAnn?.object?.id) { if (selectedAnn?.object?.id) {
annotationApiRef?.current?.updateAnnotation?.( annotationApiRef?.current?.updateAnnotation?.(
selectedAnn.object.pageIndex ?? 0, selectedAnn.object.pageIndex ?? 0,
selectedAnn.object.id as string, selectedAnn.object.id,
{ {
color, color,
strokeColor: strokeColor:
@@ -146,7 +146,7 @@ function executePdfJs(
change: "", change: "",
rc: true, rc: true,
willCommit: false, willCommit: false,
target: null as null, target: null,
}; };
try { try {
@@ -377,7 +377,7 @@ export function FormFillProvider({
const [providerMode, setProviderModeState] = useState<"pdflib" | "pdfbox">( const [providerMode, setProviderModeState] = useState<"pdflib" | "pdfbox">(
initialMode, initialMode,
); );
const providerModeRef = useRef(initialMode as "pdflib" | "pdfbox"); const providerModeRef = useRef(initialMode);
providerModeRef.current = providerMode; providerModeRef.current = providerMode;
const provider = const provider =
providerProp ?? providerProp ??
@@ -22,12 +22,7 @@ import type {
ButtonAction, ButtonAction,
} from "@app/tools/formFill/types"; } from "@app/tools/formFill/types";
import type { IFormDataProvider } from "@app/tools/formFill/providers/types"; import type { IFormDataProvider } from "@app/tools/formFill/providers/types";
import type { import type { PDFDict } from "@cantoo/pdf-lib";
PDFDict,
PDFString,
PDFHexString,
PDFName,
} from "@cantoo/pdf-lib";
interface PDFAcroField { interface PDFAcroField {
dict: PDFDict; dict: PDFDict;
@@ -317,7 +312,7 @@ export class PdfiumFormProvider implements IFormDataProvider {
const decodeText = (obj: unknown): string => { const decodeText = (obj: unknown): string => {
if (obj instanceof PDFString || obj instanceof PDFHexString) if (obj instanceof PDFString || obj instanceof PDFHexString)
return (obj as PDFString | PDFHexString).decodeText(); return obj.decodeText();
return String(obj ?? ""); return String(obj ?? "");
}; };
@@ -410,28 +405,27 @@ export class PdfiumFormProvider implements IFormDataProvider {
const decodeText = (obj: unknown): string | null => { const decodeText = (obj: unknown): string | null => {
if (obj instanceof PDFString || obj instanceof PDFHexString) if (obj instanceof PDFString || obj instanceof PDFHexString)
return (obj as PDFString | PDFHexString).decodeText(); return obj.decodeText();
if (obj instanceof PDFName) if (obj instanceof PDFName)
return (obj as PDFName).asString() ?? String(obj).replace(/^\//, ""); return obj.asString() ?? String(obj).replace(/^\//, "");
return null; return null;
}; };
const parseActionDict = (aObj: unknown): ButtonAction | null => { const parseActionDict = (aObj: unknown): ButtonAction | null => {
if (!(aObj instanceof PDFDict)) return null; if (!(aObj instanceof PDFDict)) return null;
// @cantoo/pdf-lib ships without individual .d.ts files so instanceof can't narrow `unknown` // @cantoo/pdf-lib ships without individual .d.ts files so instanceof can't narrow `unknown`
const a = aObj as PDFDict; const a = aObj;
const sObj = a.lookup(PDFName.of("S")); const sObj = a.lookup(PDFName.of("S"));
if (!(sObj instanceof PDFName)) return null; if (!(sObj instanceof PDFName)) return null;
const actionType: string = const actionType: string =
(sObj as PDFName).asString() ?? String(sObj).replace(/^\//, ""); sObj.asString() ?? String(sObj).replace(/^\//, "");
switch (actionType) { switch (actionType) {
case "Named": { case "Named": {
const nObj = a.lookup(PDFName.of("N")); const nObj = a.lookup(PDFName.of("N"));
const name = const name =
nObj instanceof PDFName nObj instanceof PDFName
? ((nObj as PDFName).asString() ?? ? (nObj.asString() ?? String(nObj).replace(/^\//, ""))
String(nObj).replace(/^\//, ""))
: ""; : "";
return { type: "named", namedAction: name }; return { type: "named", namedAction: name };
} }
+1 -3
View File
@@ -33,9 +33,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
<input <input
ref={(el) => { ref={(el) => {
if (typeof ref === "function") ref(el); if (typeof ref === "function") ref(el);
else if (ref) else if (ref) ref.current = el;
(ref as React.MutableRefObject<HTMLInputElement | null>).current =
el;
if (el) el.indeterminate = !!indeterminate; if (el) el.indeterminate = !!indeterminate;
}} }}
type="checkbox" type="checkbox"
@@ -107,7 +107,7 @@ function parseCsvFallback(input: string, max: number): Set<number> {
} }
function clampToRange(v: number, min: number, max: number): number { function clampToRange(v: number, min: number, max: number): number {
if (!Number.isFinite(v)) return NaN as unknown as number; if (!Number.isFinite(v)) return NaN;
return Math.min(Math.max(v, min), max); return Math.min(Math.max(v, min), max);
} }
@@ -281,7 +281,7 @@ class ExpressionParser {
if (!word) return null; if (!word) return null;
const lower = word.toLowerCase(); const lower = word.toLowerCase();
if (lower === "even" || lower === "odd") { if (lower === "even" || lower === "odd") {
return lower as "even" | "odd"; return lower;
} }
// Not a keyword; rewind // Not a keyword; rewind
this.idx = start; this.idx = start;
@@ -371,7 +371,10 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
} }
} else { } else {
setError(message); setError(message);
const fallbackStatus = endpoints.reduce( const fallbackStatus = endpoints.reduce<{
status: Record<string, boolean>;
details: Record<string, EndpointAvailabilityDetails>;
}>(
(acc, endpointName) => { (acc, endpointName) => {
const fallbackDetail: EndpointAvailabilityDetails = { const fallbackDetail: EndpointAvailabilityDetails = {
enabled: false, enabled: false,
@@ -382,8 +385,8 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
return acc; return acc;
}, },
{ {
status: {} as Record<string, boolean>, status: {},
details: {} as Record<string, EndpointAvailabilityDetails>, details: {},
}, },
); );
@@ -38,7 +38,7 @@ let configured = false;
export function ensureSaasSupabase() { export function ensureSaasSupabase() {
if (!isSaasSupabaseConfigured) return null; if (!isSaasSupabaseConfigured) return null;
if (!configured) { if (!configured) {
configureSupabase({ url: url as string, key: key as string }); configureSupabase({ url: url, key: key });
configured = true; configured = true;
} }
return getSupabaseClient(); return getSupabaseClient();
@@ -1,6 +1,6 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MethodBadge, Tabs, type HttpMethod, type TabItem } from "@app/ui"; import { MethodBadge, Tabs, type TabItem } from "@app/ui";
import { VERTICALS, ALL_ENDPOINTS } from "@portal/data/endpoints"; import { VERTICALS, ALL_ENDPOINTS } from "@portal/data/endpoints";
import { DocsSection } from "@portal/components/docs/DocsSection"; import { DocsSection } from "@portal/components/docs/DocsSection";
import "@portal/theme/surface.css"; import "@portal/theme/surface.css";
@@ -61,7 +61,7 @@ export function EndpointReferenceSection() {
</div> </div>
{v.endpoints.map((e) => ( {v.endpoints.map((e) => (
<div key={e.endpoint} className="portal-docs__endpoint-row"> <div key={e.endpoint} className="portal-docs__endpoint-row">
<MethodBadge method={"POST" as HttpMethod} /> <MethodBadge method={"POST"} />
<code className="portal-docs__endpoint-path">{e.endpoint}</code> <code className="portal-docs__endpoint-path">{e.endpoint}</code>
<span className="portal-docs__endpoint-name">{e.name}</span> <span className="portal-docs__endpoint-name">{e.name}</span>
<span className="portal-docs__endpoint-fields"> <span className="portal-docs__endpoint-fields">
@@ -275,8 +275,7 @@ describe("PipelineStepSettings: every tool's settings render in the portal", ()
const Settings = entry.automationSettings as ComponentType< const Settings = entry.automationSettings as ComponentType<
ToolAutomationSettingsProps<ErasedToolParams> ToolAutomationSettingsProps<ErasedToolParams>
>; >;
const params = (entry.operationConfig?.defaultParameters ?? const params = entry.operationConfig?.defaultParameters ?? {};
{}) as ErasedToolParams;
const caught: { error: Error | null } = { error: null }; const caught: { error: Error | null } = { error: null };
// The sentinel sibling commits only once the lazy Settings actually renders, so we wait for a // The sentinel sibling commits only once the lazy Settings actually renders, so we wait for a
@@ -457,7 +457,7 @@ function PolicySetupWizardBody({
variant="underline" variant="underline"
ariaLabel={t("portal.policies.wizard.tabs.ariaLabel")} ariaLabel={t("portal.policies.wizard.tabs.ariaLabel")}
activeKey={step} activeKey={step}
onChange={(k) => setStep(k as Step)} onChange={(k) => setStep(k)}
items={[ items={[
{ key: "workflow", label: t("portal.policies.wizard.tabs.workflow") }, { key: "workflow", label: t("portal.policies.wizard.tabs.workflow") },
{ key: "settings", label: t("portal.policies.wizard.tabs.settings") }, { key: "settings", label: t("portal.policies.wizard.tabs.settings") },
@@ -134,10 +134,7 @@ export function useFlowParticles({
for (let i = 0; i < meanInterval.length; i++) { for (let i = 0; i < meanInterval.length; i++) {
if (!Number.isFinite(meanInterval[i]) || !g.srcs[i]) continue; if (!Number.isFinite(meanInterval[i]) || !g.srcs[i]) continue;
if (now >= nextEmit[i] && particles.length < MAX_PARTICLES) { if (now >= nextEmit[i] && particles.length < MAX_PARTICLES) {
const c = document.createElementNS( const c = document.createElementNS(NS, "circle");
NS,
"circle",
) as SVGCircleElement;
c.setAttribute("r", "2.5"); c.setAttribute("r", "2.5");
c.setAttribute("opacity", "0.75"); c.setAttribute("opacity", "0.75");
c.style.fill = "var(--c-primary)"; c.style.fill = "var(--c-primary)";
@@ -35,7 +35,7 @@ function nextId(categoryId: string): string {
} }
function categoryId(wire: WirePolicy): string { function categoryId(wire: WirePolicy): string {
return (wire.output?.options?.categoryId as string | undefined) ?? ""; return wire.output?.options?.categoryId ?? "";
} }
export const policiesHandlers = [ export const policiesHandlers = [
@@ -256,7 +256,7 @@ export const procurementSaasHandlers = [
stage: "quote", stage: "quote",
licensed: true, licensed: true,
latestQuote: quote, latestQuote: quote,
} as never; };
return HttpResponse.json(quote); return HttpResponse.json(quote);
}), }),
http.post(`${SAAS}/api/v1/procurement/trial/extend`, () => { http.post(`${SAAS}/api/v1/procurement/trial/extend`, () => {
@@ -11,6 +11,6 @@ export function toAsyncState<T>(query: UseQueryResult<T>): AsyncState<T> {
return { return {
data: query.data ?? null, data: query.data ?? null,
loading: query.isPending, loading: query.isPending,
error: (query.error as Error | null) ?? null, error: query.error ?? null,
}; };
} }
+1 -1
View File
@@ -50,7 +50,7 @@ global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(), observe: vi.fn(),
unobserve: vi.fn(), unobserve: vi.fn(),
disconnect: vi.fn(), disconnect: vi.fn(),
})) as unknown as typeof IntersectionObserver; }));
Object.defineProperty(window, "matchMedia", { Object.defineProperty(window, "matchMedia", {
writable: true, writable: true,
@@ -12,11 +12,7 @@ import apiClient from "@app/services/apiClient";
// + oauthNavigation seam, so springAuth routes through the mocks below. // + oauthNavigation seam, so springAuth routes through the mocks below.
import "@app/auth/configureSpringAuth"; import "@app/auth/configureSpringAuth";
import { allowConsole, expectConsole } from "@app/tests/failOnConsole"; import { allowConsole, expectConsole } from "@app/tests/failOnConsole";
import { import { AxiosError, type InternalAxiosRequestConfig } from "axios";
AxiosError,
type AxiosResponse,
type InternalAxiosRequestConfig,
} from "axios";
// Mock apiClient // Mock apiClient
vi.mock("@app/services/apiClient"); vi.mock("@app/services/apiClient");
@@ -59,7 +55,7 @@ describe("SpringAuthClient", () => {
vi.mocked(apiClient.get).mockResolvedValueOnce({ vi.mocked(apiClient.get).mockResolvedValueOnce({
status: 200, status: 200,
data: { user: mockUser }, data: { user: mockUser },
} as unknown as AxiosResponse); });
const result = await springAuth.getSession(); const result = await springAuth.getSession();
@@ -176,7 +172,7 @@ describe("SpringAuthClient", () => {
expires_in: 3600, expires_in: 3600,
}, },
}, },
} as unknown as AxiosResponse); });
// Spy on window.dispatchEvent // Spy on window.dispatchEvent
const dispatchEventSpy = vi.spyOn(window, "dispatchEvent"); const dispatchEventSpy = vi.spyOn(window, "dispatchEvent");
@@ -235,7 +231,7 @@ describe("SpringAuthClient", () => {
vi.mocked(apiClient.post).mockResolvedValueOnce({ vi.mocked(apiClient.post).mockResolvedValueOnce({
status: 200, status: 200,
data: {}, data: {},
} as unknown as AxiosResponse); });
const result = await springAuth.signOut(); const result = await springAuth.signOut();
@@ -288,7 +284,7 @@ describe("SpringAuthClient", () => {
expires_in: 3600, expires_in: 3600,
}, },
}, },
} as unknown as AxiosResponse); });
const result = await springAuth.refreshSession(); const result = await springAuth.refreshSession();
@@ -375,7 +371,7 @@ describe("SpringAuthClient", () => {
expect(isSafePostLoginRedirect("")).toBe(false); expect(isSafePostLoginRedirect("")).toBe(false);
expect(isSafePostLoginRedirect(null)).toBe(false); expect(isSafePostLoginRedirect(null)).toBe(false);
expect(isSafePostLoginRedirect(undefined)).toBe(false); expect(isSafePostLoginRedirect(undefined)).toBe(false);
expect(isSafePostLoginRedirect(42 as unknown)).toBe(false); expect(isSafePostLoginRedirect(42)).toBe(false);
}); });
it("rejects protocol-relative and absolute URLs (open-redirect guard)", () => { it("rejects protocol-relative and absolute URLs (open-redirect guard)", () => {
@@ -46,7 +46,7 @@ function mapUser(user: SbUser): AuthUser {
"", "",
role: readRole(user), role: readRole(user),
is_anonymous: user.is_anonymous, is_anonymous: user.is_anonymous,
app_metadata: user.app_metadata as Record<string, unknown>, app_metadata: user.app_metadata,
}; };
} }
@@ -7,20 +7,19 @@ import type { PolicyRunRecord } from "@app/components/policies/policyRunStore";
* effect used to skip those runs entirely, so `imported` never flipped and the * effect used to skip those runs entirely, so `imported` never flipped and the
* file's badge + blocking overlay spun forever - on every engine. * file's badge + blocking overlay spun forever - on every engine.
*/ */
const run = (overrides: Partial<PolicyRunRecord> = {}): PolicyRunRecord => const run = (overrides: Partial<PolicyRunRecord> = {}): PolicyRunRecord => ({
({ runId: "r",
runId: "r", categoryId: "security",
categoryId: "security", fileId: "f",
fileId: "f", fileName: "f.pdf",
fileName: "f.pdf", fileSize: 1,
fileSize: 1, target: "saas",
target: "saas", status: "COMPLETED",
status: "COMPLETED", outputs: [],
outputs: [], error: null,
error: null, startedAt: 0,
startedAt: 0, ...overrides,
...overrides, });
}) as PolicyRunRecord;
describe("finishedWithNothingToDeliver", () => { describe("finishedWithNothingToDeliver", () => {
it("settles a completed run that produced no output", () => { it("settles a completed run that produced no output", () => {
@@ -99,7 +99,7 @@ export function useClientSideClassification(): void {
if (claimed.current.has(key)) continue; if (claimed.current.has(key)) continue;
claimed.current.add(key); claimed.current.add(key);
const verdict = await classifyStub( const verdict = await classifyStub(
stub.id as FileId, stub.id,
stub.name, stub.name,
stub.size ?? 0, stub.size ?? 0,
); );
@@ -108,11 +108,11 @@ export function useClientSideClassification(): void {
if (verdict == null) continue; if (verdict == null) continue;
// Deliver unconditionally - a re-render must never discard a computed // Deliver unconditionally - a re-render must never discard a computed
// (and already metered) result. Writes are idempotent. // (and already metered) result. Writes are idempotent.
updateStirlingFileStub(stub.id as FileId, { updateStirlingFileStub(stub.id, {
classificationLabels: verdict.labels, classificationLabels: verdict.labels,
classificationConfidence: verdict.confidence, classificationConfidence: verdict.confidence,
}); });
const ok = await fileStorage.updateFileMetadata(stub.id as FileId, { const ok = await fileStorage.updateFileMetadata(stub.id, {
classificationLabels: verdict.labels, classificationLabels: verdict.labels,
classificationConfidence: verdict.confidence, classificationConfidence: verdict.confidence,
}); });
@@ -768,7 +768,7 @@ async function importOutputs(
// Mark the outputs handled BEFORE adding them (belt-and-suspenders session // Mark the outputs handled BEFORE adding them (belt-and-suspenders session
// guard on top of derivedFromTool) so the auto-run never enforces the policy // guard on top of derivedFromTool) so the auto-run never enforces the policy
// on its own output — that would version endlessly in a loop. // on its own output — that would version endlessly in a loop.
for (const s of categorized) markHandled(s.id as string); for (const s of categorized) markHandled(s.id);
deliveredIds = categorized.map((s) => s.id as string); deliveredIds = categorized.map((s) => s.id as string);
if (ctx.parentStub) { if (ctx.parentStub) {
// Input is in the active workspace: version it in place, silently — the // Input is in the active workspace: version it in place, silently — the
@@ -799,7 +799,7 @@ async function importOutputs(
derivedFromTool: true, derivedFromTool: true,
}); });
// Belt-and-suspenders session guard on top of derivedFromTool. // Belt-and-suspenders session guard on top of derivedFromTool.
for (const f of added) markHandled(f.fileId as string); for (const f of added) markHandled(f.fileId);
deliveredIds = added.map((f) => f.fileId as string); deliveredIds = added.map((f) => f.fileId as string);
// Mark each new-file output as tool-derived (the versioned path gets this from the // Mark each new-file output as tool-derived (the versioned path gets this from the
// CONSUME_FILES reducer; the addFiles path doesn't). This is the real loop guard: the dispatch // CONSUME_FILES reducer; the addFiles path doesn't). This is the real loop guard: the dispatch
@@ -46,7 +46,7 @@ export function FileSidebarGroupControls({
const ids = new Set<string>(); const ids = new Set<string>();
for (const key of category.labelKeys) { for (const key of category.labelKeys) {
for (const stub of byLabel.get(key)?.stubs ?? []) { for (const stub of byLabel.get(key)?.stubs ?? []) {
ids.add(stub.id as string); ids.add(stub.id);
} }
} }
counts.set(category.id, ids.size); counts.set(category.id, ids.size);
@@ -460,7 +460,7 @@ export default function InviteMembersModal({
<SegmentedControl <SegmentedControl
value={inviteMode} value={inviteMode}
onChange={(value) => { onChange={(value) => {
setInviteMode(value as "email" | "direct" | "link"); setInviteMode(value);
setGeneratedInviteLink(null); setGeneratedInviteLink(null);
}} }}
options={[ options={[
@@ -67,15 +67,9 @@ const UpgradeBanner: React.FC = () => {
} }
}; };
window.addEventListener( window.addEventListener(UPGRADE_BANNER_TEST_EVENT, handleTestEvent);
UPGRADE_BANNER_TEST_EVENT,
handleTestEvent as EventListener,
);
return () => { return () => {
window.removeEventListener( window.removeEventListener(UPGRADE_BANNER_TEST_EVENT, handleTestEvent);
UPGRADE_BANNER_TEST_EVENT,
handleTestEvent as EventListener,
);
}; };
}, [isDev]); }, [isDev]);
@@ -3,7 +3,7 @@ import { render, screen, waitFor } from "@testing-library/react";
import { MantineProvider } from "@mantine/core"; import { MantineProvider } from "@mantine/core";
const h = vi.hoisted(() => ({ const h = vi.hoisted(() => ({
prefs: { loginLandingView: "processor" as "processor" | "editor" }, prefs: { loginLandingView: "processor" },
update: vi.fn(), update: vi.fn(),
get: vi.fn(), get: vi.fn(),
})); }));
@@ -518,11 +518,11 @@ export default function AdminConnectionsSection() {
updatedSettings: Record<string, unknown>, updatedSettings: Record<string, unknown>,
) => { ) => {
if (provider.id === "smtp") { if (provider.id === "smtp") {
setSettings({ ...settings, mail: updatedSettings as MailSettings }); setSettings({ ...settings, mail: updatedSettings });
} else if (provider.id === "telegram") { } else if (provider.id === "telegram") {
setSettings({ setSettings({
...settings, ...settings,
telegram: updatedSettings as TelegramSettingsData, telegram: updatedSettings,
}); });
} else if (provider.id === "googledrive") { } else if (provider.id === "googledrive") {
const gd = updatedSettings as GoogleDriveSettings; const gd = updatedSettings as GoogleDriveSettings;
@@ -534,7 +534,7 @@ export default function AdminConnectionsSection() {
googleDriveAppId: gd.appId, googleDriveAppId: gd.appId,
}); });
} else if (provider.id === "saml2") { } else if (provider.id === "saml2") {
setSettings({ ...settings, saml2: updatedSettings as Saml2Settings }); setSettings({ ...settings, saml2: updatedSettings });
} else if (provider.id === "oauth2-generic") { } else if (provider.id === "oauth2-generic") {
const generic = updatedSettings as OAuth2GenericSettings; const generic = updatedSettings as OAuth2GenericSettings;
setSettings({ ...settings, oauth2: { ...settings.oauth2, ...generic } }); setSettings({ ...settings, oauth2: { ...settings.oauth2, ...generic } });
@@ -332,9 +332,7 @@ const AdminUsageSection: React.FC = () => {
<Group> <Group>
<SegmentedControl <SegmentedControl
value={displayMode} value={displayMode}
onChange={(value) => onChange={(value) => setDisplayMode(value)}
setDisplayMode(value as "top10" | "top20" | "all")
}
options={[ options={[
{ {
value: "top10", value: "top10",
@@ -373,7 +371,7 @@ const AdminUsageSection: React.FC = () => {
</Text> </Text>
<SegmentedControl <SegmentedControl
value={dataType} value={dataType}
onChange={(value) => setDataType(value as "all" | "api" | "ui")} onChange={(value) => setDataType(value)}
options={[ options={[
{ {
value: "all", value: "all",
@@ -257,11 +257,9 @@ const AuditEventsTable: React.FC<AuditEventsTableProps> = ({
verticalSpacing="sm" verticalSpacing="sm"
withRowBorders withRowBorders
highlightOnHover highlightOnHover
style={ style={{
{ "--table-border-color": "var(--mantine-color-gray-3)",
"--table-border-color": "var(--mantine-color-gray-3)", }}
} as React.CSSProperties
}
> >
<Table.Thead> <Table.Thead>
<Table.Tr <Table.Tr
@@ -233,7 +233,7 @@ const LicenseKeySection: React.FC<LicenseKeySectionProps> = ({
<SegmentedControl <SegmentedControl
value={inputMethod} value={inputMethod}
onChange={(value) => { onChange={(value) => {
setInputMethod(value as "text" | "file"); setInputMethod(value);
// Clear opposite input when switching // Clear opposite input when switching
if (value === "text") setLicenseFile(null); if (value === "text") setLicenseFile(null);
if (value === "file") setLicenseKeyInput(""); if (value === "file") setLicenseKeyInput("");
@@ -32,11 +32,9 @@ const UsageAnalyticsTable: React.FC<UsageAnalyticsTableProps> = ({ data }) => {
verticalSpacing="sm" verticalSpacing="sm"
withRowBorders withRowBorders
highlightOnHover highlightOnHover
style={ style={{
{ "--table-border-color": "var(--mantine-color-gray-3)",
"--table-border-color": "var(--mantine-color-gray-3)", }}
} as React.CSSProperties
}
> >
<TableThead> <TableThead>
<TableTr style={{ backgroundColor: "var(--mantine-color-gray-0)" }}> <TableTr style={{ backgroundColor: "var(--mantine-color-gray-0)" }}>
@@ -20,7 +20,6 @@ import {
} from "@app/services/fileSidebarCategories"; } from "@app/services/fileSidebarCategories";
import { buildLabelGroups } from "@app/components/shared/fileSidebarGroupingLogic"; import { buildLabelGroups } from "@app/components/shared/fileSidebarGroupingLogic";
import { scheduleIdle } from "@app/utils/scheduleIdle"; import { scheduleIdle } from "@app/utils/scheduleIdle";
import type { FileId } from "@app/types/file";
import type { StirlingFileStub } from "@app/types/fileContext"; import type { StirlingFileStub } from "@app/types/fileContext";
import type { FileSidebarGroup } from "@core/components/shared/fileSidebarGrouping"; import type { FileSidebarGroup } from "@core/components/shared/fileSidebarGrouping";
@@ -81,7 +80,7 @@ export function useFileSidebarGroups(
if (cancelled) return; if (cancelled) return;
attempted.current.add(attemptKey(stub)); attempted.current.add(attemptKey(stub));
if (labels) { if (labels) {
const ok = await fileStorage.updateFileMetadata(stub.id as FileId, { const ok = await fileStorage.updateFileMetadata(stub.id, {
classificationLabels: labels, classificationLabels: labels,
}); });
if (ok) wrote = true; if (ok) wrote = true;
@@ -70,7 +70,7 @@ export function buildLabelGroups(
// Other = files in no visible group: unlabelled, or labelled only under hidden categories. // Other = files in no visible group: unlabelled, or labelled only under hidden categories.
const covered = new Set<string>(); const covered = new Set<string>();
for (const group of visible) { for (const group of visible) {
for (const stub of group.stubs) covered.add(stub.id as string); for (const stub of group.stubs) covered.add(stub.id);
} }
const other = stubs.filter((stub) => !covered.has(stub.id as string)); const other = stubs.filter((stub) => !covered.has(stub.id as string));
@@ -51,7 +51,7 @@ export function useFolderRunStatuses(
); );
return [folder.id, deriveStatus(runs)] as const; return [folder.id, deriveStatus(runs)] as const;
} catch { } catch {
return [folder.id, "idle" as FolderRunStatus] as const; return [folder.id, "idle"] as const;
} }
}), }),
); );
@@ -62,10 +62,8 @@ export function fromWirePolicy(policy: WirePolicy): PolicyDecodedState {
name: policy.name, name: policy.name,
enabled: policy.enabled, enabled: policy.enabled,
categoryId, categoryId,
sources: Array.isArray(raw.sources) ? (raw.sources as string[]) : [], sources: Array.isArray(raw.sources) ? raw.sources : [],
scopeTypes: Array.isArray(raw.scopeTypes) scopeTypes: Array.isArray(raw.scopeTypes) ? raw.scopeTypes : [],
? (raw.scopeTypes as string[])
: [],
reviewerEmail: str(raw.reviewerEmail), reviewerEmail: str(raw.reviewerEmail),
fieldValues: raw.fieldValues ?? {}, fieldValues: raw.fieldValues ?? {},
runOn: resolveRunOn(raw.runOn, categoryId), runOn: resolveRunOn(raw.runOn, categoryId),
@@ -10,7 +10,6 @@ import { PreferencesProvider } from "@app/contexts/PreferencesContext";
import { TestQueryProvider } from "@app/tests/utils/TestQueryProvider"; import { TestQueryProvider } from "@app/tests/utils/TestQueryProvider";
import apiClient from "@app/services/apiClient"; import apiClient from "@app/services/apiClient";
import { configureSpringAuth } from "@app/auth/config"; import { configureSpringAuth } from "@app/auth/config";
import type { AxiosInstance } from "axios";
// Mock i18n to return fallback text // Mock i18n to return fallback text
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", () => ({
@@ -137,7 +136,7 @@ describe("Login", () => {
// The shared login hook reads getSpringAuthConfig().http; in the real app, // The shared login hook reads getSpringAuthConfig().http; in the real app,
// startup points that at apiClient. Mirror that here so the mocked apiClient // startup points that at apiClient. Mirror that here so the mocked apiClient
// serves the login-ui-data fetch. // serves the login-ui-data fetch.
configureSpringAuth({ http: apiClient as unknown as AxiosInstance }); configureSpringAuth({ http: apiClient });
}); });
it("should render login form", async () => { it("should render login form", async () => {
@@ -387,8 +387,7 @@ export async function ensureRulesLoaded(): Promise<void> {
if (!loadPromise) { if (!loadPromise) {
loadPromise = import("@app/services/heuristic/heuristicRules.json").then( loadPromise = import("@app/services/heuristic/heuristicRules.json").then(
(mod) => { (mod) => {
const root = ((mod as { default?: RulesFile }).default ?? const root = (mod as { default?: RulesFile }).default ?? mod;
(mod as RulesFile)) as RulesFile;
PREPARED = prepare(root.labels ?? []); PREPARED = prepare(root.labels ?? []);
PRIORS = loadPriors(root.priors ?? {}); PRIORS = loadPriors(root.priors ?? {});
}, },
@@ -186,8 +186,7 @@ async function metadata(
} catch { } catch {
return {}; return {};
} }
const get = (k: string) => const get = (k: string) => (typeof info[k] === "string" ? info[k] : "");
typeof info[k] === "string" ? (info[k] as string) : "";
return { return {
title: get("Title"), title: get("Title"),
author: get("Author"), author: get("Author"),
@@ -222,7 +222,7 @@ export async function enforceExportPolicies(
fileId, fileId,
fileName: file.name, fileName: file.name,
fileSize: file.size, fileSize: file.size,
target: versionRun!.target, target: versionRun.target,
status: "COMPLETED", status: "COMPLETED",
outputs: versionRun.outputs, outputs: versionRun.outputs,
error: null, error: null,
@@ -17,7 +17,7 @@ function makeUser(overrides: Partial<User> = {}): User {
user_metadata: {}, user_metadata: {},
created_at: "2026-01-01T00:00:00Z", created_at: "2026-01-01T00:00:00Z",
...overrides, ...overrides,
} as User; };
} }
describe("saas deriveDisplayName", () => { describe("saas deriveDisplayName", () => {
@@ -46,12 +46,8 @@ export default function SignupRequiredBootstrap() {
return true; return true;
}); });
}; };
window.addEventListener("payg:signupRequired", handler as EventListener); window.addEventListener("payg:signupRequired", handler);
return () => return () => window.removeEventListener("payg:signupRequired", handler);
window.removeEventListener(
"payg:signupRequired",
handler as EventListener,
);
}, []); }, []);
// Map the server's gate categories to user-facing nouns. The server // Map the server's gate categories to user-facing nouns. The server
@@ -80,12 +80,8 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({
setMobilePane("content"); setMobilePane("content");
} }
}; };
window.addEventListener("appConfig:navigate", handler as EventListener); window.addEventListener("appConfig:navigate", handler);
return () => return () => window.removeEventListener("appConfig:navigate", handler);
window.removeEventListener(
"appConfig:navigate",
handler as EventListener,
);
}, []); }, []);
// When the modal opens via a /settings/<section> deep link (navigateToSettings — e.g. the // When the modal opens via a /settings/<section> deep link (navigateToSettings — e.g. the
@@ -122,9 +118,8 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({
setNotice(detail.notice); setNotice(detail.notice);
} }
}; };
window.addEventListener("appConfig:notice", handler as EventListener); window.addEventListener("appConfig:notice", handler);
return () => return () => window.removeEventListener("appConfig:notice", handler);
window.removeEventListener("appConfig:notice", handler as EventListener);
}, []); }, []);
// Full-screen overlays that live inside our React tree (e.g. the PAYG // Full-screen overlays that live inside our React tree (e.g. the PAYG
@@ -140,9 +135,8 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({
| undefined; | undefined;
setOverlayActive(Boolean(detail?.open)); setOverlayActive(Boolean(detail?.open));
}; };
window.addEventListener("appConfig:overlay", handler as EventListener); window.addEventListener("appConfig:overlay", handler);
return () => return () => window.removeEventListener("appConfig:overlay", handler);
window.removeEventListener("appConfig:overlay", handler as EventListener);
}, []); }, []);
const colors = useMemo( const colors = useMemo(
@@ -211,11 +211,9 @@ export default function StackedBarChart({
setTooltipContent(html); setTooltipContent(html);
const tooltip = tooltipRef.current; const tooltip = tooltipRef.current;
if (tooltip) tooltip.style.opacity = "1"; if (tooltip) tooltip.style.opacity = "1";
positionTooltip(event as unknown as MouseEvent); positionTooltip(event);
}) })
.on("mousemove", (event: MouseEvent) => .on("mousemove", (event: MouseEvent) => positionTooltip(event))
positionTooltip(event as unknown as MouseEvent),
)
.on("mouseleave", hideTooltip); .on("mouseleave", hideTooltip);
// Animate reveal of used segments (only on first load, not on re-renders) // Animate reveal of used segments (only on first load, not on re-renders)
@@ -562,15 +562,7 @@ const Overview: React.FC<OverviewProps> = ({ onLogoutClick }) => {
style={{ width: 16, height: 16 }} style={{ width: 16, height: 16 }}
/> />
} }
onClick={() => onClick={() => handleOAuthUpgrade(provider.id)}
handleOAuthUpgrade(
provider.id as
| "github"
| "google"
| "apple"
| "azure",
)
}
disabled={isLoading} disabled={isLoading}
> >
{provider.label} {provider.label}
@@ -547,7 +547,7 @@ const SignSettings = ({
return; return;
} }
const nextSource = allowedSignatureSources.includes( const nextSource = allowedSignatureSources.includes(
parameters.signatureType as SignatureSource, parameters.signatureType,
) )
? (parameters.signatureType as SignatureSource) ? (parameters.signatureType as SignatureSource)
: effectiveDefaultSource; : effectiveDefaultSource;
@@ -1314,9 +1314,7 @@ const SignSettings = ({
<SegmentedControl <SegmentedControl
value={signatureSource} value={signatureSource}
fullWidth fullWidth
onChange={(value) => onChange={(value) => handleSignatureSourceChange(value)}
handleSignatureSourceChange(value as SignatureSource)
}
options={sourceOptions} options={sourceOptions}
/> />
)} )}
@@ -35,9 +35,8 @@ interface AuthorizationDetails {
}; };
} }
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL as string; const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
const SUPABASE_KEY = import.meta.env const SUPABASE_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY;
.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY as string;
async function gotrue( async function gotrue(
path: string, path: string,
+1 -1
View File
@@ -130,7 +130,7 @@ Object.defineProperty(globalThis, "crypto", {
} }
return array; return array;
}), }),
} as unknown as Crypto, },
writable: true, writable: true,
configurable: true, configurable: true,
}); });