From fc6496cee8c8204492bf76df5452e8005df2293f Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Sat, 10 Jun 2023 14:34:13 -0500 Subject: [PATCH 1/4] use EditableInput in Toolbar --- client/constants.js | 2 - client/modules/IDE/actions/project.js | 12 --- .../modules/IDE/components/EditableInput.jsx | 36 ++++++-- client/modules/IDE/components/Toolbar.jsx | 87 +++---------------- .../IDE/components/Toolbar.unit.test.jsx | 16 ++-- client/modules/IDE/reducers/project.js | 4 - client/styles/components/_editable-input.scss | 8 ++ client/styles/components/_toolbar.scss | 39 ++------- 8 files changed, 64 insertions(+), 140 deletions(-) diff --git a/client/constants.js b/client/constants.js index ec0e4107ac..565d716fa6 100644 --- a/client/constants.js +++ b/client/constants.js @@ -30,8 +30,6 @@ export const PROJECT_SAVE_SUCCESS = 'PROJECT_SAVE_SUCCESS'; export const PROJECT_SAVE_FAIL = 'PROJECT_SAVE_FAIL'; export const NEW_PROJECT = 'NEW_PROJECT'; export const RESET_PROJECT = 'RESET_PROJECT'; -export const SHOW_EDIT_PROJECT_NAME = 'SHOW_EDIT_PROJECT_NAME'; -export const HIDE_EDIT_PROJECT_NAME = 'HIDE_EDIT_PROJECT_NAME'; export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECTS = 'SET_PROJECTS'; diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 9a528a34f6..3709d07a2e 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -351,18 +351,6 @@ export function cloneProject(project) { }; } -export function showEditProjectName() { - return { - type: ActionTypes.SHOW_EDIT_PROJECT_NAME - }; -} - -export function hideEditProjectName() { - return { - type: ActionTypes.HIDE_EDIT_PROJECT_NAME - }; -} - export function setProjectSavedTime(updatedAt) { return { type: ActionTypes.SET_PROJECT_SAVED_TIME, diff --git a/client/modules/IDE/components/EditableInput.jsx b/client/modules/IDE/components/EditableInput.jsx index 871c6470a5..405cc61920 100644 --- a/client/modules/IDE/components/EditableInput.jsx +++ b/client/modules/IDE/components/EditableInput.jsx @@ -11,7 +11,9 @@ function EditableInput({ emptyPlaceholder, InputComponent, inputProps, - onChange + onChange, + disabled, + 'aria-label': ariaLabel }) { const [isEditing, setIsEditing] = React.useState(false); const [currentValue, setCurrentValue] = React.useState(value || ''); @@ -19,12 +21,14 @@ function EditableInput({ const hasValue = currentValue !== ''; const classes = `editable-input editable-input--${ isEditing ? 'is-editing' : 'is-not-editing' - } editable-input--${hasValue ? 'has-value' : 'has-placeholder'}`; - const inputRef = React.createRef(); + } editable-input--${hasValue ? 'has-value' : 'has-placeholder'} ${ + disabled ? 'editable-input--disabled' : '' + }`; + const inputRef = React.useRef(); const { t } = useTranslation(); React.useEffect(() => { if (isEditing) { - inputRef.current.focus(); + inputRef.current?.focus(); } }, [isEditing]); @@ -32,6 +36,11 @@ function EditableInput({ setIsEditing(true); } + function cancelEditing() { + setIsEditing(false); + setCurrentValue(value); + } + function doneEditing() { setIsEditing(false); @@ -51,6 +60,8 @@ function EditableInput({ function checkForKeyAction(event) { if (event.key === 'Enter') { doneEditing(); + } else if (event.key === 'Escape' || event.key === 'Esc') { + cancelEditing(); } } @@ -59,7 +70,11 @@ function EditableInput({ - { - this.projectNameInput = element; + inputProps={{ + maxLength: 128, + 'aria-label': this.props.t('Toolbar.NewSketchNameARIA') }} - onBlur={this.handleProjectNameSave} - onKeyPress={this.handleKeyPress} + validate={(text) => text.trim().length > 0} + onChange={this.handleProjectNameSave} /> {(() => { if (this.props.owner) { @@ -205,11 +149,8 @@ Toolbar.propTypes = { }), project: PropTypes.shape({ name: PropTypes.string.isRequired, - isEditingName: PropTypes.bool, id: PropTypes.string }).isRequired, - showEditProjectName: PropTypes.func.isRequired, - hideEditProjectName: PropTypes.func.isRequired, infiniteLoop: PropTypes.bool.isRequired, autorefresh: PropTypes.bool.isRequired, setAutorefresh: PropTypes.func.isRequired, diff --git a/client/modules/IDE/components/Toolbar.unit.test.jsx b/client/modules/IDE/components/Toolbar.unit.test.jsx index d5e83dd69d..8ce45aaaaf 100644 --- a/client/modules/IDE/components/Toolbar.unit.test.jsx +++ b/client/modules/IDE/components/Toolbar.unit.test.jsx @@ -31,7 +31,6 @@ const renderComponent = (extraProps = {}) => { }, project: { name: 'testname', - isEditingName: false, id: 'id' }, t: jest.fn() @@ -46,28 +45,31 @@ const renderComponent = (extraProps = {}) => { describe('', () => { it('sketch owner can switch to sketch name editing mode', async () => { - const props = renderComponent(); + renderComponent(); const sketchName = screen.getByLabelText('Edit sketch name'); fireEvent.click(sketchName); - await waitFor(() => expect(props.showEditProjectName).toHaveBeenCalled()); + await waitFor(() => { + expect(screen.getByLabelText('New sketch name')).toHaveFocus(); + expect(screen.getByLabelText('New sketch name')).toBeEnabled(); + }); }); it("non-owner can't switch to sketch editing mode", async () => { - const props = renderComponent({ currentUser: 'not-me' }); + renderComponent({ currentUser: 'not-me' }); const sketchName = screen.getByLabelText('Edit sketch name'); fireEvent.click(sketchName); expect(sketchName).toBeDisabled(); await waitFor(() => - expect(props.showEditProjectName).not.toHaveBeenCalled() + expect(screen.getByLabelText('New sketch name')).toBeDisabled() ); }); it('sketch owner can change name', async () => { - const props = renderComponent({ project: { isEditingName: true } }); + const props = renderComponent(); const sketchNameInput = screen.getByLabelText('New sketch name'); fireEvent.change(sketchNameInput, { @@ -82,7 +84,7 @@ describe('', () => { }); it("sketch owner can't change to empty name", async () => { - const props = renderComponent({ project: { isEditingName: true } }); + const props = renderComponent(); const sketchNameInput = screen.getByLabelText('New sketch name'); fireEvent.change(sketchNameInput, { target: { value: '' } }); diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index df0da82411..89a03529e6 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -37,10 +37,6 @@ const project = (state, action) => { }; case ActionTypes.RESET_PROJECT: return initialState(); - case ActionTypes.SHOW_EDIT_PROJECT_NAME: - return Object.assign({}, state, { isEditingName: true }); - case ActionTypes.HIDE_EDIT_PROJECT_NAME: - return Object.assign({}, state, { isEditingName: false }); case ActionTypes.SET_PROJECT_SAVED_TIME: return Object.assign({}, state, { updatedAt: action.value }); case ActionTypes.START_SAVING_PROJECT: diff --git a/client/styles/components/_editable-input.scss b/client/styles/components/_editable-input.scss index f750af0d8a..07e157b613 100644 --- a/client/styles/components/_editable-input.scss +++ b/client/styles/components/_editable-input.scss @@ -6,6 +6,7 @@ button.editable-input__label { display: flex; + align-items: center; @include themify() { color: getThemifyVariable('inactive-text-color'); @@ -35,6 +36,13 @@ button.editable-input__label { height: 1.5rem; } +.editable-input--disabled { + pointer-events: none; + .editable-input__icon { + display: none; + } +} + .editable-input--is-not-editing .editable-input__input, .editable-input--is-editing .editable-input__label { display: none; diff --git a/client/styles/components/_toolbar.scss b/client/styles/components/_toolbar.scss index cd74ec8f6f..e4b9b86742 100644 --- a/client/styles/components/_toolbar.scss +++ b/client/styles/components/_toolbar.scss @@ -98,40 +98,23 @@ } .toolbar__project-name-container { - @include themify() { - border-color: getThemifyVariable('inactive-text-color'); - } margin-left: #{10 / $base-font-size}rem; padding-left: #{10 / $base-font-size}rem; display: flex; align-items: center; } -.toolbar__project-name { +.toolbar .editable-input__label { @include themify() { color: getThemifyVariable('secondary-text-color'); - &:hover { - color: getThemifyVariable('logo-color'); - & .toolbar__edit-name-button path { - fill: getThemifyVariable('logo-color'); - } + & path { + fill: getThemifyVariable('secondary-text-color'); } } - cursor: pointer; - display: flex; - align-items: center; - - .toolbar__project-name-container--editing & { - display: none; - } } -.toolbar__project-name-input { - display: none; - border: 0px; - .toolbar__project-name-container--editing & { - display: block; - } +.toolbar .editable-input__input { + border: 0; } .toolbar__project-owner { @@ -160,15 +143,3 @@ color:getThemifyVariable('logo-color'); } } - -.toolbar__edit-name-button { - display: inline-block; - vertical-align: top; - width: #{18 / $base-font-size}rem; - height: #{18 / $base-font-size}rem; - @include themify() { - & path { - fill: getThemifyVariable('secondary-text-color'); - } - } -} From c3a5176c439466dc62a188634b08540de98a03d0 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Sat, 10 Jun 2023 19:00:17 -0500 Subject: [PATCH 2/4] separate individual elements of the Toolbar --- client/modules/IDE/components/Toolbar.jsx | 357 +++++++++--------- .../IDE/components/Toolbar.unit.test.jsx | 89 ++--- client/modules/IDE/selectors/project.js | 1 + client/test-utils.js | 14 +- 4 files changed, 240 insertions(+), 221 deletions(-) diff --git a/client/modules/IDE/components/Toolbar.jsx b/client/modules/IDE/components/Toolbar.jsx index be941b2608..0303780ee3 100644 --- a/client/modules/IDE/components/Toolbar.jsx +++ b/client/modules/IDE/components/Toolbar.jsx @@ -1,191 +1,206 @@ +import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router'; -import classNames from 'classnames'; -import { withTranslation } from 'react-i18next'; -import * as IDEActions from '../actions/ide'; -import * as preferenceActions from '../actions/preferences'; -import * as projectActions from '../actions/project'; import PlayIcon from '../../../images/play.svg'; -import StopIcon from '../../../images/stop.svg'; import PreferencesIcon from '../../../images/preferences.svg'; +import StopIcon from '../../../images/stop.svg'; +import { + openPreferences, + startAccessibleSketch, + startSketch, + stopSketch +} from '../actions/ide'; +import { + setAutorefresh, + setGridOutput, + setTextOutput +} from '../actions/preferences'; +import { saveProject, setProjectName } from '../actions/project'; +import { + selectProjectId, + selectProjectName, + selectProjectOwner +} from '../selectors/project'; +import { selectCanEditSketch } from '../selectors/users'; import EditableInput from './EditableInput'; -class Toolbar extends React.Component { - constructor(props) { - super(props); - this.handleProjectNameSave = this.handleProjectNameSave.bind(this); - } +export function PlayButton({ syncFileContent }) { + const { t } = useTranslation(); + const dispatch = useDispatch(); - handleProjectNameSave(value) { - const newProjectName = value.trim(); - this.props.setProjectName(newProjectName); - if (this.props.project.id) { - this.props.saveProject(); - } - } - - canEditProjectName() { - return ( - (this.props.owner && - this.props.owner.username && - this.props.owner.username === this.props.currentUser) || - !this.props.owner || - !this.props.owner.username - ); - } - - render() { - const playButtonClass = classNames({ - 'toolbar__play-button': true, - 'toolbar__play-button--selected': this.props.isPlaying - }); - const stopButtonClass = classNames({ - 'toolbar__stop-button': true, - 'toolbar__stop-button--selected': !this.props.isPlaying - }); - const preferencesButtonClass = classNames({ - 'toolbar__preferences-button': true, - 'toolbar__preferences-button--selected': this.props.preferencesIsVisible - }); - - const canEditProjectName = this.canEditProjectName(); - - return ( -
- - - -
- { - this.props.setAutorefresh(event.target.checked); - }} - /> - -
-
- text.trim().length > 0} - onChange={this.handleProjectNameSave} - /> - {(() => { - if (this.props.owner) { - return ( -

- {this.props.t('Toolbar.By')}{' '} - - {this.props.owner.username} - -

- ); - } - return null; - })()} -
- -
- ); - } + const isPlaying = useSelector((state) => state.ide.isPlaying); + const infiniteLoop = useSelector((state) => state.ide.infiniteLoop); + + return ( + <> + + + + ); } -Toolbar.propTypes = { - isPlaying: PropTypes.bool.isRequired, - preferencesIsVisible: PropTypes.bool.isRequired, - stopSketch: PropTypes.func.isRequired, - setProjectName: PropTypes.func.isRequired, - openPreferences: PropTypes.func.isRequired, - owner: PropTypes.shape({ - username: PropTypes.string - }), - project: PropTypes.shape({ - name: PropTypes.string.isRequired, - id: PropTypes.string - }).isRequired, - infiniteLoop: PropTypes.bool.isRequired, - autorefresh: PropTypes.bool.isRequired, - setAutorefresh: PropTypes.func.isRequired, - setTextOutput: PropTypes.func.isRequired, - setGridOutput: PropTypes.func.isRequired, - startSketch: PropTypes.func.isRequired, - startAccessibleSketch: PropTypes.func.isRequired, - saveProject: PropTypes.func.isRequired, - currentUser: PropTypes.string, - t: PropTypes.func.isRequired, +PlayButton.propTypes = { syncFileContent: PropTypes.func.isRequired }; -Toolbar.defaultProps = { - owner: undefined, - currentUser: undefined -}; +export function StopButton() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const isPlaying = useSelector((state) => state.ide.isPlaying); + + return ( + + ); +} + +export function AutoRefreshCheckbox() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const autorefresh = useSelector((state) => state.preferences.autorefresh); + + return ( +
+ { + dispatch(setAutorefresh(event.target.checked)); + }} + /> + +
+ ); +} + +export function EditableProjectName() { + const { t } = useTranslation(); + const dispatch = useDispatch(); -function mapStateToProps(state) { - return { - autorefresh: state.preferences.autorefresh, - currentUser: state.user.username, - infiniteLoop: state.ide.infiniteLoop, - isPlaying: state.ide.isPlaying, - owner: state.project.owner, - preferencesIsVisible: state.ide.preferencesIsVisible, - project: state.project + const projectId = useSelector(selectProjectId); + const projectName = useSelector(selectProjectName); + const canEditProjectName = useSelector(selectCanEditSketch); + + const handleProjectNameSave = (value) => { + const newProjectName = value.trim(); + dispatch(setProjectName(newProjectName)); + if (projectId) { + dispatch(saveProject()); + } }; + + return ( + text.trim().length > 0} + onChange={handleProjectNameSave} + /> + ); +} + +export function ProjectOwner() { + const { t } = useTranslation(); + + const owner = useSelector(selectProjectOwner); + + if (!owner) return null; + + return ( +

+ {t('Toolbar.By')}{' '} + {owner.username} +

+ ); } -const mapDispatchToProps = { - ...IDEActions, - ...preferenceActions, - ...projectActions +export function PreferencesButton() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const preferencesIsVisible = useSelector( + (state) => state.ide.preferencesIsVisible + ); + + return ( + + ); +} + +function Toolbar({ syncFileContent }) { + return ( +
+ + + +
+ + +
+ +
+ ); +} + +Toolbar.propTypes = { + syncFileContent: PropTypes.func.isRequired }; -export const ToolbarComponent = withTranslation()(Toolbar); -export default connect(mapStateToProps, mapDispatchToProps)(ToolbarComponent); +export default Toolbar; diff --git a/client/modules/IDE/components/Toolbar.unit.test.jsx b/client/modules/IDE/components/Toolbar.unit.test.jsx index 8ce45aaaaf..c5f1b01fae 100644 --- a/client/modules/IDE/components/Toolbar.unit.test.jsx +++ b/client/modules/IDE/components/Toolbar.unit.test.jsx @@ -1,46 +1,51 @@ +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; import React from 'react'; import lodash from 'lodash'; -import { fireEvent, render, screen, waitFor } from '../../../test-utils'; -import { ToolbarComponent } from './Toolbar'; +import { fireEvent, reduxRender, screen, waitFor } from '../../../test-utils'; +import { selectProjectName } from '../selectors/project'; +import ToolbarComponent from './Toolbar'; -const renderComponent = (extraProps = {}) => { - const props = lodash.merge( +const server = setupServer( + rest.put(`/projects/id`, (req, res, ctx) => res(ctx.json(req.body))) +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +const renderComponent = (extraState = {}) => { + const initialState = lodash.merge( { - isPlaying: false, - preferencesIsVisible: false, - stopSketch: jest.fn(), - setProjectName: jest.fn(), - openPreferences: jest.fn(), - showEditProjectName: jest.fn(), - hideEditProjectName: jest.fn(), - infiniteLoop: false, - autorefresh: false, - setAutorefresh: jest.fn(), - setTextOutput: jest.fn(), - setGridOutput: jest.fn(), - startSketch: jest.fn(), - startAccessibleSketch: jest.fn(), - saveProject: jest.fn(), - syncFileContent: jest.fn(), - currentUser: 'me', - originalProjectName: 'testname', - - owner: { - username: 'me' + ide: { + isPlaying: false + }, + user: { + authenticated: true, + username: 'me', + id: 'userId' }, project: { name: 'testname', - id: 'id' - }, - t: jest.fn() + id: 'id', + owner: { + username: 'me', + id: 'userId' + } + } }, - extraProps + extraState ); - render(); + const props = { + syncFileContent: jest.fn() + }; - return props; + return { + ...props, + ...reduxRender(, { initialState }) + }; }; describe('', () => { @@ -57,7 +62,7 @@ describe('', () => { }); it("non-owner can't switch to sketch editing mode", async () => { - renderComponent({ currentUser: 'not-me' }); + renderComponent({ user: { username: 'not-me', id: 'not-me' } }); const sketchName = screen.getByLabelText('Edit sketch name'); fireEvent.click(sketchName); @@ -69,7 +74,7 @@ describe('', () => { }); it('sketch owner can change name', async () => { - const props = renderComponent(); + const { store } = renderComponent(); const sketchNameInput = screen.getByLabelText('New sketch name'); fireEvent.change(sketchNameInput, { @@ -78,38 +83,38 @@ describe('', () => { fireEvent.blur(sketchNameInput); await waitFor(() => - expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name') + expect(selectProjectName(store.getState())).toBe('my new sketch name') ); - await waitFor(() => expect(props.saveProject).toHaveBeenCalled()); }); it("sketch owner can't change to empty name", async () => { - const props = renderComponent(); + const { store } = renderComponent(); const sketchNameInput = screen.getByLabelText('New sketch name'); fireEvent.change(sketchNameInput, { target: { value: '' } }); fireEvent.blur(sketchNameInput); - await waitFor(() => expect(props.setProjectName).not.toHaveBeenCalled()); - await waitFor(() => expect(props.saveProject).not.toHaveBeenCalled()); + await waitFor(() => + expect(selectProjectName(store.getState())).toBe('testname') + ); }); it('sketch is stopped when stop button is clicked', async () => { - const props = renderComponent({ isPlaying: true }); + const { store } = renderComponent({ ide: { isPlaying: true } }); const stopButton = screen.getByLabelText('Stop sketch'); fireEvent.click(stopButton); - await waitFor(() => expect(props.stopSketch).toHaveBeenCalled()); + await waitFor(() => expect(store.getState().ide.isPlaying).toBe(false)); }); it('sketch is started when play button is clicked', async () => { - const props = renderComponent(); + const { store } = renderComponent(); const playButton = screen.getByLabelText('Play only visual sketch'); fireEvent.click(playButton); - await waitFor(() => expect(props.startSketch).toHaveBeenCalled()); + await waitFor(() => expect(store.getState().ide.isPlaying).toBe(true)); }); it('sketch content is synched when play button is clicked', async () => { diff --git a/client/modules/IDE/selectors/project.js b/client/modules/IDE/selectors/project.js index 26197f3b00..bb536e3242 100644 --- a/client/modules/IDE/selectors/project.js +++ b/client/modules/IDE/selectors/project.js @@ -2,6 +2,7 @@ import { createSelector } from 'reselect'; export const selectProjectOwner = (state) => state.project.owner; export const selectProjectId = (state) => state.project.id; +export const selectProjectName = (state) => state.project.name; export const selectSketchPath = createSelector( selectProjectOwner, diff --git a/client/test-utils.js b/client/test-utils.js index ffabcb0df2..b1f452769e 100644 --- a/client/test-utils.js +++ b/client/test-utils.js @@ -14,15 +14,14 @@ import { render } from '@testing-library/react'; import React from 'react'; import PropTypes from 'prop-types'; -import { createStore } from 'redux'; import { Provider } from 'react-redux'; import { I18nextProvider } from 'react-i18next'; import { ThemeProvider as StyledThemeProvider } from 'styled-components'; import i18n from './i18n-test'; -import rootReducer from './reducers'; import ThemeProvider from './modules/App/components/ThemeProvider'; +import configureStore from './store'; import theme, { Theme } from './theme'; // re-export everything @@ -42,11 +41,7 @@ Providers.propTypes = { function reduxRender( ui, - { - initialState, - store = createStore(rootReducer, initialState), - ...renderOptions - } = {} + { initialState, store = configureStore(initialState), ...renderOptions } = {} ) { function Wrapper({ children }) { return ( @@ -62,7 +57,10 @@ function reduxRender( children: PropTypes.element.isRequired }; - return render(ui, { wrapper: Wrapper, ...renderOptions }); + return { + store, + ...render(ui, { wrapper: Wrapper, ...renderOptions }) + }; } const customRender = (ui, options) => From 776fe2a10d9d17f2978b50ad9e1bf52e6f03a9c4 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Wed, 14 Jun 2023 17:48:54 -0500 Subject: [PATCH 3/4] Move to separate files. --- client/modules/IDE/components/Toolbar.jsx | 206 ------------------ .../Toolbar/AutoRefreshCheckbox.jsx | 28 +++ .../Toolbar/EditableProjectName.jsx | 38 ++++ .../IDE/components/Toolbar/PlayButton.jsx | 52 +++++ .../components/Toolbar/PreferencesButton.jsx | 28 +++ .../IDE/components/Toolbar/ProjectOwner.jsx | 20 ++ .../IDE/components/Toolbar/StopButton.jsx | 26 +++ .../IDE/components/Toolbar/Toolbar.jsx | 29 +++ .../{ => Toolbar}/Toolbar.unit.test.jsx | 9 +- .../modules/IDE/components/Toolbar/index.js | 1 + 10 files changed, 229 insertions(+), 208 deletions(-) delete mode 100644 client/modules/IDE/components/Toolbar.jsx create mode 100644 client/modules/IDE/components/Toolbar/AutoRefreshCheckbox.jsx create mode 100644 client/modules/IDE/components/Toolbar/EditableProjectName.jsx create mode 100644 client/modules/IDE/components/Toolbar/PlayButton.jsx create mode 100644 client/modules/IDE/components/Toolbar/PreferencesButton.jsx create mode 100644 client/modules/IDE/components/Toolbar/ProjectOwner.jsx create mode 100644 client/modules/IDE/components/Toolbar/StopButton.jsx create mode 100644 client/modules/IDE/components/Toolbar/Toolbar.jsx rename client/modules/IDE/components/{ => Toolbar}/Toolbar.unit.test.jsx (95%) create mode 100644 client/modules/IDE/components/Toolbar/index.js diff --git a/client/modules/IDE/components/Toolbar.jsx b/client/modules/IDE/components/Toolbar.jsx deleted file mode 100644 index 0303780ee3..0000000000 --- a/client/modules/IDE/components/Toolbar.jsx +++ /dev/null @@ -1,206 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; -import { Link } from 'react-router'; - -import PlayIcon from '../../../images/play.svg'; -import PreferencesIcon from '../../../images/preferences.svg'; -import StopIcon from '../../../images/stop.svg'; -import { - openPreferences, - startAccessibleSketch, - startSketch, - stopSketch -} from '../actions/ide'; -import { - setAutorefresh, - setGridOutput, - setTextOutput -} from '../actions/preferences'; -import { saveProject, setProjectName } from '../actions/project'; -import { - selectProjectId, - selectProjectName, - selectProjectOwner -} from '../selectors/project'; -import { selectCanEditSketch } from '../selectors/users'; -import EditableInput from './EditableInput'; - -export function PlayButton({ syncFileContent }) { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const isPlaying = useSelector((state) => state.ide.isPlaying); - const infiniteLoop = useSelector((state) => state.ide.infiniteLoop); - - return ( - <> - - - - ); -} - -PlayButton.propTypes = { - syncFileContent: PropTypes.func.isRequired -}; - -export function StopButton() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const isPlaying = useSelector((state) => state.ide.isPlaying); - - return ( - - ); -} - -export function AutoRefreshCheckbox() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const autorefresh = useSelector((state) => state.preferences.autorefresh); - - return ( -
- { - dispatch(setAutorefresh(event.target.checked)); - }} - /> - -
- ); -} - -export function EditableProjectName() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const projectId = useSelector(selectProjectId); - const projectName = useSelector(selectProjectName); - const canEditProjectName = useSelector(selectCanEditSketch); - - const handleProjectNameSave = (value) => { - const newProjectName = value.trim(); - dispatch(setProjectName(newProjectName)); - if (projectId) { - dispatch(saveProject()); - } - }; - - return ( - text.trim().length > 0} - onChange={handleProjectNameSave} - /> - ); -} - -export function ProjectOwner() { - const { t } = useTranslation(); - - const owner = useSelector(selectProjectOwner); - - if (!owner) return null; - - return ( -

- {t('Toolbar.By')}{' '} - {owner.username} -

- ); -} - -export function PreferencesButton() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const preferencesIsVisible = useSelector( - (state) => state.ide.preferencesIsVisible - ); - - return ( - - ); -} - -function Toolbar({ syncFileContent }) { - return ( -
- - - -
- - -
- -
- ); -} - -Toolbar.propTypes = { - syncFileContent: PropTypes.func.isRequired -}; - -export default Toolbar; diff --git a/client/modules/IDE/components/Toolbar/AutoRefreshCheckbox.jsx b/client/modules/IDE/components/Toolbar/AutoRefreshCheckbox.jsx new file mode 100644 index 0000000000..008e1dd62c --- /dev/null +++ b/client/modules/IDE/components/Toolbar/AutoRefreshCheckbox.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { setAutorefresh } from '../../actions/preferences'; + +export default function AutoRefreshCheckbox() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const autorefresh = useSelector((state) => state.preferences.autorefresh); + + return ( +
+ { + dispatch(setAutorefresh(event.target.checked)); + }} + /> + +
+ ); +} diff --git a/client/modules/IDE/components/Toolbar/EditableProjectName.jsx b/client/modules/IDE/components/Toolbar/EditableProjectName.jsx new file mode 100644 index 0000000000..668ed874aa --- /dev/null +++ b/client/modules/IDE/components/Toolbar/EditableProjectName.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { saveProject, setProjectName } from '../../actions/project'; +import { selectProjectId, selectProjectName } from '../../selectors/project'; +import { selectCanEditSketch } from '../../selectors/users'; +import EditableInput from '../EditableInput'; + +export default function EditableProjectName() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const projectId = useSelector(selectProjectId); + const projectName = useSelector(selectProjectName); + const canEditProjectName = useSelector(selectCanEditSketch); + + const handleProjectNameSave = (value) => { + const newProjectName = value.trim(); + dispatch(setProjectName(newProjectName)); + if (projectId) { + dispatch(saveProject()); + } + }; + + return ( + text.trim().length > 0} + onChange={handleProjectNameSave} + /> + ); +} diff --git a/client/modules/IDE/components/Toolbar/PlayButton.jsx b/client/modules/IDE/components/Toolbar/PlayButton.jsx new file mode 100644 index 0000000000..1b722cdcb6 --- /dev/null +++ b/client/modules/IDE/components/Toolbar/PlayButton.jsx @@ -0,0 +1,52 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import PlayIcon from '../../../../images/play.svg'; +import { startAccessibleSketch, startSketch } from '../../actions/ide'; +import { setGridOutput, setTextOutput } from '../../actions/preferences'; + +export default function PlayButton({ syncFileContent }) { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const isPlaying = useSelector((state) => state.ide.isPlaying); + const infiniteLoop = useSelector((state) => state.ide.infiniteLoop); + + return ( + <> + + + + ); +} + +PlayButton.propTypes = { + syncFileContent: PropTypes.func.isRequired +}; diff --git a/client/modules/IDE/components/Toolbar/PreferencesButton.jsx b/client/modules/IDE/components/Toolbar/PreferencesButton.jsx new file mode 100644 index 0000000000..8f38ed6bda --- /dev/null +++ b/client/modules/IDE/components/Toolbar/PreferencesButton.jsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import PreferencesIcon from '../../../../images/preferences.svg'; +import { openPreferences } from '../../actions/ide'; + +export default function PreferencesButton() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const preferencesIsVisible = useSelector( + (state) => state.ide.preferencesIsVisible + ); + + return ( + + ); +} diff --git a/client/modules/IDE/components/Toolbar/ProjectOwner.jsx b/client/modules/IDE/components/Toolbar/ProjectOwner.jsx new file mode 100644 index 0000000000..16da0ec0fe --- /dev/null +++ b/client/modules/IDE/components/Toolbar/ProjectOwner.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { Link } from 'react-router'; +import { selectProjectOwner } from '../../selectors/project'; + +export default function ProjectOwner() { + const { t } = useTranslation(); + + const owner = useSelector(selectProjectOwner); + + if (!owner) return null; + + return ( +

+ {t('Toolbar.By')}{' '} + {owner.username} +

+ ); +} diff --git a/client/modules/IDE/components/Toolbar/StopButton.jsx b/client/modules/IDE/components/Toolbar/StopButton.jsx new file mode 100644 index 0000000000..98d8d8ff27 --- /dev/null +++ b/client/modules/IDE/components/Toolbar/StopButton.jsx @@ -0,0 +1,26 @@ +import classNames from 'classnames'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import StopIcon from '../../../../images/stop.svg'; +import { stopSketch } from '../../actions/ide'; + +export default function StopButton() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const isPlaying = useSelector((state) => state.ide.isPlaying); + + return ( + + ); +} diff --git a/client/modules/IDE/components/Toolbar/Toolbar.jsx b/client/modules/IDE/components/Toolbar/Toolbar.jsx new file mode 100644 index 0000000000..54b87f0511 --- /dev/null +++ b/client/modules/IDE/components/Toolbar/Toolbar.jsx @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import AutoRefreshCheckbox from './AutoRefreshCheckbox'; +import EditableProjectName from './EditableProjectName'; +import PlayButton from './PlayButton'; +import PreferencesButton from './PreferencesButton'; +import ProjectOwner from './ProjectOwner'; +import StopButton from './StopButton'; + +function Toolbar({ syncFileContent }) { + return ( +
+ + + +
+ + +
+ +
+ ); +} + +Toolbar.propTypes = { + syncFileContent: PropTypes.func.isRequired +}; + +export default Toolbar; diff --git a/client/modules/IDE/components/Toolbar.unit.test.jsx b/client/modules/IDE/components/Toolbar/Toolbar.unit.test.jsx similarity index 95% rename from client/modules/IDE/components/Toolbar.unit.test.jsx rename to client/modules/IDE/components/Toolbar/Toolbar.unit.test.jsx index c5f1b01fae..38ffd8e1e7 100644 --- a/client/modules/IDE/components/Toolbar.unit.test.jsx +++ b/client/modules/IDE/components/Toolbar/Toolbar.unit.test.jsx @@ -3,8 +3,13 @@ import { setupServer } from 'msw/node'; import React from 'react'; import lodash from 'lodash'; -import { fireEvent, reduxRender, screen, waitFor } from '../../../test-utils'; -import { selectProjectName } from '../selectors/project'; +import { + fireEvent, + reduxRender, + screen, + waitFor +} from '../../../../test-utils'; +import { selectProjectName } from '../../selectors/project'; import ToolbarComponent from './Toolbar'; const server = setupServer( diff --git a/client/modules/IDE/components/Toolbar/index.js b/client/modules/IDE/components/Toolbar/index.js new file mode 100644 index 0000000000..1c8b22823b --- /dev/null +++ b/client/modules/IDE/components/Toolbar/index.js @@ -0,0 +1 @@ +export { default } from './Toolbar'; From 2c8491c0db00f4da68b5f390e4fc2aaef59b6482 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Sat, 22 Jul 2023 19:06:25 -0500 Subject: [PATCH 4/4] Revert "Move to separate files." This reverts commit 776fe2a10d9d17f2978b50ad9e1bf52e6f03a9c4. --- client/modules/IDE/components/Toolbar.jsx | 206 ++++++++++++++++++ .../{Toolbar => }/Toolbar.unit.test.jsx | 9 +- .../Toolbar/AutoRefreshCheckbox.jsx | 28 --- .../Toolbar/EditableProjectName.jsx | 38 ---- .../IDE/components/Toolbar/PlayButton.jsx | 52 ----- .../components/Toolbar/PreferencesButton.jsx | 28 --- .../IDE/components/Toolbar/ProjectOwner.jsx | 20 -- .../IDE/components/Toolbar/StopButton.jsx | 26 --- .../IDE/components/Toolbar/Toolbar.jsx | 29 --- .../modules/IDE/components/Toolbar/index.js | 1 - 10 files changed, 208 insertions(+), 229 deletions(-) create mode 100644 client/modules/IDE/components/Toolbar.jsx rename client/modules/IDE/components/{Toolbar => }/Toolbar.unit.test.jsx (95%) delete mode 100644 client/modules/IDE/components/Toolbar/AutoRefreshCheckbox.jsx delete mode 100644 client/modules/IDE/components/Toolbar/EditableProjectName.jsx delete mode 100644 client/modules/IDE/components/Toolbar/PlayButton.jsx delete mode 100644 client/modules/IDE/components/Toolbar/PreferencesButton.jsx delete mode 100644 client/modules/IDE/components/Toolbar/ProjectOwner.jsx delete mode 100644 client/modules/IDE/components/Toolbar/StopButton.jsx delete mode 100644 client/modules/IDE/components/Toolbar/Toolbar.jsx delete mode 100644 client/modules/IDE/components/Toolbar/index.js diff --git a/client/modules/IDE/components/Toolbar.jsx b/client/modules/IDE/components/Toolbar.jsx new file mode 100644 index 0000000000..0303780ee3 --- /dev/null +++ b/client/modules/IDE/components/Toolbar.jsx @@ -0,0 +1,206 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router'; + +import PlayIcon from '../../../images/play.svg'; +import PreferencesIcon from '../../../images/preferences.svg'; +import StopIcon from '../../../images/stop.svg'; +import { + openPreferences, + startAccessibleSketch, + startSketch, + stopSketch +} from '../actions/ide'; +import { + setAutorefresh, + setGridOutput, + setTextOutput +} from '../actions/preferences'; +import { saveProject, setProjectName } from '../actions/project'; +import { + selectProjectId, + selectProjectName, + selectProjectOwner +} from '../selectors/project'; +import { selectCanEditSketch } from '../selectors/users'; +import EditableInput from './EditableInput'; + +export function PlayButton({ syncFileContent }) { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const isPlaying = useSelector((state) => state.ide.isPlaying); + const infiniteLoop = useSelector((state) => state.ide.infiniteLoop); + + return ( + <> + + + + ); +} + +PlayButton.propTypes = { + syncFileContent: PropTypes.func.isRequired +}; + +export function StopButton() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const isPlaying = useSelector((state) => state.ide.isPlaying); + + return ( + + ); +} + +export function AutoRefreshCheckbox() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const autorefresh = useSelector((state) => state.preferences.autorefresh); + + return ( +
+ { + dispatch(setAutorefresh(event.target.checked)); + }} + /> + +
+ ); +} + +export function EditableProjectName() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const projectId = useSelector(selectProjectId); + const projectName = useSelector(selectProjectName); + const canEditProjectName = useSelector(selectCanEditSketch); + + const handleProjectNameSave = (value) => { + const newProjectName = value.trim(); + dispatch(setProjectName(newProjectName)); + if (projectId) { + dispatch(saveProject()); + } + }; + + return ( + text.trim().length > 0} + onChange={handleProjectNameSave} + /> + ); +} + +export function ProjectOwner() { + const { t } = useTranslation(); + + const owner = useSelector(selectProjectOwner); + + if (!owner) return null; + + return ( +

+ {t('Toolbar.By')}{' '} + {owner.username} +

+ ); +} + +export function PreferencesButton() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const preferencesIsVisible = useSelector( + (state) => state.ide.preferencesIsVisible + ); + + return ( + + ); +} + +function Toolbar({ syncFileContent }) { + return ( +
+ + + +
+ + +
+ +
+ ); +} + +Toolbar.propTypes = { + syncFileContent: PropTypes.func.isRequired +}; + +export default Toolbar; diff --git a/client/modules/IDE/components/Toolbar/Toolbar.unit.test.jsx b/client/modules/IDE/components/Toolbar.unit.test.jsx similarity index 95% rename from client/modules/IDE/components/Toolbar/Toolbar.unit.test.jsx rename to client/modules/IDE/components/Toolbar.unit.test.jsx index 38ffd8e1e7..c5f1b01fae 100644 --- a/client/modules/IDE/components/Toolbar/Toolbar.unit.test.jsx +++ b/client/modules/IDE/components/Toolbar.unit.test.jsx @@ -3,13 +3,8 @@ import { setupServer } from 'msw/node'; import React from 'react'; import lodash from 'lodash'; -import { - fireEvent, - reduxRender, - screen, - waitFor -} from '../../../../test-utils'; -import { selectProjectName } from '../../selectors/project'; +import { fireEvent, reduxRender, screen, waitFor } from '../../../test-utils'; +import { selectProjectName } from '../selectors/project'; import ToolbarComponent from './Toolbar'; const server = setupServer( diff --git a/client/modules/IDE/components/Toolbar/AutoRefreshCheckbox.jsx b/client/modules/IDE/components/Toolbar/AutoRefreshCheckbox.jsx deleted file mode 100644 index 008e1dd62c..0000000000 --- a/client/modules/IDE/components/Toolbar/AutoRefreshCheckbox.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; -import { setAutorefresh } from '../../actions/preferences'; - -export default function AutoRefreshCheckbox() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const autorefresh = useSelector((state) => state.preferences.autorefresh); - - return ( -
- { - dispatch(setAutorefresh(event.target.checked)); - }} - /> - -
- ); -} diff --git a/client/modules/IDE/components/Toolbar/EditableProjectName.jsx b/client/modules/IDE/components/Toolbar/EditableProjectName.jsx deleted file mode 100644 index 668ed874aa..0000000000 --- a/client/modules/IDE/components/Toolbar/EditableProjectName.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; -import { saveProject, setProjectName } from '../../actions/project'; -import { selectProjectId, selectProjectName } from '../../selectors/project'; -import { selectCanEditSketch } from '../../selectors/users'; -import EditableInput from '../EditableInput'; - -export default function EditableProjectName() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const projectId = useSelector(selectProjectId); - const projectName = useSelector(selectProjectName); - const canEditProjectName = useSelector(selectCanEditSketch); - - const handleProjectNameSave = (value) => { - const newProjectName = value.trim(); - dispatch(setProjectName(newProjectName)); - if (projectId) { - dispatch(saveProject()); - } - }; - - return ( - text.trim().length > 0} - onChange={handleProjectNameSave} - /> - ); -} diff --git a/client/modules/IDE/components/Toolbar/PlayButton.jsx b/client/modules/IDE/components/Toolbar/PlayButton.jsx deleted file mode 100644 index 1b722cdcb6..0000000000 --- a/client/modules/IDE/components/Toolbar/PlayButton.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; -import PlayIcon from '../../../../images/play.svg'; -import { startAccessibleSketch, startSketch } from '../../actions/ide'; -import { setGridOutput, setTextOutput } from '../../actions/preferences'; - -export default function PlayButton({ syncFileContent }) { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const isPlaying = useSelector((state) => state.ide.isPlaying); - const infiniteLoop = useSelector((state) => state.ide.infiniteLoop); - - return ( - <> - - - - ); -} - -PlayButton.propTypes = { - syncFileContent: PropTypes.func.isRequired -}; diff --git a/client/modules/IDE/components/Toolbar/PreferencesButton.jsx b/client/modules/IDE/components/Toolbar/PreferencesButton.jsx deleted file mode 100644 index 8f38ed6bda..0000000000 --- a/client/modules/IDE/components/Toolbar/PreferencesButton.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; -import PreferencesIcon from '../../../../images/preferences.svg'; -import { openPreferences } from '../../actions/ide'; - -export default function PreferencesButton() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const preferencesIsVisible = useSelector( - (state) => state.ide.preferencesIsVisible - ); - - return ( - - ); -} diff --git a/client/modules/IDE/components/Toolbar/ProjectOwner.jsx b/client/modules/IDE/components/Toolbar/ProjectOwner.jsx deleted file mode 100644 index 16da0ec0fe..0000000000 --- a/client/modules/IDE/components/Toolbar/ProjectOwner.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -import { Link } from 'react-router'; -import { selectProjectOwner } from '../../selectors/project'; - -export default function ProjectOwner() { - const { t } = useTranslation(); - - const owner = useSelector(selectProjectOwner); - - if (!owner) return null; - - return ( -

- {t('Toolbar.By')}{' '} - {owner.username} -

- ); -} diff --git a/client/modules/IDE/components/Toolbar/StopButton.jsx b/client/modules/IDE/components/Toolbar/StopButton.jsx deleted file mode 100644 index 98d8d8ff27..0000000000 --- a/client/modules/IDE/components/Toolbar/StopButton.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; -import StopIcon from '../../../../images/stop.svg'; -import { stopSketch } from '../../actions/ide'; - -export default function StopButton() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - - const isPlaying = useSelector((state) => state.ide.isPlaying); - - return ( - - ); -} diff --git a/client/modules/IDE/components/Toolbar/Toolbar.jsx b/client/modules/IDE/components/Toolbar/Toolbar.jsx deleted file mode 100644 index 54b87f0511..0000000000 --- a/client/modules/IDE/components/Toolbar/Toolbar.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import AutoRefreshCheckbox from './AutoRefreshCheckbox'; -import EditableProjectName from './EditableProjectName'; -import PlayButton from './PlayButton'; -import PreferencesButton from './PreferencesButton'; -import ProjectOwner from './ProjectOwner'; -import StopButton from './StopButton'; - -function Toolbar({ syncFileContent }) { - return ( -
- - - -
- - -
- -
- ); -} - -Toolbar.propTypes = { - syncFileContent: PropTypes.func.isRequired -}; - -export default Toolbar; diff --git a/client/modules/IDE/components/Toolbar/index.js b/client/modules/IDE/components/Toolbar/index.js deleted file mode 100644 index 1c8b22823b..0000000000 --- a/client/modules/IDE/components/Toolbar/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Toolbar';