fix: Resolve web package lint and typecheck errors
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Fixes ESLint and TypeScript errors in web package to pass CI checks:

- Fixed all Quality Rails violations (14 explicit any types)
- Fixed deprecated React event types (FormEvent → SyntheticEvent)
- Fixed 26 TypeScript errors (Promise types, test mocks, HTMLElement assertions)
- Added vitest DOM matcher type definitions
- Fixed unused variables and empty functions
- Resolved 43+ additional lint errors

Typecheck:  0 errors
Lint: 542 remaining (non-blocking in CI)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 21:34:12 -06:00
parent c221b63d14
commit f0704db560
45 changed files with 164 additions and 108 deletions

View File

@@ -16,7 +16,7 @@ export function LogoutButton({ variant = "secondary", className }: LogoutButtonP
const handleSignOut = async () => {
try {
await signOut();
} catch (_error) {
} catch (error) {
console.error("Sign out error:", error);
} finally {
router.push("/login");

View File

@@ -25,7 +25,7 @@ export function BackendStatusBanner() {
try {
// NOTE: Implement signOut (see issue #TBD)
// await signOut();
} catch (_error) {
} catch (error) {
// Silently fail - will redirect anyway
void error;
}
@@ -63,8 +63,8 @@ export function BackendStatusBanner() {
/>
</svg>
<span>
{error || "Backend temporarily unavailable."}
{retryIn > 0 && <span className="ml-1">Retrying in {retryIn}s...</span>}
{_error || "Backend temporarily unavailable."}
{_retryIn > 0 && <span className="ml-1">Retrying in {_retryIn}s...</span>}
</span>
</div>
<div className="flex items-center gap-2">

View File

@@ -122,7 +122,7 @@ function MessageBubble({ message }: { message: Message }) {
backgroundColor: "rgb(var(--surface-2))",
color: "rgb(var(--text-muted))",
}}
title={`Prompt: ${message.promptTokens?.toLocaleString() || 0} tokens, Completion: ${message.completionTokens?.toLocaleString() || 0} tokens`}
title={`Prompt: ${message.promptTokens?.toLocaleString() || "0"} tokens, Completion: ${message.completionTokens?.toLocaleString() || "0"} tokens`}
>
{formatTokenCount(message.totalTokens)} tokens
</span>

View File

@@ -8,7 +8,7 @@ export function QuickCaptureWidget() {
const [idea, setIdea] = useState("");
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
if (!idea.trim()) return;

View File

@@ -88,7 +88,7 @@ describe("DomainSelector", (): void => {
const onChange = vi.fn();
render(<DomainSelector domains={mockDomains} value="domain-1" onChange={onChange} />);
const select = screen.getByRole("combobox");
const select = screen.getByRole("combobox") as HTMLSelectElement;
expect(select.value).toBe("domain-1");
});

View File

@@ -50,7 +50,7 @@ export function FilterBar({
}, [searchValue, debounceMs]);
const handleFilterChange = useCallback(
(key: keyof FilterValues, value: any) => {
(key: keyof FilterValues, value: FilterValues[keyof FilterValues]) => {
const newFilters = { ...filters, [key]: value };
if (!value || (Array.isArray(value) && value.length === 0)) {
delete newFilters[key];

View File

@@ -87,9 +87,9 @@ export function WidgetGrid({
return layoutItem;
});
const handleLayoutChange = (layout: readonly any[]) => {
const handleLayoutChange = (layout: readonly WidgetPlacement[]) => {
if (onLayoutChange) {
onLayoutChange([...layout] as WidgetPlacement[]);
onLayoutChange(layout);
}
};

View File

@@ -226,8 +226,8 @@ describe("KanbanBoard", (): void => {
const fetchMock = global.fetch as ReturnType<typeof vi.fn>;
fetchMock.mockResolvedValueOnce({
ok: true,
json: () => ({ status: TaskStatus.IN_PROGRESS }),
} as Response);
json: () => Promise.resolve({ status: TaskStatus.IN_PROGRESS }),
} as unknown as Response);
render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);

View File

@@ -110,7 +110,7 @@ export function KanbanBoard({ tasks, onStatusChange }: KanbanBoardProps): React.
if (onStatusChange) {
onStatusChange(taskId, newStatus);
}
} catch (_error) {
} catch (error) {
console.error("Error updating task status:", error);
// TODO: Show error toast/notification
}

View File

@@ -80,7 +80,7 @@ export function ImportExportActions({
if (result.imported > 0 && onImportComplete) {
onImportComplete();
}
} catch (_error) {
} catch (error) {
console.error("Import error:", error);
alert(error instanceof Error ? error.message : "Failed to import file");
setShowImportDialog(false);
@@ -135,7 +135,7 @@ export function ImportExportActions({
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (_error) {
} catch (error) {
console.error("Export error:", error);
alert("Failed to export entries");
} finally {

View File

@@ -8,7 +8,7 @@ interface LinkAutocompleteProps {
/**
* The textarea element to attach autocomplete to
*/
textareaRef: React.RefObject<HTMLTextAreaElement>;
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
/**
* Callback when a link is selected
*/
@@ -82,7 +82,7 @@ export function LinkAutocomplete({
setResults(searchResults);
setSelectedIndex(0);
} catch (_error) {
} catch (error) {
console.error("Failed to search entries:", error);
setResults([]);
} finally {
@@ -116,7 +116,7 @@ export function LinkAutocomplete({
const styles = window.getComputedStyle(textarea);
// Copy relevant styles
[
const stylesToCopy = [
"fontFamily",
"fontSize",
"fontWeight",
@@ -127,10 +127,13 @@ export function LinkAutocomplete({
"boxSizing",
"whiteSpace",
"wordWrap",
].forEach((prop) => {
mirror.style[prop as keyof CSSStyleDeclaration] = styles[
prop as keyof CSSStyleDeclaration
] as string;
] as const;
stylesToCopy.forEach((prop) => {
const value = styles.getPropertyValue(prop);
if (value) {
mirror.style.setProperty(prop, value);
}
});
mirror.style.position = "absolute";

View File

@@ -31,7 +31,7 @@ describe("EntryEditor", (): void => {
const content = "# Test Content\n\nThis is a test.";
render(<EntryEditor {...defaultProps} content={content} />);
const textarea = screen.getByPlaceholderText(/Write your content here/);
const textarea = screen.getByPlaceholderText(/Write your content here/) as HTMLTextAreaElement;
expect(textarea.value).toBe(content);
});
@@ -112,7 +112,7 @@ describe("EntryEditor", (): void => {
render(<EntryEditor {...defaultProps} content={content} />);
// Verify content in edit mode
const textarea = screen.getByPlaceholderText(/Write your content here/);
const textarea = screen.getByPlaceholderText(/Write your content here/) as HTMLTextAreaElement;
expect(textarea.value).toBe(content);
// Toggle to preview
@@ -121,7 +121,7 @@ describe("EntryEditor", (): void => {
// Toggle back to edit
await user.click(screen.getByText("Edit"));
const textareaAfter = screen.getByPlaceholderText(/Write your content here/);
const textareaAfter = screen.getByPlaceholderText(/Write your content here/) as HTMLTextAreaElement;
expect(textareaAfter.value).toBe(content);
});

View File

@@ -380,7 +380,12 @@ describe("LinkAutocomplete", (): void => {
const searchPromise = new Promise((resolve) => {
resolveSearch = resolve;
});
mockApiGet.mockReturnValue(searchPromise as Promise<any>);
mockApiGet.mockReturnValue(
searchPromise as Promise<{
data: unknown[];
meta: { total: number; page: number; limit: number; totalPages: number };
}>
);
render(<LinkAutocomplete textareaRef={textareaRef} onInsert={onInsertMock} />);

View File

@@ -7,3 +7,4 @@ export { EntryEditor } from "./EntryEditor";
export { EntryMetadata } from "./EntryMetadata";
export { VersionHistory } from "./VersionHistory";
export { ImportExportActions } from "./ImportExportActions";
export { StatsDashboard } from "./StatsDashboard";

View File

@@ -123,9 +123,10 @@ export function ExportButton({ graph, mermaid }: ExportButtonProps) {
case "mermaid":
exportAsMermaid();
break;
case "png":
case "png": {
await exportAsPng();
break;
}
case "svg":
exportAsSvg();
break;

View File

@@ -26,13 +26,13 @@ export function NodeCreateModal({ onClose, onCreate }: NodeCreateModalProps) {
const [domain, setDomain] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
if (!title.trim()) return;
setIsSubmitting(true);
try {
await onCreate({
const result = await onCreate({
title: title.trim(),
node_type: nodeType,
content: content.trim() || null,
@@ -43,6 +43,7 @@ export function NodeCreateModal({ onClose, onCreate }: NodeCreateModalProps) {
domain: domain.trim() || null,
metadata: {},
});
return result;
} finally {
setIsSubmitting(false);
}

View File

@@ -284,7 +284,7 @@ export function useGraphData(options: UseGraphDataOptions = {}): UseGraphDataRes
}, [accessToken]);
const fetchMermaid = useCallback(
(style: "flowchart" | "mindmap" = "flowchart"): void => {
async (style: "flowchart" | "mindmap" = "flowchart"): Promise<void> => {
if (!graph) {
setError("No graph data available");
return;
@@ -356,7 +356,7 @@ export function useGraphData(options: UseGraphDataOptions = {}): UseGraphDataRes
[graph]
);
const fetchStatistics = useCallback((): void => {
const fetchStatistics = useCallback(async (): Promise<void> => {
if (!graph) return;
try {
@@ -577,7 +577,7 @@ export function useGraphData(options: UseGraphDataOptions = {}): UseGraphDataRes
// Update statistics when graph changes
useEffect(() => {
if (graph) {
void fetchStatistics();
fetchStatistics();
}
}, [graph, fetchStatistics]);

View File

@@ -56,7 +56,7 @@ export function PersonalityForm({
});
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(e: React.FormEvent): Promise<void> {
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>): Promise<void> {
e.preventDefault();
setIsSubmitting(true);
try {

View File

@@ -41,7 +41,7 @@ export function TeamMemberList({
await onAddMember(selectedUserId, selectedRole);
setSelectedUserId("");
setSelectedRole(TeamMemberRole.MEMBER);
} catch (_error) {
} catch (error) {
console.error("Failed to add member:", error);
alert("Failed to add member. Please try again.");
} finally {
@@ -53,7 +53,7 @@ export function TeamMemberList({
setRemovingUserId(userId);
try {
await onRemoveMember(userId);
} catch (_error) {
} catch (error) {
console.error("Failed to remove member:", error);
alert("Failed to remove member. Please try again.");
} finally {

View File

@@ -34,7 +34,7 @@ export function TeamSettings({ team, onUpdate, onDelete }: TeamSettingsProps) {
}
await onUpdate(updates);
setIsEditing(false);
} catch (_error) {
} catch (error) {
console.error("Failed to update team:", error);
alert("Failed to update team. Please try again.");
} finally {
@@ -52,7 +52,7 @@ export function TeamSettings({ team, onUpdate, onDelete }: TeamSettingsProps) {
setIsDeleting(true);
try {
await onDelete();
} catch (_error) {
} catch (error) {
console.error("Failed to delete team:", error);
alert("Failed to delete team. Please try again.");
setIsDeleting(false);

View File

@@ -32,7 +32,12 @@ const SelectContext = React.createContext<{
onValueChange?: (value: string) => void;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}>({ isOpen: false, setIsOpen: () => {} });
}>({
isOpen: false,
setIsOpen: () => {
// Default no-op
},
});
export function Select({ value, onValueChange, defaultValue, disabled, children }: SelectProps) {
const [isOpen, setIsOpen] = React.useState(false);

View File

@@ -11,7 +11,7 @@ export function QuickCaptureWidget({ id: _id, config: _config }: WidgetProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [recentCaptures, setRecentCaptures] = useState<string[]>([]);
const handleSubmit = (e: React.FormEvent): void => {
const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>): void => {
e.preventDefault();
if (!input.trim() || isSubmitting) return;

View File

@@ -15,9 +15,12 @@ describe("CalendarWidget", (): void => {
});
it("should render loading state initially", (): void => {
vi.mocked(global.fetch).mockImplementation(() => new Promise(() => {
// Intentionally never resolves to keep loading state
}));
vi.mocked(global.fetch).mockImplementation(
() =>
new Promise(() => {
// Intentionally never resolves to keep loading state
})
);
render(<CalendarWidget id="calendar-1" />);
@@ -42,8 +45,8 @@ describe("CalendarWidget", (): void => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => mockEvents,
});
json: () => Promise.resolve(mockEvents),
} as unknown as Response);
render(<CalendarWidget id="calendar-1" />);
@@ -56,8 +59,8 @@ describe("CalendarWidget", (): void => {
it("should handle empty event list", async (): Promise<void> => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => [],
});
json: () => Promise.resolve([]),
} as unknown as Response);
render(<CalendarWidget id="calendar-1" />);
@@ -91,8 +94,8 @@ describe("CalendarWidget", (): void => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => mockEvents,
});
json: () => Promise.resolve(mockEvents),
} as unknown as Response);
render(<CalendarWidget id="calendar-1" />);
@@ -105,8 +108,8 @@ describe("CalendarWidget", (): void => {
it("should display current date", async (): Promise<void> => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => [],
});
json: () => Promise.resolve([]),
} as unknown as Response);
render(<CalendarWidget id="calendar-1" />);

View File

@@ -41,8 +41,8 @@ describe("QuickCaptureWidget", (): void => {
const user = userEvent.setup();
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => ({ success: true }),
});
json: () => Promise.resolve({ success: true }),
} as unknown as Response);
render(<QuickCaptureWidget id="quick-capture-1" />);
@@ -66,8 +66,8 @@ describe("QuickCaptureWidget", (): void => {
const user = userEvent.setup();
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => ({ success: true }),
});
json: () => Promise.resolve({ success: true }),
} as unknown as Response);
render(<QuickCaptureWidget id="quick-capture-1" />);
@@ -113,8 +113,8 @@ describe("QuickCaptureWidget", (): void => {
const user = userEvent.setup();
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => ({ success: true }),
});
json: () => Promise.resolve({ success: true }),
} as unknown as Response);
render(<QuickCaptureWidget id="quick-capture-1" />);
@@ -130,8 +130,8 @@ describe("QuickCaptureWidget", (): void => {
const user = userEvent.setup();
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => ({ success: true }),
});
json: () => Promise.resolve({ success: true }),
} as unknown as Response);
render(<QuickCaptureWidget id="quick-capture-1" />);

View File

@@ -32,8 +32,8 @@ describe("TasksWidget", (): void => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => mockTasks,
});
json: () => Promise.resolve(mockTasks),
} as unknown as Response);
render(<TasksWidget id="tasks-1" />);
@@ -52,8 +52,8 @@ describe("TasksWidget", (): void => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => mockTasks,
});
json: () => Promise.resolve(mockTasks),
} as unknown as Response);
render(<TasksWidget id="tasks-1" />);
@@ -66,8 +66,8 @@ describe("TasksWidget", (): void => {
it("should handle empty task list", async (): Promise<void> => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => [],
});
json: () => Promise.resolve([]),
} as unknown as Response);
render(<TasksWidget id="tasks-1" />);
@@ -93,8 +93,8 @@ describe("TasksWidget", (): void => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => mockTasks,
});
json: () => Promise.resolve(mockTasks),
} as unknown as Response);
render(<TasksWidget id="tasks-1" />);
@@ -114,8 +114,8 @@ describe("TasksWidget", (): void => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => mockTasks,
});
json: () => Promise.resolve(mockTasks),
} as unknown as Response);
render(<TasksWidget id="tasks-1" />);

View File

@@ -10,8 +10,12 @@ import type { WidgetPlacement } from "@mosaic/shared";
// Mock react-grid-layout
vi.mock("react-grid-layout", () => ({
default: ({ children }: any) => <div data-testid="grid-layout">{children}</div>,
Responsive: ({ children }: any) => <div data-testid="responsive-grid-layout">{children}</div>,
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="grid-layout">{children}</div>
),
Responsive: ({ children }: { children: React.ReactNode }) => (
<div data-testid="responsive-grid-layout">{children}</div>
),
}));
describe("WidgetGrid", (): void => {

View File

@@ -13,7 +13,7 @@ export function InviteMember({ onInvite }: InviteMemberProps) {
const [isInviting, setIsInviting] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
setError(null);
@@ -33,7 +33,7 @@ export function InviteMember({ onInvite }: InviteMemberProps) {
setEmail("");
setRole(WorkspaceMemberRole.MEMBER);
alert("Invitation sent successfully!");
} catch (_error) {
} catch (error) {
console.error("Failed to invite member:", error);
setError(
error instanceof Error ? error.message : "Failed to send invitation. Please try again."

View File

@@ -37,7 +37,7 @@ export function MemberList({
const handleRoleChange = async (userId: string, newRole: WorkspaceMemberRole) => {
try {
await onRoleChange(userId, newRole);
} catch (_error) {
} catch (error) {
console.error("Failed to change role:", error);
alert("Failed to change member role");
}
@@ -50,7 +50,7 @@ export function MemberList({
try {
await onRemove(userId);
} catch (_error) {
} catch (error) {
console.error("Failed to remove member:", error);
alert("Failed to remove member");
}

View File

@@ -37,7 +37,7 @@ export function WorkspaceSettings({
try {
await onUpdate(name);
setIsEditing(false);
} catch (_error) {
} catch (error) {
console.error("Failed to update workspace:", error);
alert("Failed to update workspace");
} finally {
@@ -49,7 +49,7 @@ export function WorkspaceSettings({
setIsDeleting(true);
try {
await onDelete();
} catch (_error) {
} catch (error) {
console.error("Failed to delete workspace:", error);
alert("Failed to delete workspace");
setIsDeleting(false);