diff --git a/client/modules/IDE/components/Editor/MobileEditor.jsx b/client/modules/IDE/components/Editor/MobileEditor.jsx index c3e56afd79..90f239ead7 100644 --- a/client/modules/IDE/components/Editor/MobileEditor.jsx +++ b/client/modules/IDE/components/Editor/MobileEditor.jsx @@ -33,6 +33,7 @@ export const EditorContainer = styled.div` export const EditorHolder = styled.div` min-height: 100%; + pointer-events: ${(props) => (props.readOnly ? 'none' : '')}; `; export const PreviewWrapper = styled.div` diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx index cd060d3bb5..ad9d66389f 100644 --- a/client/modules/IDE/components/Editor/index.jsx +++ b/client/modules/IDE/components/Editor/index.jsx @@ -41,7 +41,7 @@ import { CSSLint } from 'csslint'; import { HTMLHint } from 'htmlhint'; import classNames from 'classnames'; import { debounce } from 'lodash'; -import { connect } from 'react-redux'; +import { connect, useSelector } from 'react-redux'; import { bindActionCreators } from 'redux'; import MediaQuery from 'react-responsive'; import '../../../../utils/htmlmixed'; @@ -505,7 +505,14 @@ class Editor extends React.Component { const editorHolderClass = classNames({ 'editor-holder': true, 'editor-holder--hidden': - this.props.file.fileType === 'folder' || this.props.file.url + this.props.file.fileType === 'folder' || this.props.file.url, + // eslint-disable-next-line no-dupe-keys + 'editor-holder--readonly': + // Check if there is a project owner, the user has a username, + // and the project owner's username is not the same as the user's username + this.props.project.owner && this.props.user.username + ? this.props.project.owner?.username !== this.props.user.username + : '' }); return ( @@ -567,6 +574,12 @@ class Editor extends React.Component { </header> <section> <EditorHolder + readOnly={ + this.props.project.owner && + this.props.user.username && + this.props.project.owner.username !== + this.props.user.username + } ref={(element) => { this.codemirrorContainer = element; }} @@ -588,6 +601,10 @@ class Editor extends React.Component { } Editor.propTypes = { + // eslint-disable-next-line react/forbid-prop-types + user: PropTypes.object.isRequired, + // eslint-disable-next-line react/forbid-prop-types + project: PropTypes.object.isRequired, autocloseBracketsQuotes: PropTypes.bool.isRequired, autocompleteHinter: PropTypes.bool.isRequired, lineNumbers: PropTypes.bool.isRequired, diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx index 9d81cf6d22..7b6e86aeb5 100644 --- a/client/modules/IDE/components/Header/MobileNav.jsx +++ b/client/modules/IDE/components/Header/MobileNav.jsx @@ -35,6 +35,8 @@ import { setLanguage } from '../../actions/preferences'; import Overlay from '../../../App/components/Overlay'; import ProjectName from './ProjectName'; import CollectionCreate from '../../../User/components/CollectionCreate'; +import { cloneProject } from '../../actions/project'; +import EditIcon from '../../../../images/pencil.svg'; const Nav = styled(NavBar)` background: ${prop('MobilePanel.default.background')}; @@ -67,15 +69,20 @@ const LogoContainer = styled.div` const Title = styled.div` display: flex; - flex-direction: column; - gap: ${remSize(2)}; + gap: ${remSize(10)}; * { padding: 0; margin: 0; } - > h5 { + p { + margin-left: 2px; + padding: 3px 8px; + } + + h5 { + margin-top: 2px; font-size: ${remSize(13)}; font-weight: normal; } @@ -205,6 +212,8 @@ const MobileNav = () => { const user = useSelector((state) => state.user); const { t } = useTranslation(); + const theme = useSelector((state) => state.preferences.theme); + console.log(theme); const editorLink = useSelector(selectSketchPath); const pageName = useWhatPage(); @@ -228,7 +237,7 @@ const MobileNav = () => { } const title = useMemo(resolveTitle, [pageName, project.name]); - + const dispatch = useDispatch(); const Logo = AsteriskIcon; return ( <Nav> @@ -236,10 +245,35 @@ const MobileNav = () => { <Logo /> </LogoContainer> <Title> - <h1>{title === project.name ? <ProjectName /> : title}</h1> - {project?.owner && title === project.name && ( - <h5>by {project?.owner?.username}</h5> - )} + <div> + <h1>{title === project.name ? <ProjectName /> : title}</h1> + {project?.owner && title === project.name && ( + <h5>by {project?.owner?.username}</h5> + )} + </div> + + <div> + {title === project.name && + project.owner && + user.username !== project.owner.username && ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions + <p + className={`toolbar__duplicatetoedit ${ + theme === 'contrast' ? 'contrast' : 'normal' + }`} + onClick={() => dispatch(cloneProject())} + > + {t('Toolbar.DuplicateToEdit')}{' '} + <EditIcon + className={`toolbar__icon ${ + theme === 'contrast' ? 'contrast' : 'normal' + }`} + focusable="false" + aria-hidden="true" + /> + </p> + )} + </div> </Title> {/* check if the user is in login page */} {pageName === 'login' || pageName === 'signup' ? ( diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx index f61f56ed0e..048f66abe5 100644 --- a/client/modules/IDE/components/Header/Toolbar.jsx +++ b/client/modules/IDE/components/Header/Toolbar.jsx @@ -20,15 +20,18 @@ import PlayIcon from '../../../../images/play.svg'; import StopIcon from '../../../../images/stop.svg'; import PreferencesIcon from '../../../../images/preferences.svg'; import ProjectName from './ProjectName'; +import { cloneProject } from '../../actions/project'; +import EditIcon from '../../../../images/pencil.svg'; const Toolbar = (props) => { const { isPlaying, infiniteLoop, preferencesIsVisible } = useSelector( (state) => state.ide ); + const user = useSelector((state) => state.user); const project = useSelector((state) => state.project); + const theme = useSelector((state) => state.preferences.theme); const autorefresh = useSelector((state) => state.preferences.autorefresh); const dispatch = useDispatch(); - const { t } = useTranslation(); const playButtonClass = classNames({ @@ -97,20 +100,40 @@ const Toolbar = (props) => { </label> </div> <div className="toolbar__project-name-container"> - <ProjectName /> - {(() => { - if (project.owner) { - return ( - <p className="toolbar__project-project.owner"> - {t('Toolbar.By')}{' '} - <Link to={`/${project.owner.username}/sketches`}> - {project.owner.username} - </Link> - </p> - ); - } - return null; - })()} + <div className="toolbar__projectname"> + <ProjectName /> + {(() => { + if (project.owner) { + return ( + <p className="toolbar__project-project.owner"> + {t('Toolbar.By')}{' '} + <Link to={`/${project.owner.username}/sketches`}> + {project.owner.username} + </Link> + </p> + ); + } + return null; + })()} + </div> + {project.owner && user.username !== project.owner.username && ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions + <p + className={`toolbar__duplicatetoedit ${ + theme === 'contrast' ? 'contrast' : 'normal' + }`} + onClick={() => dispatch(cloneProject())} + > + {t('Toolbar.DuplicateToEdit')}{' '} + <EditIcon + className={`toolbar__icon ${ + theme === 'contrast' ? 'contrast' : 'normal' + }`} + focusable="false" + aria-hidden="true" + /> + </p> + )} </div> <button className={preferencesButtonClass} 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 45a7f72f52..b5b51550f8 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 @@ -139,10 +139,7 @@ exports[`Nav renders dashboard version for mobile 1`] = ` display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - gap: 0.16666666666666666rem; + gap: 0.8333333333333334rem; } .c2 * { @@ -150,7 +147,13 @@ exports[`Nav renders dashboard version for mobile 1`] = ` margin: 0; } -.c2 > h5 { +.c2 p { + margin-left: 2px; + padding: 3px 8px; +} + +.c2 h5 { + margin-top: 2px; font-size: 1.0833333333333333rem; font-weight: normal; } @@ -277,35 +280,38 @@ exports[`Nav renders dashboard version for mobile 1`] = ` <div class="c2" > - <h1> - <span - class="editable-input editable-input--is-not-editing editable-input--has-value " - > - <button - aria-hidden="false" - aria-label="Edit sketch name" - class="editable-input__label" + <div> + <h1> + <span + class="editable-input editable-input--is-not-editing editable-input--has-value " > - <span> - Test project name - </span> - <test-file-stub + <button + aria-hidden="false" + aria-label="Edit sketch name" + class="editable-input__label" + > + <span> + Test project name + </span> + <test-file-stub + aria-hidden="true" + classname="editable-input__icon" + focusable="false" + /> + </button> + <input aria-hidden="true" - classname="editable-input__icon" - focusable="false" + aria-label="New sketch name" + class="editable-input__input" + disabled="" + maxlength="128" + type="text" + value="Test project name" /> - </button> - <input - aria-hidden="true" - aria-label="New sketch name" - class="editable-input__input" - disabled="" - maxlength="128" - type="text" - value="Test project name" - /> - </span> - </h1> + </span> + </h1> + </div> + <div /> </div> <div class="c3" @@ -769,10 +775,7 @@ exports[`Nav renders editor version for mobile 1`] = ` display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - gap: 0.16666666666666666rem; + gap: 0.8333333333333334rem; } .c2 * { @@ -780,7 +783,13 @@ exports[`Nav renders editor version for mobile 1`] = ` margin: 0; } -.c2 > h5 { +.c2 p { + margin-left: 2px; + padding: 3px 8px; +} + +.c2 h5 { + margin-top: 2px; font-size: 1.0833333333333333rem; font-weight: normal; } @@ -907,35 +916,38 @@ exports[`Nav renders editor version for mobile 1`] = ` <div class="c2" > - <h1> - <span - class="editable-input editable-input--is-not-editing editable-input--has-value " - > - <button - aria-hidden="false" - aria-label="Edit sketch name" - class="editable-input__label" + <div> + <h1> + <span + class="editable-input editable-input--is-not-editing editable-input--has-value " > - <span> - Test project name - </span> - <test-file-stub + <button + aria-hidden="false" + aria-label="Edit sketch name" + class="editable-input__label" + > + <span> + Test project name + </span> + <test-file-stub + aria-hidden="true" + classname="editable-input__icon" + focusable="false" + /> + </button> + <input aria-hidden="true" - classname="editable-input__icon" - focusable="false" + aria-label="New sketch name" + class="editable-input__input" + disabled="" + maxlength="128" + type="text" + value="Test project name" /> - </button> - <input - aria-hidden="true" - aria-label="New sketch name" - class="editable-input__input" - disabled="" - maxlength="128" - type="text" - value="Test project name" - /> - </span> - </h1> + </span> + </h1> + </div> + <div /> </div> <div class="c3" diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index d021374d9f..7474ca7ae8 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -96,6 +96,7 @@ const IDEView = () => { const isUserOwner = useSelector(getIsUserOwner); const dispatch = useDispatch(); const { t } = useTranslation(); + const user = useSelector((state) => state.user); const params = useParams(); @@ -218,6 +219,8 @@ const IDEView = () => { className="editor-preview-subpanel" > <Editor + user={user} + project={project} provideController={(ctl) => { cmRef.current = ctl; }} diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index 111970e16a..af7a3852cc 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -378,6 +378,9 @@ pre.CodeMirror-line { &.editor-holder--hidden .CodeMirror { display: none; } + &.editor-holder--readonly { + pointer-events: none; + } } .editor__header { diff --git a/client/styles/components/_toolbar.scss b/client/styles/components/_toolbar.scss index 797fe7e46e..30ead1b75d 100644 --- a/client/styles/components/_toolbar.scss +++ b/client/styles/components/_toolbar.scss @@ -98,12 +98,20 @@ } .toolbar__project-name-container { - margin-left: #{10 / $base-font-size}rem; - padding-left: #{10 / $base-font-size}rem; + margin-left: #{10 / $base-font-size}rem; + padding-left: #{10 / $base-font-size}rem; display: flex; + gap: 13px; align-items: center; } +.toolbar__projectname{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + .toolbar .editable-input__label { @include themify() { color: getThemifyVariable('secondary-text-color'); @@ -118,7 +126,6 @@ } .toolbar__project-owner { - margin-left: #{5 / $base-font-size}rem; @include themify() { color: getThemifyVariable('secondary-text-color'); } @@ -147,3 +154,36 @@ accent-color:getThemifyVariable('logo-color'); } } + +.toolbar__duplicatetoedit { + display: flex; + gap: 2px; + cursor: pointer; + padding: 3px 8px; + align-items: center; + justify-content: center; + border-radius: 5px; + + &.contrast { + background-color: #F5DC23; + color: black; + } + + &.normal { + background-color: $p5js-pink; + color: white; + } +} + +.toolbar__icon { + width: 1.5rem; + height: 1.5rem; + + &.contrast { + fill: black; + } + + &.normal { + fill: white; + } +} \ No newline at end of file diff --git a/translations/locales/be/translations.json b/translations/locales/be/translations.json index a3ecb8f1f4..21c74066e9 100644 --- a/translations/locales/be/translations.json +++ b/translations/locales/be/translations.json @@ -119,7 +119,8 @@ "StopSketchARIA": "স্কেচ বন্ধ করুন", "EditSketchARIA": "স্কেচের নাম সম্পাদনা করুন", "NewSketchNameARIA": "নতুন স্কেচের নাম", - "By": " দ্বারা " + "By": " দ্বারা ", + "DuplicateToEdit": "সম্পাদনা করতে নকল করুন" }, "Console": { "Title": "কনসোল", diff --git a/translations/locales/de/translations.json b/translations/locales/de/translations.json index a6a9b1cb0b..bcd86721e6 100644 --- a/translations/locales/de/translations.json +++ b/translations/locales/de/translations.json @@ -115,7 +115,8 @@ "StopSketchARIA": "Sketch stoppen", "EditSketchARIA": "Sketch Namen bearbeiten", "NewSketchNameARIA": "Neuer Sketch name", - "By": " von " + "By": " von ", + "DuplicateToEdit": "Dupliceren om te bewerken" }, "Console": { "Title": "Konsole", diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json index 7097411097..a7bfba9616 100644 --- a/translations/locales/en-US/translations.json +++ b/translations/locales/en-US/translations.json @@ -118,7 +118,8 @@ "StopSketchARIA": "Stop sketch", "EditSketchARIA": "Edit sketch name", "NewSketchNameARIA": "New sketch name", - "By": " by " + "By": " by ", + "DuplicateToEdit" : "Duplicate to edit" }, "Console": { "Title": "Console", diff --git a/translations/locales/es-419/translations.json b/translations/locales/es-419/translations.json index 2964d83a18..02f8098e41 100644 --- a/translations/locales/es-419/translations.json +++ b/translations/locales/es-419/translations.json @@ -115,7 +115,8 @@ "StopSketchARIA": "Detener bosquejo", "EditSketchARIA": "Editar nombre de bosquejo", "NewSketchNameARIA": "Nuevo nombre de bosquejo", - "By": " de " + "By": " de ", + "DuplicateToEdit": "Duplicar para editar" }, "Console": { "Title": "Consola", diff --git a/translations/locales/fr-CA/translations.json b/translations/locales/fr-CA/translations.json index 36a6558c0b..052ae9eff0 100644 --- a/translations/locales/fr-CA/translations.json +++ b/translations/locales/fr-CA/translations.json @@ -116,7 +116,8 @@ "StopSketchARIA": "Arrêter le croquis", "EditSketchARIA": "Éditer le nom du croquis", "NewSketchNameARIA": "Nouveau nom de croquis", - "By": " par " + "By": " par ", + "DuplicateToEdit": "Dupliquer pour éditer" }, "Console": { "Title": "Console", diff --git a/translations/locales/hi/translations.json b/translations/locales/hi/translations.json index 4bdacb5349..7197316c3e 100644 --- a/translations/locales/hi/translations.json +++ b/translations/locales/hi/translations.json @@ -115,7 +115,8 @@ "StopSketchARIA": "स्केच बंद करे", "EditSketchARIA": "स्केच का नाम संपादित करें", "NewSketchNameARIA": "नया स्केच नाम", - "By": " द्वारा " + "By": " द्वारा ", + "DuplicateToEdit": "संपादित करने के लिए नक़ल बनाएं" }, "Console": { "Title": "कंसोल", diff --git a/translations/locales/it/translations.json b/translations/locales/it/translations.json index 91345c5a18..8fc5d3d96e 100644 --- a/translations/locales/it/translations.json +++ b/translations/locales/it/translations.json @@ -118,7 +118,8 @@ "StopSketchARIA": "Ferma sketch", "EditSketchARIA": "Modifica nome sketch", "NewSketchNameARIA": "Nome nuovo sketch", - "By": " da " + "By": " da ", + "DuplicateToEdit": "Duplica per modificare" }, "Console": { "Title": "Terminale", diff --git a/translations/locales/ja/translations.json b/translations/locales/ja/translations.json index b711bf6535..3a523376ae 100644 --- a/translations/locales/ja/translations.json +++ b/translations/locales/ja/translations.json @@ -115,7 +115,8 @@ "StopSketchARIA": "スケッチを停止", "EditSketchARIA": "スケッチ名を編集", "NewSketchNameARIA": "新しいスケッチ名", - "By": " by " + "By": " by ", + "DuplicateToEdit": "編集のために複製" }, "Console": { "Title": "コンソール", diff --git a/translations/locales/ko/translations.json b/translations/locales/ko/translations.json index ed5903d10b..c36da9c33f 100644 --- a/translations/locales/ko/translations.json +++ b/translations/locales/ko/translations.json @@ -101,7 +101,8 @@ "StopSketchARIA": "스케치 실행 중지", "EditSketchARIA": "스케치 이름 수정하기", "NewSketchNameARIA": "새로운 스케치 이름", - "By": " 제작: " + "By": " 제작: ", + "DuplicateToEdit": "편집을 위해 복제" }, "Console": { "Title": "콘솔", diff --git a/translations/locales/pt-BR/translations.json b/translations/locales/pt-BR/translations.json index 4f109049f0..476dda0e22 100644 --- a/translations/locales/pt-BR/translations.json +++ b/translations/locales/pt-BR/translations.json @@ -115,7 +115,8 @@ "StopSketchARIA": "Parar esboço", "EditSketchARIA": "Mudar nome do esboço", "NewSketchNameARIA": "Novo nome de esboço", - "By": " por " + "By": " por ", + "DuplicateToEdit": "Duplicar para editar" }, "Console": { "Title": "Terminal", diff --git a/translations/locales/sv/translations.json b/translations/locales/sv/translations.json index 1af93ce34d..b7ec2151d0 100644 --- a/translations/locales/sv/translations.json +++ b/translations/locales/sv/translations.json @@ -118,7 +118,8 @@ "StopSketchARIA": "Stoppa sketch", "EditSketchARIA": "Redigera sketchnamn", "NewSketchNameARIA": "Nytt sketchnamn", - "By": " av " + "By": " av ", + "DuplicateToEdit": "Duplicera för redigering" }, "Console": { "Title": "Konsoll", diff --git a/translations/locales/tr/translations.json b/translations/locales/tr/translations.json index ab85670cbf..1571b9c511 100644 --- a/translations/locales/tr/translations.json +++ b/translations/locales/tr/translations.json @@ -118,7 +118,8 @@ "StopSketchARIA": "Eskizi Durdur", "EditSketchARIA": "Eskiz Adını Düzenle", "NewSketchNameARIA": "Yeni Eskiz Adı", - "By": " tarafından " + "By": " tarafından ", + "DuplicateToEdit": "Düzenlemek için kopyala" }, "Console": { "Title": "Konsol", diff --git a/translations/locales/uk-UA/translations.json b/translations/locales/uk-UA/translations.json index 953367a856..84292523af 100644 --- a/translations/locales/uk-UA/translations.json +++ b/translations/locales/uk-UA/translations.json @@ -115,7 +115,8 @@ "StopSketchARIA": "Зупинити скетч", "EditSketchARIA": "Редагувати ім'я скетча", "NewSketchNameARIA": "Нове ім'я скетча", - "By": " користувачем " + "By": " користувачем ", + "DuplicateToEdit": "Дублювати для редагування" }, "Console": { "Title": "Консоль", diff --git a/translations/locales/ur/translations.json b/translations/locales/ur/translations.json index 289cc9fc29..0e66558a33 100644 --- a/translations/locales/ur/translations.json +++ b/translations/locales/ur/translations.json @@ -118,7 +118,8 @@ "StopSketchARIA": "خاکہ بنانا بند کریں", "EditSketchARIA": "خاکے کے نام میں ترمیم کریں۔", "NewSketchNameARIA": "خاکے کا نیا نام", - "By": " کی طرف سے " + "By": " کی طرف سے ", + "DuplicateToEdit": "ترتيب ديکر ترتيب ديکھيں" }, "Console": { "Title": "تسلی", diff --git a/translations/locales/zh-CN/translations.json b/translations/locales/zh-CN/translations.json index b78fceb99b..95e83b0c4f 100644 --- a/translations/locales/zh-CN/translations.json +++ b/translations/locales/zh-CN/translations.json @@ -115,7 +115,8 @@ "StopSketchARIA": "停止运行", "EditSketchARIA": "修改项目名称", "NewSketchNameARIA": "新项目名称", - "By": "作者 " + "By": "作者 ", + "DuplicateToEdit": "复制以编辑" }, "Console": { "Title": "控制台", diff --git a/translations/locales/zh-TW/translations.json b/translations/locales/zh-TW/translations.json index dd3765b29c..1606b5dbf0 100644 --- a/translations/locales/zh-TW/translations.json +++ b/translations/locales/zh-TW/translations.json @@ -115,7 +115,8 @@ "StopSketchARIA": "停止", "EditSketchARIA": "編輯草稿名稱", "NewSketchNameARIA": "新草稿名稱", - "By": " 作者: " + "By": " 作者: ", + "DuplicateToEdit": "複製以編輯" }, "Console": { "Title": "主控台",