MOBILE-3807 mainmenu: Add new user menu modal

main
Pau Ferrer Ocaña 2021-10-22 12:13:43 +02:00
parent 289e8e1ce7
commit 81fdd00902
12 changed files with 431 additions and 132 deletions

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button';
import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu';
@NgModule({
declarations: [
CoreMainMenuUserButtonComponent,
CoreMainMenuUserMenuComponent,
],
imports: [
CoreSharedModule,
],
exports: [
CoreMainMenuUserButtonComponent,
CoreMainMenuUserMenuComponent,
],
})
export class CoreMainMenuComponentsModule {}

View File

@ -0,0 +1,9 @@
<core-user-avatar
*ngIf="siteInfo"
[user]="siteInfo"
class="core-bar-button-image clickable"
[linkProfile]="false"
(ariaButtonClick)="openUserMenu($event)"
role="button"
tabindex="0"
></core-user-avatar>

View File

@ -0,0 +1,55 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { CoreSiteInfo } from '@classes/site';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreMainMenuUserMenuComponent } from '../user-menu/user-menu';
/**
* Component to display an avatar on the header to open user menu.
*
* Example: <core-user-menu-button></core-user-menu-button>
*/
@Component({
selector: 'core-user-menu-button',
templateUrl: 'user-menu-button.html',
})
export class CoreMainMenuUserButtonComponent {
siteInfo?: CoreSiteInfo;
constructor() {
const currentSite = CoreSites.getRequiredCurrentSite();
// @TODO: Check if the page where I currently am is at level 0.
this.siteInfo = currentSite.getInfo();
}
/**
* Open User menu
*
* @param event Click event.
*/
openUserMenu(event: Event): void {
event.preventDefault();
event.stopPropagation();
CoreDomUtils.openModal<void>({
component: CoreMainMenuUserMenuComponent,
});
}
}

View File

@ -0,0 +1,63 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button fill="clear" (click)="close($event)" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
<h1>
{{'core.user.account' | translate}}
</h1>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="true">
<ion-list>
<ion-item class="ion-text-center core-user-profile-maininfo" *ngIf="siteInfo">
<core-user-avatar [user]="siteInfo" [userId]="siteInfo.userid" [linkProfile]="false"></core-user-avatar>
<ion-label>
<h2>{{ siteInfo.fullname }}</h2>
<p class="core-usermenu-siteinfo core-usermenu-sitename">
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
</core-format-text>
</p>
<p class="core-usermenu-siteinfo core-usermenu-siteurl">{{ siteUrl }}</p>
</ion-label>
</ion-item>
<ion-item button class="ion-text-wrap core-usermenu-handler" (click)="openUserProfile($event)"
[attr.aria-label]="'core.user.details' | translate" detail="true">
<ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.user.details' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
<ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label>
</ion-item>
<ion-item button *ngFor="let handler of handlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
[ngClass]="['core-user-menu-handler', handler.class || '']" [hidden]="handler.hidden"
[attr.aria-label]="handler.title | translate" detail="true">
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ handler.title | translate }}</p>
</ion-label>
</ion-item>
<ion-item button (click)="openPreferences($event)" [attr.aria-label]="'core.settings.preferences' | translate" detail="true">
<ion-icon name="fas-wrench" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
</ion-label>
</ion-item>
</ion-list>
</core-loading>
</ion-content>
<ion-footer class="ion-padding">
<ion-button (click)="logout($event)" expand="block" color="danger" [attr.aria-label]="logoutLabel | translate" class="ion-text-wrap">
<ion-icon name="fas-sign-out-alt" slot="start" aria-hidden="true"></ion-icon>
{{ logoutLabel | translate }}
</ion-button>
</ion-footer>

View File

@ -0,0 +1,38 @@
@import "~theme/globals";
:host {
.core-user-profile-maininfo::part(native) {
flex-direction: column;
}
::ng-deep {
core-user-avatar {
display: block;
--core-avatar-size: var(--core-large-avatar-size);
height: calc(var(--core-avatar-size) + 16px);
img {
margin: 8px auto;
}
}
}
}
@if ($core-user-hide-siteinfo) {
.core-usermenu-siteinfo {
display: none;
}
}
@if ($core-user-hide-sitename) {
.core-usermenu-sitename {
display: none;
}
}
@if ($core-user-hide-siteurl) {
.core-usermenu-siteurl {
display: none;
}
}

View File

@ -0,0 +1,153 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { CoreSiteInfo } from '@classes/site';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CoreUserProfileHandlerData, CoreUserDelegate, CoreUserDelegateService } from '@features/user/services/user-delegate';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { ModalController } from '@singletons';
import { Subscription } from 'rxjs';
/**
* Component to display a user menu.
*/
@Component({
selector: 'core-main-menu-user-menu',
templateUrl: 'user-menu.html',
styleUrls: ['user-menu.scss'],
})
export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
siteInfo?: CoreSiteInfo;
siteName?: string;
logoutLabel = 'core.mainmenu.changesite';
siteUrl?: string;
handlers: CoreUserProfileHandlerData[] = [];
handlersLoaded = false;
user?: CoreUserProfile;
protected subscription!: Subscription;
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
const currentSite = CoreSites.getRequiredCurrentSite();
this.siteInfo = currentSite.getInfo();
this.siteName = currentSite.getSiteName();
this.siteUrl = currentSite.getURL();
this.logoutLabel = CoreLoginHelper.getLogoutLabel(currentSite);
// Load the handlers.
if (this.siteInfo) {
this.user = await CoreUser.getProfile(this.siteInfo.userid);
this.subscription = CoreUserDelegate.getProfileHandlersFor(this.user).subscribe((handlers) => {
if (!handlers || !this.user) {
return;
}
this.handlers = [];
handlers.forEach((handler) => {
if (handler.type == CoreUserDelegateService.TYPE_NEW_PAGE) {
this.handlers.push(handler.data);
}
});
this.handlersLoaded = CoreUserDelegate.areHandlersLoaded(this.user.id);
});
}
}
/**
* Opens User profile page.
*
* @param event Click event.
*/
async openUserProfile(event: Event): Promise<void> {
if (!this.siteInfo) {
return;
}
await this.close(event);
CoreNavigator.navigateToSitePath('user/about', {
params: {
userId: this.siteInfo.userid,
},
});
}
/**
* Opens preferences.
*
* @param event Click event.
*/
async openPreferences(event: Event): Promise<void> {
await this.close(event);
CoreNavigator.navigateToSitePath('preferences');
}
/**
* A handler was clicked.
*
* @param event Click event.
* @param handler Handler that was clicked.
*/
async handlerClicked(event: Event, handler: CoreUserProfileHandlerData): Promise<void> {
if (!this.user) {
return;
}
await this.close(event);
handler.action(event, this.user);
}
/**
* Logout the user.
*
* @param event Click event
*/
async logout(event: Event): Promise<void> {
await this.close(event);
CoreSites.logout();
}
/**
* Close modal.
*/
async close(event: Event): Promise<void> {
event.preventDefault();
event.stopPropagation();
await ModalController.dismiss();
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}

View File

@ -5,97 +5,76 @@
</ion-buttons>
<h1><core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"></core-format-text></h1>
<ion-buttons slot="end">
<core-user-menu-button></core-user-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="!loggedOut">
<ion-list>
<ion-item button *ngIf="siteInfo" class="ion-text-wrap" core-user-link [userId]="siteInfo.userid" detail="true">
<core-user-avatar [user]="siteInfo" slot="start"></core-user-avatar>
<ion-list>
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
<ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label>
</ion-item>
<ion-item button *ngFor="let handler of handlers" [ngClass]="['core-moremenu-handler', handler.class || '']"
(click)="openHandler(handler)" [attr.aria-label]="handler.title | translate" detail="true">
<ion-icon [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ handler.title | translate}}</p>
</ion-label>
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
{{handler.badge}}
</ion-badge>
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
</span>
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate">
</ion-spinner>
</ion-item>
<ng-container *ngFor="let item of customItems">
<ion-item button *ngIf="item.type != 'embedded'" [href]="item.url" [attr.aria-label]="item.label" core-link
[capture]="item.type == 'app'" [inApp]="item.type == 'inappbrowser'" class="core-moremenu-customitem" detail="true"
[detailIcon]="item.type == 'browser' ? 'open-outline' : 'chevron-forward'">
<ion-icon [name]="item.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{siteInfo.fullname}}</p>
<p class="core-moremenu-siteinfo core-moremenu-sitename">
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
</core-format-text>
</p>
<p class="core-moremenu-siteinfo core-moremenu-siteurl">{{ siteUrl }}</p>
<p class="item-heading">{{item.label}}</p>
</ion-label>
</ion-item>
<core-spacer></core-spacer>
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
<ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label>
</ion-item>
<ion-item button *ngFor="let handler of handlers" [ngClass]="['core-moremenu-handler', handler.class || '']"
(click)="openHandler(handler)" [attr.aria-label]="handler.title | translate" detail="true">
<ion-icon [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-item button *ngIf="item.type == 'embedded'" (click)="openItem(item)" [attr.aria-label]="item.label"
class="core-moremenu-customitem" detail="true">
<ion-icon [name]="item.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ handler.title | translate}}</p>
</ion-label>
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
{{handler.badge}}
</ion-badge>
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
</span>
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate">
</ion-spinner>
</ion-item>
<ng-container *ngFor="let item of customItems">
<ion-item button *ngIf="item.type != 'embedded'" [href]="item.url" [attr.aria-label]="item.label" core-link
[capture]="item.type == 'app'" [inApp]="item.type == 'inappbrowser'" class="core-moremenu-customitem" detail="true"
[detailIcon]="item.type == 'browser' ? 'open-outline' : 'chevron-forward'">
<ion-icon [name]="item.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{item.label}}</p>
</ion-label>
</ion-item>
<ion-item button *ngIf="item.type == 'embedded'" (click)="openItem(item)" [attr.aria-label]="item.label"
class="core-moremenu-customitem" detail="true">
<ion-icon [name]="item.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{item.label}}</p>
</ion-label>
</ion-item>
</ng-container>
<ion-item button *ngIf="showScanQR" (click)="scanQR()" detail="true">
<ion-icon name="fas-qrcode" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.scanqr' | translate }}</p>
<p class="item-heading">{{item.label}}</p>
</ion-label>
</ion-item>
<ion-item button *ngIf="showWeb && siteInfo" [href]="siteInfo.siteurl" core-link autoLogin="yes"
[attr.aria-label]="'core.mainmenu.website' | translate" detail="true" detailIcon="open-outline">
<ion-icon name="fas-globe" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.mainmenu.website' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button *ngIf="showHelp" [href]="docsUrl" core-link autoLogin="no"
[attr.aria-label]="'core.mainmenu.help' | translate" detail="true" detailIcon="open-outline">
<ion-icon name="far-life-ring" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.mainmenu.help' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button (click)="openPreferences()" [attr.aria-label]="'core.settings.preferences' | translate" detail="true">
<ion-icon name="fas-wrench" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button (click)="logout()" [attr.aria-label]="logoutLabel | translate" detail="true">
<ion-icon name="fas-sign-out-alt" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ logoutLabel | translate }}</p>
</ion-label>
</ion-item>
<core-spacer></core-spacer>
<ion-item button (click)="openSettings()" [attr.aria-label]="'core.settings.appsettings' | translate" detail="true">
<ion-icon name="fas-cogs" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.settings.appsettings' | translate }}</p>
</ion-label>
</ion-item>
</ion-list>
</core-loading>
</ng-container>
<ion-item button *ngIf="showScanQR" (click)="scanQR()" detail="true">
<ion-icon name="fas-qrcode" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.scanqr' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button *ngIf="showWeb && siteInfo" [href]="siteInfo.siteurl" core-link autoLogin="yes"
[attr.aria-label]="'core.mainmenu.website' | translate" detail="true" detailIcon="open-outline">
<ion-icon name="fas-globe" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.mainmenu.website' | translate }}</p>
</ion-label>
</ion-item>
<ion-item button *ngIf="showHelp" [href]="docsUrl" core-link autoLogin="no"
[attr.aria-label]="'core.mainmenu.help' | translate" detail="true" detailIcon="open-outline">
<ion-icon name="far-life-ring" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.mainmenu.help' | translate }}</p>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
<ion-footer>
<ion-item button (click)="openSettings()" [attr.aria-label]="'core.settings.appsettings' | translate" detail="true">
<ion-icon name="fas-cogs" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading">{{ 'core.settings.appsettings' | translate }}</p>
</ion-label>
</ion-item>
</ion-footer>

View File

@ -19,10 +19,12 @@ import { CoreSharedModule } from '@/core/shared.module';
import { CoreMainMenuMorePage } from './more';
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
@NgModule({
imports: [
CoreSharedModule,
CoreMainMenuComponentsModule,
],
providers: [
{

View File

@ -17,21 +17,3 @@ ion-item {
color: var(--core-more-icon, inherit);
}
}
@if ($core-more-hide-siteinfo) {
.core-moremenu-siteinfo {
display: none;
}
}
@if ($core-more-hide-sitename) {
.core-moremenu-sitename {
display: none;
}
}
@if ($core-more-hide-siteurl) {
.core-moremenu-siteurl {
display: none;
}
}

View File

@ -18,7 +18,6 @@ import { Subscription } from 'rxjs';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreSiteInfo } from '@classes/site';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/mainmenu-delegate';
import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
@ -39,19 +38,16 @@ import { Translate } from '@singletons';
export class CoreMainMenuMorePage implements OnInit, OnDestroy {
handlers?: CoreMainMenuHandlerData[];
allHandlers?: CoreMainMenuHandlerData[];
handlersLoaded = false;
siteInfo?: CoreSiteInfo;
siteName?: string;
logoutLabel = 'core.mainmenu.changesite';
showScanQR: boolean;
showWeb?: boolean;
showHelp?: boolean;
docsUrl?: string;
customItems?: CoreMainMenuCustomItem[];
siteUrl?: string;
loggedOut = false;
protected allHandlers?: CoreMainMenuHandlerData[];
protected subscription!: Subscription;
protected langObserver: CoreEventObserver;
protected updateSiteObserver: CoreEventObserver;
@ -70,7 +66,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
}
/**
* Initialize component.
* @inheritdoc
*/
ngOnInit(): void {
// Load the handlers.
@ -84,7 +80,7 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
}
/**
* Page destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
window.removeEventListener('resize', this.initHandlers.bind(this));
@ -116,16 +112,10 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
* Load the site info required by the view.
*/
protected async loadSiteInfo(): Promise<void> {
const currentSite = CoreSites.getCurrentSite();
if (!currentSite) {
return;
}
const currentSite = CoreSites.getRequiredCurrentSite();
this.siteInfo = currentSite.getInfo();
this.siteName = currentSite.getSiteName();
this.siteUrl = currentSite.getURL();
this.logoutLabel = CoreLoginHelper.getLogoutLabel(currentSite);
this.showWeb = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_website');
this.showHelp = !currentSite.isFeatureDisabled('CoreMainMenuDelegate_help');
@ -154,13 +144,6 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
CoreNavigator.navigateToSitePath('viewer/iframe', { params: { title: item.label, url: item.url } });
}
/**
* Open preferences.
*/
openPreferences(): void {
CoreNavigator.navigateToSitePath('preferences');
}
/**
* Open settings.
*/
@ -200,12 +183,4 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
}
}
/**
* Logout the user.
*/
logout(): void {
this.loggedOut = true;
CoreSites.logout();
}
}

View File

@ -1,5 +1,6 @@
{
"address": "Address",
"account": "Account",
"city": "City/town",
"contact": "Contact",
"country": "Country",
@ -14,6 +15,7 @@
"interests": "Interests",
"lastname": "Surname",
"manager": "Manager",
"myprofile": "My profile",
"newpicture": "New picture",
"noparticipants": "No participants found for this course",
"participants": "Participants",

View File

@ -127,7 +127,15 @@ $core-login-loading-color-dark: $text-color-dark !default;
$core-login-hide-forgot-password: false !default;
$core-login-hide-need-help: false !default;
// Configuration options for more page.
// Configuration options for more page. (deprecated on 4.0)
$core-more-hide-siteinfo: false !default;
$core-more-hide-sitename: false !default;
$core-more-hide-siteurl: false !default;
// Configuration options for user page.
$core-user-hide-siteinfo: $core-more-hide-siteinfo !default;
$core-user-hide-sitename: $core-more-hide-sitename !default;
$core-user-hide-siteurl: $core-more-hide-siteurl !default;