Skip to content

Commit

Permalink
added details sidebar for multiple selection
Browse files Browse the repository at this point in the history
  • Loading branch information
lookacat committed Aug 5, 2021
1 parent 406b5d0 commit c719dc2
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Add multiple selection Sidebar

We've changed the sidebar so if a user selects multiple files or folders
he sees a detailed view of his selection in the sidebar.

https://github.com/owncloud/web/issues/5164
https://github.com/owncloud/web/pull/5630
75 changes: 64 additions & 11 deletions packages/web-app-files/src/components/SideBar/SideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
:class="{
'is-active':
appSidebarActivePanel === panelMeta.app || (!appSidebarActivePanel && panelMeta.default),
'sidebar-panel--default': panelMeta.default
'sidebar-panel--default': panelMeta.default,
'sidebar-multiple-selected': multipleSelected
}"
>
<div class="sidebar-panel__header header">
Expand All @@ -25,7 +26,7 @@
class="header__back"
appearance="raw"
:aria-label="accessibleLabelBack"
@click="() => SET_APP_SIDEBAR_ACTIVE_PANEL(null)"
@click="closePanel"
>
<oc-icon name="chevron_left" />
{{ defaultPanelMeta.component.title($gettext) }}
Expand All @@ -44,18 +45,25 @@
<oc-icon name="close" />
</oc-button>
</div>
<file-info class="sidebar-panel__file_info" :is-content-displayed="isContentDisplayed" />
<file-info
v-if="!multipleSelected"
class="sidebar-panel__file_info"
:is-content-displayed="isContentDisplayed"
/>
<div class="sidebar-panel__body">
<template v-if="isContentDisplayed">
<component :is="panelMeta.component" />
<component
:is="panelMeta.component"
v-if="[activePanel, oldPanel].includes(panelMeta.app)"
/>

<div v-if="panelMeta.default && panelMetas.length > 1" class="sidebar-panel__navigation">
<oc-button
v-for="panelSelect in panelMetas.filter(p => !p.default)"
:id="`sidebar-panel-${panelSelect.app}-select`"
:key="`panel-select-${panelSelect.app}`"
appearance="raw"
@click="SET_APP_SIDEBAR_ACTIVE_PANEL(panelSelect.app)"
@click="openPanel(panelSelect.app)"
>
<oc-icon :name="panelSelect.icon" />
{{ panelSelect.component.title($gettext) }}
Expand All @@ -76,26 +84,32 @@ import MixinRoutes from '../../mixins/routes'
import { VisibilityObserver } from 'web-pkg/src/observer'
let visibilityObserver
let hiddenObserver
export default {
components: { FileInfo },
mixins: [MixinRoutes],
data() {
return {
focused: undefined
focused: undefined,
oldPanel: null
}
},
computed: {
...mapGetters('Files', ['highlightedFile']),
...mapGetters('Files', ['highlightedFile', 'selectedFiles']),
...mapGetters(['fileSideBars', 'capabilities']),
...mapState('Files', ['appSidebarActivePanel']),
activePanel() {
return this.appSidebarActivePanel || this.defaultPanelMeta.app
},
panelMetas() {
const { panels } = this.fileSideBars.reduce(
(result, panelGenerator) => {
const panel = panelGenerator({
capabilities: this.capabilities,
highlightedFile: this.highlightedFile,
route: this.$route
route: this.$route,
multipleSelection: this.multipleSelected
})
if (panel.enabled) {
Expand All @@ -110,7 +124,7 @@ export default {
return panels
},
defaultPanelMeta() {
return this.panelMetas.find(panel => panel.default)
return this.panelMetas.find(panel => panel.default && panel.enabled)
},
accessibleLabelBack() {
const translated = this.$gettext('Back to %{panel} panel')
Expand All @@ -134,21 +148,34 @@ export default {
}
return null
},
multipleSelected() {
return this.selectedFiles.length > 1
}
},
watch: {
appSidebarActivePanel(panel, select) {
this.focused = panel ? `#sidebar-panel-${panel}` : `#sidebar-panel-select-${select}`
activePanel: {
handler: function(panel, select) {
this.$nextTick(() => {
this.focused = panel ? `#sidebar-panel-${panel}` : `#sidebar-panel-select-${select}`
})
},
immediate: true
}
},
beforeDestroy() {
visibilityObserver.disconnect()
hiddenObserver.disconnect()
},
mounted() {
visibilityObserver = new VisibilityObserver({
root: document.querySelector('#files-sidebar'),
threshold: 0.9
})
hiddenObserver = new VisibilityObserver({
root: document.querySelector('#files-sidebar'),
threshold: 0.05
})
const doFocus = () => {
const selector = document.querySelector(this.focused)
Expand All @@ -160,13 +187,21 @@ export default {
selector.focus()
}
const clearOldPanel = () => {
this.oldPanel = null
}
this.$refs.panels.forEach(panel => {
visibilityObserver.observe(panel, {
onEnter: doFocus,
onExit: doFocus
})
hiddenObserver.observe(panel, {
onExit: clearOldPanel
})
})
},
methods: {
...mapMutations('Files', ['SET_APP_SIDEBAR_ACTIVE_PANEL']),
close() {
Expand All @@ -186,6 +221,20 @@ export default {
) {
this.close()
}
},
setOldPanel() {
this.oldPanel = this.activePanel || this.defaultPanelMeta.app
},
openPanel(panel) {
this.setOldPanel()
this.SET_APP_SIDEBAR_ACTIVE_PANEL(panel)
},
closePanel() {
this.setOldPanel()
this.SET_APP_SIDEBAR_ACTIVE_PANEL(null)
}
}
}
Expand Down Expand Up @@ -220,6 +269,10 @@ export default {
transition-duration: 0.001ms !important;
}
&.sidebar-multiple-selected {
grid-template-rows: 50px 1fr;
}
&--default {
#files-sidebar.has-active & {
transform: translateX(-30%);
Expand Down
43 changes: 35 additions & 8 deletions packages/web-app-files/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import FilesDrop from './views/FilesDrop.vue'
import LocationPicker from './views/LocationPicker.vue'
import PublicFiles from './views/PublicFiles.vue'
import FileDetails from './components/SideBar/Details/FileDetails.vue'
import FileDetailsMultiple from './components/SideBar/Details/FileDetailsMultiple.vue'
import FileActions from './components/SideBar/Actions/FileActions.vue'
import FileVersions from './components/SideBar/Versions/FileVersions.vue'
import FileShares from './components/SideBar/Shares/FileShares.vue'
Expand All @@ -20,6 +21,9 @@ import translationsJson from '../l10n/translations.json'
import quickActionsImport from './quickActions'
import store from './store'
import { isTrashbinRoute } from './helpers/route'
import { FilterSearch, SDKSearch } from './search'
import { bus } from 'web-pkg/src/instance'
import { Registry } from './services'

// just a dummy function to trick gettext tools
function $gettext(msg) {
Expand All @@ -35,27 +39,39 @@ const appInfo = {
fileSideBars: [
// We don't have file details in the trashbin, yet.
// Only allow `actions` panel on trashbin route for now.
({ route }) => ({
({ route, multipleSelection }) => ({
app: 'details-item',
icon: 'info_outline',
component: FileDetails,
default: !isTrashbinRoute(route),
get enabled() {
return !isTrashbinRoute(route)
return !isTrashbinRoute(route) && !multipleSelection
}
}),
({ route }) => ({
({ route, multipleSelection }) => ({
app: 'details-item',
icon: 'info_outline',
component: FileDetailsMultiple,
default: !isTrashbinRoute(route),
get enabled() {
return !isTrashbinRoute(route) && multipleSelection
}
}),
({ route, multipleSelection }) => ({
app: 'actions-item',
component: FileActions,
icon: 'slideshow',
default: isTrashbinRoute(route),
enabled: true
get enabled() {
return !multipleSelection
}
}),
({ capabilities, route }) => ({
({ capabilities, route, multipleSelection }) => ({
app: 'sharing-item',
icon: 'group',
component: FileShares,
get enabled() {
if (multipleSelection) return false
if (isTrashbinRoute(route)) {
return false
}
Expand All @@ -66,11 +82,12 @@ const appInfo = {
return false
}
}),
({ capabilities, route }) => ({
({ capabilities, route, multipleSelection }) => ({
app: 'links-item',
icon: 'link',
component: FileLinks,
get enabled() {
if (multipleSelection) return false
if (isTrashbinRoute(route)) {
return false
}
Expand All @@ -81,11 +98,12 @@ const appInfo = {
return false
}
}),
({ capabilities, highlightedFile, route }) => ({
({ capabilities, highlightedFile, route, multipleSelection }) => ({
app: 'versions-item',
icon: 'file_version',
component: FileVersions,
get enabled() {
if (multipleSelection) return false
if (isTrashbinRoute(route)) {
return false
}
Expand Down Expand Up @@ -300,5 +318,14 @@ export default {
routes,
navItems,
quickActions,
translations
translations,
mounted({ router: runtimeRouter, store: runtimeStore }) {
Registry.filterSearch = new FilterSearch(runtimeStore, runtimeRouter)
Registry.sdkSearch = new SDKSearch(runtimeStore, runtimeRouter)

// when discussing the boot process of applications we need to implement a
// registry that does not rely on call order, aka first register "on" and only after emit.
bus.emit('app.search.register.provider', Registry.filterSearch)
bus.emit('app.search.register.provider', Registry.sdkSearch)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Vuex from 'vuex'
import FileDetailsMultiple from 'packages/web-app-files/src/components/SideBar/Details/FileDetailsMultiple.vue'
import stubs from '../../../../../../../tests/unit/stubs'
import GetTextPlugin from 'vue-gettext'
import AsyncComputed from 'vue-async-computed'

const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(AsyncComputed)
localVue.use(GetTextPlugin, {
translations: 'does-not-matter.json',
silent: true
})

const selectors = {
selectedFilesText: '[data-testid="selectedFilesText"]',
filesCount: '[data-testid="filesCount"]',
foldersCount: '[data-testid="foldersCount"]',
size: '[data-testid="size"]'
}

const folderA = {
type: 'folder',
ownerId: 'marie',
ownerDisplayName: 'Marie',
mdate: 'Wed, 21 Oct 2015 07:28:00 GMT',
size: '740'
}
const folderB = {
type: 'folder',
ownerId: 'marie',
ownerDisplayName: 'Marie',
mdate: 'Wed, 21 Oct 2015 07:28:00 GMT',
size: '740'
}
const fileA = {
type: 'file',
ownerId: 'marie',
ownerDisplayName: 'Marie',
mdate: 'Wed, 21 Oct 2015 07:28:00 GMT',
size: '740'
}
const fileB = {
type: 'file',
ownerId: 'marie',
ownerDisplayName: 'Marie',
mdate: 'Wed, 21 Oct 2015 07:28:00 GMT',
size: '740'
}

describe('Details Multiple Selection SideBar Item', () => {
it('should display information for two selected folders', () => {
const wrapper = createWrapper([folderA, folderB])
expect(wrapper.find(selectors.selectedFilesText).text()).toBe('2 items selected')
expect(wrapper.find(selectors.filesCount).text()).toBe('Files 0')
expect(wrapper.find(selectors.foldersCount).text()).toBe('Folders 2')
expect(wrapper.find(selectors.size).text()).toBe('Size 1 KB')
})
it('should display information for two selected files', () => {
const wrapper = createWrapper([fileA, fileB])
expect(wrapper.find(selectors.selectedFilesText).text()).toBe('2 items selected')
expect(wrapper.find(selectors.filesCount).text()).toBe('Files 2')
expect(wrapper.find(selectors.foldersCount).text()).toBe('Folders 0')
expect(wrapper.find(selectors.size).text()).toBe('Size 1 KB')
})
it('should display information for one selected file, one selected folder', () => {
const wrapper = createWrapper([fileA, folderA])
expect(wrapper.find(selectors.selectedFilesText).text()).toBe('2 items selected')
expect(wrapper.find(selectors.filesCount).text()).toBe('Files 1')
expect(wrapper.find(selectors.foldersCount).text()).toBe('Folders 1')
expect(wrapper.find(selectors.size).text()).toBe('Size 1 KB')
})
})

function createWrapper(testResource) {
return shallowMount(FileDetailsMultiple, {
store: new Vuex.Store({
modules: {
Files: {
namespaced: true,
getters: {
selectedFiles: function() {
return testResource
}
}
}
}
}),
localVue,
stubs: stubs
})
}

0 comments on commit c719dc2

Please sign in to comment.