From 94cc47871e973f706d9e3df8cf2b2f8578aa96da Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Mon, 17 May 2021 15:16:13 -0400 Subject: [PATCH 1/6] WIP - initial commit of multiselect dashboard UI --- .../scripts/common/components/modal/modal.js | 2 +- .../shared/actions/dashboard-actions.js | 16 ++++ .../shared/components/button.scss | 15 ++++ .../shared/components/dashboard-hoc.js | 4 + .../shared/components/dashboard.jsx | 89 +++++++++++++------ .../shared/components/dashboard.scss | 16 ++++ .../shared/components/module.jsx | 29 ++++-- .../shared/components/module.scss | 9 +- .../pages/page-dashboard-server.jsx | 2 + .../shared/reducers/dashboard-reducer.js | 18 +++- 10 files changed, 162 insertions(+), 38 deletions(-) diff --git a/packages/app/obojobo-document-engine/src/scripts/common/components/modal/modal.js b/packages/app/obojobo-document-engine/src/scripts/common/components/modal/modal.js index 4d699bdfb5..7bdd0c4df2 100644 --- a/packages/app/obojobo-document-engine/src/scripts/common/components/modal/modal.js +++ b/packages/app/obojobo-document-engine/src/scripts/common/components/modal/modal.js @@ -64,7 +64,7 @@ class Modal extends React.Component { type="text" onFocus={this.onTabTrapFocus} /> - {this.props.onClose ? ( + {!this.props.onClose ? ( ) : null}
diff --git a/packages/app/obojobo-repository/shared/actions/dashboard-actions.js b/packages/app/obojobo-repository/shared/actions/dashboard-actions.js index 13b97db092..2883d4b01a 100644 --- a/packages/app/obojobo-repository/shared/actions/dashboard-actions.js +++ b/packages/app/obojobo-repository/shared/actions/dashboard-actions.js @@ -222,6 +222,18 @@ const filterModules = searchString => ({ searchString }) +const SELECT_MODULE = 'SELECT_MODULE' +const selectModule = module => ({ + type: SELECT_MODULE, + module +}) + +const DESELECT_MODULE = 'DESELECT_MODULE' +const deselectModule = module => ({ + type: DESELECT_MODULE, + module +}) + const SHOW_MODULE_MORE = 'SHOW_MODULE_MORE' const showModuleMore = module => ({ type: SHOW_MODULE_MORE, @@ -279,12 +291,16 @@ module.exports = { DELETE_MODULE_PERMISSIONS, DELETE_MODULE, FILTER_MODULES, + SELECT_MODULE, + DESELECT_MODULE, SHOW_MODULE_MORE, SHOW_VERSION_HISTORY, RESTORE_VERSION, IMPORT_MODULE_FILE, CHECK_MODULE_LOCK, filterModules, + selectModule, + deselectModule, deleteModule, closeModal, deleteModulePermissions, diff --git a/packages/app/obojobo-repository/shared/components/button.scss b/packages/app/obojobo-repository/shared/components/button.scss index 8b76bba89a..7c08a51e3c 100644 --- a/packages/app/obojobo-repository/shared/components/button.scss +++ b/packages/app/obojobo-repository/shared/components/button.scss @@ -62,4 +62,19 @@ background-color: $color-dangerous-minor; } } + + &.secondary-button.dangerous-button { + border: 0.1em solid $color-dangerous; + background-color: white; + color: $color-dangerous; + + &:hover { + background-color: $color-dangerous; + color: white; + } + } + + &.multi-select { + padding: 0.6em 0.8em; + } } diff --git a/packages/app/obojobo-repository/shared/components/dashboard-hoc.js b/packages/app/obojobo-repository/shared/components/dashboard-hoc.js index 7424290af1..7983b3a738 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard-hoc.js +++ b/packages/app/obojobo-repository/shared/components/dashboard-hoc.js @@ -7,6 +7,8 @@ const { deleteModulePermissions, createNewModule, filterModules, + selectModule, + deselectModule, deleteModule, showModulePermissions, showVersionHistory, @@ -22,6 +24,8 @@ const mapActionsToProps = { loadUsersForModule, deleteModulePermissions, filterModules, + selectModule, + deselectModule, deleteModule, showModulePermissions, showVersionHistory, diff --git a/packages/app/obojobo-repository/shared/components/dashboard.jsx b/packages/app/obojobo-repository/shared/components/dashboard.jsx index 61a6443456..ad6f9a0cf5 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard.jsx +++ b/packages/app/obojobo-repository/shared/components/dashboard.jsx @@ -91,6 +91,14 @@ const renderModalDialog = props => { ) } +const getModuleCount = modules => { + if (modules.length === 1) { + return '1 Module Selected: ' + } else { + return `${modules.length} Modules Selected:` + } +} + const getSortMethod = sortOrder => { let sortFn switch (sortOrder) { @@ -110,19 +118,13 @@ const getSortMethod = sortOrder => { return sortFn } -const renderModules = (modules, sortOrder, newModuleId) => { - const sortFn = getSortMethod(sortOrder) - return modules - .sort(sortFn) - .map(draft => ( - - )) -} - -const Dashboard = props => { +function Dashboard(props) { const [sortOrder, setSortOrder] = useState(props.sortOrder) const [newModuleId, setNewModuleId] = useState(null) + const moduleList = props.filteredModules ? props.filteredModules : props.myModules + const [selectStates, setSelectStates] = useState(moduleList.map(() => false)) + const handleCreateNewModule = useTutorial => { props.createNewModule(useTutorial).then(data => { data.payload.value.sort(getSortMethod('newest')) @@ -130,6 +132,13 @@ const Dashboard = props => { }) } + const handleSelectModule = (draft, index) => { + selectStates[index] = !selectStates[index] + setSelectStates([...selectStates]) + + selectStates[index] ? props.selectModule(draft) : props.deselectModule(draft) + } + // Set a cookie when sortOrder changes on the client // can't undefine document to test this 'else' case without breaking everything - maybe later /* istanbul ignore else */ @@ -141,29 +150,44 @@ const Dashboard = props => { }, [sortOrder]) } + // idea: SELECT_MODULES/DESELECT_MODULES in redux + // takes array instead of single module + return ( + noticeCount={0} />
-
- - - - - - -
+ {props.multiSelectMode ? +
+ {getModuleCount(props.selectedModules)} +
+ +
+
+ + +
+ + +
: +
+ + + + + + +
+ }
My Modules
@@ -179,11 +203,18 @@ const Dashboard = props => {
- {renderModules( - props.filteredModules ? props.filteredModules : props.myModules, - sortOrder, - newModuleId - )} + {moduleList + .sort(getSortMethod(sortOrder)) + .map((draft, index) => ( + handleSelectModule(draft, index)} + key={draft.draftId} + hasMenu={true} + {...draft} /> + ))}
diff --git a/packages/app/obojobo-repository/shared/components/dashboard.scss b/packages/app/obojobo-repository/shared/components/dashboard.scss index ec25830948..e60af2ef30 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard.scss +++ b/packages/app/obojobo-repository/shared/components/dashboard.scss @@ -6,5 +6,21 @@ justify-content: space-between; align-items: center; margin-bottom: $size-spacing-vertical-big; + + &.is-multi-select-mode { + border: 1px solid $color-banner-bg; + border-radius: 0.15em; + + .stuff { + font-size: 0.8em; + margin-left: 1.0em; + width: 17em; + } + + button.secondary-button { + margin: 0.3em 0.2em; + font-size: 0.65em; + } + } } } diff --git a/packages/app/obojobo-repository/shared/components/module.jsx b/packages/app/obojobo-repository/shared/components/module.jsx index 500524aecc..9c36b5b37e 100644 --- a/packages/app/obojobo-repository/shared/components/module.jsx +++ b/packages/app/obojobo-repository/shared/components/module.jsx @@ -13,6 +13,15 @@ const Module = props => { setMenuOpen(!isMenuOpen) e.preventDefault() // block the event from bubbling out to the parent href } + const handleClick = e => { + if (props.isMultiSelectMode) { + props.onSelect() + } else { + onToggleMenu() + } + + e.preventDefault(); + } // Handle keyboard focus const onBlurHandler = () => { timeOutId = setTimeout(() => { @@ -29,19 +38,27 @@ const Module = props => { className={ 'repository--module-icon ' + (isMenuOpen ? 'is-open ' : 'is-not-open ') + + (props.isSelected ? 'is-selected ' : 'is-not-selected ') + (props.isNew ? 'is-new' : 'is-not-new') } onBlur={onBlurHandler} onFocus={onFocusHandler} > + {props.hasMenu ? ( - ) : ( diff --git a/packages/app/obojobo-repository/shared/components/module.scss b/packages/app/obojobo-repository/shared/components/module.scss index 1e2cd432b8..2475823239 100644 --- a/packages/app/obojobo-repository/shared/components/module.scss +++ b/packages/app/obojobo-repository/shared/components/module.scss @@ -33,7 +33,14 @@ } } - &.is-open { + > input { + position: absolute; + z-index: 1; + left: 0.3em; + top: 0.3em; + } + + &.is-open, &.is-selected { background-color: $color-banner-bg; } diff --git a/packages/app/obojobo-repository/shared/components/pages/page-dashboard-server.jsx b/packages/app/obojobo-repository/shared/components/pages/page-dashboard-server.jsx index 951d720f3a..f7cba4ff84 100644 --- a/packages/app/obojobo-repository/shared/components/pages/page-dashboard-server.jsx +++ b/packages/app/obojobo-repository/shared/components/pages/page-dashboard-server.jsx @@ -22,6 +22,8 @@ PageDashboardServer.defaultProps = { selectedModule: {}, draftPermissions: {}, myModules: [], + selectedModules: [], + multiSelectMode: false, moduleSearchString: '', shareSearchString: '', versionHistory: { diff --git a/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js b/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js index 865a81fd93..40fe434d62 100644 --- a/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js +++ b/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js @@ -13,9 +13,11 @@ const { DELETE_MODULE, CREATE_NEW_MODULE, FILTER_MODULES, + SELECT_MODULE, SHOW_MODULE_MORE, SHOW_VERSION_HISTORY, - RESTORE_VERSION + RESTORE_VERSION, + DESELECT_MODULE } = require('../actions/dashboard-actions') const searchPeopleResultsState = (isFetching = false, hasFetched = false, items = []) => ({ @@ -97,6 +99,20 @@ function DashboardReducer(state, action) { moduleSearchString: action.searchString } + case SELECT_MODULE: + return { + ...state, + selectedModules: [...state.selectedModules, action.module], + multiSelectMode: true + } + + case DESELECT_MODULE: + return { + ...state, + selectedModules: state.selectedModules.filter(m => m.draftId !== action.module.draftId), + multiSelectMode: state.selectedModules.length > 1 + } + case CLEAR_PEOPLE_SEARCH_RESULTS: return { ...state, searchPeople: searchPeopleResultsState(), shareSearchString: '' } From 044ef1ba727f1323ba7dba69872c9d9632ec1a78 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Tue, 18 May 2021 16:00:11 -0400 Subject: [PATCH 2/6] WIP - mostly final frontend functionality --- .../scripts/common/components/modal/modal.js | 2 +- .../shared/actions/dashboard-actions.js | 24 +++++++------- .../shared/components/dashboard-hoc.js | 8 ++--- .../shared/components/dashboard.jsx | 31 +++++++++++++------ .../shared/components/module.jsx | 13 +++----- .../shared/components/module.scss | 11 ++++++- .../shared/reducers/dashboard-reducer.js | 16 +++++----- 7 files changed, 62 insertions(+), 43 deletions(-) diff --git a/packages/app/obojobo-document-engine/src/scripts/common/components/modal/modal.js b/packages/app/obojobo-document-engine/src/scripts/common/components/modal/modal.js index 7bdd0c4df2..4d699bdfb5 100644 --- a/packages/app/obojobo-document-engine/src/scripts/common/components/modal/modal.js +++ b/packages/app/obojobo-document-engine/src/scripts/common/components/modal/modal.js @@ -64,7 +64,7 @@ class Modal extends React.Component { type="text" onFocus={this.onTabTrapFocus} /> - {!this.props.onClose ? ( + {this.props.onClose ? ( ) : null}
diff --git a/packages/app/obojobo-repository/shared/actions/dashboard-actions.js b/packages/app/obojobo-repository/shared/actions/dashboard-actions.js index 2883d4b01a..04c2f125b9 100644 --- a/packages/app/obojobo-repository/shared/actions/dashboard-actions.js +++ b/packages/app/obojobo-repository/shared/actions/dashboard-actions.js @@ -222,16 +222,16 @@ const filterModules = searchString => ({ searchString }) -const SELECT_MODULE = 'SELECT_MODULE' -const selectModule = module => ({ - type: SELECT_MODULE, - module +const SELECT_MODULES = 'SELECT_MODULES' +const selectModules = draftIds => ({ + type: SELECT_MODULES, + draftIds }) -const DESELECT_MODULE = 'DESELECT_MODULE' -const deselectModule = module => ({ - type: DESELECT_MODULE, - module +const DESELECT_MODULES = 'DESELECT_MODULES' +const deselectModules = draftIds => ({ + type: DESELECT_MODULES, + draftIds }) const SHOW_MODULE_MORE = 'SHOW_MODULE_MORE' @@ -291,16 +291,16 @@ module.exports = { DELETE_MODULE_PERMISSIONS, DELETE_MODULE, FILTER_MODULES, - SELECT_MODULE, - DESELECT_MODULE, + SELECT_MODULES, + DESELECT_MODULES, SHOW_MODULE_MORE, SHOW_VERSION_HISTORY, RESTORE_VERSION, IMPORT_MODULE_FILE, CHECK_MODULE_LOCK, filterModules, - selectModule, - deselectModule, + selectModules, + deselectModules, deleteModule, closeModal, deleteModulePermissions, diff --git a/packages/app/obojobo-repository/shared/components/dashboard-hoc.js b/packages/app/obojobo-repository/shared/components/dashboard-hoc.js index 7983b3a738..ae7af9a269 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard-hoc.js +++ b/packages/app/obojobo-repository/shared/components/dashboard-hoc.js @@ -7,8 +7,8 @@ const { deleteModulePermissions, createNewModule, filterModules, - selectModule, - deselectModule, + selectModules, + deselectModules, deleteModule, showModulePermissions, showVersionHistory, @@ -24,8 +24,8 @@ const mapActionsToProps = { loadUsersForModule, deleteModulePermissions, filterModules, - selectModule, - deselectModule, + selectModules, + deselectModules, deleteModule, showModulePermissions, showVersionHistory, diff --git a/packages/app/obojobo-repository/shared/components/dashboard.jsx b/packages/app/obojobo-repository/shared/components/dashboard.jsx index ad6f9a0cf5..e8ef450471 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard.jsx +++ b/packages/app/obojobo-repository/shared/components/dashboard.jsx @@ -121,10 +121,11 @@ const getSortMethod = sortOrder => { function Dashboard(props) { const [sortOrder, setSortOrder] = useState(props.sortOrder) const [newModuleId, setNewModuleId] = useState(null) + const [lastSelectedIndex, setLastSelectedIndex] = useState(null) const moduleList = props.filteredModules ? props.filteredModules : props.myModules const [selectStates, setSelectStates] = useState(moduleList.map(() => false)) - + const handleCreateNewModule = useTutorial => { props.createNewModule(useTutorial).then(data => { data.payload.value.sort(getSortMethod('newest')) @@ -132,11 +133,26 @@ function Dashboard(props) { }) } - const handleSelectModule = (draft, index) => { - selectStates[index] = !selectStates[index] + const handleSelectModule = (event, draftId, index) => { + if (event.shiftKey) { + // Accommodates for group selecting backwards in the list and prevents duplicate selections + const [startIdx, endIdx] = lastSelectedIndex < index ? [lastSelectedIndex + 1, index + 1] : [index, lastSelectedIndex] + const idList = moduleList.map(m => m.draftId) + selectStates.fill(true, startIdx, endIdx) + props.selectModules(idList.slice(startIdx, endIdx)) + } else { + selectStates[index] = !selectStates[index] + selectStates[index] ? props.selectModules([draftId]) : props.deselectModules([draftId]) + } + setSelectStates([...selectStates]) + setLastSelectedIndex(index) + } - selectStates[index] ? props.selectModule(draft) : props.deselectModule(draft) + const deselectAll = () => { + props.deselectModules(props.selectedModules) + selectStates.fill(false) + setSelectStates([...selectStates]) } // Set a cookie when sortOrder changes on the client @@ -150,9 +166,6 @@ function Dashboard(props) { }, [sortOrder]) } - // idea: SELECT_MODULES/DESELECT_MODULES in redux - // takes array instead of single module - return ( Download JSON
- +
:
@@ -210,7 +223,7 @@ function Dashboard(props) { isNew={draft.draftId === newModuleId} isSelected={selectStates[index]} isMultiSelectMode={props.multiSelectMode} - onSelect={() => handleSelectModule(draft, index)} + onSelect={e => handleSelectModule(e, draft.draftId, index)} key={draft.draftId} hasMenu={true} {...draft} /> diff --git a/packages/app/obojobo-repository/shared/components/module.jsx b/packages/app/obojobo-repository/shared/components/module.jsx index 9c36b5b37e..b6ec2b0677 100644 --- a/packages/app/obojobo-repository/shared/components/module.jsx +++ b/packages/app/obojobo-repository/shared/components/module.jsx @@ -9,18 +9,14 @@ const Module = props => { let timeOutId const [isMenuOpen, setMenuOpen] = useState(false) const onCloseMenu = () => setMenuOpen(false) - const onToggleMenu = e => { - setMenuOpen(!isMenuOpen) - e.preventDefault() // block the event from bubbling out to the parent href - } const handleClick = e => { - if (props.isMultiSelectMode) { - props.onSelect() + if (props.isMultiSelectMode || e.shiftKey || e.metaKey) { + props.onSelect(e) } else { - onToggleMenu() + setMenuOpen(!isMenuOpen) } - e.preventDefault(); + e.preventDefault() // block the event from bubbling out to the parent href } // Handle keyboard focus const onBlurHandler = () => { @@ -45,6 +41,7 @@ const Module = props => { onFocus={onFocusHandler} > input { + input { position: absolute; + display: none; z-index: 1; left: 0.3em; top: 0.3em; + + &.is-multi-select-mode { + display: block; + } } &.is-open, &.is-selected { @@ -62,6 +67,10 @@ .repository--module-icon--menu-control-button { display: block; } + + input { + display: block; + } } .repository--module-icon--menu-control-button.is-visible { diff --git a/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js b/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js index 40fe434d62..e4d8eb34cc 100644 --- a/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js +++ b/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js @@ -13,11 +13,11 @@ const { DELETE_MODULE, CREATE_NEW_MODULE, FILTER_MODULES, - SELECT_MODULE, + SELECT_MODULES, + DESELECT_MODULES, SHOW_MODULE_MORE, SHOW_VERSION_HISTORY, - RESTORE_VERSION, - DESELECT_MODULE + RESTORE_VERSION } = require('../actions/dashboard-actions') const searchPeopleResultsState = (isFetching = false, hasFetched = false, items = []) => ({ @@ -99,18 +99,18 @@ function DashboardReducer(state, action) { moduleSearchString: action.searchString } - case SELECT_MODULE: + case SELECT_MODULES: return { ...state, - selectedModules: [...state.selectedModules, action.module], + selectedModules: [...state.selectedModules, ...action.draftIds], multiSelectMode: true } - case DESELECT_MODULE: + case DESELECT_MODULES: return { ...state, - selectedModules: state.selectedModules.filter(m => m.draftId !== action.module.draftId), - multiSelectMode: state.selectedModules.length > 1 + selectedModules: state.selectedModules.filter(m => !action.draftIds.includes(m)), + multiSelectMode: state.selectedModules.length !== action.draftIds.length } case CLEAR_PEOPLE_SEARCH_RESULTS: From 4e03bf6c6680d312a8e40c8de67980b2b4b69650 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Mon, 24 May 2021 15:15:54 -0400 Subject: [PATCH 3/6] delete all button deletes modules --- .../shared/actions/dashboard-actions.js | 8 + .../shared/actions/dashboard-actions.test.js | 61 ++++++++ .../__snapshots__/module.test.js.snap | 101 ++++++++++++- .../shared/components/dashboard-hoc.js | 2 + .../shared/components/dashboard-hoc.test.js | 3 + .../shared/components/dashboard.jsx | 79 +++++----- .../shared/components/dashboard.scss | 6 +- .../shared/components/dashboard.test.js | 142 +++++++++++++++++- .../shared/components/module.test.js | 114 ++++++++++++-- .../shared/reducers/dashboard-reducer.js | 16 ++ .../shared/reducers/dashboard-reducer.test.js | 112 ++++++++++++++ 11 files changed, 593 insertions(+), 51 deletions(-) diff --git a/packages/app/obojobo-repository/shared/actions/dashboard-actions.js b/packages/app/obojobo-repository/shared/actions/dashboard-actions.js index 04c2f125b9..3ff9d84d6f 100644 --- a/packages/app/obojobo-repository/shared/actions/dashboard-actions.js +++ b/packages/app/obojobo-repository/shared/actions/dashboard-actions.js @@ -210,6 +210,12 @@ const deleteModule = draftId => ({ promise: apiDeleteModule(draftId).then(apiGetMyModules) }) +const BULK_DELETE_MODULES = 'BULK_DELETE_MODULES' +const bulkDeleteModules = draftIds => ({ + type: BULK_DELETE_MODULES, + promise: Promise.all(draftIds.map(id => apiDeleteModule(id))).then(apiGetMyModules) +}) + const CREATE_NEW_MODULE = 'CREATE_NEW_MODULE' const createNewModule = (useTutorial = false) => ({ type: CREATE_NEW_MODULE, @@ -290,6 +296,7 @@ module.exports = { CLEAR_PEOPLE_SEARCH_RESULTS, DELETE_MODULE_PERMISSIONS, DELETE_MODULE, + BULK_DELETE_MODULES, FILTER_MODULES, SELECT_MODULES, DESELECT_MODULES, @@ -302,6 +309,7 @@ module.exports = { selectModules, deselectModules, deleteModule, + bulkDeleteModules, closeModal, deleteModulePermissions, searchForUser, diff --git a/packages/app/obojobo-repository/shared/actions/dashboard-actions.test.js b/packages/app/obojobo-repository/shared/actions/dashboard-actions.test.js index 664cc1534b..a343625419 100644 --- a/packages/app/obojobo-repository/shared/actions/dashboard-actions.test.js +++ b/packages/app/obojobo-repository/shared/actions/dashboard-actions.test.js @@ -280,6 +280,47 @@ describe('Dashboard Actions', () => { return assertDeleteModuleRunsWithOptions('/api/drafts') }) + const assertBulkDeleteModulesRunsWithOptions = (secondaryLookupUrl, fetchBody, options) => { + global.fetch.mockResolvedValue(standardFetchResponse) + const actionReply = DashboardActions.bulkDeleteModules( + ['mockDraftId1', 'mockDraftId2'], + options + ) + + expect(global.fetch).toHaveBeenCalledTimes(2) + expect(global.fetch).toHaveBeenCalledWith('/api/drafts/mockDraftId1', { + ...defaultFetchOptions, + method: 'DELETE', + body: fetchBody + }) + expect(global.fetch).toHaveBeenCalledWith('/api/drafts/mockDraftId2', { + ...defaultFetchOptions, + method: 'DELETE', + body: fetchBody + }) + global.fetch.mockReset() + global.fetch.mockResolvedValueOnce({ + json: () => ({ value: 'mockSecondaryResponse' }) + }) + + expect(actionReply).toEqual({ + type: DashboardActions.BULK_DELETE_MODULES, + promise: expect.any(Object) + }) + + return actionReply.promise.then(finalResponse => { + expect(standardFetchResponse.json).toHaveBeenCalled() + expect(global.fetch).toHaveBeenCalledWith(secondaryLookupUrl, defaultFetchOptions) + + expect(finalResponse).toEqual({ + value: 'mockSecondaryResponse' + }) + }) + } + test('bulkDeleteModules returns expected output and calls other functions', () => { + return assertBulkDeleteModulesRunsWithOptions('/api/drafts') + }) + // three (plus one default) ways of calling createNewModule plus tutorial/normal module const assertCreateNewModuleRunsWithOptions = ( createUrl, @@ -339,6 +380,26 @@ describe('Dashboard Actions', () => { }) }) + test('selectModules returns the expected output', () => { + const actionReply = DashboardActions.selectModules(['mockDraftId1', 'mockDraftId2']) + + expect(global.fetch).not.toHaveBeenCalled() + expect(actionReply).toEqual({ + type: DashboardActions.SELECT_MODULES, + draftIds: ['mockDraftId1', 'mockDraftId2'] + }) + }) + + test('deselectModules returns the expected output', () => { + const actionReply = DashboardActions.deselectModules(['mockDraftId', 'mockDraftId2']) + + expect(global.fetch).not.toHaveBeenCalled() + expect(actionReply).toEqual({ + type: DashboardActions.DESELECT_MODULES, + draftIds: ['mockDraftId', 'mockDraftId2'] + }) + }) + test('showModuleMore returns the expected output', () => { const mockModule = { draftId: 'mockDraftId', diff --git a/packages/app/obojobo-repository/shared/components/__snapshots__/module.test.js.snap b/packages/app/obojobo-repository/shared/components/__snapshots__/module.test.js.snap index 0bd00d1227..7e06803a4d 100644 --- a/packages/app/obojobo-repository/shared/components/__snapshots__/module.test.js.snap +++ b/packages/app/obojobo-repository/shared/components/__snapshots__/module.test.js.snap @@ -2,11 +2,16 @@ exports[`Module renders with expected standard props 1`] = `
+ +
+`; + exports[`Module renders with expected standard props but isNew=true 1`] = `
+ + +
+`; + +exports[`Module renders with expected standard props but isSelected=true 1`] = ` +
+ -
-
- - -
- - -
: + {getModuleCount(props.selectedModules)} + + +
+ ) : (
@@ -198,9 +209,10 @@ function Dashboard(props) { + onChange={props.filterModules} + />
- } + )}
My Modules
@@ -216,18 +228,17 @@ function Dashboard(props) {
- {moduleList - .sort(getSortMethod(sortOrder)) - .map((draft, index) => ( - handleSelectModule(e, draft.draftId, index)} - key={draft.draftId} - hasMenu={true} - {...draft} /> - ))} + {moduleList.sort(getSortMethod(sortOrder)).map((draft, index) => ( + handleSelectModule(e, draft.draftId, index)} + key={draft.draftId} + hasMenu={true} + {...draft} + /> + ))}
diff --git a/packages/app/obojobo-repository/shared/components/dashboard.scss b/packages/app/obojobo-repository/shared/components/dashboard.scss index e60af2ef30..2e7cdd305e 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard.scss +++ b/packages/app/obojobo-repository/shared/components/dashboard.scss @@ -11,10 +11,10 @@ border: 1px solid $color-banner-bg; border-radius: 0.15em; - .stuff { + .module-count { font-size: 0.8em; - margin-left: 1.0em; - width: 17em; + margin-left: 1em; + width: 40em; } button.secondary-button { diff --git a/packages/app/obojobo-repository/shared/components/dashboard.test.js b/packages/app/obojobo-repository/shared/components/dashboard.test.js index 4668c5fe8e..eae015b8c6 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard.test.js +++ b/packages/app/obojobo-repository/shared/components/dashboard.test.js @@ -18,6 +18,7 @@ import { create, act } from 'react-test-renderer' import Dashboard from './dashboard' import MultiButton from './multi-button' +import Button from './button' import Module from './module' import Search from './search' @@ -100,6 +101,8 @@ describe('Dashboard', () => { draftPermissions: {}, myCollections: [], myModules: [], + selectedModules: [], + multiSelectMode: false, sortOrder: 'alphabetical', moduleCount: 0, moduleSearchString: '', @@ -144,7 +147,8 @@ describe('Dashboard', () => { //numerous changes to check for within the main content area const mainContent = component.root.findByProps({ className: 'repository--main-content' }) //some in the control bar - const expectedControlBarClasses = 'repository--main-content--control-bar' + const expectedControlBarClasses = + 'repository--main-content--control-bar is-not-multi-select-mode' const controlBar = component.root.findByProps({ className: expectedControlBarClasses }) expect(controlBar.children.length).toBe(2) @@ -205,6 +209,28 @@ describe('Dashboard', () => { expect(component.root.findAllByType(ReactModal).length).toBe(0) } + const expectMultiSelectDashboardRender = () => { + dashboardProps.myModules = [...standardMyModules] + dashboardProps.multiSelectMode = true + const reusableComponent = + let component + act(() => { + component = create(reusableComponent) + }) + + const expectedControlBarClasses = 'repository--main-content--control-bar is-multi-select-mode' + const controlBar = component.root.findByProps({ className: expectedControlBarClasses }) + + expect(controlBar.children.length).toBe(3) + expect(component.root.findAllByType(Search).length).toBe(0) + + expectMultiSelectOptions(controlBar) + + const moduleComponents = component.root.findAllByType(Module) + expect(moduleComponents.length).toBe(5) + expect(moduleComponents[0].props.isMultiSelectMode).toBe(true) + } + const expectNormalModulesAreaClassesWithTitle = (mainContent, title) => { const expectedModulesTitleClasses = 'repository--main-content--title' expect(mainContent.children[1].props.className).toBe(expectedModulesTitleClasses) @@ -220,6 +246,12 @@ describe('Dashboard', () => { expect(multiButton.children[2].children[0].children[0]).toBe('Upload...') } + const expectMultiSelectOptions = controlBar => { + expect(controlBar.children[0].props.className).toBe('module-count') + expect(controlBar.children[1].children[0].children[0]).toBe('Delete All') + expect(controlBar.children[2].children[0].children[0]).toBe('×') + } + const expectCookiePropForPath = (prop, value, path) => { expect(document.cookie[prop].value).toBe(value) expect(document.cookie[prop].path).toBe(path) @@ -241,6 +273,10 @@ describe('Dashboard', () => { expectDashboardRender() }) + test('renders with multiSelectMode=true', () => { + expectMultiSelectDashboardRender() + }) + test('renders filtered modules properly', () => { dashboardProps.myModules = [...standardMyModules] dashboardProps.filterModules = jest.fn() @@ -347,6 +383,110 @@ describe('Dashboard', () => { dashboardProps.importModuleFile.mockReset() }) + test('"Delete All" and "Deselect All" buttons call functions appropriately', async () => { + dashboardProps.bulkDeleteModules = jest.fn() + dashboardProps.deselectModules = jest.fn() + dashboardProps.selectedModules = ['mockId2'] + dashboardProps.multiSelectMode = true + const component = create() + + const deleteAllButton = component.root.findAllByType(Button)[0] + expect(deleteAllButton.children[0].children[0]).toBe('Delete All') + + window.confirm = jest.fn() + window.confirm.mockReturnValueOnce(false) + await act(async () => { + const mockClickEvent = { + preventDefault: jest.fn() + } + deleteAllButton.props.onClick(mockClickEvent) + }) + expect(dashboardProps.bulkDeleteModules).not.toHaveBeenCalled() + + window.confirm.mockReturnValueOnce(true) + await act(async () => { + const mockClickEvent = { + preventDefault: jest.fn() + } + deleteAllButton.props.onClick(mockClickEvent) + }) + expect(dashboardProps.bulkDeleteModules).toHaveBeenCalledTimes(1) + + const deselectAllButton = component.root.findAllByType(Button)[1] + expect(deselectAllButton.children[0].children[0]).toBe('×') + + await act(async () => { + deselectAllButton.props.onClick() + }) + expect(dashboardProps.deselectModules).toHaveBeenCalled() + }) + + test('selecting module calls functions appropriately', () => { + dashboardProps.myModules = [...standardMyModules] + dashboardProps.selectModules = jest.fn() + dashboardProps.deselectModules = jest.fn() + + const component = create() + const moduleComponents = component.root.findAllByType(Module) + expect(moduleComponents[0].props.isSelected).toBe(false) + + act(() => { + const mockClickEvent = { + shiftKey: false + } + moduleComponents[0].props.onSelect(mockClickEvent) + }) + expect(dashboardProps.selectModules).toHaveBeenCalledTimes(1) + expect(moduleComponents[0].props.isSelected).toBe(true) + + act(() => { + const mockClickEvent = { + shiftKey: false + } + moduleComponents[0].props.onSelect(mockClickEvent) + }) + expect(dashboardProps.deselectModules).toHaveBeenCalledTimes(1) + expect(moduleComponents[0].props.isSelected).toBe(false) + }) + + test('selecting modules with shift calls functions appropriately', () => { + dashboardProps.myModules = [...standardMyModules] + dashboardProps.selectModules = jest.fn() + dashboardProps.deselectModules = jest.fn() + + const component = create() + const moduleComponents = component.root.findAllByType(Module) + + act(() => { + const mockClickEvent = { + shiftKey: false + } + moduleComponents[2].props.onSelect(mockClickEvent) + }) + expect(moduleComponents[0].props.isSelected).toBe(false) + expect(moduleComponents[1].props.isSelected).toBe(false) + expect(moduleComponents[2].props.isSelected).toBe(true) + + act(() => { + const mockClickEvent = { + shiftKey: true + } + moduleComponents[0].props.onSelect(mockClickEvent) + }) + expect(moduleComponents[0].props.isSelected).toBe(true) + expect(moduleComponents[1].props.isSelected).toBe(true) + expect(moduleComponents[2].props.isSelected).toBe(true) + + act(() => { + const mockClickEvent = { + shiftKey: true + } + moduleComponents[4].props.onSelect(mockClickEvent) + }) + expect(moduleComponents[3].props.isSelected).toBe(true) + expect(moduleComponents[4].props.isSelected).toBe(true) + }) + test('renders "Module Options" dialog', () => { dashboardProps.showModuleManageCollections = jest.fn() dashboardProps.showModulePermissions = jest.fn() diff --git a/packages/app/obojobo-repository/shared/components/module.test.js b/packages/app/obojobo-repository/shared/components/module.test.js index ff95e87012..5afca949e2 100644 --- a/packages/app/obojobo-repository/shared/components/module.test.js +++ b/packages/app/obojobo-repository/shared/components/module.test.js @@ -19,7 +19,9 @@ describe('Module', () => { draftId: 'mockDraftId', title: 'Mock Module Title', hasMenu: true, - isNew: false + isNew: false, + isSelected: false, + isMultiSelectMode: false } }) @@ -29,10 +31,15 @@ describe('Module', () => { expect(component.root.findAllByType(ModuleMenu).length).toBe(0) expect(component.root.children[0].props.className).toBe( - 'repository--module-icon is-not-open is-not-new' + 'repository--module-icon is-not-open is-not-selected is-not-new' ) - const mainChildComponent = component.root.children[0].children[0] + const checkboxComponent = component.root.children[0].children[0] + expect(checkboxComponent.type).toBe('input') + expect(checkboxComponent.props.checked).toBe(false) + expect(checkboxComponent.props.className).toBe('is-not-multi-select-mode') + + const mainChildComponent = component.root.children[0].children[1] expect(mainChildComponent.type).toBe('button') expect(component.toJSON()).toMatchSnapshot() @@ -45,10 +52,15 @@ describe('Module', () => { expect(component.root.findAllByType(ModuleMenu).length).toBe(0) expect(component.root.children[0].props.className).toBe( - 'repository--module-icon is-not-open is-not-new' + 'repository--module-icon is-not-open is-not-selected is-not-new' ) - const mainChildComponent = component.root.children[0].children[0] + const checkboxComponent = component.root.children[0].children[0] + expect(checkboxComponent.type).toBe('input') + expect(checkboxComponent.props.checked).toBe(false) + expect(checkboxComponent.props.className).toBe('is-not-multi-select-mode') + + const mainChildComponent = component.root.children[0].children[1] expect(mainChildComponent.type).toBe('a') expect(mainChildComponent.props.href).toBe('/library/mockDraftId') @@ -62,10 +74,57 @@ describe('Module', () => { expect(component.root.findAllByType(ModuleMenu).length).toBe(0) expect(component.root.children[0].props.className).toBe( - 'repository--module-icon is-not-open is-new' + 'repository--module-icon is-not-open is-not-selected is-new' + ) + + const checkboxComponent = component.root.children[0].children[0] + expect(checkboxComponent.type).toBe('input') + expect(checkboxComponent.props.checked).toBe(false) + expect(checkboxComponent.props.className).toBe('is-not-multi-select-mode') + + const mainChildComponent = component.root.children[0].children[1] + expect(mainChildComponent.type).toBe('button') + + expect(component.toJSON()).toMatchSnapshot() + }) + + test('renders with expected standard props but isSelected=true', () => { + defaultProps.isSelected = true + const component = create() + + expect(component.root.findAllByType(ModuleMenu).length).toBe(0) + + expect(component.root.children[0].props.className).toBe( + 'repository--module-icon is-not-open is-selected is-not-new' ) - const mainChildComponent = component.root.children[0].children[0] + const checkboxComponent = component.root.children[0].children[0] + expect(checkboxComponent.type).toBe('input') + expect(checkboxComponent.props.checked).toBe(true) + expect(checkboxComponent.props.className).toBe('is-not-multi-select-mode') + + const mainChildComponent = component.root.children[0].children[1] + expect(mainChildComponent.type).toBe('button') + + expect(component.toJSON()).toMatchSnapshot() + }) + + test('renders with expected standard props but isMultiSelectMode=true', () => { + defaultProps.isMultiSelectMode = true + const component = create() + + expect(component.root.findAllByType(ModuleMenu).length).toBe(0) + + expect(component.root.children[0].props.className).toBe( + 'repository--module-icon is-not-open is-not-selected is-not-new' + ) + + const checkboxComponent = component.root.children[0].children[0] + expect(checkboxComponent.type).toBe('input') + expect(checkboxComponent.props.checked).toBe(false) + expect(checkboxComponent.props.className).toBe('is-multi-select-mode') + + const mainChildComponent = component.root.children[0].children[1] expect(mainChildComponent.type).toBe('button') expect(component.toJSON()).toMatchSnapshot() @@ -84,13 +143,48 @@ describe('Module', () => { preventDefault: jest.fn() } act(() => { - component.root.children[0].children[0].props.onClick(mockClickEvent) + component.root.children[0].children[1].props.onClick(mockClickEvent) component.update(reusableComponent) }) expect(mockClickEvent.preventDefault).toHaveBeenCalledTimes(1) expect(component.root.findAllByType(ModuleMenu).length).toBe(1) }) + test('clicking the main child component when isMultiSelectMode=true selects the module', () => { + defaultProps.isMultiSelectMode = true + defaultProps.onSelect = jest.fn() + const reusableComponent = + let component + act(() => { + component = create(reusableComponent) + }) + + const mockClickEvent = { + preventDefault: jest.fn() + } + act(() => { + component.root.children[0].children[1].props.onClick(mockClickEvent) + component.update(reusableComponent) + }) + expect(mockClickEvent.preventDefault).toHaveBeenCalledTimes(1) + expect(defaultProps.onSelect).toHaveBeenCalledTimes(1) + }) + + test('clicking the input component selects the module', () => { + defaultProps.onSelect = jest.fn() + const reusableComponent = + let component + act(() => { + component = create(reusableComponent) + }) + + act(() => { + component.root.children[0].children[0].props.onClick() + component.update(reusableComponent) + }) + expect(defaultProps.onSelect).toHaveBeenCalledTimes(1) + }) + test('the module menu is not rendered after onMouseLeave is called', () => { const reusableComponent = let component @@ -104,7 +198,7 @@ describe('Module', () => { const mockClickEvent = { preventDefault: jest.fn() } - component.root.children[0].children[0].props.onClick(mockClickEvent) + component.root.children[0].children[1].props.onClick(mockClickEvent) component.update(reusableComponent) }) expect(component.root.findAllByType(ModuleMenu).length).toBe(1) @@ -137,7 +231,7 @@ describe('Module', () => { const mockClickEvent = { preventDefault: jest.fn() } - component.root.children[0].children[0].props.onClick(mockClickEvent) + component.root.children[0].children[1].props.onClick(mockClickEvent) component.update(reusableComponent) }) expect(component.root.findAllByType(ModuleMenu).length).toBe(1) diff --git a/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js b/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js index e4d8eb34cc..b6fd4db597 100644 --- a/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js +++ b/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.js @@ -11,6 +11,7 @@ const { CLEAR_PEOPLE_SEARCH_RESULTS, DELETE_MODULE_PERMISSIONS, DELETE_MODULE, + BULK_DELETE_MODULES, CREATE_NEW_MODULE, FILTER_MODULES, SELECT_MODULES, @@ -71,6 +72,21 @@ function DashboardReducer(state, action) { } }) + case BULK_DELETE_MODULES: + return handle(state, action, { + // update myModules, re-apply the filter, and exit multi-select mode + success: prevState => { + const filteredModules = filterModules(action.payload.value, state.moduleSearchString) + return { + ...prevState, + myModules: action.payload.value, + filteredModules, + selectedModules: [], + multiSelectMode: false + } + } + }) + case SHOW_MODULE_MORE: return { ...state, diff --git a/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.test.js b/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.test.js index 2c3047664c..dea6f8be9f 100644 --- a/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.test.js +++ b/packages/app/obojobo-repository/shared/reducers/dashboard-reducer.test.js @@ -16,8 +16,11 @@ const { CLEAR_PEOPLE_SEARCH_RESULTS, DELETE_MODULE_PERMISSIONS, DELETE_MODULE, + BULK_DELETE_MODULES, CREATE_NEW_MODULE, FILTER_MODULES, + SELECT_MODULES, + DESELECT_MODULES, SHOW_MODULE_MORE, SHOW_VERSION_HISTORY, RESTORE_VERSION @@ -192,6 +195,53 @@ describe('Dashboard Reducer', () => { runCreateOrDeleteModuleActionTest(DELETE_MODULE) }) + test('BULK_DELETE_MODULES action modifies state correctly', () => { + const mockModuleList = [ + { + draftId: 'mockDraftId', + title: 'A Mock Module' + }, + { + draftId: 'mockDraftId2', + title: 'B Mock Module' + } + ] + + const initialState = { + multiSelectMode: true, + moduleSearchString: '', + selectedModules: ['mockDraftId', 'mockDraftId3'], + myModules: [ + { + draftId: 'oldMockDraftId', + title: 'Old Mock Module' + } + ], + filteredModules: [ + { + draftId: 'oldMockDraftId', + title: 'Old Mock Module' + } + ] + } + + const action = { + type: BULK_DELETE_MODULES, + payload: { + value: mockModuleList + } + } + + const handler = dashboardReducer(initialState, action) + + const newState = handleSuccess(handler) + expect(newState.myModules).not.toEqual(initialState.myModules) + expect(newState.myModules).toEqual(mockModuleList) + expect(newState.filteredModules).toEqual(mockModuleList) + expect(newState.selectedModules).toEqual([]) + expect(newState.multiSelectMode).toBe(false) + }) + test('SHOW_MODULE_MORE action modifies state correctly', () => { const initialState = { dialog: null, @@ -289,6 +339,68 @@ describe('Dashboard Reducer', () => { expect(newState.moduleSearchString).toBe('B') }) + test('SELECT_MODULES action modifies state correctly', () => { + const initialState = { + multiSelectMode: false, + myModules: [ + { + draftId: 'mockDraftId', + title: 'A Mock Module' + }, + { + draftId: 'mockDraftId2', + title: 'B Mock Module' + }, + { + draftId: 'mockDraftId3', + title: 'C Mock Module' + } + ], + selectedModules: [] + } + const action = { + type: SELECT_MODULES, + draftIds: ['mockDraftId', 'mockDraftId3'] + } + + // SELECT_MODULES is a synchronous action - state changes immediately + const newState = dashboardReducer(initialState, action) + expect(newState.myModules).toEqual(initialState.myModules) + expect(newState.selectedModules).toEqual(['mockDraftId', 'mockDraftId3']) + expect(newState.multiSelectMode).toBe(true) + }) + + test('DESELECT_MODULES action modifies state correctly', () => { + const initialState = { + multiSelectMode: true, + myModules: [ + { + draftId: 'mockDraftId', + title: 'A Mock Module' + }, + { + draftId: 'mockDraftId2', + title: 'B Mock Module' + }, + { + draftId: 'mockDraftId3', + title: 'C Mock Module' + } + ], + selectedModules: ['mockDraftId', 'mockDraftId3'] + } + const action = { + type: DESELECT_MODULES, + draftIds: ['mockDraftId'] + } + + // DESELECT_MODULES is a synchronous action - state changes immediately + const newState = dashboardReducer(initialState, action) + expect(newState.myModules).toEqual(initialState.myModules) + expect(newState.selectedModules).toEqual(['mockDraftId3']) + expect(newState.multiSelectMode).toBe(true) + }) + test('CLEAR_PEOPLE_SEARCH_RESULTS action modifies state correctly', () => { const initialState = { shareSearchString: 'oldSearchString', From ee52fc7d0b4d90e884ad640ef32aad2634363e2d Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Tue, 25 May 2021 10:35:05 -0400 Subject: [PATCH 4/6] formatting fixes --- .../obojobo-repository/shared/components/module.jsx | 11 ++++++++--- .../obojobo-repository/shared/components/module.scss | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/app/obojobo-repository/shared/components/module.jsx b/packages/app/obojobo-repository/shared/components/module.jsx index b6ec2b0677..806893f8ca 100644 --- a/packages/app/obojobo-repository/shared/components/module.jsx +++ b/packages/app/obojobo-repository/shared/components/module.jsx @@ -49,13 +49,18 @@ const Module = props => { {props.hasMenu ? ( ) : ( diff --git a/packages/app/obojobo-repository/shared/components/module.scss b/packages/app/obojobo-repository/shared/components/module.scss index fcdfca9c78..f81cd40e5d 100644 --- a/packages/app/obojobo-repository/shared/components/module.scss +++ b/packages/app/obojobo-repository/shared/components/module.scss @@ -45,7 +45,8 @@ } } - &.is-open, &.is-selected { + &.is-open, + &.is-selected { background-color: $color-banner-bg; } From 60fa043b828364f278348b9ce4331a2745899cec Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Tue, 1 Jun 2021 10:45:22 -0400 Subject: [PATCH 5/6] fixes behavior when changing sort method in select mode --- .../shared/components/dashboard.jsx | 21 ++---- .../shared/components/dashboard.test.js | 75 ++++++++++++------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/packages/app/obojobo-repository/shared/components/dashboard.jsx b/packages/app/obojobo-repository/shared/components/dashboard.jsx index af0e4d02a5..5c83feb354 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard.jsx +++ b/packages/app/obojobo-repository/shared/components/dashboard.jsx @@ -124,7 +124,6 @@ function Dashboard(props) { const [lastSelectedIndex, setLastSelectedIndex] = useState(0) const moduleList = props.filteredModules ? props.filteredModules : props.myModules - const [selectStates, setSelectStates] = useState(moduleList.map(() => false)) const handleCreateNewModule = useTutorial => { props.createNewModule(useTutorial).then(data => { @@ -139,29 +138,22 @@ function Dashboard(props) { const [startIdx, endIdx] = lastSelectedIndex < index ? [lastSelectedIndex, index + 1] : [index, lastSelectedIndex + 1] const idList = moduleList.map(m => m.draftId) - selectStates.fill(true, startIdx, endIdx) props.selectModules( idList.slice(startIdx, endIdx).filter(id => !props.selectedModules.includes(id)) ) } else { - selectStates[index] = !selectStates[index] - selectStates[index] ? props.selectModules([draftId]) : props.deselectModules([draftId]) + props.selectedModules.includes(draftId) + ? props.deselectModules([draftId]) + : props.selectModules([draftId]) } - setSelectStates([...selectStates]) setLastSelectedIndex(index) } - const deselectAll = () => { - props.deselectModules(props.selectedModules) - setSelectStates([...selectStates.fill(false)]) - } - const deleteModules = draftIds => { const response = confirm(`Delete ${draftIds.length} selected modules?`) //eslint-disable-line no-alert, no-undef if (!response) return props.bulkDeleteModules(draftIds) - setSelectStates([...selectStates.fill(false)]) } // Set a cookie when sortOrder changes on the client @@ -195,7 +187,10 @@ function Dashboard(props) { > Delete All -
@@ -231,7 +226,7 @@ function Dashboard(props) { {moduleList.sort(getSortMethod(sortOrder)).map((draft, index) => ( handleSelectModule(e, draft.draftId, index)} key={draft.draftId} diff --git a/packages/app/obojobo-repository/shared/components/dashboard.test.js b/packages/app/obojobo-repository/shared/components/dashboard.test.js index eae015b8c6..7b732d875b 100644 --- a/packages/app/obojobo-repository/shared/components/dashboard.test.js +++ b/packages/app/obojobo-repository/shared/components/dashboard.test.js @@ -383,12 +383,15 @@ describe('Dashboard', () => { dashboardProps.importModuleFile.mockReset() }) - test('"Delete All" and "Deselect All" buttons call functions appropriately', async () => { + test('"Delete All" button calls functions appropriately', async () => { dashboardProps.bulkDeleteModules = jest.fn() - dashboardProps.deselectModules = jest.fn() - dashboardProps.selectedModules = ['mockId2'] + dashboardProps.selectedModules = ['mockId', 'mockId2'] dashboardProps.multiSelectMode = true - const component = create() + const reusableComponent = + let component + act(() => { + component = create(reusableComponent) + }) const deleteAllButton = component.root.findAllByType(Button)[0] expect(deleteAllButton.children[0].children[0]).toBe('Delete All') @@ -411,22 +414,36 @@ describe('Dashboard', () => { deleteAllButton.props.onClick(mockClickEvent) }) expect(dashboardProps.bulkDeleteModules).toHaveBeenCalledTimes(1) + }) + + test('"Deselect All" button calls functions appropriately', () => { + dashboardProps.deselectModules = jest.fn() + dashboardProps.selectedModules = ['mockId', 'mockId2'] + dashboardProps.multiSelectMode = true + const reusableComponent = + let component + act(() => { + component = create(reusableComponent) + }) const deselectAllButton = component.root.findAllByType(Button)[1] expect(deselectAllButton.children[0].children[0]).toBe('×') - await act(async () => { + act(() => { deselectAllButton.props.onClick() }) - expect(dashboardProps.deselectModules).toHaveBeenCalled() + expect(dashboardProps.deselectModules).toHaveBeenCalledWith(['mockId', 'mockId2']) }) test('selecting module calls functions appropriately', () => { dashboardProps.myModules = [...standardMyModules] dashboardProps.selectModules = jest.fn() dashboardProps.deselectModules = jest.fn() + let component + act(() => { + component = create() + }) - const component = create() const moduleComponents = component.root.findAllByType(Module) expect(moduleComponents[0].props.isSelected).toBe(false) @@ -437,6 +454,14 @@ describe('Dashboard', () => { moduleComponents[0].props.onSelect(mockClickEvent) }) expect(dashboardProps.selectModules).toHaveBeenCalledTimes(1) + expect(dashboardProps.selectModules).toHaveBeenCalledWith(['mockDraftId2']) + + dashboardProps.selectedModules = ['mockDraftId2'] + dashboardProps.multiSelectMode = true + + act(() => { + component.update() + }) expect(moduleComponents[0].props.isSelected).toBe(true) act(() => { @@ -446,45 +471,41 @@ describe('Dashboard', () => { moduleComponents[0].props.onSelect(mockClickEvent) }) expect(dashboardProps.deselectModules).toHaveBeenCalledTimes(1) - expect(moduleComponents[0].props.isSelected).toBe(false) + expect(dashboardProps.deselectModules).toHaveBeenCalledWith(['mockDraftId2']) }) test('selecting modules with shift calls functions appropriately', () => { dashboardProps.myModules = [...standardMyModules] dashboardProps.selectModules = jest.fn() - dashboardProps.deselectModules = jest.fn() - - const component = create() - const moduleComponents = component.root.findAllByType(Module) - + let component act(() => { - const mockClickEvent = { - shiftKey: false - } - moduleComponents[2].props.onSelect(mockClickEvent) + component = create() }) - expect(moduleComponents[0].props.isSelected).toBe(false) - expect(moduleComponents[1].props.isSelected).toBe(false) - expect(moduleComponents[2].props.isSelected).toBe(true) + + const moduleComponents = component.root.findAllByType(Module) act(() => { const mockClickEvent = { shiftKey: true } - moduleComponents[0].props.onSelect(mockClickEvent) + moduleComponents[2].props.onSelect(mockClickEvent) }) - expect(moduleComponents[0].props.isSelected).toBe(true) - expect(moduleComponents[1].props.isSelected).toBe(true) - expect(moduleComponents[2].props.isSelected).toBe(true) + expect(dashboardProps.selectModules).toHaveBeenCalledTimes(1) + expect(dashboardProps.selectModules).toHaveBeenCalledWith([ + 'mockDraftId2', + 'mockDraftId4', + 'mockDraftId3' + ]) + dashboardProps.selectModules.mockReset() act(() => { const mockClickEvent = { shiftKey: true } - moduleComponents[4].props.onSelect(mockClickEvent) + moduleComponents[1].props.onSelect(mockClickEvent) }) - expect(moduleComponents[3].props.isSelected).toBe(true) - expect(moduleComponents[4].props.isSelected).toBe(true) + expect(dashboardProps.selectModules).toHaveBeenCalledTimes(1) + expect(dashboardProps.selectModules).toHaveBeenCalledWith(['mockDraftId4', 'mockDraftId3']) }) test('renders "Module Options" dialog', () => { From 85da9bc001c0319167a3e9d736ef4ed43b4da548 Mon Sep 17 00:00:00 2001 From: Jacob Peterson Date: Thu, 3 Jun 2021 15:54:32 -0400 Subject: [PATCH 6/6] adds escape to deselect all + some behavior & CSS tweaks --- .../__snapshots__/module.test.js.snap | 5 ++ .../shared/components/dashboard.jsx | 19 +++++- .../shared/components/dashboard.scss | 5 ++ .../shared/components/dashboard.test.js | 63 ++++++++++++++++--- .../shared/components/module.jsx | 8 ++- .../shared/components/module.scss | 8 +-- 6 files changed, 91 insertions(+), 17 deletions(-) diff --git a/packages/app/obojobo-repository/shared/components/__snapshots__/module.test.js.snap b/packages/app/obojobo-repository/shared/components/__snapshots__/module.test.js.snap index 7e06803a4d..30a658dda2 100644 --- a/packages/app/obojobo-repository/shared/components/__snapshots__/module.test.js.snap +++ b/packages/app/obojobo-repository/shared/components/__snapshots__/module.test.js.snap @@ -10,6 +10,7 @@ exports[`Module renders with expected standard props 1`] = `