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