Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate app switcher from app navigation sidebar #2669

Merged
merged 1 commit into from
Dec 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
},
"apps" : [
"files"
]
],
"applications" : []
}
14 changes: 12 additions & 2 deletions config.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,15 @@
"url": "https://demo.owncloud.com/index.php/apps/oauth2/api/v1/token",
"authUrl": "https://demo.owncloud.com/index.php/apps/oauth2/authorize"
},
"apps" : ["files"]
}
"apps" : ["files"],
"applications": [{
"title": {
"en": "Files",
"de": "Dateien",
"fr": "Fichiers",
"zh_CN": "文件"
},
"icon": "folder",
"url": "http://demo.owncloud.com/index.html#/files/list"
}]
}
76 changes: 70 additions & 6 deletions src/Phoenix.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<router-view name="fullscreen"></router-view>
</template>
<template v-else>
<message-bar />
<top-bar></top-bar>
<side-menu></side-menu>
<message-bar :active-messages="activeMessages" @deleteMessage="$_deleteMessage" />
<top-bar :applicationsList="$_applicationsList" :activeNotifications="activeNotifications" :user-id="user.id" :user-display-name="user.displayname" :hasAppNavigation="!!appNavigationEntries.length" @toggleAppNavigation="$_toggleAppNavigation(!appNavigationVisible)"></top-bar>
<side-menu :visible="appNavigationVisible" :entries="appNavigationEntries" @closed="$_toggleAppNavigation(false)"></side-menu>
<main id="main">
<router-view id="oc-app-container" name="app" class="uk-height-1-1"></router-view>
</main>
Expand All @@ -31,6 +31,17 @@ export default {
TopBar,
SkipTo
},
data () {
return {
appNavigationVisible: false,
$_notificationsInterval: null
}
},
destroyed () {
if (this.$_notificationsInterval) {
clearInterval(this.$_notificationsInterval)
}
},
metaInfo () {
const metaInfo = {
title: this.configuration.theme.general.name
Expand All @@ -46,8 +57,28 @@ export default {
this.initAuth()
},
computed: {
...mapState(['route']),
...mapGetters(['configuration']),
...mapState(['route', 'user']),
...mapGetters(['configuration', 'activeNotifications', 'activeMessages', 'capabilities']),
$_applicationsList () {
return this.configuration.applications
},

appNavigationEntries () {
if (this.publicPage()) {
return []
}
// FIXME: use store or other ways, not $root
return this.$root.navItems.filter(item => {
// FIXME: filter to only show current app
if (item.enabled === undefined) {
return true
}
if (this.capabilities === undefined) {
return false
}
return item.enabled(this.capabilities)
})
},
showHeader () {
return this.$route.meta.hideHeadbar !== true
},
Expand All @@ -56,7 +87,40 @@ export default {
}
},
methods: {
...mapActions(['initAuth'])
...mapActions(['initAuth', 'fetchNotifications', 'deleteMessage']),
$_toggleAppNavigation (state) {
this.appNavigationVisible = state
},
$_updateNotifications () {
this.fetchNotifications(this.$client).catch((error) => {
console.error('Error while loading notifications: ', error)
clearInterval(this.$_notificationsInterval)
})
},
$_deleteMessage (item) {
this.deleteMessage(item)
}
},
watch: {
$route () {
this.appNavigationVisible = false
},
capabilities (caps) {
if (!caps) {
// capabilities not loaded yet
return
}

// setup periodic loading of notifications if the server supports them
if (caps.notifications) {
this.$nextTick(() => {
this.$_updateNotifications()
})
this.$_notificationsInterval = setInterval(() => {
this.$_updateNotifications()
}, 30000)
}
}
}
}
</script>
Expand Down
88 changes: 88 additions & 0 deletions src/components/ApplicationsMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<template>
<div v-if="!!applicationsList.length">
<oc-button id="_appSwitcherButton" icon="apps" variation="primary" class="oc-topbar-menu-burger uk-height-1-1" aria-label="$gettext('Application Switcher')" ref="menubutton" />
<oc-drop toggle="#_appSwitcherButton" mode="click" :options="{pos:'bottom-right'}" class="uk-width-large" ref="menu">
<div class="uk-grid-small uk-text-center" uk-grid>
<div class="uk-width-1-3" v-for="(n, nid) in $_applicationsList" :key="nid">
<a target="_blank" :href="n.url">
<oc-icon v-if="n.iconMaterial" :name="n.iconMaterial" size="large" />
<oc-icon v-if="n.iconUrl" :url="n.iconUrl" size="large" />
<div>{{ n.title }}</div>
</a>
</div>
</div>
</oc-drop>
</div>
</template>

<script>
export default {
props: {
visible: {
type: Boolean,
required: false,
default: false
},
applicationsList: {
type: Array,
required: false,
default: () => null
}
},
watch: {
visible (val) {
if (val) {
this.focusFirstLink()
} else {
this.$emit('closed')
}
}
},
computed: {
$_applicationsList () {
return this.applicationsList.map((item) => {
const lang = this.$language.current
// TODO: move language resolution to a common function
// FIXME: need to handle logic for variants like en_US vs en_GB
let title = item.title.en
let iconMaterial
let iconUrl
if (item.title[lang]) {
title = item.title[lang]
}

if (!item.icon) {
iconMaterial = 'deprecated' // broken icon
} else if (item.icon.indexOf('.') < 0) {
// not a file name or URL, treat as a material icon name instead
iconMaterial = item.icon
} else {
iconUrl = item.icon
}
return {
iconMaterial: iconMaterial,
iconUrl: iconUrl,
title: title,
url: item.url
}
})
}
},
methods: {
logout () {
this.visible = false
this.$store.dispatch('logout')
},
focusFirstLink () {
/*
* Delay for two reasons:
* - for screen readers Virtual buffer
* - to outsmart uikit's focus management
*/
setTimeout(() => {
this.$refs.menu.$el.querySelector('a:first-of-type').focus()
}, 500)
}
}
}
</script>
22 changes: 19 additions & 3 deletions src/components/Avatar.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div v-if="enabled">
<oc-avatar :width=42 :height=42 :loading="loading" :src="avatarSource" />
</div>
<component :is="type" v-if="enabled">
<oc-avatar :width="width" :height="width" :loading="loading" :src="avatarSource" />
</component>
</template>
<script>
import { mapGetters } from 'vuex'
Expand Down Expand Up @@ -73,11 +73,27 @@ export default {
}
},
props: {
/**
* The html element used for the avatar container.
* `div, span`
*/
type: {
type: String,
default: 'div',
validator: value => {
return value.match(/(div|span)/)
}
},
userid: {
/**
* Allow empty string to show placeholder
*/
default: ''
},
width: {
type: Number,
required: false,
default: 42
}
}
}
Expand Down
69 changes: 19 additions & 50 deletions src/components/Menu.vue
Original file line number Diff line number Diff line change
@@ -1,70 +1,38 @@
<template>
<oc-application-menu name="coreMenu" v-model="sidebarIsVisible" :inert="!sidebarIsVisible" @close="sidebarIsVisible = false" ref="sidebar">
<oc-sidebar-nav-item v-for="(n, nid) in nav" :active="isActive(n)" :key="nid" :icon="n.iconMaterial" :target="n.route ? n.route.path : null" @click="openItem(n.url)">{{ translateMenu(n) }}</oc-sidebar-nav-item>
<oc-sidebar-nav-item icon="account_circle" target="/account" :isolate="true">
<translate>Account</translate>
</oc-sidebar-nav-item>

<oc-sidebar-nav-item id="logoutMenuItem" active icon="exit_to_app" @click="logout()" :isolate="true">{{ _logoutItemText }}</oc-sidebar-nav-item>

<span class="uk-position-bottom uk-padding-small">Version: {{appVersion.version}}-{{appVersion.hash}} ({{appVersion.buildDate}})</span>
<oc-application-menu name="coreMenu" v-model="visible" :inert="!visible" ref="sidebar" @close="close">
<oc-sidebar-nav-item v-for="(n, nid) in entries" :active="isActive(n)" :key="nid" :icon="n.iconMaterial" :target="n.route ? n.route.path : null" @click="openItem(n.url)">{{ translateMenu(n) }}</oc-sidebar-nav-item>
</oc-application-menu>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'
import appVersionJson from '../../build/version.json'

export default {
props: {
visible: {
type: Boolean,
required: false,
default: false
},
entries: {
type: Array,
required: false,
default: () => []
}
},
data () {
return {
isOpen: false,
appVersion: appVersionJson
}
},
watch: {
$route () {
this.toggleSidebar(false)
},
sidebarIsVisible (val) {
visible (val) {
this.isOpen = val
if (val) {
this.focusFirstLink()
}
}
},
computed: {
nav () {
return this.$root.navItems.filter(item => {
if (item.enabled === undefined) {
return true
}
if (this.capabilities === undefined) {
return false
}
return item.enabled(this.capabilities)
})
},
...mapGetters(['isSidebarVisible', 'configuration', 'capabilities']),
sidebarIsVisible: {
get () {
return this.isSidebarVisible
},
set (newVal) {
if (newVal) {
return
}
this.toggleSidebar(newVal)
}
},
_logoutItemText () {
return this.$gettextInterpolate(this.$gettext('Exit %{product}'), { product: this.configuration.theme.general.name })
}
},
methods: {
...mapActions(['toggleSidebar']),
logout () {
this.sidebarIsVisible = false
this.$store.dispatch('logout')
close () {
this.$emit('closed')
},
navigateTo (route) {
this.$router.push(route)
Expand All @@ -77,6 +45,7 @@ export default {
const win = window.open(url, '_blank')
win.focus()
}
this.close()
},
isActive (navItem) {
return navItem.route.name === this.$route.name
Expand Down
15 changes: 10 additions & 5 deletions src/components/MessageBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
props: {
activeMessages: {
type: Array,
required: false,
default: () => []
}
},
methods: {
...mapActions(['deleteMessage'])
deleteMessage (item) {
this.$emit('deleteMessage', item)
}
},
computed: {
...mapGetters(['activeMessages']),

$_ocMessages_limited () {
return this.activeMessages ? this.activeMessages.slice(0, 5) : []
}
Expand Down
Loading