diff --git a/.prettierrc.json b/.prettierrc.json index f3c0e3054..8d8787172 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -15,7 +15,7 @@ "tabWidth": 2, "trailingComma": "es5", "useTabs": false, - "importOrder": ["^((api-mocks|components|data|formio|hooks|map|routes|story-utils|types)/(.*)|(api|api-mocks|cache|Context|errors|headers|i18n|routes|sdk|sentry|types|utils))$", "^[./]"], + "importOrder": ["^((api-mocks|components|data|formio|hooks|map|routes|story-utils|types)/(.*)|(api|api-mocks|cache|Context|errors|headers|i18n|routes|sdk|sentry|types|utils))$", "^@/.*", "^[./]"], "importOrderSeparation": true, "importOrderSortSpecifiers": true } diff --git a/package-lock.json b/package-lock.json index 8aa0aa630..d03efac1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@open-formulieren/formiojs": "^4.13.14", "@open-formulieren/leaflet-tools": "^1.0.0", "@sentry/react": "^8.50.0", - "classnames": "^2.3.1", + "clsx": "^2.1.1", "date-fns": "^4.1.0", "flatpickr": "^4.6.9", "formik": "^2.2.9", @@ -3773,14 +3773,6 @@ "react-intl": "^6.6.2 || ^7.0.0" } }, - "node_modules/@open-formulieren/formio-renderer/node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, "node_modules/@open-formulieren/formiojs": { "version": "4.13.14", "resolved": "https://registry.npmjs.org/@open-formulieren/formiojs/-/formiojs-4.13.14.tgz", @@ -6318,6 +6310,14 @@ "react-dom": "18" } }, + "node_modules/@utrecht/component-library-react/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@utrecht/component-library-react/node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -6342,15 +6342,6 @@ "clsx": "2.1.1" } }, - "node_modules/@utrecht/components/node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/@utrecht/design-tokens": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@utrecht/design-tokens/-/design-tokens-2.5.0.tgz", @@ -8077,11 +8068,6 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -8147,9 +8133,9 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -8703,6 +8689,15 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/design-token-editor/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", diff --git a/package.json b/package.json index 356f68cac..3208861d6 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@open-formulieren/formiojs": "^4.13.14", "@open-formulieren/leaflet-tools": "^1.0.0", "@sentry/react": "^8.50.0", - "classnames": "^2.3.1", + "clsx": "^2.1.1", "date-fns": "^4.1.0", "flatpickr": "^4.6.9", "formik": "^2.2.9", diff --git a/src/api.js b/src/api.ts similarity index 53% rename from src/api.js rename to src/api.ts index 3fd8e01c6..a41a3bcdb 100644 --- a/src/api.js +++ b/src/api.ts @@ -12,21 +12,29 @@ import { import {CSPNonce, CSRFToken, ContentLanguage, IsFormDesigner} from './headers'; import {setLanguage} from './i18n'; -const fetchDefaults = { - credentials: 'include', // required for Firefox 60, which is used in werkplekken +interface ApiCallOptions extends Omit { + headers?: Record; +} + +const fetchDefaults: ApiCallOptions = { + credentials: 'include', }; const SessionExpiresInHeader = 'X-Session-Expires-In'; -let sessionExpiresAt = createState({expiry: null}); +interface SessionExpiryState { + expiry: Date | null; +} + +const sessionExpiresAt = createState({expiry: null}); -export const updateSessionExpiry = seconds => { +export const updateSessionExpiry = (seconds: number): void => { const newExpiry = new Date(); newExpiry.setSeconds(newExpiry.getSeconds() + seconds); sessionExpiresAt.setValue({expiry: newExpiry}); }; -const throwForStatus = async response => { +const throwForStatus = async (response: Response): Promise => { if (response.ok) return; let responseData = null; @@ -75,7 +83,10 @@ const throwForStatus = async response => { throw new ErrorClass(errorMessage, response.status, responseData.detail, responseData.code); }; -const addHeaders = (headers, method) => { +const addHeaders = ( + headers: Record | undefined, + method: string +): Record => { if (!headers) headers = {}; // add the CSP nonce request header in case the backend needs to do any post-processing @@ -94,10 +105,10 @@ const addHeaders = (headers, method) => { return headers; }; -const updateStoredHeadersValues = headers => { +const updateStoredHeadersValues = (headers: Headers): void => { const sessionExpiry = headers.get(SessionExpiresInHeader); if (sessionExpiry) { - updateSessionExpiry(parseInt(sessionExpiry), 10); + updateSessionExpiry(parseInt(sessionExpiry, 10)); } const CSRFTokenValue = headers.get(CSRFToken.headerName); @@ -117,7 +128,7 @@ const updateStoredHeadersValues = headers => { } }; -const apiCall = async (url, opts = {}) => { +const apiCall = async (url: string, opts: ApiCallOptions = {}): Promise => { const method = opts.method || 'GET'; const options = {...fetchDefaults, ...opts}; options.headers = addHeaders(options.headers, method); @@ -129,7 +140,17 @@ const apiCall = async (url, opts = {}) => { return response; }; -const get = async (url, params = {}, multiParams = []) => { +/** + * Make a GET api call to `url`, with optional query string parameters. + * + * The return data is the JSON response body, or `null` if there is no content. Specify + * the generic type parameter `T` to get typed return data. + */ +const get = async ( + url: string, + params: Record = {}, + multiParams: Record[] = [] +): Promise => { let searchParams = new URLSearchParams(); if (Object.keys(params).length) { searchParams = new URLSearchParams(params); @@ -142,16 +163,43 @@ const get = async (url, params = {}, multiParams = []) => { } url += `?${searchParams}`; const response = await apiCall(url); - const data = response.status === 204 ? null : await response.json(); + const data: T | null = response.status === 204 ? null : await response.json(); return data; }; -const _unsafe = async (method = 'POST', url, data, signal) => { - const opts = { +export interface UnsafeResponseData { + /** + * The parsed response body JSON, if there was one. + */ + data: T | null; + /** + * Whether the request completed successfully or not. + */ + ok: boolean; + /** + * The HTTP response status code. + */ + status: number; +} + +/** + * Make an unsafe (POST, PUT, PATCH) API call to `url`. + * + * The return data is the JSON response body, or `null` if there is no content. Specify + * the generic type parameter `T` to get typed return data, and `U` for strongly typing + * the request data (before JSON serialization). + */ +const _unsafe = async ( + method = 'POST', + url: string, + data: U, + signal?: AbortSignal +): Promise> => { + const opts: ApiCallOptions = { method, headers: { 'Content-Type': 'application/json', - [CSRFToken.headerName]: CSRFToken.getValue(), + [CSRFToken.headerName]: CSRFToken.getValue() ?? '', }, }; if (data) { @@ -161,7 +209,7 @@ const _unsafe = async (method = 'POST', url, data, signal) => { opts.signal = signal; } const response = await apiCall(url, opts); - const responseData = response.status === 204 ? null : await response.json(); + const responseData: T | null = response.status === 204 ? null : await response.json(); return { ok: response.ok, status: response.status, @@ -169,22 +217,49 @@ const _unsafe = async (method = 'POST', url, data, signal) => { }; }; -const post = async (url, data, signal) => { - const resp = await _unsafe('POST', url, data, signal); - return resp; -}; +/** + * Make a POST call to `url`. + * + * The return data is the JSON response body, or `null` if there is no content. Specify + * the generic type parameter `T` to get typed return data, and `U` for strongly typing + * the request data (before JSON serialization). + */ +const post = async ( + url: string, + data: U, + signal?: AbortSignal +): Promise> => await _unsafe('POST', url, data, signal); -const patch = async (url, data = {}) => { - const resp = await _unsafe('PATCH', url, data); - return resp; -}; +/** + * Make a PATCH call to `url`. + * + * The return data is the JSON response body, or `null` if there is no content. Specify + * the generic type parameter `T` to get typed return data, and `U` for strongly typing + * the request data (before JSON serialization). + */ +const patch = async ( + url: string, + data: U +): Promise> => await _unsafe('PATCH', url, data); -const put = async (url, data = {}) => { - const resp = await _unsafe('PUT', url, data); - return resp; -}; +/** + * Make a PUT call to `url`. + * + * The return data is the JSON response body, or `null` if there is no content. Specify + * the generic type parameter `T` to get typed return data, and `U` for strongly typing + * the request data (before JSON serialization). + */ +const put = async ( + url: string, + data: U +): Promise> => await _unsafe('PUT', url, data); -const destroy = async url => { +/** + * Make a DELETE call to `url`. + * + * If the delete was not successfull, an error is thrown. + */ +const destroy = async (url: string): Promise => { const opts = { method: 'DELETE', }; diff --git a/src/components/Anchor/Anchor.jsx b/src/components/Anchor/Anchor.jsx index 50b48005b..0f0446180 100644 --- a/src/components/Anchor/Anchor.jsx +++ b/src/components/Anchor/Anchor.jsx @@ -1,5 +1,5 @@ import {Link as UtrechtLink} from '@utrecht/component-library-react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import PropTypes from 'prop-types'; export const ANCHOR_MODIFIERS = [ @@ -18,7 +18,7 @@ const Anchor = ({ ...extraProps }) => { // extend with our own modifiers - const className = classNames( + const className = clsx( 'utrecht-link--openforms', // always apply our own modifier { 'utrecht-link--current': modifiers.includes('current'), diff --git a/src/components/AppDebug.jsx b/src/components/AppDebug.tsx similarity index 58% rename from src/components/AppDebug.jsx rename to src/components/AppDebug.tsx index 3df89e751..c712d2b37 100644 --- a/src/components/AppDebug.jsx +++ b/src/components/AppDebug.tsx @@ -1,29 +1,37 @@ import {FormattedDate, FormattedRelativeTime, useIntl} from 'react-intl'; import {useState as useGlobalState} from 'state-pool'; -import {sessionExpiresAt} from 'api'; -import {getVersion} from 'utils'; +import {sessionExpiresAt} from '@/api'; +import {getVersion} from '@/utils'; -const DebugInfo = ({label, value, children}) => ( +export interface DebugInfoProps { + label: string; + children: React.ReactNode; +} + +const DebugInfo: React.FC = ({label, children}) => (
{label}
-
{value ?? children}
+
{children}
); -const AppDebug = () => { +const AppDebug: React.FC = () => { const {locale} = useIntl(); const [{expiry}] = useGlobalState(sessionExpiresAt); - const expiryDelta = (expiry - new Date()) / 1000; return (
- + {locale} {expiry ? ( <>  ( - + ) ) : ( diff --git a/src/components/AppDisplay.tsx b/src/components/AppDisplay.tsx index 6b258f18a..c14a13378 100644 --- a/src/components/AppDisplay.tsx +++ b/src/components/AppDisplay.tsx @@ -1,4 +1,4 @@ -import classNames from 'classnames'; +import clsx from 'clsx'; export interface AppDisplayProps { /** @@ -41,7 +41,7 @@ export const AppDisplay: React.FC = ({ router, }) => (
{ - const Component = `${component}`; - return ( - - {children} - - ); -}; - -Caption.propTypes = { - children: PropTypes.node.isRequired, -}; - -export default Caption; diff --git a/src/components/Caption.tsx b/src/components/Caption.tsx new file mode 100644 index 000000000..e4759cf34 --- /dev/null +++ b/src/components/Caption.tsx @@ -0,0 +1,17 @@ +import {getBEMClassName} from '@/utils'; + +export type CaptionProps = { + children?: React.ReactNode; + component?: T; +} & React.ComponentPropsWithoutRef; + +const Caption: React.FC = ({children, component, ...props}) => { + const Component = component || 'caption'; + return ( + + {children} + + ); +}; + +export default Caption; diff --git a/src/components/Errors/ErrorBoundary.jsx b/src/components/Errors/ErrorBoundary.jsx index 8eb62f127..06784bae5 100644 --- a/src/components/Errors/ErrorBoundary.jsx +++ b/src/components/Errors/ErrorBoundary.jsx @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/react'; -import {getEnv} from 'env.mjs'; +import {getEnv} from 'env'; import PropTypes from 'prop-types'; import React from 'react'; import {FormattedMessage, useIntl} from 'react-intl'; diff --git a/src/components/FAIcon.jsx b/src/components/FAIcon.jsx index 8d1ad54d9..4a9bfce45 100644 --- a/src/components/FAIcon.jsx +++ b/src/components/FAIcon.jsx @@ -1,4 +1,4 @@ -import classNames from 'classnames'; +import clsx from 'clsx'; import PropTypes from 'prop-types'; // not all icons need to be seen by assistive technologies. @@ -10,7 +10,7 @@ const FAIcon = ({ noAriaHidden = false, ...props }) => { - const className = classNames( + const className = clsx( 'fa', 'fas', 'fa-icon', diff --git a/src/components/Loader.jsx b/src/components/Loader.tsx similarity index 67% rename from src/components/Loader.jsx rename to src/components/Loader.tsx index 1e527862b..c07e53b33 100644 --- a/src/components/Loader.jsx +++ b/src/components/Loader.tsx @@ -1,11 +1,15 @@ -import PropTypes from 'prop-types'; import {FormattedMessage} from 'react-intl'; -import {getBEMClassName} from 'utils'; +import {getBEMClassName} from '@/utils'; -export const MODIFIERS = ['centered', 'only-child', 'small', 'gray']; +export const MODIFIERS = ['centered', 'only-child', 'small', 'gray'] as const; -const Loader = ({modifiers = [], withoutTranslation}) => { +export interface LoaderProps { + modifiers?: (typeof MODIFIERS)[number][]; + withoutTranslation?: boolean; +} + +const Loader: React.FC = ({modifiers = [], withoutTranslation}) => { const className = getBEMClassName('loading', modifiers); return (
@@ -21,9 +25,4 @@ const Loader = ({modifiers = [], withoutTranslation}) => { ); }; -Loader.propTypes = { - withoutTranslation: PropTypes.bool, - modifiers: PropTypes.arrayOf(PropTypes.oneOf(MODIFIERS)), -}; - export default Loader; diff --git a/src/components/PreviousLink.jsx b/src/components/PreviousLink.jsx index 48f4b56e4..b4b36b2a2 100644 --- a/src/components/PreviousLink.jsx +++ b/src/components/PreviousLink.jsx @@ -1,4 +1,4 @@ -import classNames from 'classnames'; +import clsx from 'clsx'; import PropTypes from 'prop-types'; import FAIcon from 'components/FAIcon'; @@ -8,7 +8,7 @@ import {Literal} from 'components/Literal'; const VARIANTS = ['start', 'end']; const PreviousLink = ({to, onClick, position}) => { - const className = classNames('openforms-previous-link', { + const className = clsx('openforms-previous-link', { [`openforms-previous-link--${position}`]: position, }); return ( diff --git a/src/components/Price.jsx b/src/components/Price.tsx similarity index 65% rename from src/components/Price.jsx rename to src/components/Price.tsx index 2832c2591..a66aefc80 100644 --- a/src/components/Price.jsx +++ b/src/components/Price.tsx @@ -1,9 +1,12 @@ -import PropTypes from 'prop-types'; import {FormattedMessage, FormattedNumber} from 'react-intl'; -import {getBEMClassName} from 'utils'; +import {getBEMClassName} from '@/utils'; -const Price = ({price = ''}) => { +export interface PriceProps { + price?: string | number; // the API serializes decimals to strings to not lose precision +} + +const Price: React.FC = ({price = '0'}) => { return (
@@ -11,7 +14,7 @@ const Price = ({price = ''}) => {
{ ); }; -Price.propTypes = { - price: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - export default Price; diff --git a/src/components/forms/SelectField/SelectField.jsx b/src/components/forms/SelectField/SelectField.jsx index a050b376a..7c924b3d1 100644 --- a/src/components/forms/SelectField/SelectField.jsx +++ b/src/components/forms/SelectField/SelectField.jsx @@ -1,6 +1,6 @@ import {HelpText, Label, ValidationErrors} from '@open-formulieren/formio-renderer'; import {FormField} from '@utrecht/component-library-react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import {Field, useFormikContext} from 'formik'; import omit from 'lodash/omit'; import PropTypes from 'prop-types'; @@ -86,7 +86,7 @@ const SelectField = ({ inputId={id} classNames={{ control: state => - classNames('utrecht-select', 'utrecht-select--openforms', { + clsx('utrecht-select', 'utrecht-select--openforms', { 'utrecht-select--focus': state.isFocused, 'utrecht-select--focus-visible': state.isFocused, 'utrecht-select--disabled': disabled, @@ -95,7 +95,7 @@ const SelectField = ({ }), menu: () => 'rs-menu', option: state => - classNames('rs-menu__option', { + clsx('rs-menu__option', { 'rs-menu__option--focus': state.isFocused, 'rs-menu__option--visible-focus': state.isFocused, }), diff --git a/src/env.mjs b/src/env.ts similarity index 90% rename from src/env.mjs rename to src/env.ts index 5ef617cac..63c0d517b 100644 --- a/src/env.mjs +++ b/src/env.ts @@ -10,7 +10,7 @@ const env = import.meta.env; const envVarPrefix = 'VITE'; const DEBUG = env.MODE === 'development'; -const getEnv = name => { +const getEnv = (name: string): string | undefined => { const fullName = `${envVarPrefix}_${name}`; return env[fullName]; }; diff --git a/src/formio/components/Component.js b/src/formio/components/Component.js index 1a3d10dd5..4dabf5eb6 100644 --- a/src/formio/components/Component.js +++ b/src/formio/components/Component.js @@ -1,4 +1,4 @@ -import classNames from 'classnames'; +import clsx from 'clsx'; import {Formio} from 'react-formio'; import {applyPrefix} from 'utils'; @@ -19,7 +19,7 @@ const FormioComponent = Formio.Components.components.component; * @todo: check impact of --hidden modifier */ function getClassName() { - return classNames( + return clsx( applyPrefix('form-control'), applyPrefix(`form-control--${this.component.type}`), {[applyPrefix(`form-control--${this.key}`)]: this.key}, diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index ce6bae936..000000000 --- a/src/utils.js +++ /dev/null @@ -1,37 +0,0 @@ -import classNames from 'classnames'; - -import {getEnv} from './env'; - -export {DEBUG} from './env'; - -const VERSION = getEnv('VERSION'); - -export const PREFIX = 'openforms'; - -export const getFormattedDateString = (intl, dateString) => { - if (!dateString) return ''; - return intl.formatDate(new Date(dateString)); -}; - -export const getFormattedTimeString = (intl, dateTimeString) => { - if (!dateTimeString) return ''; - return intl.formatTime(new Date(dateTimeString)); -}; - -/** - * Prefix a name/string/identifier with the Open Forms specific prefix. - */ -export const applyPrefix = name => { - return `${PREFIX}-${name}`; -}; - -export const getBEMClassName = (base, modifiers = []) => { - const prefixedBase = applyPrefix(base); - const prefixedModifiers = modifiers.map(mod => applyPrefix(`${base}--${mod}`)); - return classNames(prefixedBase, ...prefixedModifiers); -}; - -// usage: await sleep(3000); -export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); - -export const getVersion = () => VERSION || 'unknown'; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..10332c52a --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,38 @@ +import clsx from 'clsx'; +import type {IntlShape} from 'react-intl'; + +import {getEnv} from './env'; + +export {DEBUG} from './env'; + +const VERSION = getEnv('VERSION'); + +export const PREFIX = 'openforms'; + +export const getFormattedDateString = (intl: IntlShape, dateString: string): string => { + if (!dateString) return ''; + return intl.formatDate(new Date(dateString)); +}; + +export const getFormattedTimeString = (intl: IntlShape, dateTimeString: string): string => { + if (!dateTimeString) return ''; + return intl.formatTime(new Date(dateTimeString)); +}; + +/** + * Prefix a name/string/identifier with the Open Forms specific prefix. + */ +export const applyPrefix = (name: string): string => { + return `${PREFIX}-${name}`; +}; + +export const getBEMClassName = (base: string, modifiers: string[] = []): string => { + const prefixedBase = applyPrefix(base); + const prefixedModifiers = modifiers.map(mod => applyPrefix(`${base}--${mod}`)); + return clsx(prefixedBase, ...prefixedModifiers); +}; + +// usage: await sleep(3000); +export const sleep = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); + +export const getVersion = (): string => VERSION || 'unknown'; diff --git a/tsconfig.json b/tsconfig.json index 3464dd4ea..485d53111 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,7 @@ "@/sb-decorators": ["../.storybook/decorators.tsx"] }, "types": [ + "vite/client", "vitest/globals" ] },