Skip to content

Commit

Permalink
Merge pull request #804 from open-formulieren/chore/445-convert-to-ty…
Browse files Browse the repository at this point in the history
…pescript

Convert some low-hanging fruit to TypeScript
  • Loading branch information
sergei-maertens authored Mar 11, 2025
2 parents 19fc530 + 7dc4a84 commit 575138f
Show file tree
Hide file tree
Showing 20 changed files with 228 additions and 151 deletions.
2 changes: 1 addition & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
47 changes: 21 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
131 changes: 103 additions & 28 deletions src/api.js → src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestInit, 'headers'> {
headers?: Record<string, string>;
}

const fetchDefaults: ApiCallOptions = {
credentials: 'include',
};

const SessionExpiresInHeader = 'X-Session-Expires-In';

let sessionExpiresAt = createState({expiry: null});
interface SessionExpiryState {
expiry: Date | null;
}

const sessionExpiresAt = createState<SessionExpiryState>({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<void> => {
if (response.ok) return;

let responseData = null;
Expand Down Expand Up @@ -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<string, string> | undefined,
method: string
): Record<string, string> => {
if (!headers) headers = {};

// add the CSP nonce request header in case the backend needs to do any post-processing
Expand All @@ -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);
Expand All @@ -117,7 +128,7 @@ const updateStoredHeadersValues = headers => {
}
};

const apiCall = async (url, opts = {}) => {
const apiCall = async (url: string, opts: ApiCallOptions = {}): Promise<Response> => {
const method = opts.method || 'GET';
const options = {...fetchDefaults, ...opts};
options.headers = addHeaders(options.headers, method);
Expand All @@ -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 <T = unknown>(
url: string,
params: Record<string, string> = {},
multiParams: Record<string, string>[] = []
): Promise<T | null> => {
let searchParams = new URLSearchParams();
if (Object.keys(params).length) {
searchParams = new URLSearchParams(params);
Expand All @@ -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<T = unknown> {
/**
* 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 <T = unknown, U = unknown>(
method = 'POST',
url: string,
data: U,
signal?: AbortSignal
): Promise<UnsafeResponseData<T>> => {
const opts: ApiCallOptions = {
method,
headers: {
'Content-Type': 'application/json',
[CSRFToken.headerName]: CSRFToken.getValue(),
[CSRFToken.headerName]: CSRFToken.getValue() ?? '',
},
};
if (data) {
Expand All @@ -161,30 +209,57 @@ 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,
data: responseData,
};
};

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 <T = unknown, U = unknown>(
url: string,
data: U,
signal?: AbortSignal
): Promise<UnsafeResponseData<T>> => await _unsafe<T, U>('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 <T = unknown, U = unknown>(
url: string,
data: U
): Promise<UnsafeResponseData<T>> => await _unsafe<T, U>('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 <T = unknown, U = unknown>(
url: string,
data: U
): Promise<UnsafeResponseData<T>> => await _unsafe<T, U>('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<void> => {
const opts = {
method: 'DELETE',
};
Expand Down
4 changes: 2 additions & 2 deletions src/components/Anchor/Anchor.jsx
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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'),
Expand Down
24 changes: 16 additions & 8 deletions src/components/AppDebug.jsx → src/components/AppDebug.tsx
Original file line number Diff line number Diff line change
@@ -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<DebugInfoProps> = ({label, children}) => (
<div className="debug-info">
<div className="debug-info__label">{label}</div>
<div className="debug-info__value">{value ?? children}</div>
<div className="debug-info__value">{children}</div>
</div>
);

const AppDebug = () => {
const AppDebug: React.FC = () => {
const {locale} = useIntl();
const [{expiry}] = useGlobalState(sessionExpiresAt);
const expiryDelta = (expiry - new Date()) / 1000;
return (
<div className="debug-info-container" title="Debug information (only available in dev)">
<DebugInfo label="Current locale" value={locale} />
<DebugInfo label="Current locale">{locale}</DebugInfo>
<DebugInfo label="Session expires at">
{expiry ? (
<>
<FormattedDate value={expiry} hour="numeric" minute="numeric" second="numeric" />
&nbsp;(
<FormattedRelativeTime value={expiryDelta} numeric="auto" updateIntervalInSeconds={1} />
<FormattedRelativeTime
value={(expiry.getTime() - new Date().getTime()) / 1000}
numeric="auto"
updateIntervalInSeconds={1}
/>
)
</>
) : (
Expand Down
4 changes: 2 additions & 2 deletions src/components/AppDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import classNames from 'classnames';
import clsx from 'clsx';

export interface AppDisplayProps {
/**
Expand Down Expand Up @@ -41,7 +41,7 @@ export const AppDisplay: React.FC<AppDisplayProps> = ({
router,
}) => (
<div
className={classNames('openforms-app', {
className={clsx('openforms-app', {
'openforms-app--no-progress-indicator': !progressIndicator,
'openforms-app--no-language-switcher': !languageSwitcher,
})}
Expand Down
Loading

0 comments on commit 575138f

Please sign in to comment.