Port user menu to Vue

Signed-off-by: Christopher Ng <chrng8@gmail.com>
pull/36232/head
Christopher Ng 3 years ago
parent e47d56ac36
commit c77998209f
  1. 2
      apps/user_status/appinfo/info.xml
  2. 7
      apps/user_status/src/UserStatus.vue
  3. 47
      apps/user_status/src/menu.js
  4. 2
      core/css/header.css
  5. 2
      core/css/header.css.map
  6. 136
      core/css/header.scss
  7. 2
      core/css/server.css
  8. 2
      core/css/server.css.map
  9. 45
      core/src/components/UserMenu.js
  10. 106
      core/src/components/UserMenu/UserMenuEntry.vue
  11. 184
      core/src/views/UserMenu.vue
  12. 40
      core/templates/layout.user.php
  13. 4
      dist/core-common.js
  14. 2
      dist/core-common.js.LICENSE.txt
  15. 2
      dist/core-common.js.map
  16. 4
      dist/core-main.js
  17. 2
      dist/core-main.js.map
  18. 4
      dist/user_status-menu.js
  19. 2
      dist/user_status-menu.js.map
  20. 4
      lib/private/TemplateLayout.php
  21. 6
      tests/acceptance/features/bootstrap/SettingsMenuContext.php

@ -13,7 +13,7 @@
<bugs>https://github.com/nextcloud/server</bugs>
<navigations>
<navigation>
<id>user_status-menuitem</id>
<id>user_status-menu-entry</id>
<name>User status</name>
<order>1</order>
<type>settings</type>

@ -26,7 +26,7 @@
<a v-if="!inline"
class="user-status-menu-item__header"
:href="profilePageLink"
@click="loadProfilePage">
@click.exact="loadProfilePage">
<div class="user-status-menu-item__header-content">
<div class="user-status-menu-item__header-content-displayname">{{ displayName }}</div>
<div v-if="!loadingProfilePage" class="user-status-menu-item__header-content-placeholder" />
@ -234,11 +234,6 @@ export default {
align-items: flex-start !important;
color: var(--color-main-text) !important;
&:focus-visible {
padding: 6px 8px 1px 8px !important;
margin: 2px !important;
}
&:not([href]) {
height: var(--header-menu-item-height) !important;
color: var(--color-text-maxcontrast) !important;

@ -24,10 +24,11 @@
import Vue from 'vue'
import { getRequestToken } from '@nextcloud/auth'
import UserStatus from './UserStatus'
import store from './store'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar'
import { loadState } from '@nextcloud/initial-state'
import { subscribe } from '@nextcloud/event-bus'
import UserStatus from './UserStatus.vue'
import store from './store/index.js'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(getRequestToken())
@ -35,31 +36,23 @@ __webpack_nonce__ = btoa(getRequestToken())
Vue.prototype.t = t
Vue.prototype.$t = t
const avatarDiv = document.getElementById('avatardiv-menu')
const userStatusData = loadState('user_status', 'status')
const propsData = {
preloadedUserStatus: {
message: userStatusData.message,
icon: userStatusData.icon,
status: userStatusData.status,
},
user: avatarDiv.dataset.user,
displayName: avatarDiv.dataset.displayname,
disableMenu: true,
disableTooltip: true,
}
const mountPoint = document.getElementById('user_status-menu-entry')
const NcAvatarInMenu = Vue.extend(NcAvatar)
new NcAvatarInMenu({ propsData }).$mount('#avatardiv-menu')
const mountMenuEntry = () => {
const mountPoint = document.getElementById('user_status-menu-entry')
// eslint-disable-next-line no-new
new Vue({
el: mountPoint,
render: h => h(UserStatus),
store,
})
}
// Register settings menu entry
export default new Vue({
el: 'li[data-id="user_status-menuitem"]',
// eslint-disable-next-line vue/match-component-file-name
name: 'UserStatusRoot',
render: h => h(UserStatus),
store,
})
if (mountPoint) {
mountMenuEntry()
} else {
subscribe('core:user-menu:mounted', mountMenuEntry)
}
// Register dashboard status
document.addEventListener('DOMContentLoaded', function() {

File diff suppressed because one or more lines are too long

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["header.scss","variables.scss"],"names":[],"mappings":"AAiBA,mBAEC,yBACA,sBACA,qBACA,6PACC,aAGD,+QACC,YACA,kBACA,2BACA,WACA,WACA,kBACA,2CACA,SACA,UAGD,gLACC,WAIA,kPACC,WAGD,+HACC,SAOH,+DAGC,oBACA,kBACA,MACA,WACA,aACA,OC2Ce,KD1Cf,sBACA,8BAID,WACC,cACA,kBACA,kBACA,wBACA,sBACA,UACA,mBACA,aACA,eACA,gBACA,WAEA,mCACC,UAaD,gCACC,8CACA,sDACA,yCACA,sBACA,aACA,kBACA,gBAfD,gBACA,oCAgBC,UACA,SACA,SACA,gBAEA,kDACC,aAID,sCACC,gCACA,iDACA,YACA,YACA,SACA,QACA,kBACA,oBACA,WAGD,uEAEC,iCAzCF,gBACA,oCA6CC,iDACC,YACA,aACA,sBACA,QAEC,sDACC,kBACA,oBACA,mBACA,OAlDuB,KAmDvB,6BACA,kBACA,sBACA,mBACA,kBACA,WACA,wHAEC,+CAED,0HAEC,4CAED,oEACC,gDACA,aAED,2DACC,qBACA,iBACA,6BACA,mBACA,gBACA,uBACA,gBAED,0EACC,kBACA,0BAED,oHAEC,WACA,kBACA,YACA,WACA,wCAML,cACC,oBACA,yFACA,4BACA,wBACA,2BACA,WACA,kBACA,UACA,QACA,WAEA,gFAGD,kCACC,aACA,mBACA,cAGD,sFAEC,oBACA,mBAGD,0CACC,SACA,mBACA,YAGD,4CACC,yBACA,cAKA,qDAEC,YACA,kBACA,6EACC,aACA,uBACA,mBACA,MClIY,KDmIZ,YACA,eACA,YACA,UACA,aAEA,yFACC,UAGD,yGACC,aASL,0CACC,YAKD,gBACC,gCACA,eACA,iBACA,SACA,UACA,kBACA,gBACA,uBAEA,cAGD,kBACC,gCACA,kBACA,gBACA,eACA,iBACA,gBACA,uBAID,UACC,qBACA,YACA,eACA,cAGA,kBACC,UACA,kBAEA,yEAGC,gCAEA,4OAEC,kBACA,2CACA,YAED,0GACC,2CAED,kIACC,UAKF,6BACC,eACA,YACA,WAEA,iCACC,UACA,eAGD,gEACC,aAIF,qCACC,YACA,WACA,eAGA,oDACC,UAKF,wCACC,YACA,SACA,QACA,kBACA,oBACA,WACA,YACA,cACA,WACA,WACA,kBACA,2CAIF,2BACC,WAKF,cACC,kBACA,gBACA,aACA,WACA,SACA,YACA,aAEA,2BACC,IC/Qc,KDuRf,gDACC,mBACA,eAED,gJAEC,qBACA,YACA","file":"header.css"}
{"version":3,"sourceRoot":"","sources":["header.scss","variables.scss"],"names":[],"mappings":"AAiBA,mBAEC,yBACA,sBACA,qBACA,6PACC,aAGD,+QACC,YACA,kBACA,2BACA,WACA,WACA,kBACA,2CACA,SACA,UAGD,gLACC,WAIA,kPACC,WAGD,+HACC,SAOH,+DAGC,oBACA,kBACA,MACA,WACA,aACA,OC2Ce,KD1Cf,sBACA,8BAID,WACC,cACA,kBACA,kBACA,wBACA,sBACA,UACA,mBACA,aACA,eACA,gBACA,WAEA,mCACC,UAaD,gCACC,8CACA,sDACA,yCACA,sBACA,aACA,kBACA,gBAfD,gBACA,oCAgBC,UACA,SACA,SACA,gBAEA,kDACC,aAID,sCACC,gCACA,iDACA,YACA,YACA,SACA,QACA,kBACA,oBACA,WAGD,uEAEC,iCAzCF,gBACA,oCA4CA,cACC,oBACA,yFACA,4BACA,wBACA,2BACA,WACA,kBACA,UACA,QACA,WAEA,gFAGD,kCACC,aACA,mBACA,cAGD,sFAEC,oBACA,mBAGD,0CACC,SACA,mBACA,YAGD,4CACC,yBACA,cAKA,qDAEC,YACA,kBACA,6EACC,aACA,uBACA,mBACA,MC3EY,KD4EZ,YACA,eACA,YACA,UACA,aAEA,yFACC,UAGD,yGACC,aASL,0CACC,YAKD,gBACC,gCACA,eACA,iBACA,SACA,UACA,kBACA,gBACA,uBAEA,cAGD,kBACC,gCACA,kBACA,gBACA,eACA,iBACA,gBACA,uBAID,cACC,kBACA,gBACA,aACA,WACA,SACA,YACA,aAEA,2BACC,ICxIc,KD+If,gDACC,mBACA,eAED,gJAEC,qBACA,YACA","file":"header.css"}

@ -130,61 +130,6 @@
-webkit-overflow-scrolling: touch;
@include header-menu-height();
}
/* Use by the settings right menu */
&.settings-menu > ul {
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
li {
a {
border-radius: 6px;
display: inline-flex;
align-items: center;
height: $header-menu-entry-height;
color: var(--color-main-text);
padding: 10px 12px;
box-sizing: border-box;
white-space: nowrap;
position: relative;
width: 100%;
&:hover,
&:focus {
background-color: var(--color-background-hover);
}
&:active,
&.active {
background-color: var(--color-primary-light);
}
&:focus-visible {
box-shadow: inset 0 0 0 2px var(--color-primary);
outline: none;
}
span {
display: inline-block;
padding-bottom: 0;
color: var(--color-main-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 110px;
}
.icon-loading-small {
margin-right: 10px;
background-size: 16px 16px;
}
img,
svg {
opacity: .7;
margin-right: 10px;
height: 16px;
width: 16px;
filter: var(--background-invert-if-dark);
}
}
}
}
}
.logo {
display: inline-flex;
@ -284,86 +229,6 @@
text-overflow: ellipsis;
}
/* USER MENU -----------------------------------------------------------------*/
#settings {
display: inline-block;
height: 100%;
cursor: pointer;
flex: 0 0 auto;
/* User menu on the right */
#expand {
opacity: 1; /* override icon opacity */
margin-right: 12px;
&:hover,
&:focus,
&:active {
color: var(--color-primary-text);
#expandDisplayName,
.avatardiv{
border-radius: 50%;
border: 2px solid var(--color-primary-text);
margin: -2px;
}
.avatardiv{
background-color: var(--color-primary-text);
}
#expandDisplayName {
opacity: 1;
}
}
/* Profile picture in header */
.avatardiv {
cursor: pointer;
height: 32px;
width: 32px;
img {
opacity: 1;
cursor: pointer;
}
/* do not show display name when profile picture is present */
&.avatardiv-shown + #expandDisplayName {
display: none;
}
}
#expandDisplayName {
padding: 8px;
opacity: .6;
cursor: pointer;
/* full opacity for gear icon if active */
#body-settings & {
opacity: 1;
}
}
/* show triangle below user menu if active */
#body-settings &:before {
content: ' ';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
bottom: 2px;
z-index: 100;
display: block;
width: 10px;
height: 5px;
border-radius: 3px;
background-color: var(--color-primary-text);
}
}
#expanddiv:after {
right: 22px;
}
}
/* Skip navigation links – show only on keyboard focus */
#skip-actions {
position: absolute;
@ -379,7 +244,6 @@
}
}
/* Empty content messages in the header e.g. notifications, contacts menu, … */
header #emptycontent,
header .emptycontent {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -2,6 +2,7 @@
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Christopher Ng <chrng8@gmail.com>
*
* @license AGPL-3.0-or-later
*
@ -20,41 +21,17 @@
*
*/
import OC from '../OC'
import Vue from 'vue'
import $ from 'jquery'
import UserMenu from '../views/UserMenu.vue'
export const setUp = () => {
const $menu = $('#header #settings')
// Using page terminoogy as below
const $excludedPageClasses = [
'user-status-menu-item__header',
]
// show loading feedback
$menu.delegate('a', 'click', event => {
let $page = $(event.target)
if (!$page.is('a')) {
$page = $page.closest('a')
}
if (event.which === 1 && !event.ctrlKey && !event.metaKey) {
if (!$excludedPageClasses.includes($page.attr('class'))) {
$page.find('img').remove()
$page.find('div').remove() // prevent odd double-clicks
$page.prepend($('<div></div>').addClass('icon-loading-small'))
}
} else {
// Close navigation when opening menu entry in
// a new tab
OC.hideMenus(() => false)
}
})
$menu.delegate('a', 'mouseup', event => {
if (event.which === 2) {
// Close navigation when opening app in
// a new tab via middle click
OC.hideMenus(() => false)
}
})
const mountPoint = document.getElementById('user-menu')
if (mountPoint) {
// eslint-disable-next-line no-new
new Vue({
el: mountPoint,
render: h => h(UserMenu),
})
}
}

@ -0,0 +1,106 @@
<!--
- @copyright 2023 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<li :id="id"
class="menu-entry">
<a v-if="href"
:href="href"
:class="{ active }"
@click.exact="handleClick">
<NcLoadingIcon v-if="loading"
class="menu-entry__loading-icon"
:size="18" />
<img v-else :src="cachedIcon" alt="" />
{{ name }}
</a>
<button v-else>
<img :src="cachedIcon" alt="" />
{{ name }}
</button>
</li>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
const versionHash = loadState('core', 'versionHash', '')
export default {
name: 'UserMenuEntry',
components: {
NcLoadingIcon,
},
props: {
id: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
href: {
type: String,
required: true,
},
active: {
type: Boolean,
required: true,
},
icon: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
}
},
computed: {
cachedIcon() {
return `${this.icon}?v=${versionHash}`
},
},
methods: {
handleClick() {
this.loading = true
},
},
}
</script>
<style lang="scss" scoped>
.menu-entry {
&__loading-icon {
margin-right: 8px;
}
}
</style>

@ -0,0 +1,184 @@
<!--
- @copyright 2023 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<NcHeaderMenu id="user-menu"
class="user-menu"
:aria-label="t('core', 'Open settings menu')">
<template #trigger>
<NcAvatar class="user-menu__avatar"
:disable-menu="true"
:disable-tooltip="true"
:user="userId" />
</template>
<nav class="user-menu__nav"
:aria-label="t('core', 'Settings menu')">
<ul>
<UserMenuEntry v-for="entry in settingsNavEntries"
v-bind="entry"
:key="entry.id" />
</ul>
</nav>
</NcHeaderMenu>
</template>
<script>
import { emit } from '@nextcloud/event-bus'
import { getCurrentUser } from '@nextcloud/auth'
import { loadState } from '@nextcloud/initial-state'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
import UserMenuEntry from '../components/UserMenu/UserMenuEntry.vue'
const settingsNavEntries = loadState('core', 'settingsNavEntries', [])
export default {
name: 'UserMenu',
components: {
NcAvatar,
NcHeaderMenu,
UserMenuEntry,
},
data() {
return {
settingsNavEntries,
userId: getCurrentUser()?.uid,
}
},
mounted() {
emit('core:user-menu:mounted')
},
}
</script>
<style lang="scss" scoped>
.user-menu {
margin-right: 12px;
&:deep {
.header-menu {
&__trigger {
opacity: 1 !important;
&:focus-visible {
.user-menu__avatar {
border: 2px solid var(--color-primary-text);
}
}
}
&__carret {
display: none !important;
}
&__content {
width: fit-content !important;
}
}
}
&__avatar {
&:active,
&:focus,
&:hover {
border: 2px solid var(--color-primary-text);
}
}
&__nav {
display: flex;
width: 100%;
ul {
display: flex;
flex-direction: column;
gap: 2px;
&:deep {
li {
a,
button {
border-radius: 6px;
display: inline-flex;
align-items: center;
height: var(--header-menu-item-height);
color: var(--color-main-text);
padding: 10px 8px;
box-sizing: border-box;
white-space: nowrap;
position: relative;
width: 100%;
&:hover {
background-color: var(--color-background-hover);
}
&:focus-visible {
background-color: var(--color-background-hover) !important;
box-shadow: inset 0 0 0 2px var(--color-primary) !important;
outline: none !important;
}
&:active,
&.active {
background-color: var(--color-primary-light);
}
span {
padding-bottom: 0;
color: var(--color-main-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 110px;
}
img {
width: 16px;
height: 16px;
margin-right: 10px;
}
img,
svg {
opacity: .7;
filter: var(--background-invert-if-dark);
}
}
// Override global button styles
button {
background-color: transparent;
border: none;
font-weight: normal;
margin: 0;
}
}
}
}
}
}
</style>

@ -70,45 +70,7 @@ p($theme->getTitle());
<div id="unified-search"></div>
<div id="notifications"></div>
<div id="contactsmenu"></div>
<div id="settings">
<div id="expand" tabindex="0" role="button" class="menutoggle"
aria-label="<?php p($l->t('Open settings menu'));?>"
aria-haspopup="true" aria-controls="expanddiv" aria-expanded="false">
<div id="avatardiv-menu" class="avatardiv<?php if ($_['userAvatarSet']) {
print_unescaped(' avatardiv-shown');
} else {
print_unescaped('" style="display: none');
} ?>"
data-user="<?php p($_['user_uid']); ?>"
data-displayname="<?php p($_['user_displayname']); ?>"
<?php
if ($_['userAvatarSet']) {
$avatar32 = $getUserAvatar(32); ?> data-avatar="<?php p($avatar32); ?>"
<?php
} ?>>
<?php
if ($_['userAvatarSet']) {?>
<img alt="" width="32" height="32"
src="<?php p($avatar32);?>"
srcset="<?php p($getUserAvatar(64));?> 2x, <?php p($getUserAvatar(128));?> 4x"
>
<?php } ?>
</div>
</div>
<nav class="settings-menu" id="expanddiv" style="display:none;">
<ul>
<?php foreach ($_['settingsnavigation'] as $entry):?>
<li data-id="<?php p($entry['id']); ?>">
<a href="<?php print_unescaped($entry['href'] !== '' ? $entry['href'] : '#'); ?>"
<?php if ($entry["active"]): ?> class="active"<?php endif; ?>>
<img alt="" src="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>">
<?php p($entry['name']) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</nav>
</div>
<div id="user-menu"></div>
</div>
</header>

File diff suppressed because one or more lines are too long

@ -366,6 +366,8 @@ object-assign
/*! For license information please see NcEmojiPicker.js.LICENSE.txt */
/*! For license information please see NcLoadingIcon.js.LICENSE.txt */
/*! For license information please see NcModal.js.LICENSE.txt */
/*! For license information please see NcNoteCard.js.LICENSE.txt */

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -130,7 +130,7 @@ class TemplateLayout extends \OC_Template {
$navigation = $this->navigationManager->getAll();
$this->assign('navigation', $navigation);
$settingsNavigation = $this->navigationManager->getAll('settings');
$this->assign('settingsnavigation', $settingsNavigation);
$this->initialState->provideInitialState('core', 'settingsNavEntries', $settingsNavigation);
foreach ($navigation as $entry) {
if ($entry['active']) {
@ -268,7 +268,7 @@ class TemplateLayout extends \OC_Template {
$this->assign('cssfiles', []);
$this->assign('printcssfiles', []);
$this->assign('versionHash', self::$versionHash);
$this->initialState->provideInitialState('core', 'versionHash', self::$versionHash);
foreach ($cssFiles as $info) {
$web = $info[1];
$file = $info[2];

@ -32,7 +32,7 @@ class SettingsMenuContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function settingsSectionInHeader() {
return Locator::forThe()->xpath("//*[@id = 'header']//*[@id = 'settings']")->
return Locator::forThe()->xpath("//*[@id = 'header']//*[@id = 'user-menu']")->
describedAs("Settings menu section in the header");
}
@ -40,7 +40,7 @@ class SettingsMenuContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function settingsMenuButton() {
return Locator::forThe()->id("expand")->
return Locator::forThe()->css(".header-menu__trigger")->
descendantOf(self::settingsSectionInHeader())->
describedAs("Settings menu button");
}
@ -49,7 +49,7 @@ class SettingsMenuContext implements Context, ActorAwareInterface {
* @return Locator
*/
public static function settingsMenu() {
return Locator::forThe()->id("expanddiv")->
return Locator::forThe()->css(".user-menu__nav")->
descendantOf(self::settingsSectionInHeader())->
describedAs("Settings menu");
}

Loading…
Cancel
Save