diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index c0e2cc42c5..5d9d3c545b 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -7,9 +7,11 @@ import { connect } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { bindActionCreators } from 'redux'; import * as ProjectsActions from '../actions/projects'; +import * as ProjectActions from '../actions/project'; import * as CollectionsActions from '../actions/collections'; import * as ToastActions from '../actions/toast'; import * as SortingActions from '../actions/sorting'; +import { getConfig } from '../../../utils/getConfig'; import getSortedSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; import Overlay from '../../App/components/Overlay'; @@ -18,6 +20,8 @@ import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; import SketchListRowBase from './SketchListRowBase'; +const ROOT_URL = getConfig('API_URL'); + const SketchList = ({ user, getProjects, @@ -27,12 +31,72 @@ const SketchList = ({ sorting, toggleDirectionForField, resetSorting, - mobile + mobile, + changeVisibility, + deleteProject }) => { const [isInitialDataLoad, setIsInitialDataLoad] = useState(true); const [sketchToAddToCollection, setSketchToAddToCollection] = useState(null); + const [selectedSketches, setSelectedSketches] = useState([]); const { t } = useTranslation(); + const handleSelectSketch = (sketchId) => { + setSelectedSketches((prev) => + prev.includes(sketchId) + ? prev.filter((id) => id !== sketchId) + : [...prev, sketchId] + ); + }; + + const handleSelectAll = () => { + if (selectedSketches.length === sketches.length) { + setSelectedSketches([]); + } else { + setSelectedSketches(sketches.map((sketch) => sketch.id)); + } + }; + + const clearSelection = () => setSelectedSketches([]); + + const handleBatchVisibilityChange = (visibility) => { + if (!visibility) return; + selectedSketches.forEach((id) => { + const sketch = sketches.find((s) => s.id === id); + if (sketch) { + changeVisibility(id, sketch.name, visibility, t); + } + }); + clearSelection(); + }; + + const handleBatchDownload = () => { + // TODO: implement batch download + selectedSketches.forEach((id) => { + const sketch = sketches.find((s) => s.id === id); + if (sketch) { + const downloadLink = document.createElement('a'); + downloadLink.href = `${ROOT_URL}/projects/${id}/zip`; + downloadLink.download = `${sketch.name}.zip`; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + } + }); + }; + + const handleBatchDelete = () => { + if ( + window.confirm( + t('SketchList.BatchDeleteConfirmation', { + count: selectedSketches.length + }) + ) + ) { + selectedSketches.forEach((id) => deleteProject(id)); + clearSelection(); + } + }; + useEffect(() => { getProjects(username); resetSorting(); @@ -128,6 +192,27 @@ const SketchList = ({ {renderLoader()} {renderEmptyTable()} + {selectedSketches.length > 0 && ( +
+ + {t('SketchList.SelectedCount', { count: selectedSketches.length })} + + + + + +
+ )} {hasSketches() && ( + {renderFieldHeader('name', t('SketchList.HeaderName'))} {renderFieldHeader( 'createdAt', @@ -162,6 +258,8 @@ const SketchList = ({ user={user} username={username} onAddToCollection={() => setSketchToAddToCollection(sketch)} + selected={selectedSketches.includes(sketch.id)} + onSelect={() => handleSelectSketch(sketch.id)} t={t} /> ))} @@ -204,6 +302,8 @@ SketchList.propTypes = { field: PropTypes.string.isRequired, direction: PropTypes.string.isRequired }).isRequired, + changeVisibility: PropTypes.func.isRequired, + deleteProject: PropTypes.func.isRequired, mobile: PropTypes.bool }; @@ -229,7 +329,11 @@ function mapDispatchToProps(dispatch) { ProjectsActions, CollectionsActions, ToastActions, - SortingActions + SortingActions, + { + changeVisibility: ProjectActions.changeVisibility, + deleteProject: ProjectActions.deleteProject + } ), dispatch ); diff --git a/client/modules/IDE/components/SketchListRowBase.jsx b/client/modules/IDE/components/SketchListRowBase.jsx index e1c4c80f13..b71f041a2b 100644 --- a/client/modules/IDE/components/SketchListRowBase.jsx +++ b/client/modules/IDE/components/SketchListRowBase.jsx @@ -28,7 +28,9 @@ const SketchListRowBase = ({ changeVisibility, t, mobile, - onAddToCollection + onAddToCollection, + selected, + onSelect }) => { const [renameOpen, setRenameOpen] = useState(false); const [renameValue, setRenameValue] = useState(sketch.name); @@ -122,6 +124,14 @@ const SketchListRowBase = ({ return ( + @@ -179,12 +189,16 @@ SketchListRowBase.propTypes = { showShareModal: PropTypes.func.isRequired, changeVisibility: PropTypes.func.isRequired, onAddToCollection: PropTypes.func.isRequired, + selected: PropTypes.bool, + onSelect: PropTypes.func, mobile: PropTypes.bool, t: PropTypes.func.isRequired }; SketchListRowBase.defaultProps = { - mobile: false + mobile: false, + selected: false, + onSelect: () => {} }; function mapDispatchToPropsSketchListRow(dispatch) { diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss index 5d597596ac..9e5f83b238 100644 --- a/client/styles/components/_nav.scss +++ b/client/styles/components/_nav.scss @@ -21,13 +21,14 @@ @include themify() { border-bottom: 1px dashed map-get($theme-map, 'nav-border-color'); } + // padding-left: #{math.div(20, $base-font-size)}rem; } .nav__menubar { display: flex; flex-direction: row; - width:100%; + width: 100%; justify-content: space-between; } @@ -57,11 +58,11 @@ } // base focus styles -.nav__item button:focus { +.nav__item button:focus { @include themify() { background-color: getThemifyVariable('nav-hover-color'); } - + .nav__item-header { @include themify() { color: getThemifyVariable('button-hover-color'); @@ -73,11 +74,12 @@ @include themify() { fill: getThemifyVariable('button-hover-color'); } - } + } } .nav__dropdown-item { + & button:focus, & a:focus { @include themify() { @@ -85,6 +87,7 @@ background-color: getThemifyVariable('nav-hover-color'); } } + & button:focus .nav__keyboard-shortcut, & a:focus .nav__keyboard-shortcut { @include themify() { @@ -93,6 +96,7 @@ } &.nav__dropdown-item--disabled { + & button, & a, & button:hover, @@ -131,8 +135,9 @@ color: getThemifyVariable('button-hover-color'); } } - - & g, & path { + + & g, + & path { @include themify() { fill: getThemifyVariable('nav-hover-color'); } @@ -144,13 +149,22 @@ fill: getThemifyVariable('button-hover-color'); } } + + .nav__back-icon g, + .nav__back-icon path { + @include themify() { + fill: getThemifyVariable('button-hover-color'); + } + } } .nav__item-header:hover { @include themify() { color: getThemifyVariable('nav-hover-color'); } - & g, & path { + + & g, + & path { @include themify() { fill: getThemifyVariable('nav-hover-color'); } @@ -158,17 +172,17 @@ } .nav__item-header-triangle { - margin-left: #{math.div(5, $base-font-size)}rem; + margin-left: #{math.div(5, $base-font-size)}rem; } .nav__dropdown { @include themify() { - color: getThemifyVariable('nav-hover-color'); - } + color: getThemifyVariable('nav-hover-color'); + } } .nav__item-header-triangle { - margin-left: #{math.div(5, $base-font-size)}rem; + margin-left: #{math.div(5, $base-font-size)}rem; } .nav__dropdown { @@ -176,6 +190,7 @@ display: none; max-height: 60vh; overflow-y: auto; + .nav__item--open & { display: flex; } @@ -211,6 +226,7 @@ // } .nav__dropdown-item { + & button, & a { width: 100%; @@ -240,8 +256,8 @@ } -.svg__logo g path{ - +.svg__logo g path { + @include themify() { // Set internal color of the logo; fill: getThemifyVariable('logo-background-color'); @@ -266,15 +282,19 @@ } .nav__back-icon { - & g, & path { + + & g, + & path { opacity: 1; + @include themify() { fill: getThemifyVariable('inactive-text-color'); } } + margin-right: #{math.div(5, $base-font-size)}rem; } .nav__back-link { display: flex; -} +} \ No newline at end of file diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 401f575728..0f4c299ccf 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -40,7 +40,7 @@ } .sketch-visibility_ul li::before { - content: "\2022"; + content: '\2022'; @include themify() { color: getThemifyVariable('hint-arrow-background-color'); @@ -50,9 +50,6 @@ margin-right: 10px; } - - - .sketches-table-container { overflow-y: auto; max-width: 100%; @@ -60,7 +57,7 @@ @media (max-width: 770px) { @include themify() { - background-color: getThemifyVariable("modal-background-color"); + background-color: getThemifyVariable('modal-background-color'); } .sketches-table { @@ -79,8 +76,6 @@ flex-direction: column; gap: #{math.div(12, $base-font-size)}rem; - - .sketches-table__row { margin: 0; position: relative; @@ -91,8 +86,10 @@ gap: #{math.div(8, $base-font-size)}rem; @include themify() { - border: 1px solid getThemifyVariable("modal-border-color"); - background-color: getThemifyVariable("search-background-color") !important; + border: 1px solid getThemifyVariable('modal-border-color'); + background-color: getThemifyVariable( + 'search-background-color' + ) !important; } .sketches-table_name { @@ -102,14 +99,13 @@ align-items: center; } - - >td { + > td { padding-left: 0; width: 30%; font-size: #{math.div(14, $base-font-size)}rem; @include themify() { - color: getThemifyVariable("modal-border-color"); + color: getThemifyVariable('modal-border-color'); } } @@ -152,7 +148,7 @@ z-index: 1; @include themify() { - background-color: getThemifyVariable("background-color"); + background-color: getThemifyVariable('background-color'); } } @@ -168,7 +164,7 @@ & svg { margin-left: #{math.div(5, $base-font-size)}rem; @include themify() { - fill: getThemifyVariable("inactive-text-color"); + fill: getThemifyVariable('inactive-text-color'); } } } @@ -178,13 +174,13 @@ padding: #{math.div(3, $base-font-size)}rem 0; @include themify() { - color: getThemifyVariable("inactive-text-color"); + color: getThemifyVariable('inactive-text-color'); } } .sketches-table__header--selected { @include themify() { - border-color: getThemifyVariable("logo-color"); + border-color: getThemifyVariable('logo-color'); } } @@ -200,27 +196,26 @@ .sketches-table__row:nth-child(odd) { @include themify() { - background: getThemifyVariable("table-row-stripe-color"); + background: getThemifyVariable('table-row-stripe-color'); } } -.sketches-table__row>th:nth-child(1) { +.sketches-table__row > th:nth-child(2) { padding-left: #{math.div(12, $base-font-size)}rem; } -.sketches-table__row>td { +.sketches-table__row > td { padding-left: #{math.div(8, $base-font-size)}rem; } .sketches-table__row a { @include themify() { text-decoration: underline; - color: getThemifyVariable("primary-text-color"); + color: getThemifyVariable('primary-text-color'); } - } -.sketches-table__row.is-deleted-or-private>* { +.sketches-table__row.is-deleted-or-private > * { font-style: italic; } @@ -228,7 +223,7 @@ font-size: #{math.div(12, $base-font-size)}rem; @include themify() { - color: getThemifyVariable("inactive-text-color"); + color: getThemifyVariable('inactive-text-color'); } } @@ -251,9 +246,69 @@ font-size: #{math.div(16, $base-font-size)}rem; padding: #{math.div(42, $base-font-size)}rem 0; } -.sketches-table__row a:hover{ +.sketches-table__row a:hover { @include themify() { - color: getThemifyVariable("logo-color"); + color: getThemifyVariable('logo-color'); } text-decoration-thickness: 0.1em; -} \ No newline at end of file +} + +.sketch-list-toolbar { + display: flex; + align-items: center; + gap: #{math.div(12, $base-font-size)}rem; + padding: #{math.div(12, $base-font-size)}rem; + margin-bottom: #{math.div(16, $base-font-size)}rem; + + @include themify() { + background-color: getThemifyVariable('button-background-color'); + border: 1px solid getThemifyVariable('modal-border-color'); + color: getThemifyVariable('primary-text-color'); + } + + border-radius: 4px; + + span { + font-weight: bold; + } + + button, + select { + padding: #{math.div(6, $base-font-size)}rem #{math.div(12, $base-font-size)}rem; + border: 1px solid; + + @include themify() { + background-color: getThemifyVariable('background-color'); + color: getThemifyVariable('primary-text-color'); + border-color: getThemifyVariable('modal-border-color'); + } + + border-radius: 4px; + cursor: pointer; + + &:hover { + @include themify() { + background-color: getThemifyVariable('button-hover-color'); + } + } + } + + select { + min-width: #{math.div(150, $base-font-size)}rem; + } +} + +.sketches-table__row > td:first-child { + padding-left: #{math.div(12, $base-font-size)}rem; + width: #{math.div(40, $base-font-size)}rem; + text-align: center; +} + +.sketches-table thead th:first-child { + width: #{math.div(40, $base-font-size)}rem; + text-align: center; +} + +.sketches-table thead th:nth-child(2) { + padding-left: #{math.div(12, $base-font-size)}rem; +} diff --git a/server/config/passport.js b/server/config/passport.js index f9ec1192bb..ff1c4fa44e 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -234,7 +234,7 @@ passport.use( { clientID: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET, - callbackURL: 'https://editor.p5js.org/auth/google/callback', + callbackURL: '/auth/google/callback', passReqToCallback: true, scope: ['openid email'] }, diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json index 1d254a5afc..ca18e2983c 100644 --- a/translations/locales/en-US/translations.json +++ b/translations/locales/en-US/translations.json @@ -595,7 +595,14 @@ "HeaderCreatedAt_mobile": "Created", "HeaderUpdatedAt": "Date Updated", "HeaderUpdatedAt_mobile": "Updated", - "NoSketches": "No sketches." + "NoSketches": "No sketches.", + "SelectAll": "Select all sketches", + "SelectedCount": "{{count}} sketch(es) selected", + "ClearSelection": "Clear selection", + "BatchDownload": "Download selected", + "BatchDelete": "Delete selected", + "BatchDeleteConfirmation": "Are you sure you want to delete {{count}} sketch(es)? This action cannot be undone.", + "ChangeVisibility": "Change visibility" }, "AddToCollectionSketchList": { "Title": "p5.js Web Editor | My sketches",
+ 0 + } + onChange={handleSelectAll} + aria-label={t('SketchList.SelectAll')} + /> +
+ + {name} {formatDateCell(sketch.createdAt, mobile)} {formatDateCell(sketch.updatedAt, mobile)}