diff --git a/biome.json b/biome.json index 8e9368f4..17780e48 100644 --- a/biome.json +++ b/biome.json @@ -1,14 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.1.1/schema.json", - "files": { - "experimentalScannerIgnores": [ - "**/node_modules/**", - "**/dist/**", - "**/build/**", - "**/.devenv/**", - "**/.direnv/**" - ] - }, + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", "formatter": { "enabled": true, "indentStyle": "space", @@ -24,9 +15,75 @@ }, "a11y": { "noStaticElementInteractions": "off" + }, + "style": { + "noDefaultExport": "error", + "noExportedImports": "error", + "noInferrableTypes": "error", + "noMagicNumbers": "error", + "noNegationElse": "error", + "noNestedTernary": "error", + "noNonNullAssertion": "error", + "noParameterAssign": "error", + "noParameterProperties": "error", + "noShoutyConstants": "error", + "noUselessElse": "error", + "noYodaExpression": "error", + "useArrayLiterals": "error", + "useAsConstAssertion": "error", + "useBlockStatements": "error", + "useCollapsedElseIf": "error", + "useCollapsedIf": "error", + "useConsistentArrayType": { + "level": "error", + "options": { + "syntax": "shorthand" + } + }, + "useConsistentCurlyBraces": "error", + "useConsistentObjectDefinitions": { + "level": "error", + "options": { + "syntax": "shorthand" + } + }, + "useConst": "error", + "useDefaultParameterLast": "error", + "useDefaultSwitchClause": "error", + "useEnumInitializers": "error", + "useExplicitLengthCheck": "error", + "useForOf": "error", + "useFragmentSyntax": "error", + "useImportType": "error", + "useLiteralEnumMembers": "error", + "useNumberNamespace": "error", + "useNumericSeparators": "error", + "useObjectSpread": "error", + "useReactFunctionComponents": "error", + "useReadonlyClassProperties": "error", + "useSelfClosingElements": "error", + "useShorthandFunctionType": "error", + "useSingleVarDeclarator": "error", + "useTemplate": "error", + "useThrowNewError": "error", + "useThrowOnlyError": "error" } } }, + "overrides": [ + { + "includes": [ + "**/*.test.ts" + ], + "linter": { + "rules": { + "style": { + "noMagicNumbers": "off" + } + } + } + } + ], "assist": { "actions": { "source": { diff --git a/plugin/src/api/domain/task.ts b/plugin/src/api/domain/task.ts index c930b008..84ce62ba 100644 --- a/plugin/src/api/domain/task.ts +++ b/plugin/src/api/domain/task.ts @@ -29,10 +29,17 @@ export type Task = { order: number; }; -export type Priority = 1 | 2 | 3 | 4; +export const Priorities = { + P4: 1, + P3: 2, + P2: 3, + P1: 4, +} as const; + +export type Priority = (typeof Priorities)[keyof typeof Priorities]; export type CreateTaskParams = { - priority: number; + priority: Priority; projectId: ProjectId; description?: string; sectionId?: SectionId; diff --git a/plugin/src/api/fetcher.ts b/plugin/src/api/fetcher.ts index 9da4f096..e1701f89 100644 --- a/plugin/src/api/fetcher.ts +++ b/plugin/src/api/fetcher.ts @@ -4,6 +4,25 @@ export interface WebFetcher { fetch(params: RequestParams): Promise; } +export type StatusCode = number; + +export const StatusCode = { + isError(code: StatusCode): boolean { + return code >= StatusCodes.BadRequest; + }, + isServerError(code: StatusCode): boolean { + return code >= StatusCodes.InternalServerError; + }, +} as const; + +export const StatusCodes = { + OK: 200, + BadRequest: 400, + Unauthorized: 401, + Forbidden: 403, + InternalServerError: 500, +} as const; + export type RequestParams = { url: string; method: string; @@ -12,7 +31,7 @@ export type RequestParams = { }; export type WebResponse = { - statusCode: number; + statusCode: StatusCode; body: string; }; diff --git a/plugin/src/api/index.ts b/plugin/src/api/index.ts index fda6bb59..644fefcc 100644 --- a/plugin/src/api/index.ts +++ b/plugin/src/api/index.ts @@ -6,8 +6,8 @@ import type { Project } from "@/api/domain/project"; import type { Section } from "@/api/domain/section"; import type { CreateTaskParams, Task, TaskId } from "@/api/domain/task"; import type { UserInfo } from "@/api/domain/user"; -import type { RequestParams, WebFetcher, WebResponse } from "@/api/fetcher"; -import debug from "@/log"; +import { type RequestParams, StatusCode, type WebFetcher, type WebResponse } from "@/api/fetcher"; +import { debug } from "@/log"; type PaginatedResponse = { results: T[]; @@ -15,8 +15,8 @@ type PaginatedResponse = { }; export class TodoistApiClient { - private token: string; - private fetcher: WebFetcher; + private readonly token: string; + private readonly fetcher: WebFetcher; constructor(token: string, fetcher: WebFetcher) { this.token = token; @@ -26,14 +26,14 @@ export class TodoistApiClient { public async getTasks(filter?: string): Promise { if (filter !== undefined) { return await this.doPaginated("/tasks/filter", { query: filter }); - } else { - return await this.doPaginated("/tasks"); } + + return await this.doPaginated("/tasks"); } public async createTask(content: string, options?: CreateTaskParams): Promise { const body = snakify({ - content: content, + content, ...(options ?? {}), }); const response = await this.do("/tasks", "POST", body); @@ -87,7 +87,7 @@ export class TodoistApiClient { private async do(path: string, method: string, json?: object): Promise { const params: RequestParams = { url: `https://api.todoist.com/api/v1${path}`, - method: method, + method, headers: { Authorization: `Bearer ${this.token}`, }, @@ -110,7 +110,7 @@ export class TodoistApiClient { context: response, }); - if (response.statusCode >= 400) { + if (StatusCode.isError(response.statusCode)) { throw new TodoistApiError(params, response); } @@ -119,7 +119,7 @@ export class TodoistApiClient { } export class TodoistApiError extends Error { - public statusCode: number; + public statusCode: StatusCode; constructor(request: RequestParams, response: WebResponse) { const message = `[${request.method}] ${request.url} returned '${response.statusCode}: ${response.body}`; diff --git a/plugin/src/commands/index.ts b/plugin/src/commands/index.ts index 555eb308..54e118fc 100644 --- a/plugin/src/commands/index.ts +++ b/plugin/src/commands/index.ts @@ -8,7 +8,7 @@ import { import { t } from "@/i18n"; import type { Translations } from "@/i18n/translation"; import type TodoistPlugin from "@/index"; -import debug from "@/log"; +import { debug } from "@/log"; export type MakeCommand = ( plugin: TodoistPlugin, diff --git a/plugin/src/data/deadline.ts b/plugin/src/data/deadline.ts index 5b577b0c..a5d448b3 100644 --- a/plugin/src/data/deadline.ts +++ b/plugin/src/data/deadline.ts @@ -84,6 +84,8 @@ const formatDeadline = (info: DeadlineInfo): string => { return i18n.lastWeekday(getFormatter("weekday").format(info.raw)); case "nextWeek": return getFormatter("weekday").format(info.raw); + default: + break; } if (!info.isCurrentYear) { diff --git a/plugin/src/data/dueDate.ts b/plugin/src/data/dueDate.ts index 568bde16..3a0fe9a1 100644 --- a/plugin/src/data/dueDate.ts +++ b/plugin/src/data/dueDate.ts @@ -170,6 +170,8 @@ const formatDate = (info: DateInfo): string => { return i18n.lastWeekday(getFormatter("weekday").format(info.raw)); case "nextWeek": return getFormatter("weekday").format(info.raw); + default: + break; } if (!info.isCurrentYear) { @@ -194,6 +196,8 @@ const formatDueDateHeader = (due: DueDate): string => { case "tomorrow": parts.push(i18n.tomorrow); break; + default: + break; } return parts.join(" ‧ "); diff --git a/plugin/src/data/index.ts b/plugin/src/data/index.ts index 9d20bf24..0c52fe37 100644 --- a/plugin/src/data/index.ts +++ b/plugin/src/data/index.ts @@ -4,6 +4,7 @@ import type { Project, ProjectId } from "@/api/domain/project"; import type { Section, SectionId } from "@/api/domain/section"; import type { Task as ApiTask, CreateTaskParams, TaskId } from "@/api/domain/task"; import type { UserInfo } from "@/api/domain/user"; +import { StatusCode, StatusCodes } from "@/api/fetcher"; import { Repository, type RepositoryReader } from "@/data/repository"; import { SubscriptionManager, type UnsubscribeCallback } from "@/data/subscriptions"; import type { Task } from "@/data/task"; @@ -146,10 +147,10 @@ export class TodoistAdapter { description: apiTask.description, project: project ?? makeUnknownProject(apiTask.projectId), - section: section, + section, parentId: apiTask.parentId ?? undefined, - labels: labels, + labels, priority: apiTask.priority, due: apiTask.due ?? undefined, @@ -253,13 +254,13 @@ class Subscription { kind: QueryErrorKind.Unknown, }; if (error instanceof TodoistApiError) { - if (error.statusCode === 400) { + if (error.statusCode === StatusCodes.BadRequest) { result.kind = QueryErrorKind.BadRequest; - } else if (error.statusCode === 401) { + } else if (error.statusCode === StatusCodes.Unauthorized) { result.kind = QueryErrorKind.Unauthorized; - } else if (error.statusCode === 403) { + } else if (error.statusCode === StatusCodes.Forbidden) { result.kind = QueryErrorKind.Forbidden; - } else if (error.statusCode > 500) { + } else if (StatusCode.isServerError(error.statusCode)) { result.kind = QueryErrorKind.ServerError; } } diff --git a/plugin/src/data/subscriptions.ts b/plugin/src/data/subscriptions.ts index d80e4fcd..68b39d3b 100644 --- a/plugin/src/data/subscriptions.ts +++ b/plugin/src/data/subscriptions.ts @@ -3,7 +3,7 @@ export type UnsubscribeCallback = () => void; export class SubscriptionManager { private readonly subscriptions: Map = new Map(); - private generator: Generator = subscriptionIdGenerator(); + private readonly generator: Generator = subscriptionIdGenerator(); public subscribe(value: T): UnsubscribeCallback { const id = this.generator.next().value; diff --git a/plugin/src/data/transformations/grouping.ts b/plugin/src/data/transformations/grouping.ts index 37422fae..3403f6fb 100644 --- a/plugin/src/data/transformations/grouping.ts +++ b/plugin/src/data/transformations/grouping.ts @@ -24,7 +24,7 @@ export function groupBy(tasks: Task[], groupBy: GroupVariant): GroupedTasks[] { case GroupVariant.Label: return groupByLabel(tasks); default: - throw Error(`Cannot group by ${groupBy}`); + throw new Error(`Cannot group by ${groupBy}`); } } diff --git a/plugin/src/data/transformations/relationships.ts b/plugin/src/data/transformations/relationships.ts index 19f059bb..ed98b4dd 100644 --- a/plugin/src/data/transformations/relationships.ts +++ b/plugin/src/data/transformations/relationships.ts @@ -22,7 +22,7 @@ export function buildTaskTree(tasks: Task[]): TaskTree[] { if (parent !== undefined) { const child = mapping.get(task.id); if (child === undefined) { - throw Error("Expected to find task in map"); + throw new Error("Expected to find task in map"); } parent.children.push(child); } @@ -31,7 +31,7 @@ export function buildTaskTree(tasks: Task[]): TaskTree[] { return roots.map((id) => { const tree = mapping.get(id); if (tree === undefined) { - throw Error("Expected to find task in map"); + throw new Error("Expected to find task in map"); } return tree; }); diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 12625234..d6a1574b 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -11,6 +11,7 @@ import { makeServices, type Services } from "@/services"; import { type Settings, useSettingsStore } from "@/settings"; import { SettingsTab } from "@/ui/settings"; +// biome-ignore lint/style/noDefaultExport: We must use default export for Obsidian plugins export default class TodoistPlugin extends Plugin { public readonly services: Services; diff --git a/plugin/src/infra/time.ts b/plugin/src/infra/time.ts index 74c73b11..dd004f6c 100644 --- a/plugin/src/infra/time.ts +++ b/plugin/src/infra/time.ts @@ -17,3 +17,8 @@ export const now = (): ZonedDateTime => { export const timezone = (): string => { return getLocalTimeZone(); }; + +const millisInSecond = 1000; +export const secondsToMillis = (seconds: number): number => { + return seconds * millisInSecond; +}; diff --git a/plugin/src/log.ts b/plugin/src/log.ts index 31120466..f7b8954d 100644 --- a/plugin/src/log.ts +++ b/plugin/src/log.ts @@ -1,6 +1,6 @@ import { useSettingsStore } from "@/settings"; -export default function debug(log: string | LogMessage) { +export function debug(log: string | LogMessage) { if (!useSettingsStore.getState().debugLogging) { return; } diff --git a/plugin/src/query/injector.tsx b/plugin/src/query/injector.tsx index 50d6603d..a7105ae0 100644 --- a/plugin/src/query/injector.tsx +++ b/plugin/src/query/injector.tsx @@ -5,7 +5,7 @@ import { createRoot, type Root } from "react-dom/client"; import { create, type StoreApi, type UseBoundStore } from "zustand"; import type TodoistPlugin from "@/index"; -import debug from "@/log"; +import { debug } from "@/log"; import { parseQuery } from "@/query/parser"; import { applyReplacements } from "@/query/replacements"; import { @@ -95,15 +95,19 @@ class ReactRenderer extends MarkdownRenderChild { for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { - if (addedNode instanceof HTMLElement) { - if (addedNode.classList.contains("edit-block-button")) { - addedNode.hide(); - this.store.setState({ - click: () => addedNode.click(), - }); - return; - } + if (!(addedNode instanceof HTMLElement)) { + continue; } + + if (!addedNode.classList.contains("edit-block-button")) { + continue; + } + + addedNode.hide(); + this.store.setState({ + click: () => addedNode.click(), + }); + return; } } }); diff --git a/plugin/src/services/modals.tsx b/plugin/src/services/modals.tsx index 7d7136ee..9101e4f2 100644 --- a/plugin/src/services/modals.tsx +++ b/plugin/src/services/modals.tsx @@ -29,7 +29,7 @@ class ReactModal extends Modal { const modal: ModalInfo = { close: () => this.close(), - popoverContainerEl: popoverContainerEl, + popoverContainerEl, }; if (opts.dontCloseOnExternalClick ?? false) { diff --git a/plugin/src/ui/components/markdown/index.tsx b/plugin/src/ui/components/markdown/index.tsx index 4bfe976e..70fefed2 100644 --- a/plugin/src/ui/components/markdown/index.tsx +++ b/plugin/src/ui/components/markdown/index.tsx @@ -9,7 +9,7 @@ interface MarkdownProps { className?: string; } -const Markdown: React.FC = ({ content, className }) => { +export const Markdown: React.FC = ({ content, className }) => { const renderChild = RenderChildContext.use(); const ref = useRef(null); @@ -38,5 +38,3 @@ const Markdown: React.FC = ({ content, className }) => { return
; }; - -export default Markdown; diff --git a/plugin/src/ui/components/token-validation-icon/index.tsx b/plugin/src/ui/components/token-validation-icon/index.tsx index bf8310bb..adb38053 100644 --- a/plugin/src/ui/components/token-validation-icon/index.tsx +++ b/plugin/src/ui/components/token-validation-icon/index.tsx @@ -16,5 +16,9 @@ export const TokenValidationIcon: React.FC<{ return ; case "success": return ; + default: { + const _: never = status; + throw new Error("Unknown token validation status"); + } } }; diff --git a/plugin/src/ui/createTaskModal/DueDateSelector.tsx b/plugin/src/ui/createTaskModal/DueDateSelector.tsx index e9cd3bf2..6dc47fb7 100644 --- a/plugin/src/ui/createTaskModal/DueDateSelector.tsx +++ b/plugin/src/ui/createTaskModal/DueDateSelector.tsx @@ -272,8 +272,9 @@ type TimeDialogProps = { setTimeInfo: (timeInfo: DueDate["timeInfo"] | undefined) => void; }; +const segmentDurationMins = 15; // We want enough options to get to 23h 45m. -const MAX_DURATION_SEGMENTS = (24 * 60 - 15) / 15; +const numDurationSegments = (24 * 60 - segmentDurationMins) / segmentDurationMins; const TimeDialog: React.FC = ({ selectedTimeInfo, setTimeInfo }) => { const i18n = t().createTaskModal.dateSelector.timeDialog; @@ -282,10 +283,10 @@ const TimeDialog: React.FC = ({ selectedTimeInfo, setTimeInfo } undefined, ...Array.from( { - length: MAX_DURATION_SEGMENTS, + length: numDurationSegments, }, (_, i) => ({ - amount: (i + 1) * 15, + amount: (i + 1) * segmentDurationMins, unit: "minute" as const, }), ), diff --git a/plugin/src/ui/createTaskModal/Popover.tsx b/plugin/src/ui/createTaskModal/Popover.tsx index 20f42f86..b8d9e139 100644 --- a/plugin/src/ui/createTaskModal/Popover.tsx +++ b/plugin/src/ui/createTaskModal/Popover.tsx @@ -5,6 +5,8 @@ import { Popover as AriaPopover, type PopoverProps } from "react-aria-components import { ModalContext } from "@/ui/context"; +const defaultMaxHeight = 500; + type Props = { maxHeight?: number; defaultPlacement?: PlacementDetails["placement"]; @@ -19,7 +21,7 @@ export const Popover: React.FC> = ({ return ( void; }; -const options: Priority[] = [4, 3, 2, 1]; +const options: Priority[] = [Priorities.P1, Priorities.P2, Priorities.P3, Priorities.P4]; export const PrioritySelector: React.FC = ({ selected, setSelected }) => { const onSelected = (key: Key) => { if (typeof key === "string") { - throw Error("unexpected key type"); + throw new Error("unexpected key type"); } // Should be a safe cast since we only use valid priorities @@ -66,13 +66,17 @@ const getLabel = ( i18n: Translations["createTaskModal"]["prioritySelector"], ): string => { switch (priority) { - case 1: + case Priorities.P4: return i18n.p4; - case 2: + case Priorities.P3: return i18n.p3; - case 3: + case Priorities.P2: return i18n.p2; - case 4: + case Priorities.P1: return i18n.p1; + default: { + const _: never = priority; + throw new Error("Unknown priority"); + } } }; diff --git a/plugin/src/ui/createTaskModal/ProjectSelector.tsx b/plugin/src/ui/createTaskModal/ProjectSelector.tsx index b94b857f..b9716d12 100644 --- a/plugin/src/ui/createTaskModal/ProjectSelector.tsx +++ b/plugin/src/ui/createTaskModal/ProjectSelector.tsx @@ -40,7 +40,7 @@ export const ProjectSelector: React.FC = ({ selected, setSelected }) => { const onSelect = (key: Key) => { if (typeof key === "number") { - throw Error("Unexpected key type: number"); + throw new Error("Unexpected key type: number"); } const [id, isSection] = ItemKey.parse(key); @@ -48,7 +48,7 @@ export const ProjectSelector: React.FC = ({ selected, setSelected }) => { if (isSection) { const section = todoistData.sections.byId(id); if (section === undefined) { - throw Error("Could not find selected section"); + throw new Error("Could not find selected section"); } setSelected({ @@ -245,7 +245,7 @@ const ButtonLabel: React.FC = ({ projectId, sectionId }) => { const selectedProject = projects.byId(projectId); if (selectedProject === undefined) { - throw Error("Could not find selected project"); + throw new Error("Could not find selected project"); } const selectedSection = sectionId !== undefined ? sections.byId(sectionId) : undefined; @@ -294,7 +294,7 @@ function buildProjectHierarchy(plugin: TodoistPlugin): ProjectHeirarchy { const parent = mapped.get(project.parentId); if (child === undefined) { - throw Error("Failed to find project in map"); + throw new Error("Failed to find project in map"); } // In this scenario, we could be in a weird half-way sync state. @@ -329,7 +329,7 @@ function buildProjectHierarchy(plugin: TodoistPlugin): ProjectHeirarchy { .map((project) => { const nested = mapped.get(project.id); if (nested === undefined) { - throw Error("Failed to find root project in map"); + throw new Error("Failed to find root project in map"); } return nested; diff --git a/plugin/src/ui/createTaskModal/index.tsx b/plugin/src/ui/createTaskModal/index.tsx index af08dece..fb3a37f9 100644 --- a/plugin/src/ui/createTaskModal/index.tsx +++ b/plugin/src/ui/createTaskModal/index.tsx @@ -31,6 +31,8 @@ import "./styles.scss"; import type { Translations } from "@/i18n/translation"; import { OptionsSelector } from "@/ui/createTaskModal/OptionsSelector"; +const readyCheckIntervalMs = 500; + export type LinkDestination = "content" | "description"; export type TaskCreationOptions = { @@ -71,6 +73,10 @@ const calculateDefaultDueDate = (setting: DueDateDefaultSetting): DueDate | unde date: today().add({ days: 1 }), timeInfo: undefined, }; + default: { + const _: never = setting; + throw new Error("Unknown due date default setting"); + } } }; @@ -138,7 +144,7 @@ export const CreateTaskModal: React.FC = (props) => { // biome-ignore lint/correctness/useExhaustiveDependencies: we don't want to reset this when isReady changes. useEffect(() => { - const id = window.setInterval(refreshIsReady, 500); + const id = window.setInterval(refreshIsReady, readyCheckIntervalMs); return () => window.clearInterval(id); }, []); @@ -207,7 +213,7 @@ const CreateTaskModalContent: React.FC = ({ const params: CreateTaskParams = { description: buildWithLink(description, options.appendLinkTo === "description"), - priority: priority, + priority, labels: labels.map((l) => l.name), projectId: project.projectId, sectionId: project.sectionId, @@ -242,6 +248,8 @@ const CreateTaskModalContent: React.FC = ({ case "add-copy-web": url = `https://todoist.com/app/project/${task.projectId}/task/${task.id}`; break; + default: + break; } if (url !== undefined) { @@ -270,6 +278,10 @@ const CreateTaskModalContent: React.FC = ({ return i18n.addTaskAndCopyAppLabel; case "add-copy-web": return i18n.addTaskAndCopyWebLabel; + default: { + const _: never = action; + throw new Error("Unknown add task action"); + } } }; @@ -371,7 +383,7 @@ const getInboxProject = (plugin: TodoistPlugin): ProjectIdentifier => { const i18n = t().createTaskModal; new Notice(i18n.failedToFindInboxNotice); - throw Error("Could not find inbox project"); + throw new Error("Could not find inbox project"); }; const getLinkForFile = (file: TFile): string => { diff --git a/plugin/src/ui/onboardingModal/TokenInputForm.tsx b/plugin/src/ui/onboardingModal/TokenInputForm.tsx index 9daa78f8..381c9e27 100644 --- a/plugin/src/ui/onboardingModal/TokenInputForm.tsx +++ b/plugin/src/ui/onboardingModal/TokenInputForm.tsx @@ -3,10 +3,12 @@ import { useEffect, useRef, useState } from "react"; import { Button, FieldError, Group, Input, Label, TextField } from "react-aria-components"; import { t } from "@/i18n"; -import debug from "@/log"; +import { debug } from "@/log"; import { TokenValidation } from "@/token"; import { TokenValidationIcon } from "@/ui/components/token-validation-icon"; +const debouncingDelayMs = 500; + type Props = { onTokenSubmit: (token: string) => void; tester: (token: string) => Promise; @@ -58,7 +60,7 @@ export const TokenInputForm: React.FC = ({ onTokenSubmit, tester }) => { setValidationStatus(result); } }); - }, 500); + }, debouncingDelayMs); debounceTimeout.current = timeoutId; return () => { diff --git a/plugin/src/ui/query/QueryHeader.tsx b/plugin/src/ui/query/QueryHeader.tsx index 7ee33a1b..99f631c9 100644 --- a/plugin/src/ui/query/QueryHeader.tsx +++ b/plugin/src/ui/query/QueryHeader.tsx @@ -18,6 +18,10 @@ const getAddTaskCommandId = (settings: Settings): CommandId => { return "add-task-page-description"; case "off": return "add-task"; + default: { + const _: never = settings.addTaskButtonAddsPageLink; + throw new Error("Unknown add task button setting"); + } } }; diff --git a/plugin/src/ui/query/QueryRoot.tsx b/plugin/src/ui/query/QueryRoot.tsx index fcd5381f..a9df6807 100644 --- a/plugin/src/ui/query/QueryRoot.tsx +++ b/plugin/src/ui/query/QueryRoot.tsx @@ -13,6 +13,8 @@ import { QueryHeader } from "@/ui/query/QueryHeader"; import { QueryWarnings } from "@/ui/query/QueryWarnings"; import "./styles.scss"; +import { secondsToMillis } from "@/infra/time"; + const useSubscription = ( plugin: TodoistPlugin, query: Query, @@ -79,7 +81,7 @@ export const QueryRoot: React.FC = ({ query, warnings }) => { const id = window.setInterval(async () => { await refresh(); - }, interval * 1000); + }, secondsToMillis(interval)); return () => window.clearInterval(id); }, [query, settings, refresh]); @@ -128,6 +130,10 @@ const getTitle = (query: Query, result: SubscriptionResult): string => { return query.name.replace("{task_count}", result.tasks.length.toString()); case "not-ready": return ""; + default: { + const _: never = result; + throw new Error("Unknown result type"); + } } }; diff --git a/plugin/src/ui/query/task/Task.tsx b/plugin/src/ui/query/task/Task.tsx index c8cdd59b..5a974ecb 100644 --- a/plugin/src/ui/query/task/Task.tsx +++ b/plugin/src/ui/query/task/Task.tsx @@ -8,12 +8,14 @@ import type { TaskTree } from "@/data/transformations/relationships"; import { t } from "@/i18n"; import { ShowMetadataVariant } from "@/query/query"; import { useSettingsStore } from "@/settings"; -import Markdown from "@/ui/components/markdown"; +import { Markdown } from "@/ui/components/markdown"; import { PluginContext, QueryContext } from "@/ui/context"; import { showTaskContext } from "@/ui/query/task/contextMenu"; import { TaskList } from "@/ui/query/task/TaskList"; import { TaskMetadata } from "@/ui/query/task/TaskMetadata"; +const noticeDurationMs = 2000; + type Props = { tree: TaskTree; }; @@ -40,7 +42,7 @@ export const Task: React.FC = ({ tree }) => { await plugin.services.todoist.actions.closeTask(tree.id); } catch (error: unknown) { console.error("Failed to close task", error); - new Notice(t().query.failedCloseMessage, 2000); + new Notice(t().query.failedCloseMessage, noticeDurationMs); } }; diff --git a/plugin/src/ui/query/task/TaskMetadata.tsx b/plugin/src/ui/query/task/TaskMetadata.tsx index a640ca1a..626e7d3e 100644 --- a/plugin/src/ui/query/task/TaskMetadata.tsx +++ b/plugin/src/ui/query/task/TaskMetadata.tsx @@ -164,6 +164,8 @@ const getMetadataElems = ( case "after": children.push(icon); break; + default: + throw new Error(`Unknown icon orientation: ${orientation}`); } } } diff --git a/plugin/src/ui/settings/SettingItem.tsx b/plugin/src/ui/settings/SettingItem.tsx index a0278720..e4f64238 100644 --- a/plugin/src/ui/settings/SettingItem.tsx +++ b/plugin/src/ui/settings/SettingItem.tsx @@ -119,8 +119,8 @@ const DropdownControl = ({ }; export const Setting = { - Root: Root, - ButtonControl: ButtonControl, - ToggleControl: ToggleControl, - DropdownControl: DropdownControl, + Root, + ButtonControl, + ToggleControl, + DropdownControl, };