From e8b3419933286faea7829993380c1ad1dd0dccfa Mon Sep 17 00:00:00 2001 From: Jed Fox <git@jedfox.com> Date: Sun, 30 Jul 2023 07:24:55 -0700 Subject: [PATCH] Port the settings components to TS (#1405) --- .../src/components/{Page.js => Page.tsx} | 29 ++++++++++---- .../src/components/common/Select.tsx | 16 ++++---- .../{Encryption.js => Encryption.tsx} | 1 + .../src/components/settings/Experimental.tsx | 1 + .../settings/{Export.js => Export.tsx} | 2 +- .../settings/{FixSplits.js => FixSplits.tsx} | 7 +++- .../settings/{Format.js => Format.tsx} | 38 ++++++------------- .../settings/{Global.js => Global.tsx} | 2 +- .../settings/{Reset.js => Reset.tsx} | 0 .../settings/{Themes.js => Themes.tsx} | 0 .../src/components/settings/UI.tsx | 2 +- .../settings/{index.js => index.tsx} | 4 +- .../loot-core/src/client/actions/modals.ts | 27 +++++++++---- .../loot-core/src/client/actions/prefs.ts | 5 ++- .../src/client/state-types/modals.d.ts | 33 +++++++++------- packages/loot-core/src/server/main.ts | 4 +- packages/loot-core/src/server/sync/index.ts | 6 ++- packages/loot-core/src/types/prefs.d.ts | 7 +++- .../loot-core/src/types/server-handlers.d.ts | 4 +- packages/loot-core/src/types/util.d.ts | 6 ++- packages/loot-core/typings/window.d.ts | 8 ++++ upcoming-release-notes/1405.md | 6 +++ 22 files changed, 130 insertions(+), 78 deletions(-) rename packages/desktop-client/src/components/{Page.js => Page.tsx} (78%) rename packages/desktop-client/src/components/settings/{Encryption.js => Encryption.tsx} (97%) rename packages/desktop-client/src/components/settings/{Export.js => Export.tsx} (94%) rename packages/desktop-client/src/components/settings/{FixSplits.js => FixSplits.tsx} (92%) rename packages/desktop-client/src/components/settings/{Format.js => Format.tsx} (83%) rename packages/desktop-client/src/components/settings/{Global.js => Global.tsx} (97%) rename packages/desktop-client/src/components/settings/{Reset.js => Reset.tsx} (100%) rename packages/desktop-client/src/components/settings/{Themes.js => Themes.tsx} (100%) rename packages/desktop-client/src/components/settings/{index.js => index.tsx} (97%) create mode 100644 upcoming-release-notes/1405.md diff --git a/packages/desktop-client/src/components/Page.js b/packages/desktop-client/src/components/Page.tsx similarity index 78% rename from packages/desktop-client/src/components/Page.js rename to packages/desktop-client/src/components/Page.tsx index 8038a87db..aa2d8999d 100644 --- a/packages/desktop-client/src/components/Page.js +++ b/packages/desktop-client/src/components/Page.tsx @@ -1,12 +1,14 @@ -import React, { createContext, useContext } from 'react'; +import React, { createContext, type ReactNode, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; +import { type CSSProperties } from 'glamor'; + import { useResponsive } from '../ResponsiveProvider'; import { colors, styles } from '../style'; import { Modal, View, Text } from './common'; -let PageTypeContext = createContext({ type: 'page' }); +let PageTypeContext = createContext({ type: 'page', current: undefined }); export function PageTypeProvider({ type, current, children }) { return ( @@ -63,18 +65,29 @@ function PageTitle({ name, style }) { ); } -export function Page({ title, modalSize, children, titleStyle }) { +export function Page({ + title, + modalSize, + children, + titleStyle, +}: { + title: string; + modalSize?: string | { width: number; height?: number }; + children: ReactNode; + titleStyle?: CSSProperties; +}) { let { type, current } = usePageType(); let navigate = useNavigate(); let { isNarrowWidth } = useResponsive(); let HORIZONTAL_PADDING = isNarrowWidth ? 10 : 20; if (type === 'modal') { - let size = modalSize; - if (typeof modalSize === 'string') { - size = - modalSize === 'medium' ? { width: 750, height: 600 } : { width: 600 }; - } + let size = + typeof modalSize === 'string' + ? modalSize === 'medium' + ? { width: 750, height: 600 } + : { width: 600 } + : modalSize; return ( <Modal diff --git a/packages/desktop-client/src/components/common/Select.tsx b/packages/desktop-client/src/components/common/Select.tsx index f328d8479..cbc01a993 100644 --- a/packages/desktop-client/src/components/common/Select.tsx +++ b/packages/desktop-client/src/components/common/Select.tsx @@ -10,15 +10,15 @@ import { type CSSProperties, css } from 'glamor'; import ExpandArrow from '../../icons/v0/ExpandArrow'; import { colors } from '../../style'; -type CustomSelectProps = { - options: Array<[string, string]>; - value: string; +type SelectProps<Value extends string> = { + options: Array<[Value, string]>; + value: Value; defaultLabel?: string; - onChange?: (newValue: string) => void; + onChange?: (newValue: Value) => void; style?: CSSProperties; wrapperStyle?: CSSProperties; - line: number; - disabledKeys?: string[]; + line?: number; + disabledKeys?: Value[]; }; /** @@ -36,7 +36,7 @@ type CustomSelectProps = { * // <Select options={[['1', 'Option 1'], ['2', 'Option 2']]} value="3" defaultLabel="Select an option" onChange={handleOnChange} /> */ -export default function Select({ +export default function Select<Value extends string>({ options, value, defaultLabel = '', @@ -45,7 +45,7 @@ export default function Select({ wrapperStyle, line, disabledKeys = [], -}: CustomSelectProps) { +}: SelectProps<Value>) { const arrowSize = 7; const targetOption = options.filter(option => option[0] === value); return ( diff --git a/packages/desktop-client/src/components/settings/Encryption.js b/packages/desktop-client/src/components/settings/Encryption.tsx similarity index 97% rename from packages/desktop-client/src/components/settings/Encryption.js rename to packages/desktop-client/src/components/settings/Encryption.tsx index bbedaa052..4f419ee5c 100644 --- a/packages/desktop-client/src/components/settings/Encryption.js +++ b/packages/desktop-client/src/components/settings/Encryption.tsx @@ -16,6 +16,7 @@ export default function EncryptionSettings() { const missingCryptoAPI = !(window.crypto && crypto.subtle); function onChangeKey() { + // @ts-expect-error useActions() type does not properly handle overloads pushModal('create-encryption-key', { recreate: true }); } diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx index d0fe85562..0b6d1908a 100644 --- a/packages/desktop-client/src/components/settings/Experimental.tsx +++ b/packages/desktop-client/src/components/settings/Experimental.tsx @@ -32,6 +32,7 @@ function FeatureToggle({ <Checkbox checked={enabled} onChange={() => { + // @ts-expect-error key type is not correctly inferred savePrefs({ [`flags.${flag}`]: !enabled, }); diff --git a/packages/desktop-client/src/components/settings/Export.js b/packages/desktop-client/src/components/settings/Export.tsx similarity index 94% rename from packages/desktop-client/src/components/settings/Export.js rename to packages/desktop-client/src/components/settings/Export.tsx index 1417b5ff3..bc966988c 100644 --- a/packages/desktop-client/src/components/settings/Export.js +++ b/packages/desktop-client/src/components/settings/Export.tsx @@ -10,7 +10,7 @@ import { Text, Button } from '../common'; import { Setting } from './UI'; export default function ExportBudget() { - let budgetId = useSelector(state => state.prefs.local.budgetId); + let budgetId = useSelector(state => state.prefs.local.id); let encryptKeyId = useSelector(state => state.prefs.local.encryptKeyId); async function onExport() { diff --git a/packages/desktop-client/src/components/settings/FixSplits.js b/packages/desktop-client/src/components/settings/FixSplits.tsx similarity index 92% rename from packages/desktop-client/src/components/settings/FixSplits.js rename to packages/desktop-client/src/components/settings/FixSplits.tsx index fa3eeb0db..28892095f 100644 --- a/packages/desktop-client/src/components/settings/FixSplits.js +++ b/packages/desktop-client/src/components/settings/FixSplits.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { send } from 'loot-core/src/platform/client/fetch'; +import { type Handlers } from 'loot-core/src/types/handlers'; import { colors } from '../../style'; import { View, Text, ButtonWithLoading } from '../common'; @@ -8,7 +9,9 @@ import Paragraph from '../common/Paragraph'; import { Setting } from './UI'; -function renderResults(results) { +type Results = Awaited<ReturnType<Handlers['tools/fix-split-transactions']>>; + +function renderResults(results: Results) { let { numBlankPayees, numCleared, numDeleted } = results; let result = ''; if (numBlankPayees === 0 && numCleared === 0 && numDeleted === 0) { @@ -48,7 +51,7 @@ function renderResults(results) { export default function FixSplitsTool() { let [loading, setLoading] = useState(false); - let [results, setResults] = useState(null); + let [results, setResults] = useState<Results>(null); async function onFix() { setLoading(true); diff --git a/packages/desktop-client/src/components/settings/Format.js b/packages/desktop-client/src/components/settings/Format.tsx similarity index 83% rename from packages/desktop-client/src/components/settings/Format.js rename to packages/desktop-client/src/components/settings/Format.tsx index c92034ed7..666e330c9 100644 --- a/packages/desktop-client/src/components/settings/Format.js +++ b/packages/desktop-client/src/components/settings/Format.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { type ReactNode } from 'react'; import { useSelector } from 'react-redux'; import { numberFormats } from 'loot-core/src/shared/util'; +import { type LocalPrefs } from 'loot-core/src/types/prefs'; import { useActions } from '../../hooks/useActions'; import tokens from '../../tokens'; @@ -13,7 +14,7 @@ import { Setting } from './UI'; // Follows Pikaday 'firstDay' numbering // https://github.com/Pikaday/Pikaday -let daysOfWeek = [ +let daysOfWeek: { value: LocalPrefs['firstDayOfWeekIdx']; label: string }[] = [ { value: '0', label: 'Sunday' }, { value: '1', label: 'Monday' }, { value: '2', label: 'Tuesday' }, @@ -23,7 +24,7 @@ let daysOfWeek = [ { value: '6', label: 'Saturday' }, ]; -let dateFormats = [ +let dateFormats: { value: LocalPrefs['dateFormat']; label: string }[] = [ { value: 'MM/dd/yyyy', label: 'MM/DD/YYYY' }, { value: 'dd/MM/yyyy', label: 'DD/MM/YYYY' }, { value: 'yyyy-MM-dd', label: 'YYYY-MM-DD' }, @@ -31,7 +32,7 @@ let dateFormats = [ { value: 'dd.MM.yyyy', label: 'DD.MM.YYYY' }, ]; -function Column({ title, children }) { +function Column({ title, children }: { title: string; children: ReactNode }) { return ( <View style={{ @@ -50,23 +51,6 @@ function Column({ title, children }) { export default function FormatSettings() { let { savePrefs } = useActions(); - function onFirstDayOfWeek(idx) { - savePrefs({ firstDayOfWeekIdx: idx }); - } - - function onDateFormat(format) { - savePrefs({ dateFormat: format }); - } - - function onNumberFormat(format) { - savePrefs({ numberFormat: format }); - } - - function onHideFraction(e) { - let hideFraction = e.target.checked; - savePrefs({ hideFraction }); - } - let sidebar = useSidebar(); let firstDayOfWeekIdx = useSelector( state => state.prefs.local.firstDayOfWeekIdx || '0', // Sunday @@ -99,9 +83,9 @@ export default function FormatSettings() { <Column title="Numbers"> <Button bounce={false} style={{ padding: 0 }}> <Select - key={hideFraction} // needed because label does not update + key={String(hideFraction)} // needed because label does not update value={numberFormat} - onChange={onNumberFormat} + onChange={format => savePrefs({ numberFormat: format })} options={numberFormats.map(f => [ f.value, hideFraction ? f.labelNoFraction : f.label, @@ -114,7 +98,9 @@ export default function FormatSettings() { <Checkbox id="settings-textDecimal" checked={!!hideFraction} - onChange={onHideFraction} + onChange={e => + savePrefs({ hideFraction: e.currentTarget.checked }) + } /> <label htmlFor="settings-textDecimal">Hide decimal places</label> </Text> @@ -124,7 +110,7 @@ export default function FormatSettings() { <Button bounce={false} style={{ padding: 0 }}> <Select value={dateFormat} - onChange={onDateFormat} + onChange={format => savePrefs({ dateFormat: format })} options={dateFormats.map(f => [f.value, f.label])} style={{ padding: '2px 10px', fontSize: 15 }} /> @@ -135,7 +121,7 @@ export default function FormatSettings() { <Button bounce={false} style={{ padding: 0 }}> <Select value={firstDayOfWeekIdx} - onChange={onFirstDayOfWeek} + onChange={idx => savePrefs({ firstDayOfWeekIdx: idx })} options={daysOfWeek.map(f => [f.value, f.label])} style={{ padding: '2px 10px', fontSize: 15 }} /> diff --git a/packages/desktop-client/src/components/settings/Global.js b/packages/desktop-client/src/components/settings/Global.tsx similarity index 97% rename from packages/desktop-client/src/components/settings/Global.js rename to packages/desktop-client/src/components/settings/Global.tsx index 318332e8c..1ea218976 100644 --- a/packages/desktop-client/src/components/settings/Global.js +++ b/packages/desktop-client/src/components/settings/Global.tsx @@ -13,7 +13,7 @@ export default function GlobalSettings() { let { saveGlobalPrefs } = useActions(); let [documentDirChanged, setDirChanged] = useState(false); - let dirScrolled = useRef(null); + let dirScrolled = useRef<HTMLSpanElement>(null); useEffect(() => { if (dirScrolled.current) { diff --git a/packages/desktop-client/src/components/settings/Reset.js b/packages/desktop-client/src/components/settings/Reset.tsx similarity index 100% rename from packages/desktop-client/src/components/settings/Reset.js rename to packages/desktop-client/src/components/settings/Reset.tsx diff --git a/packages/desktop-client/src/components/settings/Themes.js b/packages/desktop-client/src/components/settings/Themes.tsx similarity index 100% rename from packages/desktop-client/src/components/settings/Themes.js rename to packages/desktop-client/src/components/settings/Themes.tsx diff --git a/packages/desktop-client/src/components/settings/UI.tsx b/packages/desktop-client/src/components/settings/UI.tsx index d5f9bdd9c..b3052256e 100644 --- a/packages/desktop-client/src/components/settings/UI.tsx +++ b/packages/desktop-client/src/components/settings/UI.tsx @@ -9,7 +9,7 @@ import tokens from '../../tokens'; import { View, LinkButton } from '../common'; type SettingProps = { - primaryAction: ReactNode; + primaryAction?: ReactNode; style?: CSSProperties; children: ReactNode; }; diff --git a/packages/desktop-client/src/components/settings/index.js b/packages/desktop-client/src/components/settings/index.tsx similarity index 97% rename from packages/desktop-client/src/components/settings/index.js rename to packages/desktop-client/src/components/settings/index.tsx index e0571dd2d..def408102 100644 --- a/packages/desktop-client/src/components/settings/index.js +++ b/packages/desktop-client/src/components/settings/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { type ReactNode, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { media } from 'glamor'; @@ -75,7 +75,7 @@ function About() { ); } -function IDName({ children }) { +function IDName({ children }: { children: ReactNode }) { return <Text style={{ fontWeight: 500 }}>{children}</Text>; } diff --git a/packages/loot-core/src/client/actions/modals.ts b/packages/loot-core/src/client/actions/modals.ts index bcb71af52..efc1a0ae1 100644 --- a/packages/loot-core/src/client/actions/modals.ts +++ b/packages/loot-core/src/client/actions/modals.ts @@ -1,24 +1,37 @@ import * as constants from '../constants'; import type { + OptionlessModal, CloseModalAction, - Modal, PopModalAction, PushModalAction, ReplaceModalAction, + ModalWithOptions, + ModalType, + FinanceModals, } from '../state-types/modals'; -export function pushModal<M extends Modal>( - name: M['name'], - options: M['options'], +export function pushModal<M extends keyof ModalWithOptions>( + name: M, + options: ModalWithOptions[M], +): PushModalAction; +export function pushModal(name: OptionlessModal): PushModalAction; +export function pushModal<M extends ModalType>( + name: M, + options?: FinanceModals[M], ): PushModalAction { // @ts-expect-error TS is unable to determine that `name` and `options` match let modal: M = { name, options }; return { type: constants.PUSH_MODAL, modal }; } -export function replaceModal<M extends Modal>( - name: M['name'], - options: M['options'], +export function replaceModal<M extends keyof ModalWithOptions>( + name: M, + options: ModalWithOptions[M], +): ReplaceModalAction; +export function replaceModal(name: OptionlessModal): ReplaceModalAction; +export function replaceModal<M extends ModalType>( + name: M, + options?: FinanceModals[M], ): ReplaceModalAction { // @ts-expect-error TS is unable to determine that `name` and `options` match let modal: M = { name, options }; diff --git a/packages/loot-core/src/client/actions/prefs.ts b/packages/loot-core/src/client/actions/prefs.ts index 3521e89ae..d309e0173 100644 --- a/packages/loot-core/src/client/actions/prefs.ts +++ b/packages/loot-core/src/client/actions/prefs.ts @@ -1,4 +1,5 @@ import { send } from '../../platform/client/fetch'; +import type * as prefs from '../../types/prefs'; import * as constants from '../constants'; import { closeModal } from './modals'; @@ -24,7 +25,7 @@ export function loadPrefs() { }; } -export function savePrefs(prefs) { +export function savePrefs(prefs: Partial<prefs.LocalPrefs>) { return async (dispatch: Dispatch) => { await send('save-prefs', prefs); dispatch({ @@ -46,7 +47,7 @@ export function loadGlobalPrefs() { }; } -export function saveGlobalPrefs(prefs) { +export function saveGlobalPrefs(prefs: Partial<prefs.GlobalPrefs>) { return async (dispatch: Dispatch) => { await send('save-global-prefs', prefs); dispatch({ diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index 6065b0026..c4f1fac44 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -1,13 +1,19 @@ import type { AccountEntity } from '../../types/models'; import type { RuleEntity } from '../../types/models/rule'; +import type { EmptyObject, StripNever } from '../../types/util'; import type * as constants from '../constants'; -type Modal = { - [K in keyof FinanceModals]: { - name: K; - options: FinanceModals[K]; - }; -}[keyof FinanceModals]; +export type ModalType = keyof FinanceModals; + +export type OptionlessModal = { + [K in ModalType]: EmptyObject extends FinanceModals[K] ? K : never; +}[ModalType]; + +export type ModalWithOptions = StripNever<{ + [K in ModalType]: keyof FinanceModals[K] extends never + ? never + : FinanceModals[K]; +}>; // There is a separate (overlapping!) set of modals for the management app. Fun! type FinanceModals = { @@ -17,8 +23,8 @@ type FinanceModals = { onImported: (didChange: boolean) => void; }; - 'add-account': null; - 'add-local-account': null; + 'add-account': EmptyObject; + 'add-local-account': EmptyObject; 'close-account': { account: AccountEntity; balance: number; @@ -29,16 +35,15 @@ type FinanceModals = { requisitionId: string; upgradingAccountId: string; }; - 'configure-linked-accounts': never; 'confirm-category-delete': { onDelete: () => void } & ( | { category: string } | { group: string } ); - 'load-backup': null; + 'load-backup': EmptyObject; - 'manage-rules': { payeeId: string } | null; + 'manage-rules': { payeeId?: string }; 'edit-rule': { rule: RuleEntity; onSave: (rule: RuleEntity) => void; @@ -65,10 +70,10 @@ type FinanceModals = { onSuccess: (data: unknown) => Promise<void>; }; - 'create-encryption-key': { recreate: boolean } | null; + 'create-encryption-key': { recreate?: boolean }; 'fix-encryption-key': { - hasExistingKey: boolean; - cloudFileId: string; + hasExistingKey?: boolean; + cloudFileId?: string; onSuccess?: () => void; }; diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index e070e1a2e..83e8e6f96 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -1903,9 +1903,9 @@ handlers['download-budget'] = async function ({ fileId }) { // open and sync, but don’t close handlers['sync-budget'] = async function () { setSyncingMode('enabled'); - await initialFullSync(); + let result = await initialFullSync(); - return {}; + return result; }; handlers['load-budget'] = async function ({ id }) { diff --git a/packages/loot-core/src/server/sync/index.ts b/packages/loot-core/src/server/sync/index.ts index 34dceb7dd..114c33790 100644 --- a/packages/loot-core/src/server/sync/index.ts +++ b/packages/loot-core/src/server/sync/index.ts @@ -538,12 +538,16 @@ function getTablesFromMessages(messages: Message[]): string[] { // spreadsheet to finish any processing. This is useful if we want to // perform a full sync and wait for everything to finish, usually if // you're doing an initial sync before working with a file. -export async function initialFullSync(): Promise<void> { +export async function initialFullSync(): Promise<{ + error?: { message: string; reason: string; meta: unknown }; +}> { let result = await fullSync(); if (isError(result)) { // Make sure to wait for anything in the spreadsheet to process await sheet.waitOnSpreadsheet(); + return result; } + return {}; } export const fullSync = once(async function (): Promise< diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts index 4dcd19128..4906c326e 100644 --- a/packages/loot-core/src/types/prefs.d.ts +++ b/packages/loot-core/src/types/prefs.d.ts @@ -9,7 +9,12 @@ export type FeatureFlag = export type LocalPrefs = Partial< { firstDayOfWeekIdx: `${0 | 1 | 2 | 3 | 4 | 5 | 6}`; - dateFormat: string; + dateFormat: + | 'MM/dd/yyyy' + | 'dd/MM/yyyy' + | 'yyyy-MM-dd' + | 'MM.dd.yyyy' + | 'dd.MM.yyyy'; numberFormat: (typeof numberFormats)[number]['value']; hideFraction: boolean; hideClosedAccounts: boolean; diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts index 32b56f1c1..8ce437c00 100644 --- a/packages/loot-core/src/types/server-handlers.d.ts +++ b/packages/loot-core/src/types/server-handlers.d.ts @@ -301,7 +301,9 @@ export interface ServerHandlers { 'download-budget': (arg: { fileId; replace? }) => Promise<{ error; id }>; - 'sync-budget': () => Promise<EmptyObject>; + 'sync-budget': () => Promise<{ + error?: { message: string; reason: string; meta: unknown }; + }>; 'load-budget': (arg: { id }) => Promise<{ error }>; diff --git a/packages/loot-core/src/types/util.d.ts b/packages/loot-core/src/types/util.d.ts index 253e96456..f478d9006 100644 --- a/packages/loot-core/src/types/util.d.ts +++ b/packages/loot-core/src/types/util.d.ts @@ -1 +1,5 @@ -export type EmptyObject = Record<string, never>; +export type EmptyObject = Record<never, never>; + +export type StripNever<T> = { + [K in keyof T as T[K] extends never ? never : K]: T[K]; +}; diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts index 001ef5709..00401f267 100644 --- a/packages/loot-core/typings/window.d.ts +++ b/packages/loot-core/typings/window.d.ts @@ -8,6 +8,14 @@ declare global { IS_FAKE_WEB: boolean; ACTUAL_VERSION: string; openURLInBrowser: (url: string) => void; + saveFile: ( + contents: Buffer, + filename: string, + dialogTitle: string, + ) => void; + openFileDialog: ( + opts: Parameters<import('electron').Dialog['showOpenDialogSync']>[0], + ) => Promise<string[]>; }; __navigate?: import('react-router').NavigateFunction; diff --git a/upcoming-release-notes/1405.md b/upcoming-release-notes/1405.md new file mode 100644 index 000000000..cf194652c --- /dev/null +++ b/upcoming-release-notes/1405.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [j-f1] +--- + +Port the settings-related code to TypeScript -- GitLab