@@ -179,11 +238,17 @@ const Dashboard = props => {
- {renderModules(
- props.filteredModules ? props.filteredModules : props.myModules,
- sortOrder,
- newModuleId
- )}
+ {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 ec25830948..0f9367e9bf 100644
--- a/packages/app/obojobo-repository/shared/components/dashboard.scss
+++ b/packages/app/obojobo-repository/shared/components/dashboard.scss
@@ -6,5 +6,26 @@
justify-content: space-between;
align-items: center;
margin-bottom: $size-spacing-vertical-big;
+
+ &.is-multi-select-mode {
+ border: 1px solid $color-banner-bg;
+ background-color: $color-banner-bg;
+ border-radius: 0.15em;
+
+ .module-count {
+ font-size: 0.8em;
+ margin-left: 1em;
+ width: 40em;
+ }
+
+ button.secondary-button {
+ margin: 0.3em 0.2em;
+ font-size: 0.65em;
+ }
+
+ button.close-button {
+ background: none;
+ }
+ }
}
}
diff --git a/packages/app/obojobo-repository/shared/components/dashboard.test.js b/packages/app/obojobo-repository/shared/components/dashboard.test.js
index 4668c5fe8e..aba20ed5ac 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)
@@ -203,6 +207,32 @@ describe('Dashboard', () => {
// Shouldn't be any modal dialogs open, either
expect(component.root.findAllByType(ReactModal).length).toBe(0)
+
+ component.unmount()
+ }
+
+ 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)
+
+ component.unmount()
}
const expectNormalModulesAreaClassesWithTitle = (mainContent, title) => {
@@ -220,6 +250,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 +277,10 @@ describe('Dashboard', () => {
expectDashboardRender()
})
+ test('renders with multiSelectMode=true', () => {
+ expectMultiSelectDashboardRender()
+ })
+
test('renders filtered modules properly', () => {
dashboardProps.myModules = [...standardMyModules]
dashboardProps.filterModules = jest.fn()
@@ -289,6 +329,8 @@ describe('Dashboard', () => {
expect(moduleComponents.length).toBe(2)
expect(moduleComponents[0].props.draftId).toBe('mockDraftId4')
expect(moduleComponents[1].props.draftId).toBe('mockDraftId3')
+
+ component.unmount()
})
test('"New Module" and "Upload..." buttons call functions appropriately', async () => {
@@ -345,6 +387,162 @@ describe('Dashboard', () => {
})
expect(dashboardProps.importModuleFile).toHaveBeenCalledTimes(1)
dashboardProps.importModuleFile.mockReset()
+
+ component.unmount()
+ })
+
+ test('"Delete All" button calls functions appropriately', async () => {
+ dashboardProps.bulkDeleteModules = jest.fn()
+ dashboardProps.selectedModules = ['mockId', 'mockId2']
+ dashboardProps.multiSelectMode = true
+ const reusableComponent =
+ let component
+ act(() => {
+ component = create(reusableComponent)
+ })
+
+ const mockClickEvent = {
+ preventDefault: jest.fn()
+ }
+
+ const deleteAllButton = component.root.findAllByType(Button)[0]
+ expect(deleteAllButton.children[0].children[0]).toBe('Delete All')
+
+ window.prompt = jest.fn()
+ window.prompt.mockReturnValueOnce('NOT DELETE')
+ await act(async () => {
+ deleteAllButton.props.onClick(mockClickEvent)
+ })
+ expect(dashboardProps.bulkDeleteModules).not.toHaveBeenCalled()
+
+ window.prompt.mockReturnValueOnce('DELETE')
+ await act(async () => {
+ deleteAllButton.props.onClick(mockClickEvent)
+ })
+ expect(dashboardProps.bulkDeleteModules).toHaveBeenCalledTimes(1)
+
+ component.unmount()
+ })
+
+ 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('×')
+
+ act(() => {
+ deselectAllButton.props.onClick()
+ })
+ expect(dashboardProps.deselectModules).toHaveBeenCalledWith(['mockId', 'mockId2'])
+
+ component.unmount()
+ })
+
+ test('pressing Esc when multiSelectMode=true calls functions appropriately', () => {
+ dashboardProps.deselectModules = jest.fn()
+ let component
+ act(() => {
+ component = create(
)
+ })
+
+ // eslint-disable-next-line no-undef
+ document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }))
+ expect(dashboardProps.deselectModules).toHaveBeenCalledTimes(0)
+
+ dashboardProps.multiSelectMode = true
+ act(() => {
+ component.update(
)
+ })
+
+ // eslint-disable-next-line no-undef
+ document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }))
+ expect(dashboardProps.deselectModules).toHaveBeenCalledTimes(1)
+
+ component.unmount()
+ })
+
+ test('selecting module calls functions appropriately', () => {
+ dashboardProps.myModules = [...standardMyModules]
+ dashboardProps.selectModules = jest.fn()
+ dashboardProps.deselectModules = jest.fn()
+ let component
+ act(() => {
+ 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(dashboardProps.selectModules).toHaveBeenCalledWith(['mockDraftId2'])
+
+ dashboardProps.selectedModules = ['mockDraftId2']
+ dashboardProps.multiSelectMode = true
+
+ act(() => {
+ component.update(
)
+ })
+ expect(moduleComponents[0].props.isSelected).toBe(true)
+
+ act(() => {
+ const mockClickEvent = {
+ shiftKey: false
+ }
+ moduleComponents[0].props.onSelect(mockClickEvent)
+ })
+ expect(dashboardProps.deselectModules).toHaveBeenCalledTimes(1)
+ expect(dashboardProps.deselectModules).toHaveBeenCalledWith(['mockDraftId2'])
+
+ component.unmount()
+ })
+
+ test('selecting modules with shift calls functions appropriately', () => {
+ dashboardProps.myModules = [...standardMyModules]
+ dashboardProps.selectModules = jest.fn()
+ let component
+ act(() => {
+ component = create(
)
+ })
+
+ const moduleComponents = component.root.findAllByType(Module)
+
+ act(() => {
+ const mockClickEvent = {
+ shiftKey: true
+ }
+ moduleComponents[2].props.onSelect(mockClickEvent)
+ })
+ expect(dashboardProps.selectModules).toHaveBeenCalledTimes(1)
+ expect(dashboardProps.selectModules).toHaveBeenCalledWith([
+ 'mockDraftId2',
+ 'mockDraftId4',
+ 'mockDraftId3'
+ ])
+ dashboardProps.selectModules.mockReset()
+
+ act(() => {
+ const mockClickEvent = {
+ shiftKey: true
+ }
+ moduleComponents[1].props.onSelect(mockClickEvent)
+ })
+ expect(dashboardProps.selectModules).toHaveBeenCalledTimes(1)
+ expect(dashboardProps.selectModules).toHaveBeenCalledWith(['mockDraftId4', 'mockDraftId3'])
+
+ component.unmount()
})
test('renders "Module Options" dialog', () => {
@@ -367,6 +565,8 @@ describe('Dashboard', () => {
dialogComponent.props.onClose()
expectMethodToBeCalledOnceWith(dashboardProps.closeModal)
+
+ component.unmount()
})
test('renders "Module Access" dialog', () => {
@@ -389,6 +589,8 @@ describe('Dashboard', () => {
dialogComponent.props.onClose()
expectMethodToBeCalledOnceWith(dashboardProps.closeModal)
+
+ component.unmount()
})
test('renders "Version History" dialog and runs callbacks properly', () => {
@@ -410,6 +612,8 @@ describe('Dashboard', () => {
dialogComponent.props.onClose()
expect(dashboardProps.closeModal).toHaveBeenCalledTimes(1)
+
+ component.unmount()
})
test('renders no dialogs if props.dialog value is unsupported', () => {
@@ -420,5 +624,7 @@ describe('Dashboard', () => {
})
expect(component.root.findAllByType(ReactModal).length).toBe(0)
+
+ component.unmount()
})
})
diff --git a/packages/app/obojobo-repository/shared/components/module.jsx b/packages/app/obojobo-repository/shared/components/module.jsx
index 500524aecc..3467e5352a 100644
--- a/packages/app/obojobo-repository/shared/components/module.jsx
+++ b/packages/app/obojobo-repository/shared/components/module.jsx
@@ -9,10 +9,19 @@ const Module = props => {
let timeOutId
const [isMenuOpen, setMenuOpen] = useState(false)
const onCloseMenu = () => setMenuOpen(false)
- const onToggleMenu = e => {
- setMenuOpen(!isMenuOpen)
+ const handleClick = e => {
+ if (props.isMultiSelectMode || e.shiftKey || e.metaKey) {
+ onSelectModule(e)
+ } else {
+ setMenuOpen(!isMenuOpen)
+ }
+
e.preventDefault() // block the event from bubbling out to the parent href
}
+ const onSelectModule = e => {
+ onCloseMenu()
+ props.onSelect(e)
+ }
// Handle keyboard focus
const onBlurHandler = () => {
timeOutId = setTimeout(() => {
@@ -29,19 +38,33 @@ 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 ? (
-