Skip to content

Commit 6108c60

Browse files
authored
Merge pull request #3705 from clairep94/pr05_finals_clientModulesUser
Pr05 Typescript Migration #21: `client/modules/User`
2 parents 15c7797 + 64e8912 commit 6108c60

35 files changed

+314
-328
lines changed

client/common/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export interface ButtonProps extends React.HTMLAttributes<HTMLElement> {
8080
/**
8181
* If using a native button, specifies on an onClick action
8282
*/
83-
onClick?: () => void;
83+
onClick?: (evt: React.MouseEvent<HTMLButtonElement>) => void;
8484
/**
8585
* If using a button, then type is defines the type of button
8686
*/

client/common/useSyncFormTranslations.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
import { useEffect, MutableRefObject } from 'react';
2+
import type { FormApi } from 'final-form';
23

3-
export interface FormLike {
4-
getState(): { values: Record<string, unknown> };
5-
reset(): void;
6-
change(field: string, value: unknown): void;
7-
}
4+
// Generic FormLike that mirrors FormApi for any form value type
5+
export interface FormLike<FormValues = Record<string, unknown>>
6+
extends Pick<FormApi<FormValues>, 'getState' | 'reset' | 'change'> {}
87

98
/**
109
* This hook ensures that form values are preserved when the language changes.
1110
* @param formRef
1211
* @param language
1312
*/
14-
export const useSyncFormTranslations = (
15-
formRef: MutableRefObject<FormLike>,
13+
export const useSyncFormTranslations = <FormValues extends Record<string, any>>(
14+
formRef: MutableRefObject<FormLike<FormValues> | null>,
1615
language: string
1716
) => {
1817
useEffect(() => {
19-
const form = formRef.current;
18+
const form = formRef?.current;
2019
if (!form) return;
2120

2221
const { values } = form.getState();
2322
form.reset();
2423

25-
Object.keys(values).forEach((field) => {
26-
if (values[field]) {
27-
form.change(field, values[field]);
24+
(Object.keys(values) as (keyof FormValues)[]).forEach((field) => {
25+
const value = values[field];
26+
if (value !== undefined && value !== null && value !== '') {
27+
form.change(field, value);
2828
}
2929
});
3030
}, [language]);

client/modules/App/App.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { showReduxDevTools } from '../../store';
66
import DevTools from './components/DevTools';
77
import { setPreviousPath } from '../IDE/actions/ide';
88
import { setLanguage } from '../IDE/actions/preferences';
9-
import CookieConsent from '../User/components/CookieConsent';
9+
import { CookieConsent } from '../User/components/CookieConsent';
1010

1111
function hideCookieConsent(pathname) {
1212
if (pathname.includes('/full/') || pathname.includes('/embed/')) {

client/modules/IDE/components/Header/Toolbar.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import StopIcon from '../../../../images/stop.svg';
2020
import PreferencesIcon from '../../../../images/preferences.svg';
2121
import ProjectName from './ProjectName';
2222
import VersionIndicator from '../VersionIndicator';
23-
import VisibilityDropdown from '../../../User/components/VisibilityDropdown';
23+
import { VisibilityDropdown } from '../../../User/components/VisibilityDropdown';
2424
import { changeVisibility } from '../../actions/project';
2525

2626
const Toolbar = (props) => {

client/modules/IDE/components/SketchListRowBase.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { TableDropdown } from '../../../components/Dropdown/TableDropdown';
1010
import { MenuItem } from '../../../components/Dropdown/MenuItem';
1111
import { formatDateToString } from '../../../utils/formatDate';
1212
import { getConfig } from '../../../utils/getConfig';
13-
import VisibilityDropdown from '../../User/components/VisibilityDropdown';
13+
import { VisibilityDropdown } from '../../User/components/VisibilityDropdown';
1414

1515
const ROOT_URL = getConfig('API_URL');
1616

client/modules/User/components/APIKeyForm.jsx renamed to client/modules/User/components/APIKeyForm.tsx

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,28 @@
1-
import PropTypes from 'prop-types';
21
import React, { useState } from 'react';
32
import { useTranslation } from 'react-i18next';
43
import { useDispatch, useSelector } from 'react-redux';
54
import { Button, ButtonTypes } from '../../../common/Button';
65
import { PlusIcon } from '../../../common/icons';
76
import CopyableInput from '../../IDE/components/CopyableInput';
87
import { createApiKey, removeApiKey } from '../actions';
8+
import { APIKeyList } from './APIKeyList';
9+
import { RootState } from '../../../reducers';
10+
import { SanitisedApiKey } from '../../../../common/types';
911

10-
import APIKeyList from './APIKeyList';
11-
12-
export const APIKeyPropType = PropTypes.shape({
13-
id: PropTypes.string.isRequired,
14-
token: PropTypes.string,
15-
label: PropTypes.string.isRequired,
16-
createdAt: PropTypes.string.isRequired,
17-
lastUsedAt: PropTypes.string
18-
});
19-
20-
const APIKeyForm = () => {
12+
export const APIKeyForm = () => {
2113
const { t } = useTranslation();
22-
const apiKeys = useSelector((state) => state.user.apiKeys);
14+
const apiKeys = useSelector((state: RootState) => state.user.apiKeys) ?? [];
2315
const dispatch = useDispatch();
2416

2517
const [keyLabel, setKeyLabel] = useState('');
2618

27-
const addKey = (event) => {
19+
const addKey = (event: React.FormEvent) => {
2820
event.preventDefault();
2921
dispatch(createApiKey(keyLabel));
3022
setKeyLabel('');
3123
};
3224

33-
const removeKey = (key) => {
25+
const removeKey = (key: SanitisedApiKey) => {
3426
const message = t('APIKeyForm.ConfirmDelete', {
3527
key_label: key.label
3628
});
@@ -77,7 +69,7 @@ const APIKeyForm = () => {
7769
<Button
7870
disabled={keyLabel === ''}
7971
iconBefore={<PlusIcon />}
80-
label="Create new key"
72+
aria-label="Create new key"
8173
type={ButtonTypes.SUBMIT}
8274
>
8375
{t('APIKeyForm.CreateTokenSubmit')}
@@ -94,7 +86,7 @@ const APIKeyForm = () => {
9486
</p>
9587
<CopyableInput
9688
label={keyWithToken.label}
97-
value={keyWithToken.token}
89+
value={keyWithToken.token ?? ''}
9890
/>
9991
</div>
10092
)}
@@ -109,5 +101,3 @@ const APIKeyForm = () => {
109101
</div>
110102
);
111103
};
112-
113-
export default APIKeyForm;

client/modules/User/components/APIKeyList.jsx renamed to client/modules/User/components/APIKeyList.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import PropTypes from 'prop-types';
21
import React from 'react';
32
import { orderBy } from 'lodash';
43
import { useTranslation } from 'react-i18next';
5-
6-
import { APIKeyPropType } from './APIKeyForm';
7-
4+
import type { SanitisedApiKey } from '../../../../common/types';
85
import {
96
distanceInWordsToNow,
107
formatDateToString
118
} from '../../../utils/formatDate';
129
import TrashCanIcon from '../../../images/trash-can.svg';
1310

14-
function APIKeyList({ apiKeys, onRemove }) {
11+
export interface APIKeyListProps {
12+
apiKeys: SanitisedApiKey[];
13+
onRemove: (key: SanitisedApiKey) => void;
14+
}
15+
export function APIKeyList({ apiKeys, onRemove }: APIKeyListProps) {
1516
const { t } = useTranslation();
1617
return (
1718
<table className="api-key-list">
@@ -50,10 +51,3 @@ function APIKeyList({ apiKeys, onRemove }) {
5051
</table>
5152
);
5253
}
53-
54-
APIKeyList.propTypes = {
55-
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
56-
onRemove: PropTypes.func.isRequired
57-
};
58-
59-
export default APIKeyList;

client/modules/User/components/AccountForm.jsx renamed to client/modules/User/components/AccountForm.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@ import { Button, ButtonTypes } from '../../../common/Button';
66
import { validateSettings } from '../../../utils/reduxFormUtils';
77
import { updateSettings, initiateVerification } from '../actions';
88
import { apiClient } from '../../../utils/apiClient';
9+
import {
10+
DuplicateUserCheckQuery,
11+
UpdateSettingsRequestBody
12+
} from '../../../../common/types';
13+
import { RootState } from '../../../reducers';
14+
import type { AccountForm as AccountFormType } from '../../../utils/reduxFormUtils';
915

10-
function asyncValidate(fieldToValidate, value) {
16+
function asyncValidate(fieldToValidate: 'username' | 'email', value: string) {
1117
if (!value || value.trim().length === 0) {
1218
return '';
1319
}
14-
const queryParams = {};
20+
const queryParams: DuplicateUserCheckQuery = {
21+
check_type: fieldToValidate
22+
};
1523
queryParams[fieldToValidate] = value;
16-
queryParams.check_type = fieldToValidate;
1724
return apiClient
1825
.get('/signup/duplicate_check', { params: queryParams })
1926
.then((response) => {
@@ -24,27 +31,27 @@ function asyncValidate(fieldToValidate, value) {
2431
});
2532
}
2633

27-
function AccountForm() {
34+
export function AccountForm() {
2835
const { t } = useTranslation();
29-
const user = useSelector((state) => state.user);
36+
const user = useSelector((state: RootState) => state.user);
3037
const dispatch = useDispatch();
3138

32-
const handleInitiateVerification = (evt) => {
39+
const handleInitiateVerification = (evt: React.MouseEvent) => {
3340
evt.preventDefault();
3441
dispatch(initiateVerification());
3542
};
3643

37-
function validateUsername(username) {
44+
function validateUsername(username: AccountFormType['username']) {
3845
if (username === user.username) return '';
3946
return asyncValidate('username', username);
4047
}
4148

42-
function validateEmail(email) {
49+
function validateEmail(email: AccountFormType['email']) {
4350
if (email === user.email) return '';
4451
return asyncValidate('email', email);
4552
}
4653

47-
function onSubmit(formProps) {
54+
function onSubmit(formProps: UpdateSettingsRequestBody) {
4855
return dispatch(updateSettings(formProps));
4956
}
5057

@@ -54,11 +61,13 @@ function AccountForm() {
5461
validate={validateSettings}
5562
onSubmit={onSubmit}
5663
>
57-
{({ handleSubmit, submitting, invalid, restart }) => (
64+
{({ handleSubmit, submitting, invalid, form }) => (
5865
<form
5966
className="form"
60-
onSubmit={(event) => {
61-
handleSubmit(event).then(restart);
67+
onSubmit={async (event: React.FormEvent) => {
68+
const result = await handleSubmit(event);
69+
form.restart();
70+
return result;
6271
}}
6372
>
6473
<Field
@@ -183,5 +192,3 @@ function AccountForm() {
183192
</Form>
184193
);
185194
}
186-
187-
export default AccountForm;

client/modules/User/components/AccountForm.unit.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
act,
99
waitFor
1010
} from '../../../test-utils';
11-
import AccountForm from './AccountForm';
11+
import { AccountForm } from './AccountForm';
1212
import { initialTestState } from '../../../testData/testReduxStore';
1313
import * as actions from '../actions';
1414

client/modules/User/components/CollectionMetadata.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketc
1111
import EditableInput from '../../IDE/components/EditableInput';
1212
import { SketchSearchbar } from '../../IDE/components/Searchbar';
1313
import { getCollection } from '../../IDE/selectors/collections';
14-
import ShareURL from './CollectionShareButton';
14+
import { ShareURL } from './CollectionShareButton';
1515

1616
function CollectionMetadata({ collectionId }) {
1717
const { t } = useTranslation();

0 commit comments

Comments
 (0)