From fc6496cee8c8204492bf76df5452e8005df2293f Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Sat, 10 Jun 2023 14:34:13 -0500 Subject: [PATCH 01/31] 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 62c710bd3a1054ef4cea85f3319182e4f92bb799 Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Sun, 6 Aug 2023 23:02:49 +0530 Subject: [PATCH 02/31] initial MobileNav component --- .../IDE/components/Header/MobileNav.jsx | 380 ++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 client/modules/IDE/components/Header/MobileNav.jsx diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx new file mode 100644 index 0000000000..6ec618646d --- /dev/null +++ b/client/modules/IDE/components/Header/MobileNav.jsx @@ -0,0 +1,380 @@ +import React, { useContext, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router'; +import PropTypes from 'prop-types'; +import { prop, remSize } from '../../../../theme'; +import AstriskIcon from '../../../../images/p5-asterisk.svg'; +import IconButton from '../../../../components/mobile/IconButton'; +import { AccountIcon, EditorIcon, MoreIcon } from '../../../../common/icons'; +import { newFile, newFolder, openPreferences } from '../../actions/ide'; +import { logoutUser } from '../../../User/actions'; +import { useSketchActions } from '../../hooks'; +import { CmControllerContext } from '../../pages/IDEView'; +import { selectSketchPath } from '../../selectors/project'; + +const Nav = styled.div` + background: ${prop('MobilePanel.default.background')}; + color: ${prop('primaryTextColor')}; + padding: ${remSize(8)} 0; + gap: ${remSize(14)}; + display: flex; + align-items: center; + font-size: ${remSize(10)}; + box-shadow: #00000030 0px 2px 8px 0px; + z-index: 10; +`; + +const LogoContainer = styled.div` + width: ${remSize(28)}; + aspect-ratio: 1; + margin-left: ${remSize(14)}; + display: flex; + justify-content: center; + align-items: center; + > svg { + width: 100%; + height: 100%; + > path { + fill: ${prop('accentColor')}; + stroke: ${prop('accentColor')}; + } + } +`; + +const Title = styled.div` + display: flex; + flex-direction: column; + gap: ${remSize(2)}; + + > * { + padding: 0; + margin: 0; + } + + > h5 { + font-size: ${remSize(13)}; + font-weight: normal; + } +`; + +const Options = styled.div` + margin-left: auto; + display: flex; + /* transform: translateX(${remSize(12)}); */ + svg { + fill: ${(props) => props.color}; + } + + /* Drop Down menu */ + + > div > button:focus + ul, + > div > ul > button:focus ~ div > ul { + transform: scale(1); + opacity: 1; + } + + > div { + position: relative; + ul { + ${prop('MobilePanel.default')} + position: absolute; + overflow: hidden; + z-index: 10; + right: 10px; + transform: translateX(${remSize(6)}); + width: max-content; + transform: scale(0); + opacity: 0; + transform-origin: top; + transition: opacity 100ms; + transition: transform 100ms 200ms; + display: flex; + flex-direction: column; + gap: ${remSize(2)}; + box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, + rgba(60, 64, 67, 0.15) 0px 1px 3px 1px; + min-width: ${remSize(120)}; + border-radius: ${remSize(2)}; + + /* User */ + li.user { + color: ${prop('colors.processingBlue')}; + font-size: ${remSize(16)}; + font-weight: bold; + margin: ${remSize(6)} ${remSize(12)}; + } + + > b { + margin: ${remSize(6)} ${remSize(12)}; + position: relative; + width: min-content; + &::after { + content: ''; + position: absolute; + opacity: calc(0.5); + top: 50%; + transform: translate(100%, -50%); + right: ${remSize(-6)}; + width: ${remSize(55)}; + height: 1px; + background-color: ${prop('Button.primary.default.foreground')}; + } + } + + > li { + display: flex; + width: 100%; + > button { + width: 100%; + padding: ${remSize(8)} ${remSize(15)} ${remSize(8)} ${remSize(10)}; + font-size: ${remSize(18)}; + text-align: left; + &:hover { + background-color: ${prop('Button.primary.hover.background')}; + color: ${prop('Button.primary.hover.foreground')}; + } + } + } + } + } +`; + +const MobileNav = (props) => { + const project = useSelector((state) => state.project); + const user = useSelector((state) => state.user); + const [title, setTitle] = useState(); + const navigate = useNavigate(); + + const { pathname } = useLocation(); + const editorLink = useSelector(selectSketchPath); + + function resolveTitle(path) { + switch (path) { + case '/': + return project.name; + case '/login': + return 'Login'; + case '/signup': + return 'Signup'; + case '/account': + return 'Account Settings'; + case '/p5/sketches': + case '/p5/collections': + return 'Examples'; + case `/${user.username}/assets`: + case `/${user.username}/collections`: + case `/${user.username}/sketches`: + return 'My Stuff'; + default: + return project.name; + } + } + + useEffect(() => { + setTitle(resolveTitle(pathname)); + }, [pathname, project]); + + const Logo = AstriskIcon; + return ( + + ); +}; + +const AccoutnMenu = () => { + const user = useSelector((state) => state.user); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + return ( +
+ +
    +
  • {user.username}
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ ); +}; + +const MoreMenu = () => { + // TODO: use selectRootFile selector + const rootFile = useSelector( + (state) => state.files.filter((file) => file.name === 'root')[0] + ); + const dispatch = useDispatch(); + const { t } = useTranslation(); + const { newSketch, saveSketch } = useSketchActions(); + const navigate = useNavigate(); + + const cmRef = useContext(CmControllerContext); + + return ( +
+ +
    + {t('Nav.File.Title')} +
  • + +
  • +
  • + +
  • +
  • + +
  • + {t('Nav.Edit.Title')} +
  • + +
  • +
  • + +
  • + {t('Nav.Sketch.Title')} +
  • + +
  • +
  • + +
  • + {/* TODO: Add Translations */} + Settings +
  • + +
  • +
  • + +
  • + {t('Nav.Help.Title')} +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ ); +}; + +MoreMenu.propTypes = { + cmController: PropTypes.shape({ + tidyCode: PropTypes.func, + showFind: PropTypes.func, + showReplace: PropTypes.func, + getContent: PropTypes.func + }) +}; + +MoreMenu.defaultProps = { + cmController: {} +}; + +MobileNav.propTypes = { + cmController: PropTypes.shape({ + tidyCode: PropTypes.func, + showFind: PropTypes.func, + showReplace: PropTypes.func, + getContent: PropTypes.func + }) +}; + +MobileNav.defaultProps = { + cmController: {} +}; + +export default MobileNav; From 07fd1a9fbc43b519d3f1a4852c5b8799253f9b44 Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Sun, 6 Aug 2023 23:26:07 +0530 Subject: [PATCH 03/31] account and editor icon --- client/common/icons.jsx | 4 ++++ client/images/account.svg | 3 +++ client/images/editor.svg | 4 ++++ 3 files changed, 11 insertions(+) create mode 100644 client/images/account.svg create mode 100644 client/images/editor.svg diff --git a/client/common/icons.jsx b/client/common/icons.jsx index ff526c63f4..571dec3a74 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -13,6 +13,8 @@ import DropdownArrow from '../images/down-filled-triangle.svg'; import Preferences from '../images/preferences.svg'; import Play from '../images/triangle-arrow-right.svg'; import More from '../images/more.svg'; +import Editor from '../images/editor.svg'; +import Account from '../images/account.svg'; import Code from '../images/code.svg'; import Save from '../images/save.svg'; import Terminal from '../images/terminal.svg'; @@ -83,6 +85,8 @@ export const GoogleIcon = withLabel(Google); export const PlusIcon = withLabel(Plus); export const CloseIcon = withLabel(Close); export const ExitIcon = withLabel(Exit); +export const EditorIcon = withLabel(Editor); +export const AccountIcon = withLabel(Account); export const DropdownArrowIcon = withLabel(DropdownArrow); export const PreferencesIcon = withLabel(Preferences); export const PlayIcon = withLabel(Play); diff --git a/client/images/account.svg b/client/images/account.svg new file mode 100644 index 0000000000..503028f286 --- /dev/null +++ b/client/images/account.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/images/editor.svg b/client/images/editor.svg new file mode 100644 index 0000000000..31f79b34ad --- /dev/null +++ b/client/images/editor.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 122c5f217b59dbee8b9e3f9d901b61d56f408064 Mon Sep 17 00:00:00 2001 From: dewanshDT Date: Sun, 6 Aug 2023 23:29:31 +0530 Subject: [PATCH 04/31] useHistory --- .../IDE/components/Header/MobileNav.jsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx index 6ec618646d..f22d37194f 100644 --- a/client/modules/IDE/components/Header/MobileNav.jsx +++ b/client/modules/IDE/components/Header/MobileNav.jsx @@ -2,10 +2,11 @@ import React, { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router'; +import { useLocation } from 'react-router'; import PropTypes from 'prop-types'; +import { useHistory } from 'react-router'; import { prop, remSize } from '../../../../theme'; -import AstriskIcon from '../../../../images/p5-asterisk.svg'; +import AsteriskIcon from '../../../../images/p5-asterisk.svg'; import IconButton from '../../../../components/mobile/IconButton'; import { AccountIcon, EditorIcon, MoreIcon } from '../../../../common/icons'; import { newFile, newFolder, openPreferences } from '../../actions/ide'; @@ -145,7 +146,10 @@ const MobileNav = (props) => { const project = useSelector((state) => state.project); const user = useSelector((state) => state.user); const [title, setTitle] = useState(); - const navigate = useNavigate(); + + // use the useNavigate hook in the react router v6 + const history = useHistory(); + const navigate = (url) => history.push(url); const { pathname } = useLocation(); const editorLink = useSelector(selectSketchPath); @@ -176,7 +180,7 @@ const MobileNav = (props) => { setTitle(resolveTitle(pathname)); }, [pathname, project]); - const Logo = AstriskIcon; + const Logo = AsteriskIcon; return (