From 9fc95a9ce39fa6608c7dae26129a7bc3e5661ced Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 13:33:24 +0000 Subject: [PATCH 01/61] client/modules/User/reducers: update to ts, no-verify --- client/modules/User/{reducers.js => reducers.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/{reducers.js => reducers.ts} (100%) 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 From fda66326749c8ffb88cf1363148cad019bdea77c Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 14:29:43 +0000 Subject: [PATCH 02/61] client/modules/Users/reducers: update to named export and add types, wip no-verify --- client/modules/User/reducers.ts | 12 +++++++++--- client/reducers.ts | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/client/modules/User/reducers.ts b/client/modules/User/reducers.ts index 2832190b28..f043eb8f0f 100644 --- a/client/modules/User/reducers.ts +++ b/client/modules/User/reducers.ts @@ -1,6 +1,14 @@ +import type { CookieConsentOptions, PublicUser } from '../../../common/types'; import * as ActionTypes from '../../constants'; -const user = (state = { authenticated: false }, action) => { +// User Action: +export type UserAction = { + user?: PublicUser; + cookieConsent?: CookieConsentOptions; + type: any; +}; + +export const user = (state = { authenticated: false }, action: UserAction) => { switch (action.type) { case ActionTypes.AUTH_USER: return { @@ -47,5 +55,3 @@ const user = (state = { authenticated: false }, action) => { return state; } }; - -export default user; diff --git a/client/reducers.ts b/client/reducers.ts index 2c65e555ca..47d9627f80 100644 --- a/client/reducers.ts +++ b/client/reducers.ts @@ -4,7 +4,7 @@ import ide from './modules/IDE/reducers/ide'; import { preferences } from './modules/IDE/reducers/preferences'; import project from './modules/IDE/reducers/project'; import editorAccessibility from './modules/IDE/reducers/editorAccessibility'; -import user from './modules/User/reducers'; +import { user } from './modules/User/reducers'; import sketches from './modules/IDE/reducers/projects'; import toast from './modules/IDE/reducers/toast'; import console from './modules/IDE/reducers/console'; From 9623321e2a87efe471859a1ff753bd71d33fd3f3 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 14:30:30 +0000 Subject: [PATCH 03/61] server/types/apiKey: add token? to apiKey --- server/types/apiKey.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/types/apiKey.ts b/server/types/apiKey.ts index 21ba15aa04..443a30e3e4 100644 --- a/server/types/apiKey.ts +++ b/server/types/apiKey.ts @@ -8,6 +8,7 @@ export interface IApiKey extends VirtualId, MongooseTimestamps { label: string; lastUsedAt?: Date; hashedKey: string; + token?: string; } /** Mongoose document object for API Key */ @@ -23,7 +24,10 @@ export interface ApiKeyDocument * and can be exposed to the client */ export interface SanitisedApiKey - extends Pick {} + extends Pick< + ApiKeyDocument, + 'id' | 'label' | 'lastUsedAt' | 'createdAt' | 'token' + > {} /** Mongoose model for API Key */ export interface ApiKeyModel extends Model {} From 353342540fabd39311da7e9a4d1b35a2f2b23c27 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 14:44:58 +0000 Subject: [PATCH 04/61] server/PublicUser: update apiKeys to be sanitised api keys --- server/types/user.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/types/user.ts b/server/types/user.ts index 6d6d53fa1f..509da313b7 100644 --- a/server/types/user.ts +++ b/server/types/user.ts @@ -2,7 +2,7 @@ import { Document, Model, Types } from 'mongoose'; import { VirtualId, MongooseTimestamps } from './mongoose'; import { UserPreferences, CookieConsentOptions } from './userPreferences'; import { EmailConfirmationStates } from './email'; -import { ApiKeyDocument } from './apiKey'; +import { ApiKeyDocument, SanitisedApiKey } from './apiKey'; import { Error, GenericResponseBody, RouteParam } from './express'; // -------- MONGOOSE -------- @@ -38,14 +38,16 @@ export interface PublicUser | 'email' | 'username' | 'preferences' - | 'apiKeys' | 'verified' | 'id' | 'totalSize' | 'github' | 'google' | 'cookieConsent' - > {} + > { + /** Can contain either raw ApiKeyDocuments (server side) or SanitisedApiKeys (client side) */ + apiKeys: SanitisedApiKey[]; +} /** Mongoose document object for User */ export interface UserDocument From 6bcdebff94d6eeac78167347b4f979ab56c54875 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 14:58:00 +0000 Subject: [PATCH 05/61] update UserState in root redux to include User --- client/modules/User/reducers.ts | 9 ++++++++- server/types/user.ts | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/client/modules/User/reducers.ts b/client/modules/User/reducers.ts index f043eb8f0f..d8a7141683 100644 --- a/client/modules/User/reducers.ts +++ b/client/modules/User/reducers.ts @@ -8,7 +8,14 @@ export type UserAction = { type: any; }; -export const user = (state = { authenticated: false }, action: UserAction) => { +export const user = ( + state: Partial & { + authenticated: boolean; + } = { + authenticated: false + }, + action: UserAction +) => { switch (action.type) { case ActionTypes.AUTH_USER: return { diff --git a/server/types/user.ts b/server/types/user.ts index 509da313b7..5d2eb61f34 100644 --- a/server/types/user.ts +++ b/server/types/user.ts @@ -22,7 +22,7 @@ export interface IUser extends VirtualId, MongooseTimestamps { tokens: { kind: string }[]; apiKeys: Types.DocumentArray; preferences: UserPreferences; - totalSize: number; + totalSize?: number; cookieConsent: CookieConsentOptions; banned: boolean; lastLoginTimestamp?: Date; @@ -44,6 +44,7 @@ export interface PublicUser | 'github' | 'google' | 'cookieConsent' + | 'totalSize' > { /** Can contain either raw ApiKeyDocuments (server side) or SanitisedApiKeys (client side) */ apiKeys: SanitisedApiKey[]; From 2c0342d4d9cf77c352ee649180bcbb4e7634f018 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 14:58:56 +0000 Subject: [PATCH 06/61] resolve remaining type-errors from PublicUser vs User apiKey differences --- client/testData/testReduxStore.ts | 6 ++++- .../user.controller/__testUtils__.ts | 5 ++-- .../user.controller/__tests__/apiKey.test.ts | 27 ++++++++++++------- .../authManagement/passwordManagement.test.ts | 26 +++++++++++------- .../authManagement/updateSettings.test.ts | 17 +++++++----- 5 files changed, 51 insertions(+), 30 deletions(-) diff --git a/client/testData/testReduxStore.ts b/client/testData/testReduxStore.ts index 33223ae950..daf663ef92 100644 --- a/client/testData/testReduxStore.ts +++ b/client/testData/testReduxStore.ts @@ -54,7 +54,11 @@ const initialTestState: RootState = { user: { email: 'happydog@example.com', username: 'happydog', - preferences: {}, + preferences: { + ...initialPrefState, + indentationAmount: 2, + isTabIndent: true + }, apiKeys: [], verified: 'sent', id: '123456789', diff --git a/server/controllers/user.controller/__testUtils__.ts b/server/controllers/user.controller/__testUtils__.ts index 9d851c49f8..1a7bf31cd6 100644 --- a/server/controllers/user.controller/__testUtils__.ts +++ b/server/controllers/user.controller/__testUtils__.ts @@ -29,7 +29,7 @@ export const mockBaseUserSanitised: PublicUser = { email: 'test@example.com', username: 'tester', preferences: mockUserPreferences, - apiKeys: ([] as unknown) as Types.DocumentArray, + apiKeys: [], verified: 'verified', id: 'abc123', totalSize: 42, @@ -42,6 +42,7 @@ export const mockBaseUserSanitised: PublicUser = { export const mockBaseUserFull: Omit = { ...mockBaseUserSanitised, name: 'test user', + apiKeys: ([] as unknown) as Types.DocumentArray, tokens: [], password: 'abweorij', resetPasswordToken: '1i14ij23', @@ -58,7 +59,7 @@ export const mockBaseUserFull: Omit = { export function createMockUser( overrides: Partial = {}, unSanitised: boolean = false -): PublicUser & Record { +): (PublicUser | UserDocument) & Record { return { ...(unSanitised ? mockBaseUserFull : mockBaseUserSanitised), ...overrides diff --git a/server/controllers/user.controller/__tests__/apiKey.test.ts b/server/controllers/user.controller/__tests__/apiKey.test.ts index 875b15b2b6..47a0b8fe8c 100644 --- a/server/controllers/user.controller/__tests__/apiKey.test.ts +++ b/server/controllers/user.controller/__tests__/apiKey.test.ts @@ -7,7 +7,11 @@ import { Types } from 'mongoose'; import { User } from '../../../models/user'; import { createApiKey, removeApiKey } from '../apiKey'; -import type { ApiKeyDocument, RemoveApiKeyRequestParams } from '../../../types'; +import type { + ApiKeyDocument, + RemoveApiKeyRequestParams, + UserDocument +} from '../../../types'; import { createMockUser } from '../__testUtils__'; jest.mock('../../../models/user'); @@ -31,7 +35,7 @@ describe('user.controller > api key', () => { describe('createApiKey', () => { it("returns an error if user doesn't exist", async () => { - request.user = createMockUser({ id: '1234' }); + request.user = createMockUser({ id: '1234' }, true); User.findById = jest.fn().mockResolvedValue(null); @@ -48,7 +52,7 @@ describe('user.controller > api key', () => { }); it('returns an error if label not provided', async () => { - request.user = createMockUser({ id: '1234' }); + request.user = createMockUser({ id: '1234' }, true); request.body = {}; const user = new User(); @@ -98,7 +102,7 @@ describe('user.controller > api key', () => { describe('removeApiKey', () => { it("returns an error if user doesn't exist", async () => { - request.user = createMockUser({ id: '1234' }); + request.user = createMockUser({ id: '1234' }, true); User.findById = jest.fn().mockResolvedValue(null); @@ -115,7 +119,7 @@ describe('user.controller > api key', () => { }); it("returns an error if specified key doesn't exist", async () => { - request.user = createMockUser({ id: '1234' }); + request.user = createMockUser({ id: '1234' }, true); request.params = { keyId: 'not-a-real-key' }; const user = new User(); user.apiKeys = ([] as unknown) as Types.DocumentArray; @@ -145,11 +149,14 @@ describe('user.controller > api key', () => { apiKeys.find = Array.prototype.find; apiKeys.pull = jest.fn(); - const user = createMockUser({ - id: '1234', - apiKeys, - save: jest.fn() - }); + const user = createMockUser( + { + id: '1234', + apiKeys, + save: jest.fn() + }, + true + ) as UserDocument; request.user = user; request.params = { keyId: 'id1' }; diff --git a/server/controllers/user.controller/__tests__/authManagement/passwordManagement.test.ts b/server/controllers/user.controller/__tests__/authManagement/passwordManagement.test.ts index 248eeac4e0..b5336bffe4 100644 --- a/server/controllers/user.controller/__tests__/authManagement/passwordManagement.test.ts +++ b/server/controllers/user.controller/__tests__/authManagement/passwordManagement.test.ts @@ -29,7 +29,7 @@ describe('user.controller > auth management > password management', () => { let response: MockResponse; let next: MockNext; let mockToken: string; - let mockUser: Partial; + let mockUser: UserDocument; const fixedTime = 100000000; beforeEach(() => { @@ -68,10 +68,13 @@ describe('user.controller > auth management > password management', () => { describe('if the user is found', () => { beforeEach(async () => { mockToken = 'mock-token'; - mockUser = createMockUser({ - email: 'test@example.com', - save: jest.fn().mockResolvedValue(null) - }); + mockUser = createMockUser( + { + email: 'test@example.com', + save: jest.fn().mockResolvedValue(null) + }, + false + ) as UserDocument; (generateToken as jest.Mock).mockResolvedValue(mockToken); User.findByEmail = jest.fn().mockResolvedValue(mockUser); @@ -143,10 +146,13 @@ describe('user.controller > auth management > password management', () => { }); it('returns unsuccessful for all other errors', async () => { mockToken = 'mock-token'; - mockUser = createMockUser({ - email: 'test@example.com', - save: jest.fn().mockResolvedValue(null) - }); + mockUser = createMockUser( + { + email: 'test@example.com', + save: jest.fn().mockResolvedValue(null) + }, + false + ) as UserDocument; (generateToken as jest.Mock).mockRejectedValue( new Error('network error') @@ -298,7 +304,7 @@ describe('user.controller > auth management > password management', () => { resetPasswordToken: 'valid-token', resetPasswordExpires: fixedTime + 10000, // still valid save: jest.fn() - }; + } as UserDocument; beforeEach(async () => { User.findOne = jest.fn().mockReturnValue({ diff --git a/server/controllers/user.controller/__tests__/authManagement/updateSettings.test.ts b/server/controllers/user.controller/__tests__/authManagement/updateSettings.test.ts index 698dfa1aea..faf6a89297 100644 --- a/server/controllers/user.controller/__tests__/authManagement/updateSettings.test.ts +++ b/server/controllers/user.controller/__tests__/authManagement/updateSettings.test.ts @@ -60,13 +60,16 @@ describe('user.controller > auth management > updateSettings (email, username, p response = new MockResponse(); next = jest.fn(); - startingUser = createMockUser({ - username: OLD_USERNAME, - email: OLD_EMAIL, - password: OLD_PASSWORD, - id: '123459', - comparePassword: jest.fn().mockResolvedValue(true) - }); + startingUser = createMockUser( + { + username: OLD_USERNAME, + email: OLD_EMAIL, + password: OLD_PASSWORD, + id: '123459', + comparePassword: jest.fn().mockResolvedValue(true) + }, + false + ) as UserDocument; testUser = { ...startingUser }; // copy to avoid mutation causing false-positive tests results From 70880de60bcdd0c5652e4bf2a4b48186e2c08e8f Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 15:16:28 +0000 Subject: [PATCH 07/61] client/modules/User/actions: update to ts, no-verify --- client/modules/User/{actions.js => actions.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/{actions.js => actions.ts} (100%) diff --git a/client/modules/User/actions.js b/client/modules/User/actions.ts similarity index 100% rename from client/modules/User/actions.js rename to client/modules/User/actions.ts From 0ddb69db766fda3a8ae4595e24e404eda994e356 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 15:26:43 +0000 Subject: [PATCH 08/61] client/modules/User/actions: add types from server user request/response types with flexibility --- client/modules/User/actions.ts | 247 ++++++++++++++++++++++++--------- 1 file changed, 185 insertions(+), 62 deletions(-) diff --git a/client/modules/User/actions.ts b/client/modules/User/actions.ts index 20e6729b0d..ad47bc4d29 100644 --- a/client/modules/User/actions.ts +++ b/client/modules/User/actions.ts @@ -1,79 +1,124 @@ import { FORM_ERROR } from 'final-form'; +import type { AnyAction, Dispatch } from 'redux'; +import type { ThunkDispatch } from 'redux-thunk'; 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 { + CreateApiKeyRequestBody, + CreateUserRequestBody, + Error, + PublicUser, + PublicUserOrError, + PublicUserOrErrorOrGeneric, + RemoveApiKeyRequestParams, + ResetOrUpdatePasswordRequestParams, + ResetPasswordInitiateRequestBody, + UpdateCookieConsentRequestBody, + UpdatePasswordRequestBody, + UpdateSettingsRequestBody, + UserPreferences, + VerifyEmailQuery +} from '../../../common/types'; +import { RootState } from '../../reducers'; -export function authError(error) { +export function authError(error: Error) { return { type: ActionTypes.AUTH_ERROR, payload: error }; } -export function signUpUser(formValues) { +/** + * - Method: `POST` + * - Endpoint: `/signup` + * - Authenticated: `false` + * - Id: `UserController.createUser` + * + * Description: + * - Create a new user + */ +export function signUpUser(formValues: CreateUserRequestBody) { return apiClient.post('/signup', formValues); } -export function loginUser(formValues) { +export function loginUser(formValues: { email: string; password: string }) { return apiClient.post('/login', formValues); } -export function authenticateUser(user) { +export function authenticateUser(user: PublicUser) { 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 }; } -export function validateAndLoginUser(formProps) { - return (dispatch, getState) => { +export function validateAndLoginUser(formProps: { + email: string; + password: string; +}) { + return ( + dispatch: ThunkDispatch, + getState: () => RootState + ) => { const state = getState(); const { previousPath } = state.ide; - return new Promise((resolve) => { - loginUser(formProps) - .then((response) => { - dispatch(authenticateUser(response.data)); - dispatch(setPreferences(response.data.preferences)); - dispatch( - setLanguage(response.data.preferences.language, { - persistPreference: false + return new Promise( + (resolve) => { + loginUser(formProps) + .then((response) => { + dispatch(authenticateUser(response.data)); + dispatch(setPreferences(response.data.preferences)); + dispatch( + setLanguage(response.data.preferences.language, { + persistPreference: false + }) + ); + dispatch(justOpenedProject()); + browserHistory.push(previousPath); + resolve(); + }) + .catch((error) => + resolve({ + [FORM_ERROR]: error.response.data.message }) ); - dispatch(justOpenedProject()); - browserHistory.push(previousPath); - resolve(); - }) - .catch((error) => - resolve({ - [FORM_ERROR]: error.response.data.message - }) - ); - }); + } + ); }; } -export function validateAndSignUpUser(formValues) { - return (dispatch, getState) => { +/** + * - Method: `POST` + * - Endpoint: `/signup` + * - Authenticated: `false` + * - Id: `UserController.createUser` + * + * Description: + * - Create a new user + */ +export function validateAndSignUpUser(formValues: CreateUserRequestBody) { + return (dispatch: Dispatch, getState: () => RootState) => { const state = getState(); const { previousPath } = state.ide; - return new Promise((resolve) => { + return new Promise((resolve) => { signUpUser(formValues) .then((response) => { dispatch(authenticateUser(response.data)); @@ -91,7 +136,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 +151,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 +161,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 +169,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 +177,7 @@ export function validateSession() { }; } -export function resetProject(dispatch) { +export function resetProject(dispatch: Dispatch) { dispatch({ type: ActionTypes.RESET_PROJECT }); @@ -143,7 +188,7 @@ export function resetProject(dispatch) { } export function logoutUser() { - return (dispatch) => { + return (dispatch: Dispatch) => { apiClient .get('/logout') .then(() => { @@ -159,9 +204,20 @@ export function logoutUser() { }; } -export function initiateResetPassword(formValues) { - return (dispatch) => - new Promise((resolve) => { +/** + * - Method: `POST` + * - Endpoint: `/reset-password` + * - Authenticated: `false` + * - Id: `UserController.resetPasswordInitiate` + * + * Description: + * - Send an Reset-Password email to the registered email account + */ +export function initiateResetPassword( + formValues: ResetPasswordInitiateRequestBody +) { + return (dispatch: Dispatch) => + new Promise((resolve) => { dispatch({ type: ActionTypes.RESET_PASSWORD_INITIATE }); @@ -179,8 +235,17 @@ export function initiateResetPassword(formValues) { }); } +/** + * - Method: `POST` + * - Endpoint: `/verify/send` + * - Authenticated: `false` + * - Id: `UserController.emailVerificationInitiate` + * + * Description: + * - Send a Confirm Email email to verify that the user owns the specified email account + */ export function initiateVerification() { - return (dispatch) => { + return (dispatch: Dispatch) => { dispatch({ type: ActionTypes.EMAIL_VERIFICATION_INITIATE }); @@ -199,8 +264,17 @@ export function initiateVerification() { }; } -export function verifyEmailConfirmation(token) { - return (dispatch) => { +/** + * - Method: `GET` + * - Endpoint: `/verify` + * - Authenticated: `false` + * - Id: `UserController.verifyEmail` + * + * Description: + * - Used in the Confirm Email's link to verify a user's email is attached to their account + */ +export function verifyEmailConfirmation(token: VerifyEmailQuery['t']) { + return (dispatch: Dispatch) => { dispatch({ type: ActionTypes.EMAIL_VERIFICATION_VERIFY, state: 'checking' @@ -229,8 +303,21 @@ export function resetPasswordReset() { }; } -export function validateResetPasswordToken(token) { - return (dispatch) => { +/** + * - Method: `GET` + * - Endpoint: `/reset-password/:token` + * - Authenticated: `false` + * - Id: `UserController.validateResetPasswordToken` + * + * Description: + * - The link in the Reset Password email, which contains a reset token that is valid for 1h + * - If valid, the user will see a form to reset their password + * - Else they will see a message that their token has expired + */ +export function validateResetPasswordToken( + token: ResetOrUpdatePasswordRequestParams['token'] +) { + return (dispatch: Dispatch) => { apiClient .get(`/reset-password/${token}`) .then(() => { @@ -244,9 +331,24 @@ export function validateResetPasswordToken(token) { }; } -export function updatePassword(formValues, token) { - return (dispatch) => - new Promise((resolve) => +/** + * - Method: `POST` + * - Endpoint: `/reset-password/:token` + * - Authenticated: `false` + * - Id: `UserController.updatePassword` + * + * Description: + * - Used by the new password form to update a user's password with the valid token + * - Returns a Generic 401 - 'Password reset token is invalid or has expired.' if the token timed out + * - Returns a PublicUser if successfully saved + * - Returns an Error if network error on save attempt + */ +export function updatePassword( + formValues: UpdatePasswordRequestBody, + token: ResetOrUpdatePasswordRequestParams['token'] +) { + return (dispatch: Dispatch) => + new Promise((resolve) => apiClient .post(`/reset-password/${token}`, formValues) .then((response) => { @@ -263,27 +365,46 @@ 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) => { +/** + * - 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 updateSettings(formValues: Partial) { + return (dispatch: ThunkDispatch) => + new Promise((resolve) => { if (!formValues.currentPassword && formValues.newPassword) { dispatch(showToast(5500)); dispatch(setToastText('Toast.EmptyCurrentPass')); resolve(); return; } - submitSettings(formValues) + submitSettings(formValues as UpdateSettingsRequestBody) .then((response) => { dispatch(updateSettingsSuccess(response.data)); dispatch(showToast(5500)); @@ -313,15 +434,15 @@ 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) => +export function createApiKey(label: CreateApiKeyRequestBody['label']) { + return (dispatch: Dispatch) => apiClient .post('/account/api-keys', { label }) .then((response) => { @@ -333,8 +454,8 @@ export function createApiKey(label) { }); } -export function removeApiKey(keyId) { - return (dispatch) => +export function removeApiKey(keyId: RemoveApiKeyRequestParams['keyId']) { + return (dispatch: Dispatch) => apiClient .delete(`/account/api-keys/${keyId}`) .then((response) => { @@ -349,8 +470,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 +486,11 @@ export function unlinkService(service) { }; } -export function setUserCookieConsent(cookieConsent) { +export function setUserCookieConsent( + cookieConsent: UpdateCookieConsentRequestBody['cookieConsent'] +) { // maybe also send this to the server rn? - return (dispatch) => { + return (dispatch: Dispatch) => { apiClient .put('/cookie-consent', { cookieConsent }) .then(() => { From aae6687fe8f6ccdaf9fba48eb90125d8bc46a827 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:05:44 +0000 Subject: [PATCH 09/61] server/types/apiKey: update to add comment about token property on last item of return of createApiKeys --- server/controllers/user.controller/apiKey.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/controllers/user.controller/apiKey.ts b/server/controllers/user.controller/apiKey.ts index e87747a75c..5a8cc613f0 100644 --- a/server/controllers/user.controller/apiKey.ts +++ b/server/controllers/user.controller/apiKey.ts @@ -67,7 +67,8 @@ export const createApiKey: RequestHandler< await user.save(); const apiKeys = user.apiKeys.map((apiKey, index) => { - const fields = apiKey.toObject!(); + const fields = apiKey.toObject(); + // only include the token of the most recently made apiKey to display in the copiable field const shouldIncludeToken = index === addedApiKeyIndex - 1; return shouldIncludeToken ? { ...fields, token: keyToBeHashed } : fields; From 90335b3ac5221b07ccd3d0340dc2c2337ce21759 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:07:08 +0000 Subject: [PATCH 10/61] server/types/apiKey: update toJson method return type to IApiKey --- server/types/apiKey.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/types/apiKey.ts b/server/types/apiKey.ts index 443a30e3e4..be21dfdbcd 100644 --- a/server/types/apiKey.ts +++ b/server/types/apiKey.ts @@ -15,8 +15,8 @@ export interface IApiKey extends VirtualId, MongooseTimestamps { export interface ApiKeyDocument extends IApiKey, Omit, 'id'> { - toJSON(options?: any): SanitisedApiKey; - toObject(options?: any): SanitisedApiKey; + toJSON(options?: any): IApiKey; + toObject(options?: any): IApiKey; } /** From 276356ff76af16420113989874634aeb7d7000a2 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:08:49 +0000 Subject: [PATCH 11/61] server/types/PublicUser: update with accurate PublicUser apiKeys, should be sanitisedApiKeys. Add method for sanitisingApiKeys --- .../user.controller/__testUtils__.ts | 12 +++++++--- .../authManagement/3rdPartyManagement.test.ts | 23 ++++++++++++------- .../user.controller/__tests__/helpers.test.ts | 19 ++++++++------- server/controllers/user.controller/helpers.ts | 20 +++++++++++++--- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/server/controllers/user.controller/__testUtils__.ts b/server/controllers/user.controller/__testUtils__.ts index 1a7bf31cd6..7bc90bf0e6 100644 --- a/server/controllers/user.controller/__testUtils__.ts +++ b/server/controllers/user.controller/__testUtils__.ts @@ -59,9 +59,15 @@ export const mockBaseUserFull: Omit = { export function createMockUser( overrides: Partial = {}, unSanitised: boolean = false -): (PublicUser | UserDocument) & Record { +): PublicUser | UserDocument { + if (unSanitised) { + return { + ...mockBaseUserFull, + ...overrides + } as UserDocument; + } return { - ...(unSanitised ? mockBaseUserFull : mockBaseUserSanitised), + ...mockBaseUserSanitised, ...overrides - }; + } as PublicUser; } diff --git a/server/controllers/user.controller/__tests__/authManagement/3rdPartyManagement.test.ts b/server/controllers/user.controller/__tests__/authManagement/3rdPartyManagement.test.ts index d2e68ee61a..2910b33569 100644 --- a/server/controllers/user.controller/__tests__/authManagement/3rdPartyManagement.test.ts +++ b/server/controllers/user.controller/__tests__/authManagement/3rdPartyManagement.test.ts @@ -5,6 +5,7 @@ import { Request, Response } from 'express'; import { unlinkGithub, unlinkGoogle } from '../../authManagement'; import { saveUser } from '../../helpers'; import { createMockUser } from '../../__testUtils__'; +import { UserDocument } from '../../../../types'; jest.mock('../../helpers', () => ({ ...jest.requireActual('../../helpers'), @@ -50,10 +51,13 @@ describe('user.controller > auth management > 3rd party auth', () => { }); }); describe('and when there is a user in the request', () => { - const user = createMockUser({ - github: 'testuser', - tokens: [{ kind: 'github' }, { kind: 'google' }] - }); + const user = createMockUser( + { + github: 'testuser', + tokens: [{ kind: 'github' }, { kind: 'google' }] + }, + true + ) as UserDocument; beforeEach(async () => { request.user = user; @@ -96,10 +100,13 @@ describe('user.controller > auth management > 3rd party auth', () => { }); }); describe('and when there is a user in the request', () => { - const user = createMockUser({ - google: 'testuser', - tokens: [{ kind: 'github' }, { kind: 'google' }] - }); + const user = createMockUser( + { + google: 'testuser', + tokens: [{ kind: 'github' }, { kind: 'google' }] + }, + true + ) as UserDocument; beforeEach(async () => { request.user = user; diff --git a/server/controllers/user.controller/__tests__/helpers.test.ts b/server/controllers/user.controller/__tests__/helpers.test.ts index a6add4fda9..b04bb2548a 100644 --- a/server/controllers/user.controller/__tests__/helpers.test.ts +++ b/server/controllers/user.controller/__tests__/helpers.test.ts @@ -10,14 +10,17 @@ import { UserDocument } from '../../../types'; jest.mock('../../../models/user'); -const mockFullUser = createMockUser({ - // sensitive fields to be removed: - name: 'bob dylan', - tokens: [], - password: 'password12314', - resetPasswordToken: 'wijroaijwoer', - banned: true -}); +const mockFullUser = createMockUser( + { + // sensitive fields to be removed: + name: 'bob dylan', + tokens: [], + password: 'password12314', + resetPasswordToken: 'wijroaijwoer', + banned: true + }, + true +) as UserDocument; const { name, diff --git a/server/controllers/user.controller/helpers.ts b/server/controllers/user.controller/helpers.ts index 36bcf86649..7d05826fb1 100644 --- a/server/controllers/user.controller/helpers.ts +++ b/server/controllers/user.controller/helpers.ts @@ -1,19 +1,33 @@ import crypto from 'crypto'; import type { Response } from 'express'; import { User } from '../../models/user'; -import { PublicUser, UserDocument } from '../../types'; +import { + ApiKeyDocument, + PublicUser, + SanitisedApiKey, + UserDocument +} from '../../types'; + +export function sanitiseApiKey(key: ApiKeyDocument): SanitisedApiKey { + return { + id: key.id, + label: key.label, + lastUsedAt: key.lastUsedAt, + createdAt: key.createdAt + }; +} /** * Sanitise user objects to remove sensitive fields * @param user * @returns Sanitised user */ -export function userResponse(user: PublicUser | UserDocument): PublicUser { +export function userResponse(user: UserDocument): PublicUser { return { email: user.email, username: user.username, preferences: user.preferences, - apiKeys: user.apiKeys, + apiKeys: user.apiKeys.map((el) => sanitiseApiKey(el)), verified: user.verified, id: user.id, totalSize: user.totalSize, From ad4df7ab33cc03400dd455e31c7b6de48acec120 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:42:18 +0000 Subject: [PATCH 12/61] client/modules/User/reducers: update root state of state.user to add password/email verification states + todo notes --- client/modules/User/reducers.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/modules/User/reducers.ts b/client/modules/User/reducers.ts index d8a7141683..08c07b94bf 100644 --- a/client/modules/User/reducers.ts +++ b/client/modules/User/reducers.ts @@ -11,6 +11,12 @@ export type UserAction = { export const user = ( state: Partial & { authenticated: boolean; + // TODO: use state of user from server as single source of truth: + // Currently using redux state below, but server also has similar info. + resetPasswordInitiate?: boolean; + resetPasswordInvalid?: boolean; + emailVerificationInitiate?: boolean; + emailVerificationTokenState?: boolean; } = { authenticated: false }, From 997180a6d72d28b4b7c8aa7a6f3e9d84483a6078 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 03:36:22 +0000 Subject: [PATCH 13/61] server/types/user: remove typo for totalSize optional --- server/types/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/types/user.ts b/server/types/user.ts index 5d2eb61f34..1f180fee37 100644 --- a/server/types/user.ts +++ b/server/types/user.ts @@ -22,7 +22,7 @@ export interface IUser extends VirtualId, MongooseTimestamps { tokens: { kind: string }[]; apiKeys: Types.DocumentArray; preferences: UserPreferences; - totalSize?: number; + totalSize: number; cookieConsent: CookieConsentOptions; banned: boolean; lastLoginTimestamp?: Date; From fa7d951d6ae0beeb1ad05673a186276d31288494 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:41:05 +0000 Subject: [PATCH 14/61] client/modules/User/pages/EmailVerificationView: update to ts, no-verify --- .../{EmailVerificationView.jsx => EmailVerificationView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/pages/{EmailVerificationView.jsx => EmailVerificationView.tsx} (100%) diff --git a/client/modules/User/pages/EmailVerificationView.jsx b/client/modules/User/pages/EmailVerificationView.tsx similarity index 100% rename from client/modules/User/pages/EmailVerificationView.jsx rename to client/modules/User/pages/EmailVerificationView.tsx From c7f73e3d655d3674504aa2b574461002a32e76f5 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:43:45 +0000 Subject: [PATCH 15/61] client/modules/User/pages/EmailVerificationView: add types for emailVerificationTokenState --- client/modules/User/pages/EmailVerificationView.tsx | 6 +++--- client/modules/User/reducers.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/modules/User/pages/EmailVerificationView.tsx b/client/modules/User/pages/EmailVerificationView.tsx index d21d2446b9..7ce98d12fa 100644 --- a/client/modules/User/pages/EmailVerificationView.tsx +++ b/client/modules/User/pages/EmailVerificationView.tsx @@ -6,14 +6,15 @@ import { useTranslation } from 'react-i18next'; import { verifyEmailConfirmation } from '../actions'; import { RootPage } from '../../../components/RootPage'; import Nav from '../../IDE/components/Header/Nav'; +import type { RootState } from '../../../reducers'; -const EmailVerificationView = () => { +export const EmailVerificationView = () => { const { t } = useTranslation(); const location = useLocation(); const dispatch = useDispatch(); const browserHistory = useHistory(); const emailVerificationTokenState = useSelector( - (state) => state.user.emailVerificationTokenState + (state: RootState) => state.user.emailVerificationTokenState ); const verificationToken = useMemo(() => { const searchParams = new URLSearchParams(location.search); @@ -53,4 +54,3 @@ const EmailVerificationView = () => { ); }; -export default EmailVerificationView; diff --git a/client/modules/User/reducers.ts b/client/modules/User/reducers.ts index 08c07b94bf..584ef24adb 100644 --- a/client/modules/User/reducers.ts +++ b/client/modules/User/reducers.ts @@ -16,7 +16,7 @@ export const user = ( resetPasswordInitiate?: boolean; resetPasswordInvalid?: boolean; emailVerificationInitiate?: boolean; - emailVerificationTokenState?: boolean; + emailVerificationTokenState?: 'checking' | 'verified' | 'invalid'; } = { authenticated: false }, From 05998d921b275ab90a7d40dfb1f415b99490f26b Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:44:47 +0000 Subject: [PATCH 16/61] client/modules/User/pages/EmailVerificationView: update to named export --- client/routes.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/routes.jsx b/client/routes.jsx index 8926a95bdd..42215ae611 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -13,7 +13,7 @@ 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'; -import EmailVerificationView from './modules/User/pages/EmailVerificationView'; +import { EmailVerificationView } from './modules/User/pages/EmailVerificationView'; import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; import CollectionView from './modules/User/pages/CollectionView'; From b56adb06b4b8633d96e3ac395b4c4e13ec4875e9 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:48:44 +0000 Subject: [PATCH 17/61] client/modules/User/pages/CollectionView: update to ts, no-verify --- .../modules/User/pages/{CollectionView.jsx => CollectionView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/pages/{CollectionView.jsx => CollectionView.tsx} (100%) diff --git a/client/modules/User/pages/CollectionView.jsx b/client/modules/User/pages/CollectionView.tsx similarity index 100% rename from client/modules/User/pages/CollectionView.jsx rename to client/modules/User/pages/CollectionView.tsx From c57ae93dbe7f6ae3194e76e452d7bf42587061db Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:49:11 +0000 Subject: [PATCH 18/61] client/modules/User/pages/CollectionView: add tests and update to named export --- client/modules/User/pages/CollectionView.tsx | 7 +++---- client/routes.jsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/modules/User/pages/CollectionView.tsx b/client/modules/User/pages/CollectionView.tsx index 8207388086..958b84737c 100644 --- a/client/modules/User/pages/CollectionView.tsx +++ b/client/modules/User/pages/CollectionView.tsx @@ -4,8 +4,9 @@ import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; import Collection from '../components/Collection'; -const CollectionView = () => { - const params = useParams(); +export const CollectionView = () => { + // eslint-disable-next-line camelcase + const params = useParams<{ collection_id: string; username: string }>(); return ( @@ -17,5 +18,3 @@ const CollectionView = () => { ); }; - -export default CollectionView; diff --git a/client/routes.jsx b/client/routes.jsx index 42215ae611..d8b56f80f1 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -16,7 +16,7 @@ import ResetPasswordView from './modules/User/pages/ResetPasswordView'; import { EmailVerificationView } from './modules/User/pages/EmailVerificationView'; import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; -import CollectionView from './modules/User/pages/CollectionView'; +import { CollectionView } from './modules/User/pages/CollectionView'; import DashboardView from './modules/User/pages/DashboardView'; import { getUser } from './modules/User/actions'; import ProtectedSketchRoute from './protected-route'; From 2fe9aa92e7e06a2e9cf8cf64e147ada6ed160eb9 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:54:38 +0000 Subject: [PATCH 19/61] client/modules/User/pages/LoginView: update to ts, no-verify --- client/modules/User/pages/{LoginView.jsx => LoginView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/pages/{LoginView.jsx => LoginView.tsx} (100%) diff --git a/client/modules/User/pages/LoginView.jsx b/client/modules/User/pages/LoginView.tsx similarity index 100% rename from client/modules/User/pages/LoginView.jsx rename to client/modules/User/pages/LoginView.tsx From 5db6d3a90698bd17f7c3e5a14af6cb188654177d Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:56:12 +0000 Subject: [PATCH 20/61] resolve merge conflict with LoginView --- client/modules/User/pages/LoginView.tsx | 4 +--- client/routes.jsx | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/client/modules/User/pages/LoginView.tsx b/client/modules/User/pages/LoginView.tsx index b931c50ea8..70acbf8b31 100644 --- a/client/modules/User/pages/LoginView.tsx +++ b/client/modules/User/pages/LoginView.tsx @@ -7,7 +7,7 @@ import SocialAuthButton from '../components/SocialAuthButton'; import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; -function LoginView() { +export function LoginView() { const { t } = useTranslation(); return ( @@ -42,5 +42,3 @@ function LoginView() { ); } - -export default LoginView; diff --git a/client/routes.jsx b/client/routes.jsx index d8b56f80f1..59f5fda169 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -6,11 +6,11 @@ import { Route as RouterRoute, Switch } from 'react-router-dom'; 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 LoginView from './modules/User/pages/LoginView'; +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 { LoginView } from './modules/User/pages/LoginView'; import SignupView from './modules/User/pages/SignupView'; import ResetPasswordView from './modules/User/pages/ResetPasswordView'; import { EmailVerificationView } from './modules/User/pages/EmailVerificationView'; From a2a5a00492e1560b533a4bfdbdce56c116f7a2c7 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:57:46 +0000 Subject: [PATCH 21/61] client/modules/User/components/SocialAuthButton: update to ts, no-verify --- .../components/{SocialAuthButton.jsx => SocialAuthButton.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{SocialAuthButton.jsx => SocialAuthButton.tsx} (100%) diff --git a/client/modules/User/components/SocialAuthButton.jsx b/client/modules/User/components/SocialAuthButton.tsx similarity index 100% rename from client/modules/User/components/SocialAuthButton.jsx rename to client/modules/User/components/SocialAuthButton.tsx From 10490e2b08b6ebdd07faf3ec07243b6b2dc8733b Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 22:59:04 +0000 Subject: [PATCH 22/61] client/modules/User/components/SocialAuthButton: update to named export, no-verify --- client/modules/User/components/SocialAuthButton.stories.jsx | 2 +- client/modules/User/components/SocialAuthButton.tsx | 4 +--- client/modules/User/pages/AccountView.jsx | 2 +- client/modules/User/pages/LoginView.tsx | 2 +- client/modules/User/pages/SignupView.jsx | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/client/modules/User/components/SocialAuthButton.stories.jsx b/client/modules/User/components/SocialAuthButton.stories.jsx index 770dd70162..a629dccc59 100644 --- a/client/modules/User/components/SocialAuthButton.stories.jsx +++ b/client/modules/User/components/SocialAuthButton.stories.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import SocialAuthButton from './SocialAuthButton'; +import { SocialAuthButton } from './SocialAuthButton'; export default { title: 'User/components/SocialAuthButton', diff --git a/client/modules/User/components/SocialAuthButton.tsx b/client/modules/User/components/SocialAuthButton.tsx index e48c660b67..6319413d43 100644 --- a/client/modules/User/components/SocialAuthButton.tsx +++ b/client/modules/User/components/SocialAuthButton.tsx @@ -34,7 +34,7 @@ const StyledButton = styled(Button)` width: ${remSize(300)}; `; -function SocialAuthButton({ service, linkStyle, isConnected }) { +export function SocialAuthButton({ service, linkStyle, isConnected }) { const { t } = useTranslation(); const ServiceIcon = icons[service]; @@ -94,5 +94,3 @@ SocialAuthButton.defaultProps = { linkStyle: false, isConnected: false }; - -export default SocialAuthButton; diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index e9b3da7c9a..5bbb1ecc4a 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { useHistory, useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import AccountForm from '../components/AccountForm'; -import SocialAuthButton from '../components/SocialAuthButton'; +import { SocialAuthButton } from '../components/SocialAuthButton'; import APIKeyForm from '../components/APIKeyForm'; import Nav from '../../IDE/components/Header/Nav'; import ErrorModal from '../../IDE/components/ErrorModal'; diff --git a/client/modules/User/pages/LoginView.tsx b/client/modules/User/pages/LoginView.tsx index 70acbf8b31..8b4776470f 100644 --- a/client/modules/User/pages/LoginView.tsx +++ b/client/modules/User/pages/LoginView.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import LoginForm from '../components/LoginForm'; -import SocialAuthButton from '../components/SocialAuthButton'; +import { SocialAuthButton } from '../components/SocialAuthButton'; import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.jsx index 16e038c6e0..ecd11503ef 100644 --- a/client/modules/User/pages/SignupView.jsx +++ b/client/modules/User/pages/SignupView.jsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { useTranslation, Trans } from 'react-i18next'; import SignupForm from '../components/SignupForm'; -import SocialAuthButton from '../components/SocialAuthButton'; +import { SocialAuthButton } from '../components/SocialAuthButton'; import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; From 01d99c7c7cb4b7ee3cc896c26ce0586f38c114e8 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:06:09 +0000 Subject: [PATCH 23/61] client/modules/User/components/SocialAuthButton: add types & add SocialAuthServices enum --- .../components/SocialAuthButton.stories.jsx | 6 ++-- .../User/components/SocialAuthButton.tsx | 32 ++++++++----------- client/modules/User/pages/AccountView.jsx | 9 ++++-- client/modules/User/pages/LoginView.tsx | 9 ++++-- client/modules/User/pages/SignupView.jsx | 9 ++++-- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/client/modules/User/components/SocialAuthButton.stories.jsx b/client/modules/User/components/SocialAuthButton.stories.jsx index a629dccc59..5bd25dc16a 100644 --- a/client/modules/User/components/SocialAuthButton.stories.jsx +++ b/client/modules/User/components/SocialAuthButton.stories.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import { SocialAuthButton } from './SocialAuthButton'; +import { SocialAuthButton, SocialAuthServices } from './SocialAuthButton'; export default { title: 'User/components/SocialAuthButton', @@ -8,13 +8,13 @@ export default { }; export const Github = () => ( - + Log in with Github ); export const Google = () => ( - + Sign up with Google ); diff --git a/client/modules/User/components/SocialAuthButton.tsx b/client/modules/User/components/SocialAuthButton.tsx index 6319413d43..56ddc9ae5d 100644 --- a/client/modules/User/components/SocialAuthButton.tsx +++ b/client/modules/User/components/SocialAuthButton.tsx @@ -20,10 +20,10 @@ const icons = { google: GoogleIcon }; -const services = { - github: 'github', - google: 'google' -}; +export enum SocialAuthServices { + github = 'github', + google = 'google' +} const servicesLabels = { github: 'GitHub', @@ -34,7 +34,16 @@ const StyledButton = styled(Button)` width: ${remSize(300)}; `; -export function SocialAuthButton({ service, linkStyle, isConnected }) { +export interface SocialAuthButtonProps { + service: SocialAuthServices; + linkStyle?: boolean; + isConnected?: boolean; +} +export function SocialAuthButton({ + service, + linkStyle = false, + isConnected = false +}: SocialAuthButtonProps) { const { t } = useTranslation(); const ServiceIcon = icons[service]; @@ -81,16 +90,3 @@ export function SocialAuthButton({ service, linkStyle, isConnected }) { ); } - -SocialAuthButton.services = services; - -SocialAuthButton.propTypes = { - service: PropTypes.oneOf(['github', 'google']).isRequired, - linkStyle: PropTypes.bool, - isConnected: PropTypes.bool -}; - -SocialAuthButton.defaultProps = { - linkStyle: false, - isConnected: false -}; diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index 5bbb1ecc4a..508b65816d 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -6,7 +6,10 @@ import { useTranslation } from 'react-i18next'; import { useHistory, useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import AccountForm from '../components/AccountForm'; -import { SocialAuthButton } from '../components/SocialAuthButton'; +import { + SocialAuthButton, + SocialAuthServices +} from '../components/SocialAuthButton'; import APIKeyForm from '../components/APIKeyForm'; import Nav from '../../IDE/components/Header/Nav'; import ErrorModal from '../../IDE/components/ErrorModal'; @@ -28,12 +31,12 @@ function SocialLoginPanel() {

diff --git a/client/modules/User/pages/LoginView.tsx b/client/modules/User/pages/LoginView.tsx index 8b4776470f..5df7b53951 100644 --- a/client/modules/User/pages/LoginView.tsx +++ b/client/modules/User/pages/LoginView.tsx @@ -3,7 +3,10 @@ import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import LoginForm from '../components/LoginForm'; -import { SocialAuthButton } from '../components/SocialAuthButton'; +import { + SocialAuthButton, + SocialAuthServices +} from '../components/SocialAuthButton'; import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; @@ -21,8 +24,8 @@ export function LoginView() {

{t('LoginView.LoginOr')}

- - + +

{t('LoginView.DontHaveAccount')} diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.jsx index ecd11503ef..3248a7bc61 100644 --- a/client/modules/User/pages/SignupView.jsx +++ b/client/modules/User/pages/SignupView.jsx @@ -3,7 +3,10 @@ import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { useTranslation, Trans } from 'react-i18next'; import SignupForm from '../components/SignupForm'; -import { SocialAuthButton } from '../components/SocialAuthButton'; +import { + SocialAuthButton, + SocialAuthServices +} from '../components/SocialAuthButton'; import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; @@ -23,8 +26,8 @@ function SignupView() {

{t('SignupView.Or')}

- - + +

Date: Sun, 26 Oct 2025 23:09:30 +0000 Subject: [PATCH 24/61] client/modules/User/components/LoginForm: update to ts, no-verfy --- client/modules/User/components/{LoginForm.jsx => LoginForm.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{LoginForm.jsx => LoginForm.tsx} (100%) diff --git a/client/modules/User/components/LoginForm.jsx b/client/modules/User/components/LoginForm.tsx similarity index 100% rename from client/modules/User/components/LoginForm.jsx rename to client/modules/User/components/LoginForm.tsx From 62cee39f61cbb0d8f59d545393b0d24be8e098e6 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:10:00 +0000 Subject: [PATCH 25/61] client/modules/User/components/LoginForm: update to named export, no-verfy --- client/modules/User/components/LoginForm.tsx | 4 +--- client/modules/User/components/LoginForm.unit.test.jsx | 2 +- client/modules/User/pages/LoginView.tsx | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/client/modules/User/components/LoginForm.tsx b/client/modules/User/components/LoginForm.tsx index 5bac902ecf..998c1090b4 100644 --- a/client/modules/User/components/LoginForm.tsx +++ b/client/modules/User/components/LoginForm.tsx @@ -8,7 +8,7 @@ import { validateLogin } from '../../../utils/reduxFormUtils'; import { validateAndLoginUser } from '../actions'; import { useSyncFormTranslations } from '../../../common/useSyncFormTranslations'; -function LoginForm() { +export function LoginForm() { const { t, i18n } = useTranslation(); const dispatch = useDispatch(); @@ -114,5 +114,3 @@ function LoginForm() { ); } - -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/pages/LoginView.tsx b/client/modules/User/pages/LoginView.tsx index 5df7b53951..4dff34b650 100644 --- a/client/modules/User/pages/LoginView.tsx +++ b/client/modules/User/pages/LoginView.tsx @@ -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, SocialAuthServices From e8750ed4def9f9925a09ea2e471ba475fe745713 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:14:02 +0000 Subject: [PATCH 26/61] client/modules/User/components/LoginForm: update with types & update useSyncFormTranslations to handle null formRef --- client/common/useSyncFormTranslations.ts | 4 ++-- client/modules/User/components/LoginForm.tsx | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) 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/User/components/LoginForm.tsx b/client/modules/User/components/LoginForm.tsx index 998c1090b4..17bdaf9393 100644 --- a/client/modules/User/components/LoginForm.tsx +++ b/client/modules/User/components/LoginForm.tsx @@ -6,17 +6,21 @@ import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; import { Button, ButtonTypes } from '../../../common/Button'; import { validateLogin } from '../../../utils/reduxFormUtils'; import { validateAndLoginUser } from '../actions'; -import { useSyncFormTranslations } from '../../../common/useSyncFormTranslations'; +import { + FormLike, + useSyncFormTranslations +} from '../../../common/useSyncFormTranslations'; +import type { LoginForm as LoginFormType } from '../../../utils/reduxFormUtils'; 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 = useRef(null); const handleVisibility = () => { setShowPassword(!showPassword); From 0fcc6616ce7e2377b404891214a7b022d4d46104 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:22:02 +0000 Subject: [PATCH 27/61] client/modules/User/components/Notification: delete unused file --- .../modules/User/components/Notification.jsx | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 client/modules/User/components/Notification.jsx diff --git a/client/modules/User/components/Notification.jsx b/client/modules/User/components/Notification.jsx deleted file mode 100644 index c7189c3af1..0000000000 --- a/client/modules/User/components/Notification.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from 'react'; -import Cookies from 'js-cookie'; -import { useDispatch } from 'react-redux'; -import { showToast, setToastText } from '../../IDE/actions/toast'; - -function Notification() { - const dispatch = useDispatch(); - useEffect(() => { - const notification = Cookies.get('p5-notification'); - if (!notification) { - // show the toast - dispatch(showToast(30000)); - const text = `There is a scheduled outage on Sunday, April 9 3AM - 5AM UTC. - The entire site will be down, so please plan accordingly.`; - dispatch(setToastText(text)); - Cookies.set('p5-notification', true, { expires: 365 }); - } - }); - return null; -} - -export default Notification; From af68d0b0802e29fcf4412669f61fe7639ddc9707 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:24:48 +0000 Subject: [PATCH 28/61] client/modules/User/pages/NewPasswordView: update to ts, no-verify --- .../User/pages/{NewPasswordView.jsx => NewPasswordView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/pages/{NewPasswordView.jsx => NewPasswordView.tsx} (100%) diff --git a/client/modules/User/pages/NewPasswordView.jsx b/client/modules/User/pages/NewPasswordView.tsx similarity index 100% rename from client/modules/User/pages/NewPasswordView.jsx rename to client/modules/User/pages/NewPasswordView.tsx From 77586121d31807c10f9b155d12a927e08c8ba26c Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:25:08 +0000 Subject: [PATCH 29/61] client/modules/User/pages/NewPasswordView: add types and update to named export --- client/modules/User/pages/NewPasswordView.tsx | 10 +++++----- client/routes.jsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/modules/User/pages/NewPasswordView.tsx b/client/modules/User/pages/NewPasswordView.tsx index 2bc310e6a3..e074f9a7a5 100644 --- a/client/modules/User/pages/NewPasswordView.tsx +++ b/client/modules/User/pages/NewPasswordView.tsx @@ -8,13 +8,15 @@ import NewPasswordForm from '../components/NewPasswordForm'; import { validateResetPasswordToken } from '../actions'; import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; +import { RootState } from '../../../reducers'; -function NewPasswordView() { +export function NewPasswordView() { const { t } = useTranslation(); - const params = useParams(); + // eslint-disable-next-line camelcase + const params = useParams<{ reset_password_token: string }>(); const resetPasswordToken = params.reset_password_token; const resetPasswordInvalid = useSelector( - (state) => state.user.resetPasswordInvalid + (state: RootState) => state.user.resetPasswordInvalid ); const dispatch = useDispatch(); @@ -48,5 +50,3 @@ function NewPasswordView() { ); } - -export default NewPasswordView; diff --git a/client/routes.jsx b/client/routes.jsx index 59f5fda169..7cb77811ab 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -14,7 +14,7 @@ import { LoginView } from './modules/User/pages/LoginView'; import SignupView from './modules/User/pages/SignupView'; import ResetPasswordView from './modules/User/pages/ResetPasswordView'; import { EmailVerificationView } from './modules/User/pages/EmailVerificationView'; -import NewPasswordView from './modules/User/pages/NewPasswordView'; +import { NewPasswordView } from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; import { CollectionView } from './modules/User/pages/CollectionView'; import DashboardView from './modules/User/pages/DashboardView'; From edbf29e2b0e750cdfebdc167799210695c092fb0 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:26:40 +0000 Subject: [PATCH 30/61] client/modules/User/components/ResponseiveForm: delete unused file --- .../User/components/ResponsiveForm.jsx | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 client/modules/User/components/ResponsiveForm.jsx diff --git a/client/modules/User/components/ResponsiveForm.jsx b/client/modules/User/components/ResponsiveForm.jsx deleted file mode 100644 index 829b83cd14..0000000000 --- a/client/modules/User/components/ResponsiveForm.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import styled from 'styled-components'; -import { remSize } from '../../../theme'; - -const ResponsiveForm = styled.div` - .form-container__content { - width: unset !important; - padding-top: ${remSize(16)}; - padding-bottom: ${remSize(64)}; - } - - .form__input { - min-width: unset; - padding: 0px ${remSize(12)}; - height: ${remSize(28)}; - } - .form-container__title { - margin-bottom: ${remSize(14)}; - } - p.form__field { - margin-top: 0px !important; - } - label.form__label { - margin-top: ${remSize(8)} !important; - } - - .form-error { - width: 100%; - } - - .nav__items-right:last-child { - display: none; - } - - .form-container { - height: 100%; - } - - .nav__dropdown { - right: 0 !important; - left: unset !important; - } - - .form-container__stack { - svg { - width: ${remSize(12)}; - height: ${remSize(12)}; - } - a { - padding: 0px; - } - } -`; - -export default ResponsiveForm; From 70d088fae2cf41bc932e3acc20d25206fe780b74 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 03:16:55 +0000 Subject: [PATCH 31/61] package.json: add react-helmet types --- package-lock.json | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) 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", From 851681a8d3ba7c8c4b324d4b783a0d4afb779cc8 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:29:11 +0000 Subject: [PATCH 32/61] client/modules/User/pages/ResetPasswordView: update to ts, no-verify --- .../User/pages/{ResetPasswordView.jsx => ResetPasswordView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/pages/{ResetPasswordView.jsx => ResetPasswordView.tsx} (100%) diff --git a/client/modules/User/pages/ResetPasswordView.jsx b/client/modules/User/pages/ResetPasswordView.tsx similarity index 100% rename from client/modules/User/pages/ResetPasswordView.jsx rename to client/modules/User/pages/ResetPasswordView.tsx From a647f19a8ad0d67f61d273ba659e918508d16437 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:29:38 +0000 Subject: [PATCH 33/61] client/modules/User/pages/ResetPasswordView: add types & update to named export --- client/modules/User/pages/ResetPasswordView.tsx | 7 +++---- client/routes.jsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/modules/User/pages/ResetPasswordView.tsx b/client/modules/User/pages/ResetPasswordView.tsx index e744311b7c..fe7a1eeec4 100644 --- a/client/modules/User/pages/ResetPasswordView.tsx +++ b/client/modules/User/pages/ResetPasswordView.tsx @@ -7,11 +7,12 @@ import { useTranslation } from 'react-i18next'; import ResetPasswordForm from '../components/ResetPasswordForm'; import { RootPage } from '../../../components/RootPage'; import Nav from '../../IDE/components/Header/Nav'; +import { RootState } from '../../../reducers'; -function ResetPasswordView() { +export function ResetPasswordView() { const { t } = useTranslation(); const resetPasswordInitiate = useSelector( - (state) => state.user.resetPasswordInitiate + (state: RootState) => state.user.resetPasswordInitiate ); const resetPasswordClass = classNames({ 'reset-password': true, @@ -48,5 +49,3 @@ function ResetPasswordView() { ); } - -export default ResetPasswordView; diff --git a/client/routes.jsx b/client/routes.jsx index 7cb77811ab..e07cec91f7 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -12,7 +12,7 @@ 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'; +import { ResetPasswordView } from './modules/User/pages/ResetPasswordView'; import { EmailVerificationView } from './modules/User/pages/EmailVerificationView'; import { NewPasswordView } from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; From 9b49e6889922223aa50134dc8bde721e3b2f0962 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:34:22 +0000 Subject: [PATCH 34/61] client/modules/User/components/NewPasswordForm: update to ts, no-verify --- .../User/components/{NewPasswordForm.jsx => NewPasswordForm.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{NewPasswordForm.jsx => NewPasswordForm.tsx} (100%) diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.tsx similarity index 100% rename from client/modules/User/components/NewPasswordForm.jsx rename to client/modules/User/components/NewPasswordForm.tsx From 8d0fe3ecb0b31083515a6eaf15ffe22dd170ab36 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:35:50 +0000 Subject: [PATCH 35/61] client/modules/User/components/NewPasswordForm: add types & update to named export --- client/modules/User/components/NewPasswordForm.tsx | 12 +++--------- .../User/components/NewPasswordForm.unit.test.jsx | 2 +- client/modules/User/pages/NewPasswordView.tsx | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/client/modules/User/components/NewPasswordForm.tsx b/client/modules/User/components/NewPasswordForm.tsx index feca326c77..9d08f678c5 100644 --- a/client/modules/User/components/NewPasswordForm.tsx +++ 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,14 @@ import { useTranslation } from 'react-i18next'; import { validateNewPassword } from '../../../utils/reduxFormUtils'; import { updatePassword } from '../actions'; import { Button, ButtonTypes } from '../../../common/Button'; +import type { NewPasswordForm as NewPasswordFormType } from '../../../utils/reduxFormUtils'; -function NewPasswordForm(props) { +export function NewPasswordForm(props: { resetPasswordToken: string }) { const { resetPasswordToken } = props; const { t } = useTranslation(); const dispatch = useDispatch(); - function onSubmit(formProps) { + function onSubmit(formProps: NewPasswordFormType) { return dispatch(updatePassword(formProps, resetPasswordToken)); } @@ -75,9 +75,3 @@ function NewPasswordForm(props) { ); } - -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/NewPasswordView.tsx b/client/modules/User/pages/NewPasswordView.tsx index e074f9a7a5..1433624df0 100644 --- a/client/modules/User/pages/NewPasswordView.tsx +++ b/client/modules/User/pages/NewPasswordView.tsx @@ -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'; From 8226165b68ffed6fe087999a5acca01f291546b3 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:37:25 +0000 Subject: [PATCH 36/61] client/modules/User/pages/SignupView: update to ts, no-verify --- client/modules/User/pages/{SignupView.jsx => SignupView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/pages/{SignupView.jsx => SignupView.tsx} (100%) diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.tsx similarity index 100% rename from client/modules/User/pages/SignupView.jsx rename to client/modules/User/pages/SignupView.tsx From fd922368ce6376f0825882e459de48fe10063f8e Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:38:13 +0000 Subject: [PATCH 37/61] client/modules/User/pages/SignupView: update to named export --- client/modules/User/pages/SignupView.tsx | 4 +--- client/routes.jsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/client/modules/User/pages/SignupView.tsx b/client/modules/User/pages/SignupView.tsx index 3248a7bc61..50b54555c4 100644 --- a/client/modules/User/pages/SignupView.tsx +++ b/client/modules/User/pages/SignupView.tsx @@ -10,7 +10,7 @@ import { import Nav from '../../IDE/components/Header/Nav'; import { RootPage } from '../../../components/RootPage'; -function SignupView() { +export function SignupView() { const { t } = useTranslation(); return ( @@ -49,5 +49,3 @@ function SignupView() { ); } - -export default SignupView; diff --git a/client/routes.jsx b/client/routes.jsx index e07cec91f7..4e399561ff 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -11,7 +11,7 @@ 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 { SignupView } from './modules/User/pages/SignupView'; import { ResetPasswordView } from './modules/User/pages/ResetPasswordView'; import { EmailVerificationView } from './modules/User/pages/EmailVerificationView'; import { NewPasswordView } from './modules/User/pages/NewPasswordView'; From ad990c0c21beb793bf121845946ac75968b5a22a Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:39:50 +0000 Subject: [PATCH 38/61] client/modules/User/components/ResetPasswordForm: update to ts, no-verify --- .../components/{ResetPasswordForm.jsx => ResetPasswordForm.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{ResetPasswordForm.jsx => ResetPasswordForm.tsx} (100%) diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.tsx similarity index 100% rename from client/modules/User/components/ResetPasswordForm.jsx rename to client/modules/User/components/ResetPasswordForm.tsx From c58e78be2a2c3afa59cd7496af852f43f93e5ebe Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:41:42 +0000 Subject: [PATCH 39/61] client/modules/User/components/ResetPasswordForm: update named export & add types --- client/modules/User/components/ResetPasswordForm.tsx | 10 +++++----- client/modules/User/pages/ResetPasswordView.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/modules/User/components/ResetPasswordForm.tsx b/client/modules/User/components/ResetPasswordForm.tsx index fe3752fdf4..f3668b9636 100644 --- a/client/modules/User/components/ResetPasswordForm.tsx +++ b/client/modules/User/components/ResetPasswordForm.tsx @@ -5,15 +5,17 @@ import { useDispatch, useSelector } from 'react-redux'; import { validateResetPassword } from '../../../utils/reduxFormUtils'; import { initiateResetPassword } from '../actions'; import { Button, ButtonTypes } from '../../../common/Button'; +import { RootState } from '../../../reducers'; +import { ResetPasswordInitiateRequestBody } from '../../../../common/types'; -function ResetPasswordForm(props) { +export function ResetPasswordForm() { const { t } = useTranslation(); const resetPasswordInitiate = useSelector( - (state) => state.user.resetPasswordInitiate + (state: RootState) => state.user.resetPasswordInitiate ); const dispatch = useDispatch(); - function onSubmit(formProps) { + function onSubmit(formProps: ResetPasswordInitiateRequestBody) { dispatch(initiateResetPassword(formProps)); } @@ -57,5 +59,3 @@ function ResetPasswordForm(props) { ); } - -export default ResetPasswordForm; diff --git a/client/modules/User/pages/ResetPasswordView.tsx b/client/modules/User/pages/ResetPasswordView.tsx index fe7a1eeec4..09916047e4 100644 --- a/client/modules/User/pages/ResetPasswordView.tsx +++ b/client/modules/User/pages/ResetPasswordView.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; -import ResetPasswordForm from '../components/ResetPasswordForm'; +import { ResetPasswordForm } from '../components/ResetPasswordForm'; import { RootPage } from '../../../components/RootPage'; import Nav from '../../IDE/components/Header/Nav'; import { RootState } from '../../../reducers'; From b8e7361f81a1c113c87d01e7dd594a248c6ae7fb Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:58:48 +0000 Subject: [PATCH 40/61] client/modules/User/components/SignupForm: update to ts, no-verify --- client/modules/User/components/{SignupForm.jsx => SignupForm.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{SignupForm.jsx => SignupForm.tsx} (100%) diff --git a/client/modules/User/components/SignupForm.jsx b/client/modules/User/components/SignupForm.tsx similarity index 100% rename from client/modules/User/components/SignupForm.jsx rename to client/modules/User/components/SignupForm.tsx From 023096bc4671e4a57bd4c2d8cef5cd8f4bff0f68 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 23:59:40 +0000 Subject: [PATCH 41/61] client/modules/User/components/SignupForm: add types and update to named export; update useSyncFormTranslations FormlikeType to take generic --- client/common/useSyncFormTranslations.ts | 22 ++++++++------- client/modules/User/components/LoginForm.tsx | 2 +- client/modules/User/components/SignupForm.tsx | 27 +++++++++++-------- client/modules/User/pages/SignupView.tsx | 2 +- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/client/common/useSyncFormTranslations.ts b/client/common/useSyncFormTranslations.ts index aed766aa43..c116db1e04 100644 --- a/client/common/useSyncFormTranslations.ts +++ b/client/common/useSyncFormTranslations.ts @@ -1,18 +1,19 @@ import { useEffect, MutableRefObject } from 'react'; +import type { FormApi } from 'final-form'; -export interface FormLike { - getState(): { values: Record }; - reset(): void; - change(field: string, value: unknown): void; -} +// Generic FormLike that mirrors FormApi for any form value type +export type FormLike> = Pick< + FormApi, + 'getState' | 'reset' | 'change' +>; /** * This hook ensures that form values are preserved when the language changes. * @param formRef * @param language */ -export const useSyncFormTranslations = ( - formRef: MutableRefObject, +export const useSyncFormTranslations = >( + formRef: MutableRefObject | null>, language: string ) => { useEffect(() => { @@ -22,9 +23,10 @@ export const useSyncFormTranslations = ( const { values } = form.getState(); form.reset(); - Object.keys(values).forEach((field) => { - if (values[field]) { - form.change(field, values[field]); + (Object.keys(values) as (keyof FormValues)[]).forEach((field) => { + const value = values[field]; + if (value !== undefined && value !== null && value !== '') { + form.change(field, value); } }); }, [language]); diff --git a/client/modules/User/components/LoginForm.tsx b/client/modules/User/components/LoginForm.tsx index 17bdaf9393..c3741ffce7 100644 --- a/client/modules/User/components/LoginForm.tsx +++ b/client/modules/User/components/LoginForm.tsx @@ -20,7 +20,7 @@ export function LoginForm() { return dispatch(validateAndLoginUser(formProps)); } const [showPassword, setShowPassword] = useState(false); - const formRef = useRef(null); + const formRef = useRef | null>(null); const handleVisibility = () => { setShowPassword(!showPassword); diff --git a/client/modules/User/components/SignupForm.tsx b/client/modules/User/components/SignupForm.tsx index 45d44da875..dbbac68932 100644 --- a/client/modules/User/components/SignupForm.tsx +++ b/client/modules/User/components/SignupForm.tsx @@ -7,16 +7,23 @@ import { validateSignup } from '../../../utils/reduxFormUtils'; import { validateAndSignUpUser } from '../actions'; import { Button, ButtonTypes } from '../../../common/Button'; import { apiClient } from '../../../utils/apiClient'; -import { useSyncFormTranslations } from '../../../common/useSyncFormTranslations'; +import { + FormLike, + useSyncFormTranslations +} from '../../../common/useSyncFormTranslations'; +import { + CreateUserRequestBody, + DuplicateUserCheckQuery +} from '../../../../common/types'; -const timeoutRef = { current: null }; +const timeoutRef: { current: (() => void) | null } = { current: null }; -function asyncValidate(fieldToValidate, value) { +function asyncValidate(fieldToValidate: 'username' | 'email', value: string) { if (!value || value.trim().length === 0) { return Promise.resolve(''); } - const queryParams = { + const queryParams: DuplicateUserCheckQuery = { [fieldToValidate]: value, check_type: fieldToValidate }; @@ -48,18 +55,18 @@ function asyncValidate(fieldToValidate, value) { }); } -function validateUsername(username) { +function validateUsername(username: CreateUserRequestBody['username']) { return asyncValidate('username', username); } -function validateEmail(email) { +function validateEmail(email: CreateUserRequestBody['email']) { return asyncValidate('email', email); } -function SignupForm() { +export function SignupForm() { const { t, i18n } = useTranslation(); const dispatch = useDispatch(); - const formRef = useRef(null); + const formRef = useRef | null>(null); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); @@ -69,7 +76,7 @@ function SignupForm() { const handleConfirmVisibility = () => setShowConfirmPassword(!showConfirmPassword); - function onSubmit(formProps) { + function onSubmit(formProps: CreateUserRequestBody) { return dispatch(validateAndSignUpUser(formProps)); } @@ -216,5 +223,3 @@ function SignupForm() { ); } - -export default SignupForm; diff --git a/client/modules/User/pages/SignupView.tsx b/client/modules/User/pages/SignupView.tsx index 50b54555c4..36006d51ef 100644 --- a/client/modules/User/pages/SignupView.tsx +++ b/client/modules/User/pages/SignupView.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { useTranslation, Trans } from 'react-i18next'; -import SignupForm from '../components/SignupForm'; +import { SignupForm } from '../components/SignupForm'; import { SocialAuthButton, SocialAuthServices From 248e2c201fc6d3c2fc3f4ab4aca150b860858cd7 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 00:14:30 +0000 Subject: [PATCH 42/61] client/modules/User/components/CookieConsent: update to ts, no-verify --- .../User/components/{CookieConsent.jsx => CookieConsent.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{CookieConsent.jsx => CookieConsent.tsx} (100%) diff --git a/client/modules/User/components/CookieConsent.jsx b/client/modules/User/components/CookieConsent.tsx similarity index 100% rename from client/modules/User/components/CookieConsent.jsx rename to client/modules/User/components/CookieConsent.tsx From ebe82159ad5df697d33f5b99c86e21cec8ae95ea Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 00:16:31 +0000 Subject: [PATCH 43/61] client/modules/User/components/CookieConsent: add type dependencies, update to named export, update types --- client/modules/App/App.jsx | 2 +- .../modules/User/components/CookieConsent.tsx | 84 ++++++++++++------- package-lock.json | 32 +++++++ package.json | 2 + 4 files changed, 89 insertions(+), 31 deletions(-) diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index 53b0c82c63..242b5244c8 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -6,7 +6,7 @@ import { showReduxDevTools } from '../../store'; import DevTools from './components/DevTools'; import { setPreviousPath } from '../IDE/actions/ide'; import { setLanguage } from '../IDE/actions/preferences'; -import CookieConsent from '../User/components/CookieConsent'; +import { CookieConsent } from '../User/components/CookieConsent'; function hideCookieConsent(pathname) { if (pathname.includes('/full/') || pathname.includes('/embed/')) { diff --git a/client/modules/User/components/CookieConsent.tsx b/client/modules/User/components/CookieConsent.tsx index 9cfff74ac6..ad8fa8e743 100644 --- a/client/modules/User/components/CookieConsent.tsx +++ b/client/modules/User/components/CookieConsent.tsx @@ -6,17 +6,21 @@ import ReactGA from 'react-ga'; import { Transition } from 'react-transition-group'; import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import PropTypes from 'prop-types'; import { getConfig } from '../../../utils/getConfig'; import { setUserCookieConsent } from '../actions'; import { remSize, prop, device } from '../../../theme'; import { Button, ButtonKinds } from '../../../common/Button'; +import { RootState } from '../../../reducers'; +import { CookieConsentOptions } from '../../../../common/types'; +interface CookieConsentContainerState { + state: string; +} const CookieConsentContainer = styled.div` position: fixed; transition: 1.6s cubic-bezier(0.165, 0.84, 0.44, 1); bottom: 0; - transform: ${({ state }) => { + transform: ${({ state }: CookieConsentContainerState) => { if (state === 'entered') { return 'translateY(0)'; } @@ -79,37 +83,49 @@ const CookieConsentButtons = styled.div` const GOOGLE_ANALYTICS_ID = getConfig('GA_MEASUREMENT_ID'); -function CookieConsent({ hide }) { - const user = useSelector((state) => state.user); - const [cookieConsent, setBrowserCookieConsent] = useState('none'); +export function CookieConsent({ hide = false }: { hide?: boolean }) { + const user = useSelector((state: RootState) => state.user); + const [ + cookieConsent, + setBrowserCookieConsent + ] = useState(CookieConsentOptions.NONE); const [inProp, setInProp] = useState(false); const dispatch = useDispatch(); const { t } = useTranslation(); function initializeCookieConsent() { if (user.authenticated) { + if (!user.cookieConsent) { + return; + } setBrowserCookieConsent(user.cookieConsent); Cookies.set('p5-cookie-consent', user.cookieConsent, { expires: 365 }); return; } - setBrowserCookieConsent('none'); - Cookies.set('p5-cookie-consent', 'none', { expires: 365 }); + setBrowserCookieConsent(CookieConsentOptions.NONE); + Cookies.set('p5-cookie-consent', CookieConsentOptions.NONE, { + expires: 365 + }); } function acceptAllCookies() { if (user.authenticated) { - dispatch(setUserCookieConsent('all')); + dispatch(setUserCookieConsent(CookieConsentOptions.ALL)); } - setBrowserCookieConsent('all'); - Cookies.set('p5-cookie-consent', 'all', { expires: 365 }); + setBrowserCookieConsent(CookieConsentOptions.ALL); + Cookies.set('p5-cookie-consent', CookieConsentOptions.ALL, { + expires: 365 + }); } function acceptEssentialCookies() { if (user.authenticated) { - dispatch(setUserCookieConsent('essential')); + dispatch(setUserCookieConsent(CookieConsentOptions.ESSENTIAL)); } - setBrowserCookieConsent('essential'); - Cookies.set('p5-cookie-consent', 'essential', { expires: 365 }); + setBrowserCookieConsent(CookieConsentOptions.ESSENTIAL); + Cookies.set('p5-cookie-consent', CookieConsentOptions.ESSENTIAL, { + expires: 365 + }); // Remove Google Analytics Cookies Cookies.remove('_ga'); Cookies.remove('_gat'); @@ -118,11 +134,29 @@ function CookieConsent({ hide }) { function mergeCookieConsent() { if (user.authenticated) { - if (user.cookieConsent === 'none' && cookieConsent !== 'none') { - dispatch(setUserCookieConsent(cookieConsent)); - } else if (user.cookieConsent !== 'none') { + if (!user.cookieConsent) { + return; + } + if ( + ![ + CookieConsentOptions.ALL, + CookieConsentOptions.ESSENTIAL, + CookieConsentOptions.NONE + ].includes(user.cookieConsent) + ) { + return; + } + + if ( + user.cookieConsent === CookieConsentOptions.NONE && + cookieConsent !== CookieConsentOptions.NONE + ) { + dispatch(setUserCookieConsent(cookieConsent as CookieConsentOptions)); + } else if (user.cookieConsent !== CookieConsentOptions.NONE) { setBrowserCookieConsent(user.cookieConsent); - Cookies.set('p5-cookie-consent', user.cookieConsent, { expires: 365 }); + Cookies.set('p5-cookie-consent', user.cookieConsent, { + expires: 365 + }); } } } @@ -130,7 +164,7 @@ function CookieConsent({ hide }) { useEffect(() => { const p5CookieConsent = Cookies.get('p5-cookie-consent'); if (p5CookieConsent) { - setBrowserCookieConsent(p5CookieConsent); + setBrowserCookieConsent(p5CookieConsent as CookieConsentOptions); } else { initializeCookieConsent(); } @@ -165,9 +199,9 @@ function CookieConsent({ hide }) { return ( - {(state) => ( + {(state: CookieConsentContainerState['state']) => ( - + {t('Cookies.Header')} @@ -191,13 +225,3 @@ function CookieConsent({ hide }) { ); } - -CookieConsent.propTypes = { - hide: PropTypes.bool -}; - -CookieConsent.defaultProps = { - hide: false -}; - -export default CookieConsent; diff --git a/package-lock.json b/package-lock.json index f948e6f8dd..f07e6ee670 100644 --- a/package-lock.json +++ b/package-lock.json @@ -164,6 +164,7 @@ "@types/classnames": "^2.3.0", "@types/friendly-words": "^1.2.2", "@types/jest": "^29.5.14", + "@types/js-cookie": "^3.0.6", "@types/mjml": "^4.7.4", "@types/node": "^16.18.126", "@types/nodemailer": "^7.0.1", @@ -173,6 +174,7 @@ "@types/react-dom": "^16.9.25", "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3", + "@types/react-transition-group": "^4.4.12", "@types/sinon": "^17.0.4", "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^5.62.0", @@ -16448,6 +16450,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/js-levenshtein": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.0.tgz", @@ -16691,6 +16700,16 @@ "@types/react-router": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react/node_modules/csstype": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", @@ -53160,6 +53179,12 @@ "pretty-format": "^29.0.0" } }, + "@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true + }, "@types/js-levenshtein": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.0.tgz", @@ -53398,6 +53423,13 @@ "@types/react-router": "*" } }, + "@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "dev": true, + "requires": {} + }, "@types/redux-devtools-themes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/redux-devtools-themes/-/redux-devtools-themes-1.0.0.tgz", diff --git a/package.json b/package.json index df7a8dc603..1f531d396f 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "@types/classnames": "^2.3.0", "@types/friendly-words": "^1.2.2", "@types/jest": "^29.5.14", + "@types/js-cookie": "^3.0.6", "@types/mjml": "^4.7.4", "@types/node": "^16.18.126", "@types/nodemailer": "^7.0.1", @@ -148,6 +149,7 @@ "@types/react-dom": "^16.9.25", "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3", + "@types/react-transition-group": "^4.4.12", "@types/sinon": "^17.0.4", "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^5.62.0", From 57ab54016d7e229af22d55131ef90a2ec8a351f7 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 00:39:03 +0000 Subject: [PATCH 44/61] client/modules/User/components/VisibilityDropdown: update to ts, no-verify --- .../components/{VisibilityDropdown.jsx => VisibilityDropdown.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{VisibilityDropdown.jsx => VisibilityDropdown.tsx} (100%) diff --git a/client/modules/User/components/VisibilityDropdown.jsx b/client/modules/User/components/VisibilityDropdown.tsx similarity index 100% rename from client/modules/User/components/VisibilityDropdown.jsx rename to client/modules/User/components/VisibilityDropdown.tsx From 3e93724d9fc0c62064d0f32824d527fe99977f54 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 00:39:23 +0000 Subject: [PATCH 45/61] client/modules/User/components/VisibilityDropdown: update to named export and add types --- .../modules/IDE/components/Header/Toolbar.jsx | 2 +- .../IDE/components/SketchListRowBase.jsx | 2 +- .../User/components/VisibilityDropdown.tsx | 54 +++++++++++-------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx index fbf0b5d091..16ed74ff3e 100644 --- a/client/modules/IDE/components/Header/Toolbar.jsx +++ b/client/modules/IDE/components/Header/Toolbar.jsx @@ -20,7 +20,7 @@ import StopIcon from '../../../../images/stop.svg'; import PreferencesIcon from '../../../../images/preferences.svg'; import ProjectName from './ProjectName'; import VersionIndicator from '../VersionIndicator'; -import VisibilityDropdown from '../../../User/components/VisibilityDropdown'; +import { VisibilityDropdown } from '../../../User/components/VisibilityDropdown'; import { changeVisibility } from '../../actions/project'; const Toolbar = (props) => { diff --git a/client/modules/IDE/components/SketchListRowBase.jsx b/client/modules/IDE/components/SketchListRowBase.jsx index e1c4c80f13..d11cb8bf40 100644 --- a/client/modules/IDE/components/SketchListRowBase.jsx +++ b/client/modules/IDE/components/SketchListRowBase.jsx @@ -10,7 +10,7 @@ import { TableDropdown } from '../../../components/Dropdown/TableDropdown'; import { MenuItem } from '../../../components/Dropdown/MenuItem'; import { formatDateToString } from '../../../utils/formatDate'; import { getConfig } from '../../../utils/getConfig'; -import VisibilityDropdown from '../../User/components/VisibilityDropdown'; +import { VisibilityDropdown } from '../../User/components/VisibilityDropdown'; const ROOT_URL = getConfig('API_URL'); diff --git a/client/modules/User/components/VisibilityDropdown.tsx b/client/modules/User/components/VisibilityDropdown.tsx index 972ccad88a..d5ea66f234 100644 --- a/client/modules/User/components/VisibilityDropdown.tsx +++ b/client/modules/User/components/VisibilityDropdown.tsx @@ -1,13 +1,29 @@ import React, { useState, useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import LockIcon from '../../../images/lock.svg'; import EarthIcon from '../../../images/earth.svg'; import CheckmarkIcon from '../../../images/checkmark.svg'; -const VisibilityDropdown = ({ sketch, onVisibilityChange, location }) => { +export interface VisibilityDropdownProps { + sketch: { + id: string; + name: string; + visibility: string; + }; + onVisibilityChange: ( + sketchId: string, + sketchName: string, + newVisibility: string + ) => void; + location?: string; +} +export const VisibilityDropdown = ({ + sketch, + onVisibilityChange, + location = 'sketchlist' +}: VisibilityDropdownProps) => { const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); + const dropdownRef = useRef(null); const { t } = useTranslation(); @@ -31,8 +47,13 @@ const VisibilityDropdown = ({ sketch, onVisibilityChange, location }) => { visibilityOptions[0]; useEffect(() => { - const handleClickOutside = (event) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node | null; + if ( + dropdownRef.current && + target && + !dropdownRef.current.contains(target) + ) { setIsOpen(false); } }; @@ -41,14 +62,17 @@ const VisibilityDropdown = ({ sketch, onVisibilityChange, location }) => { return () => document.removeEventListener('mousedown', handleClickOutside); }, []); - const handleVisibilitySelect = (newVisibility) => { + const handleVisibilitySelect = (newVisibility: string) => { if (newVisibility !== sketch.visibility) { onVisibilityChange(sketch.id, sketch.name, newVisibility); } setIsOpen(false); }; - const handleKeyDown = (event, visibility) => { + const handleKeyDown = ( + event: React.KeyboardEvent, + visibility: string + ) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleVisibilitySelect(visibility); @@ -104,19 +128,3 @@ const VisibilityDropdown = ({ sketch, onVisibilityChange, location }) => {

); }; - -VisibilityDropdown.defaultProps = { - location: 'sketchlist' -}; - -VisibilityDropdown.propTypes = { - sketch: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - visibility: PropTypes.string.isRequired - }).isRequired, - onVisibilityChange: PropTypes.func.isRequired, - location: PropTypes.string -}; - -export default VisibilityDropdown; From 730270148b332ab9a9af072cd5ff599816d0818e Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 00:50:04 +0000 Subject: [PATCH 46/61] client/modules/User/components/DashboardTabSwitcher: update to ts, no-verify --- .../{DashboardTabSwitcher.jsx => DashboardTabSwitcher.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{DashboardTabSwitcher.jsx => DashboardTabSwitcher.tsx} (100%) diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.tsx similarity index 100% rename from client/modules/User/components/DashboardTabSwitcher.jsx rename to client/modules/User/components/DashboardTabSwitcher.tsx From 239b7f325acb446428550fe041429527e6cb2638 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 00:50:19 +0000 Subject: [PATCH 47/61] client/modules/User/components/DashboardTabSwitcher: update to named export, add types --- .../User/components/DashboardTabSwitcher.tsx | 29 ++++++++++--------- client/modules/User/pages/DashboardView.jsx | 5 ++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/client/modules/User/components/DashboardTabSwitcher.tsx b/client/modules/User/components/DashboardTabSwitcher.tsx index 7afbe5aec8..d494bf6045 100644 --- a/client/modules/User/components/DashboardTabSwitcher.tsx +++ b/client/modules/User/components/DashboardTabSwitcher.tsx @@ -10,11 +10,11 @@ import { Options } from '../../IDE/components/Header/MobileNav'; import { toggleDirectionForField } from '../../IDE/actions/sorting'; import useIsMobile from '../../IDE/hooks/useIsMobile'; -export const TabKey = { - assets: 'assets', - collections: 'collections', - sketches: 'sketches' -}; +export enum TabKey { + assets = 'assets', + collections = 'collections', + sketches = 'sketches' +} // It is good for right now, because we need to separate the nav dropdown logic from the navBar before we can use it here const FilterOptions = styled(Options)` @@ -25,7 +25,16 @@ const FilterOptions = styled(Options)` } `; -const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => { +export interface DashboardTabSwitcherProps { + currentTab: TabKey; + isOwner: string; + 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 }) => { ); }; - -DashboardTabSwitcher.propTypes = { - currentTab: PropTypes.string.isRequired, - isOwner: PropTypes.bool.isRequired, - username: PropTypes.string.isRequired -}; - -export default DashboardTabSwitcher; 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 = () => {

{ownerName()}

- Date: Mon, 27 Oct 2025 01:13:18 +0000 Subject: [PATCH 48/61] CollectionShareButton: update to ts, no-verify --- .../{CollectionShareButton.jsx => CollectionShareButton.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{CollectionShareButton.jsx => CollectionShareButton.tsx} (100%) diff --git a/client/modules/User/components/CollectionShareButton.jsx b/client/modules/User/components/CollectionShareButton.tsx similarity index 100% rename from client/modules/User/components/CollectionShareButton.jsx rename to client/modules/User/components/CollectionShareButton.tsx From 1468ad2ae13356a4e232427cc0534a36fba165c4 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 01:13:42 +0000 Subject: [PATCH 49/61] CollectionShareButton: update to named export, add ref type --- client/modules/User/components/CollectionMetadata.jsx | 2 +- .../modules/User/components/CollectionShareButton.tsx | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/client/modules/User/components/CollectionMetadata.jsx b/client/modules/User/components/CollectionMetadata.jsx index dcb47e0929..4e98bf323b 100644 --- a/client/modules/User/components/CollectionMetadata.jsx +++ b/client/modules/User/components/CollectionMetadata.jsx @@ -11,7 +11,7 @@ import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketc import EditableInput from '../../IDE/components/EditableInput'; import { SketchSearchbar } from '../../IDE/components/Searchbar'; import { getCollection } from '../../IDE/selectors/collections'; -import ShareURL from './CollectionShareButton'; +import { ShareURL } from './CollectionShareButton'; function CollectionMetadata({ collectionId }) { const { t } = useTranslation(); diff --git a/client/modules/User/components/CollectionShareButton.tsx b/client/modules/User/components/CollectionShareButton.tsx index a5d8705dcd..2865773125 100644 --- a/client/modules/User/components/CollectionShareButton.tsx +++ b/client/modules/User/components/CollectionShareButton.tsx @@ -7,11 +7,11 @@ import { DropdownArrowIcon } from '../../../common/icons'; import { useModalClose } from '../../../common/useModalClose'; import CopyableInput from '../../IDE/components/CopyableInput'; -const ShareURL = ({ value }) => { +export const ShareURL = ({ value }: { value: string }) => { const [showURL, setShowURL] = useState(false); const { t } = useTranslation(); const close = useCallback(() => setShowURL(false), [setShowURL]); - const ref = useModalClose(close); + const ref = useModalClose(close); return (
@@ -29,9 +29,3 @@ const ShareURL = ({ value }) => {
); }; - -ShareURL.propTypes = { - value: PropTypes.string.isRequired -}; - -export default ShareURL; From 8b78e7069f20d154a232696cf793c8b77e7d89a4 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 01:18:17 +0000 Subject: [PATCH 50/61] clean up stray leftover PropTypes --- client/modules/User/components/CollectionShareButton.tsx | 2 -- client/modules/User/components/DashboardTabSwitcher.tsx | 1 - client/modules/User/components/SocialAuthButton.tsx | 1 - 3 files changed, 4 deletions(-) diff --git a/client/modules/User/components/CollectionShareButton.tsx b/client/modules/User/components/CollectionShareButton.tsx index 2865773125..d8e20bfbd9 100644 --- a/client/modules/User/components/CollectionShareButton.tsx +++ b/client/modules/User/components/CollectionShareButton.tsx @@ -1,7 +1,5 @@ -import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; - import { Button } from '../../../common/Button'; import { DropdownArrowIcon } from '../../../common/icons'; import { useModalClose } from '../../../common/useModalClose'; diff --git a/client/modules/User/components/DashboardTabSwitcher.tsx b/client/modules/User/components/DashboardTabSwitcher.tsx index d494bf6045..8ec810cf64 100644 --- a/client/modules/User/components/DashboardTabSwitcher.tsx +++ 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'; diff --git a/client/modules/User/components/SocialAuthButton.tsx b/client/modules/User/components/SocialAuthButton.tsx index 56ddc9ae5d..b728168b94 100644 --- a/client/modules/User/components/SocialAuthButton.tsx +++ b/client/modules/User/components/SocialAuthButton.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; From 16fc4389860c866ae06bfb4306908ac349f509b1 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 01:34:28 +0000 Subject: [PATCH 51/61] client/modules/User/pages/AccountView: update to ts, no-verify --- client/modules/User/pages/{AccountView.jsx => AccountView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/pages/{AccountView.jsx => AccountView.tsx} (100%) diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.tsx similarity index 100% rename from client/modules/User/pages/AccountView.jsx rename to client/modules/User/pages/AccountView.tsx From dc98920fefb0f66266ecfed308e94fe5b6d7cd9e Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 01:43:38 +0000 Subject: [PATCH 52/61] client/modules/User/pages/AccountView: update to named export, resolve typeerrors --- client/modules/User/pages/AccountView.tsx | 22 +++++++++++++--------- client/routes.jsx | 2 +- package-lock.json | 20 ++++++++++++++++++++ package.json | 1 + 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/client/modules/User/pages/AccountView.tsx b/client/modules/User/pages/AccountView.tsx index 508b65816d..4e3fbc8ab2 100644 --- a/client/modules/User/pages/AccountView.tsx +++ b/client/modules/User/pages/AccountView.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; @@ -13,13 +13,15 @@ import { import APIKeyForm from '../components/APIKeyForm'; import Nav from '../../IDE/components/Header/Nav'; import ErrorModal from '../../IDE/components/ErrorModal'; +import { hideErrorModal } from '../../IDE/actions/ide'; import Overlay from '../../App/components/Overlay'; import Toast from '../../IDE/components/Toast'; +import { RootState } from '../../../reducers'; function SocialLoginPanel() { const { t } = useTranslation(); - const isGithub = useSelector((state) => !!state.user.github); - const isGoogle = useSelector((state) => !!state.user.google); + const isGithub = useSelector((state: RootState) => !!state.user.github); + const isGoogle = useSelector((state: RootState) => !!state.user.google); return ( @@ -45,13 +47,13 @@ function SocialLoginPanel() { ); } -function AccountView() { +export function AccountView() { const { t } = useTranslation(); - + const dispatch = useDispatch(); const location = useLocation(); const queryParams = parse(location.search); const showError = !!queryParams.error; - const errorType = queryParams.error; + const errorType = queryParams.error as string; const accessTokensUIEnabled = window.process.env.UI_ACCESS_TOKEN_ENABLED; const history = useHistory(); @@ -72,7 +74,11 @@ function AccountView() { history.push(location.pathname); }} > - + dispatch(hideErrorModal())} + /> )} @@ -111,5 +117,3 @@ function AccountView() {
); } - -export default AccountView; diff --git a/client/routes.jsx b/client/routes.jsx index 4e399561ff..55d47453d6 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -15,7 +15,7 @@ import { SignupView } from './modules/User/pages/SignupView'; import { ResetPasswordView } from './modules/User/pages/ResetPasswordView'; import { EmailVerificationView } from './modules/User/pages/EmailVerificationView'; import { NewPasswordView } from './modules/User/pages/NewPasswordView'; -import AccountView from './modules/User/pages/AccountView'; +import { AccountView } from './modules/User/pages/AccountView'; import { CollectionView } from './modules/User/pages/CollectionView'; import DashboardView from './modules/User/pages/DashboardView'; import { getUser } from './modules/User/actions'; diff --git a/package-lock.json b/package-lock.json index f07e6ee670..1e1c123183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -174,6 +174,7 @@ "@types/react-dom": "^16.9.25", "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3", + "@types/react-tabs": "^2.3.1", "@types/react-transition-group": "^4.4.12", "@types/sinon": "^17.0.4", "@types/styled-components": "^5.1.34", @@ -16700,6 +16701,16 @@ "@types/react-router": "*" } }, + "node_modules/@types/react-tabs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/react-tabs/-/react-tabs-2.3.1.tgz", + "integrity": "sha512-4SZXSF8ibQAtHUqqfoYLO+8Rn4F7Hj/IX3CJf1712dWeFvRxYY1HjjwSoN4MgUB0SB0dY4GrdlZwNhhIKuRoNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", @@ -53423,6 +53434,15 @@ "@types/react-router": "*" } }, + "@types/react-tabs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/react-tabs/-/react-tabs-2.3.1.tgz", + "integrity": "sha512-4SZXSF8ibQAtHUqqfoYLO+8Rn4F7Hj/IX3CJf1712dWeFvRxYY1HjjwSoN4MgUB0SB0dY4GrdlZwNhhIKuRoNQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", diff --git a/package.json b/package.json index 1f531d396f..01d0ed4a7c 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "@types/react-dom": "^16.9.25", "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3", + "@types/react-tabs": "^2.3.1", "@types/react-transition-group": "^4.4.12", "@types/sinon": "^17.0.4", "@types/styled-components": "^5.1.34", From 6dc69100da997ad004d233faf32a5801f5b339fb Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 01:45:01 +0000 Subject: [PATCH 53/61] client/modules/User/components/AccountForm: update to ts, no-verify --- .../modules/User/components/{AccountForm.jsx => AccountForm.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{AccountForm.jsx => AccountForm.tsx} (100%) diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.tsx similarity index 100% rename from client/modules/User/components/AccountForm.jsx rename to client/modules/User/components/AccountForm.tsx From c53a8e4f05063c1d930b6a9cb2a48fde6a55286f Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 01:53:28 +0000 Subject: [PATCH 54/61] client/modules/User/components/AccountForm: add types & update to named export --- .../modules/User/components/AccountForm.tsx | 37 +++++++++++-------- .../User/components/AccountForm.unit.test.jsx | 2 +- client/modules/User/pages/AccountView.tsx | 2 +- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/client/modules/User/components/AccountForm.tsx b/client/modules/User/components/AccountForm.tsx index 4ef40e4298..cb84078f9f 100644 --- a/client/modules/User/components/AccountForm.tsx +++ b/client/modules/User/components/AccountForm.tsx @@ -6,14 +6,21 @@ import { Button, ButtonTypes } from '../../../common/Button'; import { validateSettings } from '../../../utils/reduxFormUtils'; import { updateSettings, initiateVerification } from '../actions'; import { apiClient } from '../../../utils/apiClient'; +import { + DuplicateUserCheckQuery, + UpdateSettingsRequestBody +} from '../../../../common/types'; +import { RootState } from '../../../reducers'; +import type { AccountForm as AccountFormType } from '../../../utils/reduxFormUtils'; -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 +31,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: UpdateSettingsRequestBody) { return dispatch(updateSettings(formProps)); } @@ -54,11 +61,13 @@ function AccountForm() { validate={validateSettings} onSubmit={onSubmit} > - {({ handleSubmit, submitting, invalid, restart }) => ( + {({ handleSubmit, submitting, invalid, form }) => (
{ - handleSubmit(event).then(restart); + onSubmit={async (event: React.FormEvent) => { + const result = await handleSubmit(event); + form.restart(); + return result; }} > ) : (
)} @@ -109,5 +111,3 @@ const APIKeyForm = () => { ); }; - -export default APIKeyForm; diff --git a/client/modules/User/pages/AccountView.tsx b/client/modules/User/pages/AccountView.tsx index 03375d7466..225e9e42e8 100644 --- a/client/modules/User/pages/AccountView.tsx +++ b/client/modules/User/pages/AccountView.tsx @@ -10,7 +10,7 @@ import { SocialAuthButton, SocialAuthServices } 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 { hideErrorModal } from '../../IDE/actions/ide'; From 7a554338ae4a04bc5d5b3ae418b503f0882ab3ee Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 02:03:54 +0000 Subject: [PATCH 57/61] client/modules/User/components/APIKeyList: update to ts,no-verify --- client/modules/User/components/{APIKeyList.jsx => APIKeyList.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/modules/User/components/{APIKeyList.jsx => APIKeyList.tsx} (100%) diff --git a/client/modules/User/components/APIKeyList.jsx b/client/modules/User/components/APIKeyList.tsx similarity index 100% rename from client/modules/User/components/APIKeyList.jsx rename to client/modules/User/components/APIKeyList.tsx From 24960271c69e939828d76d0036f8ab29b3216979 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 27 Oct 2025 02:04:29 +0000 Subject: [PATCH 58/61] client/modules/User/components/APIKeyList: add types, remove extra PropType & update to named export --- client/modules/User/components/APIKeyForm.tsx | 12 +----------- client/modules/User/components/APIKeyList.tsx | 18 ++++++------------ 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/client/modules/User/components/APIKeyForm.tsx b/client/modules/User/components/APIKeyForm.tsx index c725f0f4f4..7a9abc8df8 100644 --- a/client/modules/User/components/APIKeyForm.tsx +++ b/client/modules/User/components/APIKeyForm.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -6,19 +5,10 @@ import { Button, ButtonTypes } from '../../../common/Button'; 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 { 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 -}); - export const APIKeyForm = () => { const { t } = useTranslation(); const apiKeys = useSelector((state: RootState) => state.user.apiKeys) ?? []; diff --git a/client/modules/User/components/APIKeyList.tsx b/client/modules/User/components/APIKeyList.tsx index 17cd857354..a3692fad32 100644 --- a/client/modules/User/components/APIKeyList.tsx +++ b/client/modules/User/components/APIKeyList.tsx @@ -1,17 +1,18 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { orderBy } from 'lodash'; import { useTranslation } from 'react-i18next'; - -import { APIKeyPropType } from './APIKeyForm'; - +import type { SanitisedApiKey } from '../../../../common/types'; import { distanceInWordsToNow, formatDateToString } from '../../../utils/formatDate'; import TrashCanIcon from '../../../images/trash-can.svg'; -function APIKeyList({ apiKeys, onRemove }) { +export interface APIKeyListProps { + apiKeys: SanitisedApiKey[]; + onRemove: (key: SanitisedApiKey) => void; +} +export function APIKeyList({ apiKeys, onRemove }: APIKeyListProps) { const { t } = useTranslation(); return ( @@ -50,10 +51,3 @@ function APIKeyList({ apiKeys, onRemove }) {
); } - -APIKeyList.propTypes = { - apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired, - onRemove: PropTypes.func.isRequired -}; - -export default APIKeyList; From 79d584d382a3750342cf1fcb9a5e3f6dc6780a47 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 13:22:38 +0000 Subject: [PATCH 59/61] client/common/icons: update to ts, no-verify --- client/common/{icons.jsx => icons.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/common/{icons.jsx => icons.tsx} (100%) diff --git a/client/common/icons.jsx b/client/common/icons.tsx similarity index 100% rename from client/common/icons.jsx rename to client/common/icons.tsx From 48b37059cd7b29046e2e4e08a7fc27816cc0a6cb Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 26 Oct 2025 13:23:28 +0000 Subject: [PATCH 60/61] client/common/icons: add types & update snapshot test --- client/common/icons.tsx | 40 ++++++++++++------- .../__snapshots__/Nav.unit.test.jsx.snap | 8 ++-- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/client/common/icons.tsx b/client/common/icons.tsx index bd5a673e2a..80411bf937 100644 --- a/client/common/icons.tsx +++ b/client/common/icons.tsx @@ -26,13 +26,25 @@ import Filter from '../images/filter.svg'; import Cross from '../images/cross.svg'; import Copy from '../images/copy.svg'; +export interface IconColors { + default?: string; + hover?: string; +} + +export interface IconProps extends React.SVGProps { + 'aria-label'?: string; + Icon?: IconColors; +} + // HOC that adds the right web accessibility props // https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html // could also give these a default size, color, etc. based on the theme // Need to add size to these - like small icon, medium icon, large icon. etc. -function withLabel(SvgComponent) { - const StyledIcon = styled(SvgComponent)` +function withLabel( + SvgComponent: React.ComponentType> +) { + const StyledIcon = styled(SvgComponent)` &&& { color: ${(props) => props.Icon?.default}; & g, @@ -53,27 +65,27 @@ function withLabel(SvgComponent) { } `; - const Icon = (props) => { - const { 'aria-label': ariaLabel } = props; + // Necessary because styled components inject a different type for the ref prop + type StyledIconProps = Omit< + React.ComponentProps, + 'ref' + > & { + ref?: React.Ref; + }; + + const Icon = (props: StyledIconProps) => { + const { 'aria-label': ariaLabel, ...rest } = props; if (ariaLabel) { return ( ); } - return ; - }; - - Icon.propTypes = { - 'aria-label': PropTypes.string - }; - - Icon.defaultProps = { - 'aria-label': null + return ; }; return Icon; diff --git a/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap b/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap index 02e4776b1c..382e49aadd 100644 --- a/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap +++ b/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap @@ -351,7 +351,7 @@ exports[`Nav renders dashboard version for mobile 1`] = ` >