diff --git a/.storybook/preview.js b/.storybook/preview.js index 9260b91c98..080b022fe5 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -2,9 +2,9 @@ import React from 'react'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router'; -import configureStore from '../client/store'; +import { configureStore } from '../client/store'; import '../client/i18n-test'; -import '../client/styles/storybook.css' +import '../client/styles/storybook.css'; import { withThemeProvider, themeToolbarItem } from './decorator-theme'; const initialState = window.__INITIAL_STATE__; diff --git a/client/constants.js b/client/constants.ts similarity index 100% rename from client/constants.js rename to client/constants.ts diff --git a/client/index.integration.test.jsx b/client/index.integration.test.jsx index 66819b1536..68ecd6efc7 100644 --- a/client/index.integration.test.jsx +++ b/client/index.integration.test.jsx @@ -4,7 +4,7 @@ import React from 'react'; import Routing from './routes'; import { reduxRender, act, waitFor, screen, within } from './test-utils'; -import configureStore from './store'; +import { configureStore } from './store'; import * as Actions from './modules/User/actions'; import { userResponse } from './testData/testServerResponses'; diff --git a/client/index.jsx b/client/index.jsx index f050230c7b..8732ab99cb 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -4,7 +4,7 @@ import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import browserHistory from './browserHistory'; -import configureStore from './store'; +import { configureStore } from './store'; import Routing from './routes'; import ThemeProvider from './modules/App/components/ThemeProvider'; import Loader from './modules/App/components/loader'; diff --git a/client/middleware.js b/client/middleware.js deleted file mode 100644 index 0b4230ec9d..0000000000 --- a/client/middleware.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createListenerMiddleware } from '@reduxjs/toolkit'; - -const listenerMiddleware = createListenerMiddleware(); - -export default listenerMiddleware; diff --git a/client/middleware.ts b/client/middleware.ts new file mode 100644 index 0000000000..385600bc15 --- /dev/null +++ b/client/middleware.ts @@ -0,0 +1,3 @@ +import { createListenerMiddleware } from '@reduxjs/toolkit'; + +export const listenerMiddleware = createListenerMiddleware(); diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.ts similarity index 70% rename from client/modules/IDE/actions/preferences.js rename to client/modules/IDE/actions/preferences.ts index f6e71504ee..2c0e71f177 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.ts @@ -2,7 +2,46 @@ import i18next from 'i18next'; import { apiClient } from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; -function updatePreferences(formParams, dispatch) { +// Not included in Preferences Form to post to BE +type IdePreferencesTabIndex = number; +type IdeAllAccessibleOutput = boolean; + +// Included in Preferences Form to post to BE +type IdeFontSize = number; +type IdeLineNumber = boolean; +type IdeAutoCloseBracketQuotes = boolean; +type IdeAutoCompleteHinter = boolean; +type IdeAutoSave = boolean; +type IdeLineWrap = boolean; +type IdeLintWarning = boolean; +type IdeTextOutput = boolean; +type IdeGridOutput = boolean; +enum IdeTheme { + LIGHT = 'light', + DARK = 'dark', + CONTRAST = 'contrast' +} +type IdeAutoRefresh = boolean; +type IdeLanguage = string; + +export interface PreferencesFormParam { + preferences: Partial<{ + fontSize: IdeFontSize; + lineNumbers: IdeLineNumber; + autocloseBracketsQuotes: IdeAutoCloseBracketQuotes; + autocompleteHinter: IdeAutoCompleteHinter; + autosave: IdeAutoSave; + linewrap: IdeLineWrap; + lintWarning: IdeLintWarning; + textOutput: IdeTextOutput; + gridOutput: IdeGridOutput; + theme: IdeTheme; + autorefresh: IdeAutoRefresh; + language: IdeLanguage; + }>; +} + +function updatePreferences(formParams: PreferencesFormParam, dispatch) { apiClient .put('/preferences', formParams) .then(() => {}) @@ -14,14 +53,14 @@ function updatePreferences(formParams, dispatch) { }); } -export function setPreferencesTab(value) { +export function setPreferencesTab(value: IdePreferencesTabIndex) { return { type: ActionTypes.SET_PREFERENCES_TAB, value }; } -export function setFontSize(value) { +export function setFontSize(value: IdeFontSize) { return (dispatch, getState) => { // eslint-disable-line dispatch({ @@ -40,7 +79,7 @@ export function setFontSize(value) { }; } -export function setLineNumbers(value) { +export function setLineNumbers(value: IdeLineNumber) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_LINE_NUMBERS, @@ -58,7 +97,7 @@ export function setLineNumbers(value) { }; } -export function setAutocloseBracketsQuotes(value) { +export function setAutocloseBracketsQuotes(value: IdeAutoCloseBracketQuotes) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_AUTOCLOSE_BRACKETS_QUOTES, @@ -76,7 +115,7 @@ export function setAutocloseBracketsQuotes(value) { }; } -export function setAutocompleteHinter(value) { +export function setAutocompleteHinter(value: IdeAutoCompleteHinter) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_AUTOCOMPLETE_HINTER, @@ -94,7 +133,7 @@ export function setAutocompleteHinter(value) { }; } -export function setAutosave(value) { +export function setAutosave(value: IdeAutoSave) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_AUTOSAVE, @@ -112,7 +151,7 @@ export function setAutosave(value) { }; } -export function setLinewrap(value) { +export function setLinewrap(value: IdeLineWrap) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_LINEWRAP, @@ -130,7 +169,7 @@ export function setLinewrap(value) { }; } -export function setLintWarning(value) { +export function setLintWarning(value: IdeLintWarning) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_LINT_WARNING, @@ -148,7 +187,7 @@ export function setLintWarning(value) { }; } -export function setTextOutput(value) { +export function setTextOutput(value: IdeTextOutput) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_TEXT_OUTPUT, @@ -166,7 +205,7 @@ export function setTextOutput(value) { }; } -export function setGridOutput(value) { +export function setGridOutput(value: IdeGridOutput) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_GRID_OUTPUT, @@ -184,11 +223,7 @@ export function setGridOutput(value) { }; } -export function setTheme(value) { - // return { - // type: ActionTypes.SET_THEME, - // value - // }; +export function setTheme(value: IdeTheme) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_THEME, @@ -206,11 +241,7 @@ export function setTheme(value) { }; } -export function setAutorefresh(value) { - // return { - // type: ActionTypes.SET_AUTOREFRESH, - // value - // }; +export function setAutorefresh(value: IdeAutoRefresh) { return (dispatch, getState) => { dispatch({ type: ActionTypes.SET_AUTOREFRESH, @@ -228,14 +259,17 @@ export function setAutorefresh(value) { }; } -export function setAllAccessibleOutput(value) { +export function setAllAccessibleOutput(value: IdeAllAccessibleOutput) { return (dispatch) => { dispatch(setTextOutput(value)); dispatch(setGridOutput(value)); }; } -export function setLanguage(value, { persistPreference = true } = {}) { +export function setLanguage( + value: IdeLanguage, + { persistPreference = true } = {} +) { return (dispatch, getState) => { i18next.changeLanguage(value); dispatch({ diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.ts similarity index 100% rename from client/modules/IDE/reducers/preferences.js rename to client/modules/IDE/reducers/preferences.ts diff --git a/client/persistState.js b/client/persistState.js deleted file mode 100644 index 457dba016b..0000000000 --- a/client/persistState.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - Saves and loads a snapshot of the Redux store - state to session storage -*/ -const key = 'p5js-editor'; -const storage = sessionStorage; - -export const saveState = (state) => { - try { - storage.setItem(key, JSON.stringify(state)); - } catch (error) { - console.warn('Unable to persist state to storage:', error); - } -}; - -export const loadState = () => { - try { - return JSON.parse(storage.getItem(key)); - } catch (error) { - console.warn('Failed to retrieve initialize state from storage:', error); - return null; - } -}; - -export const clearState = () => { - storage.removeItem(key); -}; diff --git a/client/persistState.ts b/client/persistState.ts new file mode 100644 index 0000000000..3f8771ecb7 --- /dev/null +++ b/client/persistState.ts @@ -0,0 +1,31 @@ +/** + * Saves and loads a snapshot of the Redux store + * state to session storage + */ +const key = 'p5js-editor'; +const storage: Storage = sessionStorage; + +// Use a generic type for state so consumers can specify the shape +export const saveState = (state: T): void => { + try { + storage.setItem(key, JSON.stringify(state)); + } catch (error) { + console.warn('Unable to persist state to storage:', error); + } +}; + +// Returns the stored state or null if not found +export const loadState = (): T | null => { + try { + const item = storage.getItem(key); + if (!item) return null; + return JSON.parse(item) as T; + } catch (error) { + console.warn('Failed to retrieve initialize state from storage:', error); + return null; + } +}; + +export const clearState = (): void => { + storage.removeItem(key); +}; diff --git a/client/reducers.js b/client/reducers.ts similarity index 93% rename from client/reducers.js rename to client/reducers.ts index f61d2585d6..f33babf41f 100644 --- a/client/reducers.js +++ b/client/reducers.ts @@ -14,7 +14,7 @@ import sorting from './modules/IDE/reducers/sorting'; import loading from './modules/IDE/reducers/loading'; import collections from './modules/IDE/reducers/collections'; -const rootReducer = combineReducers({ +export const rootReducer = combineReducers({ ide, files, preferences, @@ -30,5 +30,3 @@ const rootReducer = combineReducers({ loading, collections }); - -export default rootReducer; diff --git a/client/store.js b/client/store.ts similarity index 64% rename from client/store.js rename to client/store.ts index e74248f010..7bc8039e78 100644 --- a/client/store.js +++ b/client/store.ts @@ -1,25 +1,38 @@ -import { configureStore } from '@reduxjs/toolkit'; -import listenerMiddleware from './middleware'; +import { + configureStore as _configureStore, + PreloadedState +} from '@reduxjs/toolkit'; +import { listenerMiddleware } from './middleware'; import DevTools from './modules/App/components/DevTools'; -import rootReducer from './reducers'; +import { rootReducer } from './reducers'; import { clearState, loadState } from './persistState'; import { getConfig } from './utils/getConfig'; +// necessary to add redux devtool extension to Window interface +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION__?: any; + } +} + // Enable DevTools only when rendering on client and during development. // Display the dock monitor only if no browser extension is found. export function showReduxDevTools() { return ( getConfig('CLIENT') && getConfig('NODE_ENV') === 'development' && + // eslint-disable-next-line no-underscore-dangle !window.__REDUX_DEVTOOLS_EXTENSION__ ); } -export default function setupStore(initialState) { - const savedState = loadState(); +export function configureStore( + initialState: PreloadedState +) { + const savedState = loadState(); clearState(); - const store = configureStore({ + const store = _configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ @@ -32,9 +45,9 @@ export default function setupStore(initialState) { enhancers: showReduxDevTools() ? [DevTools.instrument()] : [] }); - if (module.hot) { + if ((module as any).hot) { // Enable Webpack hot module replacement for reducers - module.hot.accept('./reducers', () => { + (module as any).hot.accept('./reducers', () => { const nextRootReducer = require('./reducers').default; // eslint-disable-line global-require store.replaceReducer(nextRootReducer); }); diff --git a/client/test-utils.js b/client/test-utils.js index 9b7c8aab31..0d48a4eae9 100644 --- a/client/test-utils.js +++ b/client/test-utils.js @@ -23,7 +23,7 @@ import { Context as ResponsiveContext } from 'react-responsive'; import i18n from './i18n-test'; import ThemeProvider from './modules/App/components/ThemeProvider'; -import configureStore from './store'; +import { configureStore } from './store'; import theme, { Theme } from './theme'; export const history = createMemoryHistory();