From 4c8a1c89b251ea4ba6f57390e8d693be5372fda0 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 29 Apr 2026 17:45:02 -0400 Subject: [PATCH] chore: resolve no-explicit-any and no-base-to-string lint warnings (#12217) --- app/actions/index.ts | 8 +++- app/components/Collection/CollectionForm.tsx | 4 +- app/components/ContentEditable.tsx | 19 ++++---- app/components/LazyLoad.ts | 1 + .../OAuthClient/OAuthClientForm.tsx | 4 +- app/components/PaginatedDocumentList.tsx | 4 +- app/components/PaginatedList.tsx | 4 +- app/components/Tabs.tsx | 2 +- app/components/Toasts.tsx | 4 +- app/env.ts | 1 + app/hooks/useActionContext.tsx | 12 ++--- app/hooks/useStores.ts | 11 +++-- app/hooks/useThrottledCallback.ts | 4 +- app/hooks/useUnmount.ts | 2 +- app/index.tsx | 2 +- app/models/AuthenticationProvider.ts | 11 +++-- app/models/Document.ts | 4 +- app/models/base/Model.ts | 14 +++--- app/models/decorators/Field.ts | 4 +- app/models/decorators/Lifecycle.ts | 46 +++++++++++-------- app/models/decorators/Relation.ts | 8 ++-- app/scenes/Login/Login.tsx | 2 +- .../components/AuthenticationProvider.tsx | 2 +- app/stores/EventsStore.ts | 1 + app/stores/UiStore.ts | 10 ++-- app/stores/base/Store.ts | 3 ++ app/types.ts | 8 ++-- app/utils/ApiClient.ts | 8 +++- app/utils/Logger.ts | 1 + app/utils/download.ts | 5 +- app/utils/lazyWithRetry.ts | 1 + server/logging/Logger.ts | 4 +- server/models/helpers/ProsemirrorHelper.tsx | 6 +-- server/test/support.ts | 9 +++- server/tools/documents.ts | 4 +- server/tools/fetch.ts | 2 +- server/utils/DocumentConverter.ts | 6 +-- server/utils/__mocks__/MutexLock.ts | 2 - shared/editor/components/Mentions.tsx | 9 +++- shared/editor/lib/table.ts | 5 +- shared/hooks/useStores.ts | 2 +- shared/styles/index.ts | 2 +- shared/utils/ProsemirrorHelper.ts | 6 +-- shared/utils/csv.ts | 5 +- 44 files changed, 162 insertions(+), 110 deletions(-) diff --git a/app/actions/index.ts b/app/actions/index.ts index 42572ef9a0..104685e3a7 100644 --- a/app/actions/index.ts +++ b/app/actions/index.ts @@ -17,8 +17,12 @@ import Analytics from "~/utils/Analytics"; import history from "~/utils/history"; import type { Action as KbarAction } from "kbar"; -export function resolve(value: any, context: ActionContext): T { - return typeof value === "function" ? value(context) : value; +export function resolve(value: unknown, context: ActionContext): T { + return ( + typeof value === "function" + ? (value as (context: ActionContext) => T)(context) + : value + ) as T; } export const ActionSeparator: TActionSeparator = { diff --git a/app/components/Collection/CollectionForm.tsx b/app/components/Collection/CollectionForm.tsx index e4e2538666..e35687f79e 100644 --- a/app/components/Collection/CollectionForm.tsx +++ b/app/components/Collection/CollectionForm.tsx @@ -29,7 +29,7 @@ import { useDialogContext } from "~/components/DialogContext"; const IconPicker = createLazyComponent(() => import("~/components/IconPicker")); -export interface FormData { +export type FormData = { name: string; icon: string; color: string | null; @@ -37,7 +37,7 @@ export interface FormData { permission: CollectionPermission | undefined; commenting?: boolean | null; templateManagement: CollectionPermission; -} +}; const useIconColor = (collection?: Collection) => { const { collections } = useStores(); diff --git a/app/components/ContentEditable.tsx b/app/components/ContentEditable.tsx index d4b740fff6..20b3948e81 100644 --- a/app/components/ContentEditable.tsx +++ b/app/components/ContentEditable.tsx @@ -87,22 +87,23 @@ const ContentEditable = React.forwardRef(function ContentEditable_( })); const wrappedEvent = - ( - callback: - | React.FocusEventHandler - | React.FormEventHandler - | React.KeyboardEventHandler - | undefined + >( + callback: ((event: E) => void) | undefined ) => - (event: any) => { + (event: E) => { if (readOnly) { return; } const text = event.currentTarget.textContent || ""; - if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) { - event?.preventDefault(); + if ( + maxLength && + event.nativeEvent instanceof KeyboardEvent && + isPrintableKeyEvent(event.nativeEvent) && + text.length >= maxLength + ) { + event.preventDefault(); return; } diff --git a/app/components/LazyLoad.ts b/app/components/LazyLoad.ts index 9881889807..5edf87559a 100644 --- a/app/components/LazyLoad.ts +++ b/app/components/LazyLoad.ts @@ -1,6 +1,7 @@ import * as React from "react"; import lazyWithRetry from "~/utils/lazyWithRetry"; +// oxlint-disable no-explicit-any -- ComponentType is the standard React pattern for generic component constraints export interface LazyComponent> { Component: React.LazyExoticComponent; preload: () => Promise<{ default: T }>; diff --git a/app/components/OAuthClient/OAuthClientForm.tsx b/app/components/OAuthClient/OAuthClientForm.tsx index 553a1102f3..db90378993 100644 --- a/app/components/OAuthClient/OAuthClientForm.tsx +++ b/app/components/OAuthClient/OAuthClientForm.tsx @@ -13,7 +13,7 @@ import Switch from "../Switch"; import EventBoundary from "@shared/components/EventBoundary"; import { InputClientType } from "./InputClientType"; -export interface FormData { +export type FormData = { name: string; developerName: string; developerUrl: string; @@ -22,7 +22,7 @@ export interface FormData { redirectUris: string[]; published: boolean; clientType: "confidential" | "public"; -} +}; export const OAuthClientForm = observer(function OAuthClientForm_({ handleSubmit, diff --git a/app/components/PaginatedDocumentList.tsx b/app/components/PaginatedDocumentList.tsx index 3c43d90b09..e9f091d1e8 100644 --- a/app/components/PaginatedDocumentList.tsx +++ b/app/components/PaginatedDocumentList.tsx @@ -7,7 +7,9 @@ import PaginatedList from "~/components/PaginatedList"; type Props = { documents: Document[]; - fetch: (options: any) => Promise; + // oxlint-disable-next-line no-explicit-any + fetch: (options: Record) => Promise; + // oxlint-disable-next-line no-explicit-any options?: Record; heading?: React.ReactNode; empty?: JSX.Element; diff --git a/app/components/PaginatedList.tsx b/app/components/PaginatedList.tsx index bb9145ac81..1a1d9df9b1 100644 --- a/app/components/PaginatedList.tsx +++ b/app/components/PaginatedList.tsx @@ -35,10 +35,12 @@ interface Props< * @param options Pagination and other query options */ fetch?: ( + // oxlint-disable-next-line no-explicit-any options: Record | undefined ) => Promise | undefined; /** Additional options to pass to the fetch function */ + // oxlint-disable-next-line no-explicit-any options?: Record; /** Optional header content to display above the list */ @@ -78,7 +80,7 @@ interface Props< * Function to render section headings (typically date-based) * @param name The heading text or element to render */ - renderHeading?: (name: React.ReactElement | string) => React.ReactNode; + renderHeading?: (name: React.ReactElement | string) => React.ReactNode; /** * Function to determine if an item is a duplicate of the previous item. diff --git a/app/components/Tabs.tsx b/app/components/Tabs.tsx index b64a0cfce2..a330973414 100644 --- a/app/components/Tabs.tsx +++ b/app/components/Tabs.tsx @@ -64,7 +64,7 @@ type Props = { }; const Tabs: React.FC = ({ children }: Props) => { - const ref = React.useRef(); + const ref = React.useRef(null); const [shadowVisible, setShadow] = React.useState(false); const { width } = useWindowSize(); diff --git a/app/components/Toasts.tsx b/app/components/Toasts.tsx index bc8c9755a7..2df6f70ae5 100644 --- a/app/components/Toasts.tsx +++ b/app/components/Toasts.tsx @@ -4,6 +4,7 @@ import { Toaster, useSonner } from "sonner"; import styled, { useTheme } from "styled-components"; import { useWebHaptics } from "web-haptics/react"; import useStores from "~/hooks/useStores"; +import type { ResolvedTheme } from "~/stores/UiStore"; function Toasts() { const { ui } = useStores(); @@ -26,7 +27,8 @@ function Toasts() { return ( ; diff --git a/app/hooks/useActionContext.tsx b/app/hooks/useActionContext.tsx index 18176015d8..bb56a0696c 100644 --- a/app/hooks/useActionContext.tsx +++ b/app/hooks/useActionContext.tsx @@ -69,15 +69,15 @@ export const ActionContextProvider = observer(function ActionContextProvider_({ activeDocumentId: stores.ui.activeDocumentId ?? undefined, getActiveModels: ( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ): T[] => stores.ui.getActiveModels(modelClass), getActiveModel: ( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ): T | undefined => stores.ui.getActiveModels(modelClass)[0], getActivePolicies: ( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ): Policy[] => stores.ui .getActiveModels(modelClass) @@ -97,7 +97,7 @@ export const ActionContextProvider = observer(function ActionContextProvider_({ // Override model accessors when models are provided in value const getActiveModels = valueModels && valueModels.length > 0 - ? (modelClass: new (...args: any[]) => T): T[] => { + ? (modelClass: new (...args: never[]) => T): T[] => { const matching = valueModels.filter( (model): model is T => model instanceof modelClass ); @@ -108,11 +108,11 @@ export const ActionContextProvider = observer(function ActionContextProvider_({ : baseContext.getActiveModels; const getActiveModel = ( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ): T | undefined => getActiveModels(modelClass)[0]; const getActivePolicies = ( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ): Policy[] => getActiveModels(modelClass) .map((node) => stores.policies.get(node.id)) diff --git a/app/hooks/useStores.ts b/app/hooks/useStores.ts index 71cc025368..b02ddae7d5 100644 --- a/app/hooks/useStores.ts +++ b/app/hooks/useStores.ts @@ -1,12 +1,15 @@ import { MobXProviderContext } from "mobx-react"; import { useContext } from "react"; -import type RootStore from "~/stores"; +import type RootStore from "~/stores/RootStore"; /** * Hook to access the MobX stores from the React context. * - * @returns The root store containing all application stores + * @returns The root store containing all application stores. */ -export default function useStores() { - return useContext(MobXProviderContext) as typeof RootStore; +export default function useStores(): RootStore { + const { rootStore } = useContext(MobXProviderContext) as { + rootStore: RootStore; + }; + return rootStore; } diff --git a/app/hooks/useThrottledCallback.ts b/app/hooks/useThrottledCallback.ts index 4d1beb80e2..9797f7337a 100644 --- a/app/hooks/useThrottledCallback.ts +++ b/app/hooks/useThrottledCallback.ts @@ -20,7 +20,9 @@ const defaultOptions: ThrottleSettings = { * @param dependencies The dependencies to watch for changes * @param options The throttle options */ -export default function useThrottledCallback any>( +export default function useThrottledCallback< + T extends (...args: never[]) => unknown, +>( fn: T, wait = 250, dependencies: React.DependencyList = [], diff --git a/app/hooks/useUnmount.ts b/app/hooks/useUnmount.ts index 8cf1f5cc4e..78260a5cf4 100644 --- a/app/hooks/useUnmount.ts +++ b/app/hooks/useUnmount.ts @@ -5,7 +5,7 @@ import { useRef, useEffect } from "react"; * * @param callback Function to be called on component unmount */ -const useUnmount = (callback: (...args: Array) => any) => { +const useUnmount = (callback: () => void) => { const ref = useRef(callback); ref.current = callback; diff --git a/app/index.tsx b/app/index.tsx index f8104b3d36..041bbfccbc 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -57,7 +57,7 @@ if (element) { const App = () => ( - + diff --git a/app/models/AuthenticationProvider.ts b/app/models/AuthenticationProvider.ts index cdc68ec6c5..2c1c5d804d 100644 --- a/app/models/AuthenticationProvider.ts +++ b/app/models/AuthenticationProvider.ts @@ -37,11 +37,12 @@ class AuthenticationProvider extends Model { @AfterDelete static afterDelete(model: AuthenticationProvider) { // Restore a placeholder record to allow re-connection - return (model.store as AuthenticationProvidersStore).add({ - ...model, - isEnabled: false, - isConnected: false, - }); + return (model.store as AuthenticationProvidersStore).add( + Object.assign({}, model, { + isEnabled: false, + isConnected: false, + }) + ); } } diff --git a/app/models/Document.ts b/app/models/Document.ts index ef361602de..13362d64f6 100644 --- a/app/models/Document.ts +++ b/app/models/Document.ts @@ -38,7 +38,7 @@ type SaveOptions = JSONObject & { export default class Document extends ArchivableModel implements Searchable { static modelName = "Document"; - constructor(fields: Record, store: DocumentsStore) { + constructor(fields: Record, store: DocumentsStore) { super(fields, store); this.embedsDisabled = Storage.get(`embedsDisabled-${this.id}`) ?? false; @@ -570,7 +570,7 @@ export default class Document extends ArchivableModel implements Searchable { ); // if saving is successful set the new values on the model itself - set(this, { ...params, ...model }); + set(this, Object.assign({}, params, model)); this.persistedAttributes = this.toAPI(); diff --git a/app/models/base/Model.ts b/app/models/base/Model.ts index 67f80f2037..d683a24d1f 100644 --- a/app/models/base/Model.ts +++ b/app/models/base/Model.ts @@ -28,7 +28,7 @@ export default abstract class Model { store: Store; - constructor(fields: Record, store: Store) { + constructor(fields: Record, store: Store) { this.store = store; this.updateData(fields); this.isNew = !this.id; @@ -43,7 +43,7 @@ export default abstract class Model { async loadRelations( this: Model, options: { withoutPolicies?: boolean } = {} - ): Promise { + ): Promise { // this is to ensure that multiple loads don’t happen in parallel if (this.loadingRelations) { return this.loadingRelations; @@ -90,7 +90,7 @@ export default abstract class Model { * @returns A promise that resolves with the updated model */ save = async ( - params?: Record, + params?: Record, options?: Record ): Promise => { const isNew = this.isNew; @@ -120,7 +120,7 @@ export default abstract class Model { ); // if saving is successful set the new values on the model itself - this.updateData({ ...params, ...model }); + this.updateData(Object.assign({}, params, model)); if (isNew) { LifecycleManager.executeHooks(this.constructor, "afterCreate", this); @@ -134,7 +134,7 @@ export default abstract class Model { } }; - updateData = action((data: Partial) => { + updateData = action((data: Record) => { if (this.initialized) { LifecycleManager.executeHooks(this.constructor, "beforeChange", this); } @@ -197,7 +197,7 @@ export default abstract class Model { * * @returns A plain object representation of the model */ - toAPI = (): Record => { + toAPI = (): Partial => { const fields = getFieldsForModel(this); return pick(this, fields); }; @@ -247,7 +247,7 @@ export default abstract class Model { protected persistedAttributes: Partial = {}; /** A promise that resolves when all relations have been loaded. */ - private loadingRelations: Promise | undefined; + private loadingRelations: Promise | undefined; /** A boolean representing if the constructor has been called. */ private initialized = false; diff --git a/app/models/decorators/Field.ts b/app/models/decorators/Field.ts index 28c3e43e4a..7fc4740d8e 100644 --- a/app/models/decorators/Field.ts +++ b/app/models/decorators/Field.ts @@ -12,9 +12,9 @@ export const getFieldsForModel = (target: T) => * @param target * @param propertyKey */ -const Field = (target: any, propertyKey: keyof T) => { +const Field = (target: Model, propertyKey: string | symbol) => { const className = target.constructor.name; - fields.set(className, [...(fields.get(className) || []), propertyKey]); + fields.set(className, [...(fields.get(className) ?? []), propertyKey]); }; export default Field; diff --git a/app/models/decorators/Lifecycle.ts b/app/models/decorators/Lifecycle.ts index 83db79d719..15d58b8ee1 100644 --- a/app/models/decorators/Lifecycle.ts +++ b/app/models/decorators/Lifecycle.ts @@ -1,24 +1,32 @@ -export class LifecycleManager { - private static hooks = new Map(); +type ModelClass = { readonly name: string }; +type Hook = (...args: unknown[]) => unknown; - public static getHooks(target: any, lifecycle: string) { +export class LifecycleManager { + private static hooks = new Map>(); + + public static getHooks(target: ModelClass, lifecycle: string): string[] { const key = `lifecycle:${lifecycle}`; const modelHooks = this.hooks.get(target.name); - return modelHooks?.get(key) || []; + return modelHooks?.get(key) ?? []; } - public static executeHooks(target: any, lifecycle: string, ...args: any[]) { + public static executeHooks( + target: ModelClass, + lifecycle: string, + ...args: unknown[] + ): void { const hooks = this.getHooks(target, lifecycle); - hooks.forEach((hook: keyof typeof target) => { - target[hook](...args); + hooks.forEach((hook) => { + const fn = (target as unknown as Record)[hook]; + fn(...args); }); } public static registerHook( - target: any, + target: ModelClass, propertyKey: string, lifecycle: string - ) { + ): void { const key = `lifecycle:${lifecycle}`; let modelHooks = this.hooks.get(target.name); @@ -37,42 +45,42 @@ export class LifecycleManager { } } -export function BeforeCreate(target: any, propertyKey: string) { +export function BeforeCreate(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "beforeCreate"); } -export function AfterCreate(target: any, propertyKey: string) { +export function AfterCreate(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "afterCreate"); } -export function BeforeUpdate(target: any, propertyKey: string) { +export function BeforeUpdate(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "beforeUpdate"); } -export function AfterUpdate(target: any, propertyKey: string) { +export function AfterUpdate(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "afterUpdate"); } -export function BeforeChange(target: any, propertyKey: string) { +export function BeforeChange(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "beforeChange"); } -export function AfterChange(target: any, propertyKey: string) { +export function AfterChange(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "afterChange"); } -export function BeforeRemove(target: any, propertyKey: string) { +export function BeforeRemove(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "beforeRemove"); } -export function AfterRemove(target: any, propertyKey: string) { +export function AfterRemove(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "afterRemove"); } -export function BeforeDelete(target: any, propertyKey: string) { +export function BeforeDelete(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "beforeDelete"); } -export function AfterDelete(target: any, propertyKey: string) { +export function AfterDelete(target: ModelClass, propertyKey: string) { LifecycleManager.registerHook(target, propertyKey, "afterDelete"); } diff --git a/app/models/decorators/Relation.ts b/app/models/decorators/Relation.ts index 3b55fbecc0..db100aa605 100644 --- a/app/models/decorators/Relation.ts +++ b/app/models/decorators/Relation.ts @@ -85,7 +85,7 @@ export default function Relation( classResolver: () => T, options?: RelationOptions ) { - return function (target: any, propertyKey: string) { + return function (target: Model, propertyKey: string) { const idKey = options?.multiple ? `${String(singular(propertyKey))}Ids` : `${String(propertyKey)}Id`; @@ -96,16 +96,16 @@ export default function Relation( // TODO: requestAnimationFrame is a temporary solution to a bug in rolldown compiled code that // will place static methods _after_ decorators. Temporary fix is to delay the registration until // the next frame. + const modelName = (target.constructor as typeof Model).modelName; requestAnimationFrame(() => { if (options) { - const configForClass = - relations.get(target.constructor.modelName) || new Map(); + const configForClass = relations.get(modelName) ?? new Map(); configForClass.set(propertyKey, { options, relationClassResolver: classResolver, idKey, }); - relations.set(target.constructor.modelName, configForClass); + relations.set(modelName, configForClass); } }); diff --git a/app/scenes/Login/Login.tsx b/app/scenes/Login/Login.tsx index 9a66503598..30e0d4ba5c 100644 --- a/app/scenes/Login/Login.tsx +++ b/app/scenes/Login/Login.tsx @@ -81,7 +81,7 @@ function Login({ children, onBack }: Props) { const handleGoSubdomain = React.useCallback(async (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.target)); - await navigateToSubdomain(data.subdomain.toString()); + await navigateToSubdomain(data.subdomain as string); }, []); React.useEffect(() => { diff --git a/app/scenes/Login/components/AuthenticationProvider.tsx b/app/scenes/Login/components/AuthenticationProvider.tsx index e5c275ecc1..82283c0966 100644 --- a/app/scenes/Login/components/AuthenticationProvider.tsx +++ b/app/scenes/Login/components/AuthenticationProvider.tsx @@ -104,7 +104,7 @@ function AuthenticationProvider(props: Props) { const input = document.createElement("input"); input.type = "hidden"; input.name = fieldName; - input.value = String(value); + input.value = String(value as string | number | boolean); formRef.current?.appendChild(input); } }); diff --git a/app/stores/EventsStore.ts b/app/stores/EventsStore.ts index 8da62fdecb..a34344a39e 100644 --- a/app/stores/EventsStore.ts +++ b/app/stores/EventsStore.ts @@ -2,6 +2,7 @@ import Event from "~/models/Event"; import type RootStore from "./RootStore"; import Store, { RPCAction } from "./base/Store"; +// oxlint-disable no-explicit-any -- Event generic must be `any` because the store holds events for all model types export default class EventsStore extends Store> { actions = [RPCAction.List]; diff --git a/app/stores/UiStore.ts b/app/stores/UiStore.ts index 8a3aa4271f..3c604a4157 100644 --- a/app/stores/UiStore.ts +++ b/app/stores/UiStore.ts @@ -26,6 +26,8 @@ export enum SystemTheme { Dark = "dark", } +export type ResolvedTheme = "light" | "dark" | "system"; + type PersistedData = Pick< UiStore, | "languagePromptDismissed" @@ -209,7 +211,9 @@ class UiStore { * @param modelClass the model class to filter by. * @returns array of active models of the specified type. */ - getActiveModels(modelClass: new (...args: any[]) => T): T[] { + getActiveModels( + modelClass: new (...args: never[]) => T + ): T[] { return Array.from(this.activeModels.values()).filter( (model) => model.constructor === modelClass ) as T[]; @@ -231,7 +235,7 @@ class UiStore { * @param modelClass optional model class to filter by. */ @action - clearActiveModels(modelClass?: new (...args: any[]) => Model): void { + clearActiveModels(modelClass?: new (...args: never[]) => Model): void { if (modelClass) { const modelsToRemove = this.getActiveModels(modelClass); modelsToRemove.forEach((model) => this.activeModels.delete(model.id)); @@ -247,7 +251,7 @@ class UiStore { * @returns the most recently added model of the specified type. */ getPrimaryActiveModel( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ): T | undefined { const models = this.getActiveModels(modelClass); return models[models.length - 1]; diff --git a/app/stores/base/Store.ts b/app/stores/base/Store.ts index 09834eb285..c1cf3c3224 100644 --- a/app/stores/base/Store.ts +++ b/app/stores/base/Store.ts @@ -44,6 +44,7 @@ export type PaginatedResponse = T[] & { }; }; +// oxlint-disable-next-line no-explicit-any export type FetchPageParams = PaginationParams & Record; export default abstract class Store { @@ -59,6 +60,7 @@ export default abstract class Store { @observable isLoaded = false; + // oxlint-disable-next-line no-explicit-any requests: Map> = new Map(); model: typeof Model; @@ -430,6 +432,7 @@ export default abstract class Store { @action fetchAll = async ( + // oxlint-disable-next-line no-explicit-any params?: Record ): Promise> => { const limit = params?.limit ?? Pagination.defaultLimit; diff --git a/app/types.ts b/app/types.ts index 2907719a20..2fce39484f 100644 --- a/app/types.ts +++ b/app/types.ts @@ -116,13 +116,13 @@ export type ActionContext = { // New API - work directly with Model instances getActiveModels: ( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ) => T[]; getActiveModel: ( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ) => T | undefined; getActivePolicies: ( - modelClass: new (...args: any[]) => T + modelClass: new (...args: never[]) => T ) => Policy[]; isModelActive: (model: Model) => boolean; activeModels: ReadonlySet; @@ -160,7 +160,7 @@ export type Action = BaseAction & { tooltip?: | ((context: ActionContext) => React.ReactChild | undefined) | React.ReactChild; - perform: (context: ActionContext) => any; + perform: (context: ActionContext) => unknown; }; export type InternalLinkAction = BaseAction & { diff --git a/app/utils/ApiClient.ts b/app/utils/ApiClient.ts index 4d316f7be8..91a4af051a 100644 --- a/app/utils/ApiClient.ts +++ b/app/utils/ApiClient.ts @@ -47,6 +47,7 @@ class ApiClient { shareId?: string; /** Map of in-flight POST requests for deduplication, keyed by path + body. */ + // oxlint-disable-next-line no-explicit-any private inflightRequests = new Map>(); constructor(options: Options = {}) { @@ -57,6 +58,7 @@ class ApiClient { this.shareId = shareId; }; + // oxlint-disable-next-line no-explicit-any fetch = async ( path: string, method: string, @@ -93,7 +95,7 @@ class ApiClient { // toggling Content-Type to application/json if ( typeof data === "object" && - (data || "").toString() === "[object Object]" + Object.prototype.toString.call(data) === "[object Object]" ) { body = JSON.stringify(data); } @@ -206,7 +208,7 @@ class ApiClient { const error: { message?: string; error?: string; - data?: Record; + data?: Record; } = {}; try { @@ -277,12 +279,14 @@ class ApiClient { throw err; }; + // oxlint-disable-next-line no-explicit-any get = ( path: string, data: JSONObject | undefined, options?: FetchOptions ) => this.fetch(path, "GET", data, options); + // oxlint-disable-next-line no-explicit-any post = ( path: string, data?: JSONObject | FormData, diff --git a/app/utils/Logger.ts b/app/utils/Logger.ts index af7368f5cf..7084371b53 100644 --- a/app/utils/Logger.ts +++ b/app/utils/Logger.ts @@ -13,6 +13,7 @@ type LogCategory = | "plugins" | "policies"; +// oxlint-disable-next-line no-explicit-any type Extra = Record; class Logger { diff --git a/app/utils/download.ts b/app/utils/download.ts index 44245e091b..a85d25159d 100644 --- a/app/utils/download.ts +++ b/app/utils/download.ts @@ -41,9 +41,8 @@ export default function download( } // go ahead and download dataURLs right away - if (String(x).match(/^data:[\w+-]+\/[\w+-]+[,;]/)) { - // @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1. - return saver(x); // everyone else can save dataURLs un-processed + if (typeof x === "string" && x.match(/^data:[\w+-]+\/[\w+-]+[,;]/)) { + return saver(x); } // end if dataURL passed? diff --git a/app/utils/lazyWithRetry.ts b/app/utils/lazyWithRetry.ts index 1620bee123..8bd8aa3892 100644 --- a/app/utils/lazyWithRetry.ts +++ b/app/utils/lazyWithRetry.ts @@ -1,5 +1,6 @@ import * as React from "react"; +// oxlint-disable no-explicit-any -- ComponentType is the standard React pattern for generic component constraints type ComponentPromise> = Promise<{ default: T; }>; diff --git a/server/logging/Logger.ts b/server/logging/Logger.ts index e0843f9a99..a062242fe4 100644 --- a/server/logging/Logger.ts +++ b/server/logging/Logger.ts @@ -60,8 +60,8 @@ class Logger { winston.format.printf( ({ message, level, label, ...extra }) => `${level}: ${ - label ? styleText("bold", `[${String(label)}] `) : "" - }${String(message)} ${isEmpty(extra) ? "" : JSON.stringify(extra)}` + label ? styleText("bold", `[${label as string}] `) : "" + }${message as string} ${isEmpty(extra) ? "" : JSON.stringify(extra)}` ) ), }) diff --git a/server/models/helpers/ProsemirrorHelper.tsx b/server/models/helpers/ProsemirrorHelper.tsx index e6085830fd..04e53b2e6d 100644 --- a/server/models/helpers/ProsemirrorHelper.tsx +++ b/server/models/helpers/ProsemirrorHelper.tsx @@ -371,13 +371,13 @@ export class ProsemirrorHelper extends SharedProsemirrorHelper { function replaceAttachmentUrls(node: ProsemirrorData) { if (node.attrs?.src) { - node.attrs.src = getMapping(String(node.attrs.src)); + node.attrs.src = getMapping(node.attrs.src as string); } else if (node.attrs?.href) { - node.attrs.href = getMapping(String(node.attrs.href)); + node.attrs.href = getMapping(node.attrs.href as string); } else if (node.marks) { node.marks.forEach((mark) => { if (mark.attrs?.href) { - mark.attrs.href = getMapping(String(mark.attrs.href)); + mark.attrs.href = getMapping(mark.attrs.href as string); } }); } diff --git a/server/test/support.ts b/server/test/support.ts index 7f05f48158..f2a1afbd5e 100644 --- a/server/test/support.ts +++ b/server/test/support.ts @@ -63,9 +63,14 @@ export function withAPIContext( * @param obj Object to convert to form-urlencoded string * @returns Form-urlencoded string representation of the object */ -export function toFormData(obj: Record): string { +export function toFormData( + obj: Record +): string { return Object.entries(obj) - .filter(([_, value]) => value !== undefined) + .filter( + (entry): entry is [string, string | number | boolean] => + entry[1] !== undefined && entry[1] !== null + ) .map( ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}` diff --git a/server/tools/documents.ts b/server/tools/documents.ts index f65dd1cf63..8a4954e751 100644 --- a/server/tools/documents.ts +++ b/server/tools/documents.ts @@ -356,7 +356,7 @@ export function documentTools(server: McpServer, scopes: string[]) { }, { type: "text" as const, - text: String(text ?? ""), + text: typeof text === "string" ? text : "", }, ], } satisfies CallToolResult; @@ -606,7 +606,7 @@ export function documentTools(server: McpServer, scopes: string[]) { }, { type: "text" as const, - text: String(text ?? ""), + text: typeof text === "string" ? text : "", }, ], } satisfies CallToolResult; diff --git a/server/tools/fetch.ts b/server/tools/fetch.ts index d984619744..a2b47d2c10 100644 --- a/server/tools/fetch.ts +++ b/server/tools/fetch.ts @@ -119,7 +119,7 @@ export function fetchTool(server: McpServer, scopes: string[]) { }, { type: "text" as const, - text: String(text ?? ""), + text: typeof text === "string" ? text : "", }, ], } satisfies CallToolResult; diff --git a/server/utils/DocumentConverter.ts b/server/utils/DocumentConverter.ts index 28878f446f..b67582bd79 100644 --- a/server/utils/DocumentConverter.ts +++ b/server/utils/DocumentConverter.ts @@ -299,9 +299,9 @@ export class DocumentConverter { // Replace the content-location with a data URI for each attachment. for (const attachment of parsed.attachments) { - const contentLocation = String( - attachment.headers.get("content-location") ?? "" - ); + const contentLocation = + (attachment.headers.get("content-location") as string | undefined) ?? + ""; const id = contentLocation.split("/").pop(); if (!id) { diff --git a/server/utils/__mocks__/MutexLock.ts b/server/utils/__mocks__/MutexLock.ts index df106fd325..bf843901b0 100644 --- a/server/utils/__mocks__/MutexLock.ts +++ b/server/utils/__mocks__/MutexLock.ts @@ -13,6 +13,4 @@ export class MutexLock { }), }; } - - private static redlock: any; } diff --git a/shared/editor/components/Mentions.tsx b/shared/editor/components/Mentions.tsx index 597d18ff2d..76daae135c 100644 --- a/shared/editor/components/Mentions.tsx +++ b/shared/editor/components/Mentions.tsx @@ -195,10 +195,15 @@ export const MentionURL = (props: IssueUrlProps) => { ...attrs } = getAttributesFromNode(node); - const url = String(attrs.href); - const unfurl = unfurls.get(url)?.data ?? unfurlAttr; + const url = typeof attrs.href === "string" ? attrs.href : undefined; + const unfurl = url ? (unfurls.get(url)?.data ?? unfurlAttr) : undefined; React.useEffect(() => { + if (!url) { + setLoaded(true); + return; + } + const fetchUnfurl = async () => { try { const unfurlModel = await unfurls.fetchUnfurl({ url }); diff --git a/shared/editor/lib/table.ts b/shared/editor/lib/table.ts index 2300a3e615..b71cba2502 100644 --- a/shared/editor/lib/table.ts +++ b/shared/editor/lib/table.ts @@ -142,7 +142,8 @@ export function setCellAttrs(node: Node): Attrs { attrs["data-colwidth"] = node.attrs.colwidth.map(Number).join(","); } else { attrs.style = - (attrs.style ?? "") + `min-width: ${Number(node.attrs.colwidth[0])}px;`; + ((attrs.style as string) ?? "") + + `min-width: ${Number(node.attrs.colwidth[0])}px;`; } } if (Array.isArray(node.attrs.marks)) { @@ -156,7 +157,7 @@ export function setCellAttrs(node: Node): Attrs { const color = backgroundMark.attrs!.color as string; attrs["data-bgcolor"] = color; attrs.style = - (attrs.style ?? "") + + ((attrs.style as string) ?? "") + `--cell-bg-color: ${color}; --cell-text-color: ${readableColor(color)};`; } } diff --git a/shared/hooks/useStores.ts b/shared/hooks/useStores.ts index fb92888a90..7751f9d0a3 100644 --- a/shared/hooks/useStores.ts +++ b/shared/hooks/useStores.ts @@ -2,5 +2,5 @@ import { MobXProviderContext } from "mobx-react"; import * as React from "react"; export default function useStores() { - return React.useContext(MobXProviderContext); + return React.useContext(MobXProviderContext).rootStore; } diff --git a/shared/styles/index.ts b/shared/styles/index.ts index 756ef15455..6c15bcd136 100644 --- a/shared/styles/index.ts +++ b/shared/styles/index.ts @@ -30,7 +30,7 @@ export const ellipsis = () => ` */ export const s = (key: keyof DefaultTheme) => (props: { theme: DefaultTheme }) => - String(props.theme[key]); + props.theme[key] as string; /** * Mixin to hide scrollbars. diff --git a/shared/utils/ProsemirrorHelper.ts b/shared/utils/ProsemirrorHelper.ts index 92405019f3..4dfa30bb9d 100644 --- a/shared/utils/ProsemirrorHelper.ts +++ b/shared/utils/ProsemirrorHelper.ts @@ -506,19 +506,19 @@ export class ProsemirrorHelper { if ( node.type === "image" && node.attrs?.src && - regex.test(String(node.attrs.src)) + regex.test(node.attrs.src as string) ) { node.attrs.src = env.URL + node.attrs.src; } else if ( node.type === "video" && node.attrs?.src && - regex.test(String(node.attrs.src)) + regex.test(node.attrs.src as string) ) { node.attrs.src = env.URL + node.attrs.src; } else if ( node.type === "attachment" && node.attrs?.href && - regex.test(String(node.attrs.href)) + regex.test(node.attrs.href as string) ) { node.attrs.href = env.URL + node.attrs.href; } diff --git a/shared/utils/csv.ts b/shared/utils/csv.ts index c522ebc0e9..dee128aee3 100644 --- a/shared/utils/csv.ts +++ b/shared/utils/csv.ts @@ -40,7 +40,10 @@ export class CSVHelper { return ""; } - const stringValue = String(value); + const stringValue = + typeof value === "object" + ? JSON.stringify(value) + : String(value as string | number | boolean); // If the value contains comma, quote, or newline, wrap it in quotes and escape internal quotes if (