diff --git a/client/common/icons.jsx b/client/common/icons.jsx index bd5a673e2a..784d40040d 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -73,7 +73,7 @@ function withLabel(SvgComponent) { }; Icon.defaultProps = { - 'aria-label': null + 'aria-label': undefined }; return Icon; diff --git a/client/common/useSyncFormTranslations.ts b/client/common/useSyncFormTranslations.ts index 4a90362750..aed766aa43 100644 --- a/client/common/useSyncFormTranslations.ts +++ b/client/common/useSyncFormTranslations.ts @@ -12,11 +12,11 @@ export interface FormLike { * @param language */ export const useSyncFormTranslations = ( - formRef: MutableRefObject, + formRef: MutableRefObject, language: string ) => { useEffect(() => { - const form = formRef.current; + const form = formRef?.current; if (!form) return; const { values } = form.getState(); diff --git a/client/modules/About/About.styles.js b/client/modules/About/About.styles.ts similarity index 100% rename from client/modules/About/About.styles.js rename to client/modules/About/About.styles.ts diff --git a/client/modules/About/pages/About.jsx b/client/modules/About/pages/About.tsx similarity index 85% rename from client/modules/About/pages/About.jsx rename to client/modules/About/pages/About.tsx index 840278cbb7..e6730cf96f 100644 --- a/client/modules/About/pages/About.jsx +++ b/client/modules/About/pages/About.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; -import { useTranslation } from 'react-i18next'; +import { TFunction, useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { @@ -27,8 +26,15 @@ import packageData from '../../../../package.json'; import HeartIcon from '../../../images/heart.svg'; import AsteriskIcon from '../../../images/p5-asterisk.svg'; import LogoIcon from '../../../images/p5js-square-logo.svg'; +import { RootState } from '../../../reducers'; +import { AboutSectionInfoSection } from '../statics/aboutData'; -const AboutSection = ({ section, t }) => ( +interface AboutSectionProps { + section: AboutSectionInfoSection; + t: TFunction<'translation'>; +} + +const AboutSection = ({ section, t }: AboutSectionProps) => (

{t(section.header)}

@@ -47,11 +53,13 @@ const AboutSection = ({ section, t }) => (
); -const About = () => { +export const About = () => { const { t } = useTranslation(); - const p5version = useSelector((state) => { - const index = state.files.find((file) => file.name === 'index.html'); + const p5version = useSelector((state: RootState) => { + const index = state.files.find( + (file: { name: string }) => file.name === 'index.html' + ); return index?.content.match(/\/p5@([\d.]+)\//)?.[1]; }); @@ -91,7 +99,11 @@ const About = () => { {AboutSectionInfo.map((section) => ( - + ))} @@ -152,19 +164,3 @@ const About = () => { ); }; - -AboutSection.propTypes = { - section: PropTypes.shape({ - header: PropTypes.string.isRequired, - items: PropTypes.arrayOf( - PropTypes.shape({ - url: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - description: PropTypes.string.isRequired - }) - ).isRequired - }).isRequired, - t: PropTypes.func.isRequired -}; - -export default About; diff --git a/client/modules/About/statics/aboutData.js b/client/modules/About/statics/aboutData.ts similarity index 84% rename from client/modules/About/statics/aboutData.js rename to client/modules/About/statics/aboutData.ts index a8623cfa7f..8d884acb9e 100644 --- a/client/modules/About/statics/aboutData.js +++ b/client/modules/About/statics/aboutData.ts @@ -1,4 +1,9 @@ -export const ContactSectionLinks = [ +export interface ContactSectionLink { + label: string; + href: string; +} + +export const ContactSectionLinks: ContactSectionLink[] = [ { label: 'About.Github', href: 'https://github.com/processing/p5.js-web-editor' @@ -22,7 +27,18 @@ export const ContactSectionLinks = [ } ]; -export const AboutSectionInfo = [ +export interface AboutSectionInfoItem { + url: string; + title: string; + description: string; +} + +export interface AboutSectionInfoSection { + header: string; + items: AboutSectionInfoItem[]; +} + +export const AboutSectionInfo: AboutSectionInfoSection[] = [ { header: 'About.NewP5', items: [ diff --git a/client/modules/Legal/components/PolicyContainer.jsx b/client/modules/Legal/components/PolicyContainer.tsx similarity index 86% rename from client/modules/Legal/components/PolicyContainer.jsx rename to client/modules/Legal/components/PolicyContainer.tsx index fdc311d435..21e0295e3f 100644 --- a/client/modules/Legal/components/PolicyContainer.jsx +++ b/client/modules/Legal/components/PolicyContainer.tsx @@ -2,7 +2,6 @@ import React from 'react'; import styled from 'styled-components'; import ReactMarkdown from 'react-markdown'; import remarkSlug from 'remark-slug'; -import PropTypes from 'prop-types'; import { remSize, prop } from '../../../theme'; const PolicyContainerMain = styled.main` @@ -48,16 +47,14 @@ const PolicyContainerMain = styled.main` } `; -function PolicyContainer({ policy }) { +export interface PolicyContainerProps { + policy: string; +} + +export function PolicyContainer({ policy }: PolicyContainerProps) { return ( {policy} ); } - -PolicyContainer.propTypes = { - policy: PropTypes.string.isRequired -}; - -export default PolicyContainer; diff --git a/client/modules/Legal/pages/CodeOfConduct.jsx b/client/modules/Legal/pages/CodeOfConduct.tsx similarity index 70% rename from client/modules/Legal/pages/CodeOfConduct.jsx rename to client/modules/Legal/pages/CodeOfConduct.tsx index c961ec74b6..d05e4bb78a 100644 --- a/client/modules/Legal/pages/CodeOfConduct.jsx +++ b/client/modules/Legal/pages/CodeOfConduct.tsx @@ -1,13 +1,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import Legal from './Legal'; +import { Legal } from './Legal'; -function CodeOfConduct() { +export function CodeOfConduct() { const { t } = useTranslation(); return ( ); } - -export default CodeOfConduct; diff --git a/client/modules/Legal/pages/Legal.jsx b/client/modules/Legal/pages/Legal.tsx similarity index 87% rename from client/modules/Legal/pages/Legal.jsx rename to client/modules/Legal/pages/Legal.tsx index 4ba5cb5bbc..6690b08ed7 100644 --- a/client/modules/Legal/pages/Legal.jsx +++ b/client/modules/Legal/pages/Legal.tsx @@ -1,5 +1,4 @@ import axios from 'axios'; -import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; import Helmet from 'react-helmet'; import { useTranslation } from 'react-i18next'; @@ -9,7 +8,7 @@ import { RootPage } from '../../../components/RootPage'; import { remSize } from '../../../theme'; import Loader from '../../App/components/loader'; import Nav from '../../IDE/components/Header/Nav'; -import PolicyContainer from '../components/PolicyContainer'; +import { PolicyContainer } from '../components/PolicyContainer'; const StyledTabList = styled.nav` display: flex; @@ -22,7 +21,19 @@ const StyledTabList = styled.nav` } `; -function Legal({ policyFile, title }) { +export interface LegalProps { + /** + * Used in the HTML tag. + * TODO: pass this to the Nav to use as the mobile title. + */ + title: string; + /** + * Path of the markdown '.md' file, relative to the /public directory. + */ + policyFile: string; +} + +export function Legal({ policyFile, title }: LegalProps) { const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(true); const [policy, setPolicy] = useState(''); @@ -55,17 +66,3 @@ function Legal({ policyFile, title }) { </RootPage> ); } - -Legal.propTypes = { - /** - * Used in the HTML <title> tag. - * TODO: pass this to the Nav to use as the mobile title. - */ - title: PropTypes.string.isRequired, - /** - * Path of the markdown '.md' file, relative to the /public directory. - */ - policyFile: PropTypes.string.isRequired -}; - -export default Legal; diff --git a/client/modules/Legal/pages/PrivacyPolicy.jsx b/client/modules/Legal/pages/PrivacyPolicy.tsx similarity index 70% rename from client/modules/Legal/pages/PrivacyPolicy.jsx rename to client/modules/Legal/pages/PrivacyPolicy.tsx index ef39ed876c..178736709c 100644 --- a/client/modules/Legal/pages/PrivacyPolicy.jsx +++ b/client/modules/Legal/pages/PrivacyPolicy.tsx @@ -1,13 +1,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import Legal from './Legal'; +import { Legal } from './Legal'; -function PrivacyPolicy() { +export function PrivacyPolicy() { const { t } = useTranslation(); return ( <Legal policyFile="privacy-policy.md" title={t('Legal.PrivacyPolicy')} /> ); } - -export default PrivacyPolicy; diff --git a/client/modules/Legal/pages/TermsOfUse.jsx b/client/modules/Legal/pages/TermsOfUse.tsx similarity index 70% rename from client/modules/Legal/pages/TermsOfUse.jsx rename to client/modules/Legal/pages/TermsOfUse.tsx index 6b3b553942..45bbfd93a3 100644 --- a/client/modules/Legal/pages/TermsOfUse.jsx +++ b/client/modules/Legal/pages/TermsOfUse.tsx @@ -1,11 +1,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import Legal from './Legal'; +import { Legal } from './Legal'; -function TermsOfUse() { +export function TermsOfUse() { const { t } = useTranslation(); return <Legal policyFile="terms-of-use.md" title={t('Legal.TermsOfUse')} />; } - -export default TermsOfUse; diff --git a/client/modules/User/actions.js b/client/modules/User/actions.ts similarity index 68% rename from client/modules/User/actions.js rename to client/modules/User/actions.ts index 20e6729b0d..be98cf1fa6 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.ts @@ -1,41 +1,61 @@ import { FORM_ERROR } from 'final-form'; +import { AnyAction, Dispatch } from 'redux'; import * as ActionTypes from '../../constants'; import browserHistory from '../../browserHistory'; import { apiClient } from '../../utils/apiClient'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { setLanguage } from '../IDE/actions/preferences'; import { showToast, setToastText } from '../IDE/actions/toast'; +import type { + CookieConsentOptions, + CreateUserRequestBody, + Error, + PublicUser, + ResetOrUpdatePasswordRequestParams, + ResetPasswordInitiateRequestBody, + SanitisedApiKey, + UpdatePasswordRequestBody, + UpdateSettingsRequestBody, + UserPreferences +} from '../../../common/types'; +import { RootState } from '../../reducers'; +import type { GetRootState } from '../IDE/actions/preferences.types'; -export function authError(error) { +export function authError(error: Error) { return { type: ActionTypes.AUTH_ERROR, payload: error }; } -export function signUpUser(formValues) { +export function signUpUser(formValues: CreateUserRequestBody) { return apiClient.post('/signup', formValues); } -export function loginUser(formValues) { +export function loginUser(formValues: /** TODO: replace with actual type when server/session.controller is migrated */ { + username: string; + password: string; +}) { return apiClient.post('/login', formValues); } -export function authenticateUser(user) { +export function authenticateUser( + user: PublicUser /** Not sure if UserDocument or PublicUser, to check after relevant route is migrated */ +) { return { type: ActionTypes.AUTH_USER, user }; } -export function loginUserFailure(error) { +export function loginUserFailure(error: Error) { return { type: ActionTypes.AUTH_ERROR, error }; } -export function setPreferences(preferences) { +export function setPreferences(preferences: UserPreferences) { return { type: ActionTypes.SET_PREFERENCES, preferences @@ -43,12 +63,12 @@ export function setPreferences(preferences) { } export function validateAndLoginUser(formProps) { - return (dispatch, getState) => { + return (dispatch: Dispatch, getState: GetRootState) => { const state = getState(); const { previousPath } = state.ide; - return new Promise((resolve) => { + return new Promise<void | Error>((resolve) => { loginUser(formProps) - .then((response) => { + .then((response: { data: PublicUser }) => { dispatch(authenticateUser(response.data)); dispatch(setPreferences(response.data.preferences)); dispatch( @@ -70,10 +90,10 @@ export function validateAndLoginUser(formProps) { } export function validateAndSignUpUser(formValues) { - return (dispatch, getState) => { + return (dispatch: Dispatch, getState: GetRootState) => { const state = getState(); const { previousPath } = state.ide; - return new Promise((resolve) => { + return new Promise<void | Error>((resolve) => { signUpUser(formValues) .then((response) => { dispatch(authenticateUser(response.data)); @@ -91,7 +111,7 @@ export function validateAndSignUpUser(formValues) { } export function getUser() { - return async (dispatch) => { + return async (dispatch: Dispatch) => { try { const response = await apiClient.get('/session'); const { data } = response; @@ -106,7 +126,7 @@ export function getUser() { preferences: data.preferences }); setLanguage(data.preferences.language, { persistPreference: false }); - } catch (error) { + } catch (error: any) { const message = error.response ? error.response.data.error || error.response.message : 'Unknown error.'; @@ -116,7 +136,7 @@ export function getUser() { } export function validateSession() { - return async (dispatch, getState) => { + return async (dispatch: Dispatch, getState: () => RootState) => { try { const response = await apiClient.get('/session'); const state = getState(); @@ -124,7 +144,7 @@ export function validateSession() { if (state.user.username !== response.data.username) { dispatch(showErrorModal('staleSession')); } - } catch (error) { + } catch (error: any) { if (error.response && error.response.status === 404) { dispatch(showErrorModal('staleSession')); } @@ -132,7 +152,7 @@ export function validateSession() { }; } -export function resetProject(dispatch) { +export function resetProject(dispatch: Dispatch) { dispatch({ type: ActionTypes.RESET_PROJECT }); @@ -143,7 +163,7 @@ export function resetProject(dispatch) { } export function logoutUser() { - return (dispatch) => { + return (dispatch: Dispatch) => { apiClient .get('/logout') .then(() => { @@ -159,9 +179,11 @@ export function logoutUser() { }; } -export function initiateResetPassword(formValues) { - return (dispatch) => - new Promise((resolve) => { +export function initiateResetPassword( + formValues: ResetPasswordInitiateRequestBody +) { + return (dispatch: Dispatch) => + new Promise<void | Error>((resolve) => { dispatch({ type: ActionTypes.RESET_PASSWORD_INITIATE }); @@ -180,7 +202,7 @@ export function initiateResetPassword(formValues) { } export function initiateVerification() { - return (dispatch) => { + return (dispatch: Dispatch) => { dispatch({ type: ActionTypes.EMAIL_VERIFICATION_INITIATE }); @@ -199,8 +221,10 @@ export function initiateVerification() { }; } -export function verifyEmailConfirmation(token) { - return (dispatch) => { +export function verifyEmailConfirmation( + token: ResetOrUpdatePasswordRequestParams['token'] +) { + return (dispatch: Dispatch) => { dispatch({ type: ActionTypes.EMAIL_VERIFICATION_VERIFY, state: 'checking' @@ -229,8 +253,10 @@ export function resetPasswordReset() { }; } -export function validateResetPasswordToken(token) { - return (dispatch) => { +export function validateResetPasswordToken( + token: ResetOrUpdatePasswordRequestParams['token'] +) { + return (dispatch: Dispatch) => { apiClient .get(`/reset-password/${token}`) .then(() => { @@ -244,9 +270,12 @@ export function validateResetPasswordToken(token) { }; } -export function updatePassword(formValues, token) { - return (dispatch) => - new Promise((resolve) => +export function updatePassword( + formValues: UpdatePasswordRequestBody, + token: ResetOrUpdatePasswordRequestParams['token'] +) { + return (dispatch: Dispatch) => + new Promise<void | Error>((resolve) => apiClient .post(`/reset-password/${token}`, formValues) .then((response) => { @@ -263,20 +292,30 @@ export function updatePassword(formValues, token) { ); } -export function updateSettingsSuccess(user) { +export function updateSettingsSuccess(user: PublicUser) { return { type: ActionTypes.SETTINGS_UPDATED, user }; } -export function submitSettings(formValues) { +/** + * - Method: `PUT` + * - Endpoint: `/account` + * - Authenticated: `true` + * - Id: `UserController.updateSettings` + * + * Description: + * - Used to update the user's username, email, or password on the `/account` page while authenticated + * - Currently the client only shows the `currentPassword` and `newPassword` fields if no social logins (github & google) are enabled + */ +export function submitSettings(formValues: UpdateSettingsRequestBody) { return apiClient.put('/account', formValues); } -export function updateSettings(formValues) { - return (dispatch) => - new Promise((resolve) => { +export function updateSettings(formValues: UpdateSettingsRequestBody) { + return (dispatch: Dispatch) => + new Promise<void | Error>((resolve) => { if (!formValues.currentPassword && formValues.newPassword) { dispatch(showToast(5500)); dispatch(setToastText('Toast.EmptyCurrentPass')); @@ -313,15 +352,24 @@ export function updateSettings(formValues) { }); } -export function createApiKeySuccess(user) { +export function createApiKeySuccess(user: PublicUser) { return { type: ActionTypes.API_KEY_CREATED, user }; } -export function createApiKey(label) { - return (dispatch) => +/** + * - Method: `POST` + * - Endpoint: `/account/api-keys` + * - Authenticated: `true` + * - Id: `UserController.createApiKey` + * + * Description: + * - Create API key + */ +export function createApiKey(label: SanitisedApiKey['label']) { + return (dispatch: Dispatch) => apiClient .post('/account/api-keys', { label }) .then((response) => { @@ -333,8 +381,17 @@ export function createApiKey(label) { }); } -export function removeApiKey(keyId) { - return (dispatch) => +/** + * - Method: `DELETE` + * - Endpoint: `/account/api-keys/:keyId` + * - Authenticated: `true` + * - Id: `UserController.removeApiKey` + * + * Description: + * - Remove API key + */ +export function removeApiKey(keyId: SanitisedApiKey['id']) { + return (dispatch: Dispatch) => apiClient .delete(`/account/api-keys/${keyId}`) .then((response) => { @@ -349,8 +406,8 @@ export function removeApiKey(keyId) { }); } -export function unlinkService(service) { - return (dispatch) => { +export function unlinkService(service: string) { + return (dispatch: Dispatch) => { if (!['github', 'google'].includes(service)) return; apiClient .delete(`/auth/${service}`) @@ -365,9 +422,18 @@ export function unlinkService(service) { }; } -export function setUserCookieConsent(cookieConsent) { +/** + * - Method: `PUT` + * - Endpoint: `/cookie-consent` + * - Authenticated: `true` + * - Id: `UserController.updatePreferences` + * + * Description: + * - Update user cookie consent + */ +export function setUserCookieConsent(cookieConsent: CookieConsentOptions) { // maybe also send this to the server rn? - return (dispatch) => { + return (dispatch: Dispatch) => { apiClient .put('/cookie-consent', { cookieConsent }) .then(() => { diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.tsx similarity index 83% rename from client/modules/User/components/APIKeyForm.jsx rename to client/modules/User/components/APIKeyForm.tsx index fda5945f87..3dc42ee9c8 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.tsx @@ -7,30 +7,24 @@ import { PlusIcon } from '../../../common/icons'; import CopyableInput from '../../IDE/components/CopyableInput'; import { createApiKey, removeApiKey } from '../actions'; -import APIKeyList from './APIKeyList'; +import { APIKeyList } from './APIKeyList'; +import { RootState } from '../../../reducers'; +import type { SanitisedApiKey } from '../../../../common/types'; -export const APIKeyPropType = PropTypes.shape({ - id: PropTypes.string.isRequired, - token: PropTypes.string, - label: PropTypes.string.isRequired, - createdAt: PropTypes.string.isRequired, - lastUsedAt: PropTypes.string -}); - -const APIKeyForm = () => { +export const APIKeyForm = () => { const { t } = useTranslation(); - const apiKeys = useSelector((state) => state.user.apiKeys); + const apiKeys = useSelector((state: RootState) => state.user.apiKeys); const dispatch = useDispatch(); const [keyLabel, setKeyLabel] = useState(''); - const addKey = (event) => { + const addKey = (event: React.FormEvent) => { event.preventDefault(); dispatch(createApiKey(keyLabel)); setKeyLabel(''); }; - const removeKey = (key) => { + const removeKey = (key: SanitisedApiKey) => { const message = t('APIKeyForm.ConfirmDelete', { key_label: key.label }); @@ -49,7 +43,7 @@ const APIKeyForm = () => { return <p>{t('APIKeyForm.NoTokens')}</p>; }; - const keyWithToken = apiKeys.find((k) => !!k.token); + const keyWithToken = apiKeys.find((k: SanitisedApiKey) => !!k.token); return ( <div className="api-key-form"> @@ -77,7 +71,7 @@ const APIKeyForm = () => { <Button disabled={keyLabel === ''} iconBefore={<PlusIcon />} - label="Create new key" + aria-labelledby="Create new key" type={ButtonTypes.SUBMIT} > {t('APIKeyForm.CreateTokenSubmit')} @@ -109,5 +103,3 @@ const APIKeyForm = () => { </div> ); }; - -export default APIKeyForm; diff --git a/client/modules/User/components/APIKeyList.jsx b/client/modules/User/components/APIKeyList.tsx similarity index 82% rename from client/modules/User/components/APIKeyList.jsx rename to client/modules/User/components/APIKeyList.tsx index 17cd857354..a3609833ae 100644 --- a/client/modules/User/components/APIKeyList.jsx +++ b/client/modules/User/components/APIKeyList.tsx @@ -1,17 +1,19 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { orderBy } from 'lodash'; import { useTranslation } from 'react-i18next'; - -import { APIKeyPropType } from './APIKeyForm'; - import { distanceInWordsToNow, formatDateToString } from '../../../utils/formatDate'; import TrashCanIcon from '../../../images/trash-can.svg'; +import type { SanitisedApiKey } from '../../../../common/types'; + +export interface APIKeyListProps { + apiKeys: SanitisedApiKey[]; + onRemove: (key: SanitisedApiKey) => void; +} -function APIKeyList({ apiKeys, onRemove }) { +export function APIKeyList({ apiKeys, onRemove }: APIKeyListProps) { const { t } = useTranslation(); return ( <table className="api-key-list"> @@ -50,10 +52,3 @@ function APIKeyList({ apiKeys, onRemove }) { </table> ); } - -APIKeyList.propTypes = { - apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired, - onRemove: PropTypes.func.isRequired -}; - -export default APIKeyList; diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.tsx similarity index 85% rename from client/modules/User/components/AccountForm.jsx rename to client/modules/User/components/AccountForm.tsx index 4ef40e4298..718f111de9 100644 --- a/client/modules/User/components/AccountForm.jsx +++ b/client/modules/User/components/AccountForm.tsx @@ -6,14 +6,16 @@ import { Button, ButtonTypes } from '../../../common/Button'; import { validateSettings } from '../../../utils/reduxFormUtils'; import { updateSettings, initiateVerification } from '../actions'; import { apiClient } from '../../../utils/apiClient'; +import { RootState } from '../../../reducers'; +import type { AccountForm as AccountFormType } from '../../../utils/reduxFormUtils'; +import type { DuplicateUserCheckQuery } from '../../../../common/types'; -function asyncValidate(fieldToValidate, value) { +function asyncValidate(fieldToValidate: 'username' | 'email', value?: string) { if (!value || value.trim().length === 0) { return ''; } - const queryParams = {}; + const queryParams: DuplicateUserCheckQuery = { check_type: fieldToValidate }; queryParams[fieldToValidate] = value; - queryParams.check_type = fieldToValidate; return apiClient .get('/signup/duplicate_check', { params: queryParams }) .then((response) => { @@ -24,27 +26,27 @@ function asyncValidate(fieldToValidate, value) { }); } -function AccountForm() { +export function AccountForm() { const { t } = useTranslation(); - const user = useSelector((state) => state.user); + const user = useSelector((state: RootState) => state.user); const dispatch = useDispatch(); - const handleInitiateVerification = (evt) => { + const handleInitiateVerification = (evt: React.MouseEvent) => { evt.preventDefault(); dispatch(initiateVerification()); }; - function validateUsername(username) { + function validateUsername(username: AccountFormType['username']) { if (username === user.username) return ''; return asyncValidate('username', username); } - function validateEmail(email) { + function validateEmail(email: AccountFormType['email']) { if (email === user.email) return ''; return asyncValidate('email', email); } - function onSubmit(formProps) { + function onSubmit(formProps: AccountFormType) { return dispatch(updateSettings(formProps)); } @@ -54,11 +56,13 @@ function AccountForm() { validate={validateSettings} onSubmit={onSubmit} > - {({ handleSubmit, submitting, invalid, restart }) => ( + {({ handleSubmit, submitting, invalid, form }) => ( <form className="form" - onSubmit={(event) => { - handleSubmit(event).then(restart); + onSubmit={async (event: React.FormEvent) => { + const result = await handleSubmit(event); + form.restart(); + return result; }} > <Field @@ -98,7 +102,7 @@ function AccountForm() { </span> ) : ( <Button - onClick={handleInitiateVerification} + onClick={() => handleInitiateVerification} className="form__resend-button" > {t('AccountForm.Resend')} @@ -183,5 +187,3 @@ function AccountForm() { </Form> ); } - -export default AccountForm; diff --git a/client/modules/User/components/AccountForm.unit.test.jsx b/client/modules/User/components/AccountForm.unit.test.jsx index caaa7ddc39..b9d2baebe0 100644 --- a/client/modules/User/components/AccountForm.unit.test.jsx +++ b/client/modules/User/components/AccountForm.unit.test.jsx @@ -8,7 +8,7 @@ import { act, waitFor } from '../../../test-utils'; -import AccountForm from './AccountForm'; +import { AccountForm } from './AccountForm'; import { initialTestState } from '../../../testData/testReduxStore'; import * as actions from '../actions'; diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.tsx similarity index 90% rename from client/modules/User/components/DashboardTabSwitcher.jsx rename to client/modules/User/components/DashboardTabSwitcher.tsx index 7afbe5aec8..d960a31678 100644 --- a/client/modules/User/components/DashboardTabSwitcher.jsx +++ b/client/modules/User/components/DashboardTabSwitcher.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -25,7 +24,17 @@ const FilterOptions = styled(Options)` } `; -const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => { +export interface DashboardTabSwitcherProps { + currentTab: string; + isOwner: boolean; + username: string; +} + +export const DashboardTabSwitcher = ({ + currentTab, + isOwner, + username +}: DashboardTabSwitcherProps) => { const isMobile = useIsMobile(); const { t } = useTranslation(); const dispatch = useDispatch(); @@ -89,11 +98,3 @@ const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => { </div> ); }; - -DashboardTabSwitcher.propTypes = { - currentTab: PropTypes.string.isRequired, - isOwner: PropTypes.bool.isRequired, - username: PropTypes.string.isRequired -}; - -export default DashboardTabSwitcher; diff --git a/client/modules/User/components/LoginForm.jsx b/client/modules/User/components/LoginForm.tsx similarity index 92% rename from client/modules/User/components/LoginForm.jsx rename to client/modules/User/components/LoginForm.tsx index 5bac902ecf..182ef80f07 100644 --- a/client/modules/User/components/LoginForm.jsx +++ b/client/modules/User/components/LoginForm.tsx @@ -7,16 +7,18 @@ import { Button, ButtonTypes } from '../../../common/Button'; import { validateLogin } from '../../../utils/reduxFormUtils'; import { validateAndLoginUser } from '../actions'; import { useSyncFormTranslations } from '../../../common/useSyncFormTranslations'; +import type { LoginForm as LoginFormType } from '../../../utils/reduxFormUtils'; +import type { FormLike } from '../../../common/useSyncFormTranslations'; -function LoginForm() { +export function LoginForm() { const { t, i18n } = useTranslation(); const dispatch = useDispatch(); - function onSubmit(formProps) { + function onSubmit(formProps: LoginFormType) { return dispatch(validateAndLoginUser(formProps)); } const [showPassword, setShowPassword] = useState(false); - const formRef = useRef(null); + const formRef: React.MutableRefObject<FormLike | null> = useRef(null); const handleVisibility = () => { setShowPassword(!showPassword); @@ -114,5 +116,3 @@ function LoginForm() { </Form> ); } - -export default LoginForm; diff --git a/client/modules/User/components/LoginForm.unit.test.jsx b/client/modules/User/components/LoginForm.unit.test.jsx index 2f4262c16b..1b6b982733 100644 --- a/client/modules/User/components/LoginForm.unit.test.jsx +++ b/client/modules/User/components/LoginForm.unit.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; -import LoginForm from './LoginForm'; +import { LoginForm } from './LoginForm'; import * as actions from '../actions'; import { initialTestState } from '../../../testData/testReduxStore'; import { reduxRender, screen, fireEvent, act } from '../../../test-utils'; diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.tsx similarity index 85% rename from client/modules/User/components/NewPasswordForm.jsx rename to client/modules/User/components/NewPasswordForm.tsx index feca326c77..80d4054484 100644 --- a/client/modules/User/components/NewPasswordForm.jsx +++ b/client/modules/User/components/NewPasswordForm.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { Form, Field } from 'react-final-form'; import { useDispatch } from 'react-redux'; @@ -6,13 +5,18 @@ import { useTranslation } from 'react-i18next'; import { validateNewPassword } from '../../../utils/reduxFormUtils'; import { updatePassword } from '../actions'; import { Button, ButtonTypes } from '../../../common/Button'; +import type { ResetOrUpdatePasswordRequestParams } from '../../../../common/types'; +import type { NewPasswordForm as NewPasswordFormType } from '../../../utils/reduxFormUtils'; -function NewPasswordForm(props) { - const { resetPasswordToken } = props; +export type NewPasswordFormProps = { + resetPasswordToken: ResetOrUpdatePasswordRequestParams['token']; +}; + +export function NewPasswordForm({ resetPasswordToken }: NewPasswordFormProps) { const { t } = useTranslation(); const dispatch = useDispatch(); - function onSubmit(formProps) { + function onSubmit(formProps: NewPasswordFormType) { return dispatch(updatePassword(formProps, resetPasswordToken)); } @@ -75,9 +79,3 @@ function NewPasswordForm(props) { </Form> ); } - -NewPasswordForm.propTypes = { - resetPasswordToken: PropTypes.string.isRequired -}; - -export default NewPasswordForm; diff --git a/client/modules/User/components/NewPasswordForm.unit.test.jsx b/client/modules/User/components/NewPasswordForm.unit.test.jsx index dbddf3c8cb..a55af924ed 100644 --- a/client/modules/User/components/NewPasswordForm.unit.test.jsx +++ b/client/modules/User/components/NewPasswordForm.unit.test.jsx @@ -4,7 +4,7 @@ import configureStore from 'redux-mock-store'; import { fireEvent } from '@storybook/testing-library'; import { reduxRender, screen, act, waitFor } from '../../../test-utils'; import { initialTestState } from '../../../testData/testReduxStore'; -import NewPasswordForm from './NewPasswordForm'; +import { NewPasswordForm } from './NewPasswordForm'; const mockStore = configureStore([thunk]); const store = mockStore(initialTestState); diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index e9b3da7c9a..7a151f1ec8 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -5,9 +5,9 @@ import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import { useHistory, useLocation } from 'react-router-dom'; import { parse } from 'query-string'; -import AccountForm from '../components/AccountForm'; +import { AccountForm } from '../components/AccountForm'; import SocialAuthButton from '../components/SocialAuthButton'; -import APIKeyForm from '../components/APIKeyForm'; +import { APIKeyForm } from '../components/APIKeyForm'; import Nav from '../../IDE/components/Header/Nav'; import ErrorModal from '../../IDE/components/ErrorModal'; import Overlay from '../../App/components/Overlay'; diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index 1626d0d369..8044de028a 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -18,7 +18,8 @@ import { } from '../../IDE/components/Searchbar'; import CollectionCreate from '../components/CollectionCreate'; -import DashboardTabSwitcherPublic, { +import { + DashboardTabSwitcher, TabKey } from '../components/DashboardTabSwitcher'; import useIsMobile from '../../IDE/hooks/useIsMobile'; @@ -123,7 +124,7 @@ const DashboardView = () => { <div className="dashboard-header__header"> <h2 className="dashboard-header__header__title">{ownerName()}</h2> <div className="dashboard-header__nav"> - <DashboardTabSwitcherPublic + <DashboardTabSwitcher currentTab={currentTab} isOwner={isOwner()} username={params.username} diff --git a/client/modules/User/pages/LoginView.jsx b/client/modules/User/pages/LoginView.jsx index b931c50ea8..4fcd75a834 100644 --- a/client/modules/User/pages/LoginView.jsx +++ b/client/modules/User/pages/LoginView.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; -import LoginForm from '../components/LoginForm'; +import { LoginForm } from '../components/LoginForm'; import SocialAuthButton from '../components/SocialAuthButton'; import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; diff --git a/client/modules/User/pages/NewPasswordView.jsx b/client/modules/User/pages/NewPasswordView.jsx index 2bc310e6a3..2817cded9f 100644 --- a/client/modules/User/pages/NewPasswordView.jsx +++ b/client/modules/User/pages/NewPasswordView.jsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import NewPasswordForm from '../components/NewPasswordForm'; +import { NewPasswordForm } from '../components/NewPasswordForm'; import { validateResetPasswordToken } from '../actions'; import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.ts similarity index 100% rename from client/modules/User/reducers.js rename to client/modules/User/reducers.ts diff --git a/client/routes.jsx b/client/routes.jsx index 8926a95bdd..99e900c43e 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -7,9 +7,9 @@ import App from './modules/App/App'; import IDEView from './modules/IDE/pages/IDEView'; import FullView from './modules/IDE/pages/FullView'; import About from './modules/About/pages/About'; -import CodeOfConduct from './modules/Legal/pages/CodeOfConduct'; -import PrivacyPolicy from './modules/Legal/pages/PrivacyPolicy'; -import TermsOfUse from './modules/Legal/pages/TermsOfUse'; +import { CodeOfConduct } from './modules/Legal/pages/CodeOfConduct'; +import { PrivacyPolicy } from './modules/Legal/pages/PrivacyPolicy'; +import { TermsOfUse } from './modules/Legal/pages/TermsOfUse'; import LoginView from './modules/User/pages/LoginView'; import SignupView from './modules/User/pages/SignupView'; import ResetPasswordView from './modules/User/pages/ResetPasswordView'; diff --git a/package-lock.json b/package-lock.json index 9edffa05e9..f948e6f8dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -171,6 +171,7 @@ "@types/passport": "^1.0.17", "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", + "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3", "@types/sinon": "^17.0.4", "@types/styled-components": "^5.1.34", @@ -16646,6 +16647,16 @@ "@types/react": "^16.0.0" } }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-redux": { "version": "7.1.18", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", @@ -53346,6 +53357,15 @@ "dev": true, "requires": {} }, + "@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-redux": { "version": "7.1.18", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz", diff --git a/package.json b/package.json index 643d468aa3..df7a8dc603 100644 --- a/package.json +++ b/package.json @@ -144,9 +144,9 @@ "@types/nodemailer": "^7.0.1", "@types/nodemailer-mailgun-transport": "^1.4.6", "@types/passport": "^1.0.17", - "@types/passport": "^1.0.17", "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", + "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3", "@types/sinon": "^17.0.4", "@types/styled-components": "^5.1.34", diff --git a/server/types/apiKey.ts b/server/types/apiKey.ts index 21ba15aa04..a8703ff55a 100644 --- a/server/types/apiKey.ts +++ b/server/types/apiKey.ts @@ -23,7 +23,9 @@ export interface ApiKeyDocument * and can be exposed to the client */ export interface SanitisedApiKey - extends Pick<ApiKeyDocument, 'id' | 'label' | 'lastUsedAt' | 'createdAt'> {} + extends Pick<ApiKeyDocument, 'id' | 'label' | 'lastUsedAt' | 'createdAt'> { + token?: string; // sometimes injected by userController.createApiKey +} /** Mongoose model for API Key */ export interface ApiKeyModel extends Model<ApiKeyDocument> {} @@ -37,7 +39,7 @@ export type ApiKeyResponseOrError = ApiKeyResponse | Error; /** Response for api-key related endpoints, containing list of keys */ export interface ApiKeyResponse { - apiKeys: ApiKeyDocument[]; + apiKeys: SanitisedApiKey[]; } /** userController.createApiKey - Request */