diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index cc9a20cb0ed..4b5d867f0b3 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -238,6 +238,8 @@ "sound-mode": "This adds the pleasant sound of acoustic guitar throughout the website. You'll get musical feedback as you type in the editor, complete challenges, claim certifications, and more.", "sound-volume": "Campfire Volume:", "scrollbar-width": "Editor Scrollbar Width", + "reset-editor-layout-tooltip": "Reset the editor layout to its default state", + "reset-editor-layout": "Reset Editor Layout", "shortcuts-explained": "Within a challenge, press ESC followed by the question mark to show a list of available shortcuts.", "username": { "contains invalid characters": "Username \"{{username}}\" contains invalid characters", @@ -896,6 +898,7 @@ "invalid-update-flag": "You are attempting to access forbidden resources. Please request assistance on https://forum.freecodecamp.org if this is a valid request.", "generate-exam-error": "An error occurred trying to generate your exam.", "cert-not-found": "The certification {{certSlug}} does not exist.", + "reset-editor-layout": "Your editor layout has been reset.", "ms": { "transcript": { "link-err-1": "Please include a Microsoft transcript URL in the request.", diff --git a/client/src/client-only-routes/show-settings.tsx b/client/src/client-only-routes/show-settings.tsx index 5c7177c346e..506e7d934dd 100644 --- a/client/src/client-only-routes/show-settings.tsx +++ b/client/src/client-only-routes/show-settings.tsx @@ -35,7 +35,8 @@ import { updateMySound, updateMyTheme, updateMyKeyboardShortcuts, - verifyCert + verifyCert, + resetMyEditorLayout } from '../redux/settings/actions'; const { apiLocation } = envData; @@ -47,6 +48,7 @@ type ShowSettingsProps = Pick & { navigate: (location: string) => void; showLoading: boolean; toggleSoundMode: (sound: boolean) => void; + resetEditorLayout: () => void; toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void; updateIsHonest: () => void; updateQuincyEmail: (isSendQuincyEmail: boolean) => void; @@ -80,6 +82,7 @@ const mapDispatchToProps = { updateIsHonest: updateMyHonesty, updateQuincyEmail: (sendQuincyEmail: boolean) => updateMyQuincyEmail({ sendQuincyEmail }), + resetEditorLayout: () => resetMyEditorLayout(), verifyCert }; @@ -91,6 +94,7 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element { toggleNightMode, toggleSoundMode, toggleKeyboardShortcuts, + resetEditorLayout, user: { completedChallenges, email, @@ -140,6 +144,7 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element { return ; } const sound = (store.get('fcc-sound') as boolean) ?? false; + const editorLayout = (store.get('challenge-layout') as boolean) ?? false; return ( <> @@ -161,6 +166,8 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element { currentTheme={theme} keyboardShortcuts={keyboardShortcuts} sound={sound} + editorLayout={editorLayout} + resetEditorLayout={resetEditorLayout} toggleKeyboardShortcuts={toggleKeyboardShortcuts} toggleNightMode={toggleNightMode} toggleSoundMode={toggleSoundMode} diff --git a/client/src/components/settings/misc-settings.tsx b/client/src/components/settings/misc-settings.tsx index e5c4eab557c..2518cf35a4d 100644 --- a/client/src/components/settings/misc-settings.tsx +++ b/client/src/components/settings/misc-settings.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Spacer } from '@freecodecamp/ui'; +import { Button, Spacer } from '@freecodecamp/ui'; import { FullWidthRow } from '../helpers'; import ThemeSettings, { ThemeProps } from '../../components/settings/theme'; import SoundSettings from '../../components/settings/sound'; @@ -11,15 +11,19 @@ type MiscSettingsProps = ThemeProps & { currentTheme: string; keyboardShortcuts: boolean; sound: boolean; + editorLayout: boolean | null; toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void; toggleNightMode: () => void; toggleSoundMode: (sound: boolean) => void; + resetEditorLayout: () => void; }; const MiscSettings = ({ currentTheme, keyboardShortcuts, sound, + editorLayout, + resetEditorLayout, toggleKeyboardShortcuts, toggleNightMode, toggleSoundMode @@ -41,6 +45,19 @@ const MiscSettings = ({ explain={t('settings.shortcuts-explained')?.toString()} /> + + + ); diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 931c8b6044e..a4b76b21ab9 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -491,6 +491,8 @@ export const reducer = handleActions( payload ? spreadThePayloadOnUser(state, payload) : state, [settingsTypes.updateMyPortfolioComplete]: (state, { payload }) => payload ? spreadThePayloadOnUser(state, payload) : state, + [settingsTypes.resetMyEditorLayoutComplete]: (state, { payload }) => + payload ? spreadThePayloadOnUser(state, payload) : state, [settingsTypes.verifyCertComplete]: (state, { payload }) => payload ? spreadThePayloadOnUser(state, payload) : state, [settingsTypes.submitProfileUIComplete]: (state, { payload }) => diff --git a/client/src/redux/settings/action-types.js b/client/src/redux/settings/action-types.js index 853690022b1..68931381c01 100644 --- a/client/src/redux/settings/action-types.js +++ b/client/src/redux/settings/action-types.js @@ -18,6 +18,7 @@ export const actionTypes = createTypes( ...createAsyncTypes('submitProfileUI'), ...createAsyncTypes('verifyCert'), ...createAsyncTypes('resetProgress'), + ...createAsyncTypes('resetMyEditorLayout'), ...createAsyncTypes('deleteAccount') ], ns diff --git a/client/src/redux/settings/actions.js b/client/src/redux/settings/actions.js index bb451ca2d96..466e74dfa22 100644 --- a/client/src/redux/settings/actions.js +++ b/client/src/redux/settings/actions.js @@ -105,5 +105,13 @@ export const verifyCertError = createAction(types.verifyCertError); export const resetProgress = createAction(types.resetProgress); export const resetProgressError = createAction(types.resetProgressError); +export const resetMyEditorLayout = createAction(types.resetMyEditorLayout); +export const resetMyEditorLayoutComplete = createAction( + types.resetMyEditorLayoutComplete +); +export const resetMyEditorLayoutError = createAction( + types.resetMyEditorLayoutError +); + export const deleteAccount = createAction(types.deleteAccount); export const deleteAccountError = createAction(types.deleteAccountError); diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js index 99c8692d62a..35c47eef3fb 100644 --- a/client/src/redux/settings/settings-sagas.js +++ b/client/src/redux/settings/settings-sagas.js @@ -30,6 +30,8 @@ import { } from '../../utils/ajax'; import { completedChallengesSelector } from '../selectors'; import { + resetMyEditorLayoutComplete, + resetMyEditorLayoutError, submitNewAboutComplete, submitNewAboutError, submitNewUsernameComplete, @@ -110,6 +112,26 @@ function* updateMySoundSaga({ payload: update }) { } } +function* resetMyEditorLayoutSaga() { + const layout = store.get('challenge-layout'); + + if (layout) { + try { + const data = { + message: 'flash.reset-editor-layout', + type: 'success' + }; + + store.remove('challenge-layout'); + + yield put(createFlashMessage({ ...data })); + yield put(resetMyEditorLayoutComplete({ ...data })); + } catch (e) { + yield put(resetMyEditorLayoutError); + } + } +} + function* updateMyThemeSaga({ payload: update }) { try { const { data } = yield call(putUpdateMyTheme, update); @@ -225,6 +247,7 @@ export function createSettingsSagas(types) { takeEvery(types.updateMySocials, updateMySocialsSaga), takeEvery(types.updateMyHonesty, updateMyHonestySaga), takeEvery(types.updateMySound, updateMySoundSaga), + takeEvery(types.resetMyEditorLayout, resetMyEditorLayoutSaga), takeEvery(types.updateMyTheme, updateMyThemeSaga), takeEvery(types.updateMyKeyboardShortcuts, updateMyKeyboardShortcutsSaga), takeEvery(types.updateMyQuincyEmail, updateMyQuincyEmailSaga), diff --git a/e2e/reset-editor-layout.spec.ts b/e2e/reset-editor-layout.spec.ts new file mode 100644 index 00000000000..11af13d2b95 --- /dev/null +++ b/e2e/reset-editor-layout.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +test.use({ storageState: 'playwright/.auth/certified-user.json' }); +test.describe('Reset Editor Layout', () => { + test('drag layout and reset', async ({ page, isMobile }) => { + test.skip( + isMobile, + 'The mobile layout does not have resizable panes, so this test is not applicable.' + ); + + await page.goto( + '/learn/2022/responsive-web-design/learn-basic-css-by-building-a-cafe-menu/step-15' + ); + + const desktopLayout = page.getByTestId('desktop-layout'); + const splitter = page.getByTestId('preview-left-splitter'); + const editorPane = desktopLayout.getByTestId('editor-pane'); + const initialStyle = await editorPane.getAttribute('style'); + + expect(initialStyle).toContain('flex: 1'); + + // Drag the splitter to resize the editor pane + await splitter.hover(); + await page.mouse.down(); + await page.mouse.move(100, 100); + await page.mouse.up(); + + const newStyle = await editorPane.getAttribute('style'); + expect(newStyle).not.toContain('flex: 1'); + + await page.goto('/settings#privacy-settings'); + + const resetButton = page.getByTestId('reset-layout-btn'); + await resetButton.click(); + + await expect(page.getByTestId('flash-message')).toContainText( + 'Your editor layout has been reset' + ); + + await expect(resetButton).toBeDisabled(); + + await page.goto( + '/learn/2022/responsive-web-design/learn-basic-css-by-building-a-cafe-menu/step-15' + ); + + const afterReset = await editorPane.getAttribute('style'); + expect(afterReset).toContain('flex: 1'); + }); +});