MOBILE-3807 sites: New change user feature

main
Pau Ferrer Ocaña 2021-10-22 17:02:54 +02:00
parent 357d23c082
commit 03b7660d68
19 changed files with 590 additions and 202 deletions

View File

@ -1798,6 +1798,8 @@
"core.loading": "moodle", "core.loading": "moodle",
"core.loadmore": "local_moodlemobileapp", "core.loadmore": "local_moodlemobileapp",
"core.location": "moodle", "core.location": "moodle",
"core.login.accounts": "admin",
"core.login.add": "moodle",
"core.login.auth_email": "auth_email/pluginname", "core.login.auth_email": "auth_email/pluginname",
"core.login.authenticating": "local_moodlemobileapp", "core.login.authenticating": "local_moodlemobileapp",
"core.login.cancel": "moodle", "core.login.cancel": "moodle",
@ -1894,6 +1896,7 @@
"core.login.reconnect": "local_moodlemobileapp", "core.login.reconnect": "local_moodlemobileapp",
"core.login.reconnectdescription": "local_moodlemobileapp", "core.login.reconnectdescription": "local_moodlemobileapp",
"core.login.reconnectssodescription": "local_moodlemobileapp", "core.login.reconnectssodescription": "local_moodlemobileapp",
"core.login.removeaccount": "local_moodlemobileapp",
"core.login.resendemail": "moodle", "core.login.resendemail": "moodle",
"core.login.searchby": "local_moodlemobileapp", "core.login.searchby": "local_moodlemobileapp",
"core.login.security_question": "auth", "core.login.security_question": "auth",
@ -1913,6 +1916,7 @@
"core.login.startsignup": "moodle", "core.login.startsignup": "moodle",
"core.login.stillcantconnect": "local_moodlemobileapp", "core.login.stillcantconnect": "local_moodlemobileapp",
"core.login.supplyinfo": "moodle", "core.login.supplyinfo": "moodle",
"core.login.toggleremove": "local_moodlemobileapp",
"core.login.username": "moodle", "core.login.username": "moodle",
"core.login.usernameoremail": "moodle", "core.login.usernameoremail": "moodle",
"core.login.usernamerequired": "local_moodlemobileapp", "core.login.usernamerequired": "local_moodlemobileapp",
@ -1922,9 +1926,9 @@
"core.login.youcanstillconnectwithcredentials": "local_moodlemobileapp", "core.login.youcanstillconnectwithcredentials": "local_moodlemobileapp",
"core.login.yourenteredsite": "local_moodlemobileapp", "core.login.yourenteredsite": "local_moodlemobileapp",
"core.lostconnection": "local_moodlemobileapp", "core.lostconnection": "local_moodlemobileapp",
"core.mainmenu.changesite": "local_moodlemobileapp",
"core.mainmenu.home": "moodle", "core.mainmenu.home": "moodle",
"core.mainmenu.logout": "moodle", "core.mainmenu.logout": "moodle",
"core.mainmenu.switchaccount": "local_moodlemobileapp",
"core.maxfilesize": "moodle", "core.maxfilesize": "moodle",
"core.maxsizeandattachments": "moodle", "core.maxsizeandattachments": "moodle",
"core.min": "moodle", "core.min": "moodle",
@ -2230,6 +2234,7 @@
"core.updaterequireddesc": "local_moodlemobileapp", "core.updaterequireddesc": "local_moodlemobileapp",
"core.upgraderunning": "error", "core.upgraderunning": "error",
"core.user": "moodle", "core.user": "moodle",
"core.user.account": "local_moodlemobileapp",
"core.user.address": "moodle", "core.user.address": "moodle",
"core.user.city": "moodle", "core.user.city": "moodle",
"core.user.contact": "local_moodlemobileapp", "core.user.contact": "local_moodlemobileapp",

View File

@ -0,0 +1,36 @@
// (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 { CoreLoginSiteOnboardingComponent } from './site-onboarding/site-onboarding';
import { CoreLoginSiteHelpComponent } from './site-help/site-help';
import { CoreLoginSitesComponent } from './sites/sites';
@NgModule({
declarations: [
CoreLoginSiteOnboardingComponent,
CoreLoginSiteHelpComponent,
CoreLoginSitesComponent,
],
imports: [
CoreSharedModule,
],
exports: [
CoreLoginSiteOnboardingComponent,
CoreLoginSiteHelpComponent,
CoreLoginSitesComponent,
],
})
export class CoreLoginComponentsModule {}

View File

@ -0,0 +1,92 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button fill="clear" (click)="close($event)" [attr.aria-label]="'core.back' | translate">
<ion-icon name="arrow-back" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
<h1>{{ 'core.mainmenu.switchaccount' | translate }}</h1>
<ion-buttons slot="end">
<ion-button fill="clear" *ngIf="accountsList.count > 1" (click)="toggleDelete()"
[attr.aria-label]="'core.login.toggleremove' | translate">
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list>
<ng-container *ngIf="accountsList.currentSite">
<ion-item-divider sticky="true">
<ion-label>
<h2>
<core-format-text [text]="accountsList.currentSite.siteName" clean="true"
[siteId]="accountsList.currentSite.id"></core-format-text>
</h2>
<p><a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes">{{ accountsList.currentSite.siteUrl }}</a>
</p>
</ion-label>
</ion-item-divider>
<ion-item detail="false" class="item-current">
<ion-avatar slot="start">
<img [src]="accountsList.currentSite.avatar" core-external-content [siteId]="accountsList.currentSite.id"
alt="{{ 'core.pictureof' | translate:{$a: accountsList.currentSite.fullName} }}"
onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label>
<p class="item-heading">{{accountsList.currentSite.fullName}}</p>
</ion-label>
<ion-icon color="success" name="fas-check"></ion-icon>
</ion-item>
<ng-container *ngTemplateOutlet="siteList; context: {sites: accountsList.sameSite}"></ng-container>
</ng-container>
<ng-container *ngFor="let sites of accountsList.otherSites">
<ion-item-divider sticky="true" *ngIf="sites[0]">
<ion-label>
<h2>
<core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
</h2>
<p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrl }}</a></p>
</ion-label>
</ion-item-divider>
<ng-container *ngTemplateOutlet="siteList; context: {sites: sites}"></ng-container>
</ng-container>
</ion-list>
</core-loading>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
<ion-fab-button (click)="add($event)" [attr.aria-label]="'core.login.add' | translate">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon>
<span class="sr-only">{{ 'core.login.add' | translate }}</span>
</ion-fab-button>
</ion-fab>
</ion-content>
<!-- Template to render a list of sites. -->
<ng-template #siteList let-sites="sites">
<ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
<ion-avatar slot="start">
<img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}"
onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<ion-label>
<p class="item-heading">{{site.fullName}}</p>
</ion-label>
<ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>
<span aria-hidden="true">{{site.badge}}</span>
<span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge }
}}</span>
</ion-badge>
<ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
[attr.aria-label]="'core.login.removeaccount' | translate" [@coreSlideInOut]="'fromRight'">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
</ng-template>

View File

@ -0,0 +1,131 @@
// (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 { CoreDomUtils } from '@services/utils/dom';
import { Component, OnInit } from '@angular/core';
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreNavigator } from '@services/navigator';
import { CoreFilter } from '@features/filter/services/filter';
import { CoreAnimations } from '@components/animations';
import { ModalController } from '@singletons';
/**
* Component that displays a "splash screen" while the app is being initialized.
*/
@Component({
selector: 'core-login-sites',
templateUrl: 'sites.html',
animations: [CoreAnimations.SLIDE_IN_OUT, CoreAnimations.SHOW_HIDE],
})
export class CoreLoginSitesComponent implements OnInit {
accountsList: CoreAccountsList = {
sameSite: [],
otherSites: [],
count: 0,
};
showDelete = false;
currentSiteId: string;
loaded = false;
constructor() {
this.currentSiteId = CoreSites.getRequiredCurrentSite().getId();
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.accountsList = await CoreLoginHelper.getAccountsList(this.currentSiteId);
this.loaded = true;
}
/**
* Go to the page to add a site.
*
* @param event Click event.
*/
async add(event: Event): Promise<void> {
await this.close(event, true);
await CoreLoginHelper.goToAddSite(true, true);
}
/**
* Delete a site.
*
* @param event Click event.
* @param site Site to delete.
* @return Promise resolved when done.
*/
async deleteSite(event: Event, site: CoreSiteBasicInfo): Promise<void> {
event.stopPropagation();
let siteName = site.siteName || '';
siteName = await CoreFilter.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], site.id);
try {
await CoreDomUtils.showDeleteConfirm('core.login.confirmdeletesite', { sitename: siteName });
} catch {
// User cancelled, stop.
return;
}
try {
await CoreLoginHelper.deleteAccountFromList(this.accountsList, site);
this.showDelete = false;
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.login.errordeletesite', true);
}
}
/**
* Login in a site.
*
* @param event Click event.
* @param siteId The site ID.
* @return Promise resolved when done.
*/
async login(event: Event, siteId: string): Promise<void> {
await this.close(event, true);
// This navigation will logout and navigate to the site home.
await CoreNavigator.navigateToSiteHome({ preferCurrentTab: false , siteId });
}
/**
* Toggle delete.
*/
toggleDelete(): void {
this.showDelete = !this.showDelete;
}
/**
* Close modal.
*
* @param event Click event.
*/
async close(event: Event, closeAll = false): Promise<void> {
event.preventDefault();
event.stopPropagation();
await ModalController.dismiss(closeAll);
}
}

View File

@ -1,4 +1,6 @@
{ {
"accounts": "Accounts",
"add": "Add a new account",
"auth_email": "Email-based self-registration", "auth_email": "Email-based self-registration",
"authenticating": "Authenticating", "authenticating": "Authenticating",
"cancel": "Cancel", "cancel": "Cancel",
@ -8,7 +10,7 @@
"changepasswordinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.", "changepasswordinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.",
"changepasswordlogoutinstructions": "If you prefer to change site or log out, please click the following button:", "changepasswordlogoutinstructions": "If you prefer to change site or log out, please click the following button:",
"changepasswordreconnectinstructions": "Click the following button to reconnect to the site. (Take into account that if you didn't change your password successfully, you would return to the previous screen).", "changepasswordreconnectinstructions": "Click the following button to reconnect to the site. (Take into account that if you didn't change your password successfully, you would return to the previous screen).",
"confirmdeletesite": "Are you sure you want to delete the site {{sitename}}?", "confirmdeletesite": "Are you sure you want to remove the account on {{sitename}}?",
"connect": "Connect!", "connect": "Connect!",
"connecttomoodle": "Connect to Moodle", "connecttomoodle": "Connect to Moodle",
"contactyouradministrator": "Contact your site administrator for further help.", "contactyouradministrator": "Contact your site administrator for further help.",
@ -23,7 +25,7 @@
"emailconfirmsentsuccess": "Confirmation email sent successfully", "emailconfirmsentsuccess": "Confirmation email sent successfully",
"emailnotmatch": "Emails do not match", "emailnotmatch": "Emails do not match",
"erroraccesscontrolalloworigin": "The cross-origin call you're trying to perform has been rejected. Please check https://docs.moodle.org/dev/Moodle_Mobile_development_using_Chrome_or_Chromium", "erroraccesscontrolalloworigin": "The cross-origin call you're trying to perform has been rejected. Please check https://docs.moodle.org/dev/Moodle_Mobile_development_using_Chrome_or_Chromium",
"errordeletesite": "An error occurred while deleting this site. Please try again.", "errordeletesite": "An error occurred while deleting this account. Please try again.",
"errorexampleurl": "The URL https://campus.example.edu is only an example URL, it's not a real site. <strong>Please use the URL of your school or organization's site.</strong>", "errorexampleurl": "The URL https://campus.example.edu is only an example URL, it's not a real site. <strong>Please use the URL of your school or organization's site.</strong>",
"errorqrnoscheme": "This URL isn't a valid login URL.", "errorqrnoscheme": "This URL isn't a valid login URL.",
"errorupdatesite": "An error occurred while updating the site's token.", "errorupdatesite": "An error occurred while updating the site's token.",
@ -95,11 +97,12 @@
"reconnect": "Reconnect", "reconnect": "Reconnect",
"reconnectdescription": "Your authentication token is invalid or has expired. You have to reconnect to the site.", "reconnectdescription": "Your authentication token is invalid or has expired. You have to reconnect to the site.",
"reconnectssodescription": "Your authentication token is invalid or has expired. You have to reconnect to the site. You need to log in to the site in a browser window.", "reconnectssodescription": "Your authentication token is invalid or has expired. You have to reconnect to the site. You need to log in to the site in a browser window.",
"removeaccount": "Remove account",
"resendemail": "Resend email", "resendemail": "Resend email",
"searchby": "Search by:", "searchby": "Search by:",
"security_question": "Security question", "security_question": "Security question",
"selectacountry": "Select a country", "selectacountry": "Select a country",
"selectsite": "Please select your site:", "selectsite": "Please select your account:",
"signupplugindisabled": "{{$a}} is not enabled.", "signupplugindisabled": "{{$a}} is not enabled.",
"signuprequiredfieldnotsupported": "The signup form contains a required custom field that isn't supported in the app. Please create your account using a web browser.", "signuprequiredfieldnotsupported": "The signup form contains a required custom field that isn't supported in the app. Please create your account using a web browser.",
"siteaddress": "Your site", "siteaddress": "Your site",
@ -114,6 +117,7 @@
"startsignup": "Create new account", "startsignup": "Create new account",
"stillcantconnect": "Still can't connect?", "stillcantconnect": "Still can't connect?",
"supplyinfo": "More details", "supplyinfo": "More details",
"toggleremove": "Edit accounts list",
"username": "Username", "username": "Username",
"usernameoremail": "Enter either username or email address", "usernameoremail": "Enter either username or email address",
"usernamerequired": "Username required", "usernamerequired": "Username required",

View File

@ -16,9 +16,8 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreLoginSiteHelpComponent } from './components/site-help/site-help';
import { CoreLoginSiteOnboardingComponent } from './components/site-onboarding/site-onboarding';
import { CoreLoginHasSitesGuard } from './guards/has-sites'; import { CoreLoginHasSitesGuard } from './guards/has-sites';
import { CoreLoginComponentsModule } from './components/components.module';
const routes: Routes = [ const routes: Routes = [
{ {
@ -67,11 +66,8 @@ const routes: Routes = [
@NgModule({ @NgModule({
imports: [ imports: [
CoreSharedModule, CoreSharedModule,
CoreLoginComponentsModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
], ],
declarations: [
CoreLoginSiteHelpComponent,
CoreLoginSiteOnboardingComponent,
],
}) })
export class CoreLoginLazyModule {} export class CoreLoginLazyModule {}

View File

@ -35,7 +35,9 @@ const appRoutes: Routes = [
]; ];
@NgModule({ @NgModule({
imports: [AppRoutingModule.forChild(appRoutes)], imports: [
AppRoutingModule.forChild(appRoutes),
],
providers: [ providers: [
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,

View File

@ -35,17 +35,17 @@ export class CoreLoginSitePolicyPage implements OnInit {
showInline?: boolean; showInline?: boolean;
policyLoaded?: boolean; policyLoaded?: boolean;
protected siteId?: string; protected siteId?: string;
protected currentSite?: CoreSite; protected currentSite!: CoreSite;
/** /**
* Component initialized. * @inheritdoc
*/ */
ngOnInit(): void { ngOnInit(): void {
this.siteId = CoreNavigator.getRouteParam('siteId'); this.siteId = CoreNavigator.getRouteParam('siteId');
this.currentSite = CoreSites.getCurrentSite();
if (!this.currentSite) { try {
this.currentSite = CoreSites.getRequiredCurrentSite();
} catch {
// Not logged in, stop. // Not logged in, stop.
this.cancel(); this.cancel();
@ -86,7 +86,7 @@ export class CoreLoginSitePolicyPage implements OnInit {
const extension = CoreMimetypeUtils.getExtension(mimeType, this.sitePolicy); const extension = CoreMimetypeUtils.getExtension(mimeType, this.sitePolicy);
this.showInline = extension == 'html' || extension == 'htm'; this.showInline = extension == 'html' || extension == 'htm';
} catch (error) { } catch {
// Unable to get mime type, assume it's not supported. // Unable to get mime type, assume it's not supported.
this.showInline = false; this.showInline = false;
} finally { } finally {
@ -118,7 +118,7 @@ export class CoreLoginSitePolicyPage implements OnInit {
// Success accepting, go to site initial page. // Success accepting, go to site initial page.
// Invalidate cache since some WS don't return error if site policy is not accepted. // Invalidate cache since some WS don't return error if site policy is not accepted.
await CoreUtils.ignoreErrors(this.currentSite!.invalidateWsCache()); await CoreUtils.ignoreErrors(this.currentSite.invalidateWsCache());
await CoreNavigator.navigateToSiteHome(); await CoreNavigator.navigateToSiteHome();
} catch (error) { } catch (error) {

View File

@ -4,11 +4,11 @@
<ion-back-button [text]="'core.back' | translate"></ion-back-button> <ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons> </ion-buttons>
<h1>{{ 'core.settings.sites' | translate }}</h1> <h1>{{ 'core.login.accounts' | translate }}</h1>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button fill="clear" *ngIf="sites && sites.length > 0" (click)="toggleDelete()" <ion-button fill="clear" *ngIf="accountsList.count > 0" (click)="toggleDelete()"
[attr.aria-label]="'core.delete' | translate"> [attr.aria-label]="'core.login.toggleremove' | translate">
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon> <ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
<ion-button (click)="openSettings()" [attr.aria-label]="'core.settings.appsettings' | translate"> <ion-button (click)="openSettings()" [attr.aria-label]="'core.settings.appsettings' | translate">
@ -18,31 +18,43 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-list> <core-loading [hideUntil]="loaded">
<ion-item button (click)="login(site.id)" *ngFor="let site of sites" detail="true"> <ion-list>
<ion-avatar slot="start"> <ng-container *ngFor="let sites of accountsList.otherSites">
<img [src]="site.avatar" core-external-content [siteId]="site.id" <ion-item-divider sticky="true" *ngIf="sites[0]">
alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" onError="this.src='assets/img/user-avatar.png'"> <ion-label>
</ion-avatar> <h2>
<ion-label> <core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
<p class="item-heading">{{site.fullName}}</p> </h2>
<p><core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text></p> <p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrl }}</a></p>
<p>{{site.siteUrl}}</p> </ion-label>
</ion-label> </ion-item-divider>
<ion-badge slot="end" *ngIf="!showDelete && site.badge">
<span aria-hidden="true">{{site.badge}}</span> <ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
<span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge } }}</span> <ion-avatar slot="start">
</ion-badge> <img [src]="site.avatar" core-external-content [siteId]="site.id"
<ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" onError="this.src='assets/img/user-avatar.png'">
[attr.aria-label]="'core.delete' | translate" [@coreSlideInOut]="'fromRight'"> </ion-avatar>
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon> <ion-label>
</ion-button> <p class="item-heading">{{site.fullName}}</p>
</ion-item> </ion-label>
</ion-list> <ion-badge slot="end" *ngIf="!showDelete && site.badge" @coreShowHideAnimation>
<span aria-hidden="true">{{site.badge}}</span>
<span class="sr-only">{{ 'core.login.sitebadgedescription' | translate:{ count: site.badge }
}}</span>
</ion-badge>
<ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
[attr.aria-label]="'core.login.removeaccount' | translate" [@coreSlideInOut]="'fromRight'">
<ion-icon name="fas-trash" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
</ion-list>
</core-loading>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end"> <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
<ion-fab-button (click)="add()" [attr.aria-label]="'core.add' | translate"> <ion-fab-button (click)="add()" [attr.aria-label]="'core.login.add' | translate">
<ion-icon name="fas-plus" aria-hidden="true"></ion-icon> <ion-icon name="fas-plus" aria-hidden="true"></ion-icon>
<span class="sr-only">{{ 'core.add' | translate }}</span> <span class="sr-only">{{ 'core.login.add' | translate }}</span>
</ion-fab-button> </ion-fab-button>
</ion-fab> </ion-fab>
</ion-content> </ion-content>

View File

@ -13,14 +13,11 @@
// limitations under the License. // limitations under the License.
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
import { CoreLogger } from '@singletons/logger'; import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
import { CoreFilter } from '@features/filter/services/filter'; import { CoreFilter } from '@features/filter/services/filter';
import { CoreAnimations } from '@components/animations'; import { CoreAnimations } from '@components/animations';
@ -30,40 +27,33 @@ import { CoreAnimations } from '@components/animations';
@Component({ @Component({
selector: 'page-core-login-sites', selector: 'page-core-login-sites',
templateUrl: 'sites.html', templateUrl: 'sites.html',
animations: [CoreAnimations.SLIDE_IN_OUT], animations: [CoreAnimations.SLIDE_IN_OUT, CoreAnimations.SHOW_HIDE],
}) })
export class CoreLoginSitesPage implements OnInit { export class CoreLoginSitesPage implements OnInit {
sites: CoreSiteBasicInfo[] = []; accountsList: CoreAccountsList = {
sameSite: [],
otherSites: [],
count: 0,
};
showDelete = false; showDelete = false;
loaded = false;
protected logger: CoreLogger;
constructor() {
this.logger = CoreLogger.getInstance('CoreLoginSitesPage');
}
/** /**
* Component being initialized. * @inheritdoc
*
* @return Promise resolved when done.
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
if (CoreNavigator.getRouteBooleanParam('openAddSite')) { if (CoreNavigator.getRouteBooleanParam('openAddSite')) {
this.add(); this.add();
} }
const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]); this.accountsList = await CoreLoginHelper.getAccountsList();
this.loaded = true;
// Remove protocol from the url to show more url text. if (this.accountsList.count == 0) {
this.sites = await Promise.all(sites.map(async (site) => { this.add();
site.siteUrl = site.siteUrl.replace(/^https?:\/\//, ''); }
site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0;
return site;
}));
this.showDelete = false;
} }
/** /**
@ -76,12 +66,12 @@ export class CoreLoginSitesPage implements OnInit {
/** /**
* Delete a site. * Delete a site.
* *
* @param e Click event. * @param event Click event.
* @param site Site to delete. * @param site Site to delete.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async deleteSite(e: Event, site: CoreSiteBasicInfo): Promise<void> { async deleteSite(event: Event, site: CoreSiteBasicInfo): Promise<void> {
e.stopPropagation(); event.stopPropagation();
let siteName = site.siteName || ''; let siteName = site.siteName || '';
@ -95,20 +85,15 @@ export class CoreLoginSitesPage implements OnInit {
} }
try { try {
await CoreSites.deleteSite(site.id); await CoreLoginHelper.deleteAccountFromList(this.accountsList, site);
const index = this.sites.findIndex((listedSite) => listedSite.id == site.id);
index >= 0 && this.sites.splice(index, 1);
this.showDelete = false; this.showDelete = false;
// If there are no sites left, go to add site. // If there are no sites left, go to add site.
const hasSites = await CoreSites.hasSites(); if (this.accountsList.count == 0) {
if (!hasSites) {
CoreLoginHelper.goToAddSite(true, true); CoreLoginHelper.goToAddSite(true, true);
} }
} catch (error) { } catch (error) {
this.logger.error('Error deleting site ' + site.id, error);
CoreDomUtils.showErrorModalDefault(error, 'core.login.errordeletesite', true); CoreDomUtils.showErrorModalDefault(error, 'core.login.errordeletesite', true);
} }
} }
@ -116,10 +101,14 @@ export class CoreLoginSitesPage implements OnInit {
/** /**
* Login in a site. * Login in a site.
* *
* @param event Click event.
* @param siteId The site ID. * @param siteId The site ID.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async login(siteId: string): Promise<void> { async login(event: Event, siteId: string): Promise<void> {
event.preventDefault();
event.stopPropagation();
const modal = await CoreDomUtils.showModalLoading(); const modal = await CoreDomUtils.showModalLoading();
try { try {
@ -131,7 +120,6 @@ export class CoreLoginSitesPage implements OnInit {
return; return;
} }
} catch (error) { } catch (error) {
this.logger.error('Error loading site ' + siteId, error);
CoreDomUtils.showErrorModalDefault(error, 'Error loading site.'); CoreDomUtils.showErrorModalDefault(error, 'Error loading site.');
} finally { } finally {
modal.dismiss(); modal.dismiss();

View File

@ -19,7 +19,7 @@ import { Md5 } from 'ts-md5/dist/md5';
import { CoreApp, CoreStoreConfig } from '@services/app'; import { CoreApp, CoreStoreConfig } from '@services/app';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';
import { CoreEvents, CoreEventSessionExpiredData, CoreEventSiteData } from '@singletons/events'; import { CoreEvents, CoreEventSessionExpiredData, CoreEventSiteData } from '@singletons/events';
import { CoreSites, CoreLoginSiteInfo } from '@services/sites'; import { CoreSites, CoreLoginSiteInfo, CoreSiteBasicInfo } from '@services/sites';
import { CoreWS, CoreWSExternalWarning } from '@services/ws'; import { CoreWS, CoreWSExternalWarning } from '@services/ws';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
@ -35,6 +35,8 @@ import { CoreUrl } from '@singletons/url';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreCanceledError } from '@classes/errors/cancelederror';
import { CoreCustomURLSchemes } from '@services/urlschemes'; import { CoreCustomURLSchemes } from '@services/urlschemes';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
/** /**
* Helper provider that provides some common features regarding authentication. * Helper provider that provides some common features regarding authentication.
@ -311,7 +313,7 @@ export class CoreLoginHelperProvider {
site = site || CoreSites.getCurrentSite(); site = site || CoreSites.getCurrentSite();
const config = site?.getStoredConfig(); const config = site?.getStoredConfig();
return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'changesite'); return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'switchaccount');
} }
/** /**
@ -407,8 +409,25 @@ export class CoreLoginHelperProvider {
* @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set. * @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise<void> { async goToAddSite(setRoot = false, showKeyboard = false): Promise<void> {
const [path, params] = this.getAddSiteRouteInfo(showKeyboard); let path = '/login/sites';
let params: Params = { openAddSite: true , showKeyboard };
if (CoreSites.isLoggedIn()) {
if (CoreSitePlugins.hasSitePluginsLoaded) {
// The site has site plugins so the app will be restarted. Store the data and logout.
CoreApp.storeRedirect(CoreConstants.NO_SITE_ID, path, { params });
await CoreSites.logout();
return;
}
await CoreSites.logout();
} else {
[path, params] = this.getAddSiteRouteInfo(showKeyboard);
}
await CoreNavigator.navigate(path, { params, reset: setRoot }); await CoreNavigator.navigate(path, { params, reset: setRoot });
} }
@ -1317,43 +1336,121 @@ export class CoreLoginHelperProvider {
} }
} }
/**
* Get the accounts list classified per site.
*
* @param currentSiteId If loggedin, current Site Id.
* @return Promise resolved with account list.
*/
async getAccountsList(currentSiteId?: string): Promise<CoreAccountsList> {
const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]);
const accountsList: CoreAccountsList = {
sameSite: [],
otherSites: [],
count: sites.length,
};
let siteUrl = '';
if (currentSiteId) {
const index = sites.findIndex((site) => site.id == currentSiteId);
accountsList.currentSite = sites.splice(index, 1)[0];
siteUrl = accountsList.currentSite.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
accountsList.currentSite.siteUrl = siteUrl;
}
const otherSites: Record<string, CoreSiteBasicInfo[]> = {};
// Add site counter and classify sites.
await Promise.all(sites.map(async (site) => {
site.siteUrl = site.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0;
if (site.siteUrl == siteUrl) {
accountsList.sameSite.push(site);
} else {
if (!otherSites[site.siteUrl]) {
otherSites[site.siteUrl] = [];
}
otherSites[site.siteUrl].push(site);
}
return;
}));
accountsList.otherSites = CoreUtils.objectToArray(otherSites);
return accountsList;
}
/**
* Find and delete a site from the list of sites.
*
* @param accountsList Account list.
* @param site Site to be deleted.
* @return Resolved when done.
*/
async deleteAccountFromList(accountsList: CoreAccountsList, site: CoreSiteBasicInfo): Promise<void> {
await CoreSites.deleteSite(site.id);
const siteUrl = site.siteUrl;
let index = 0;
// Found on same site.
if (accountsList.sameSite.length > 0 && accountsList.sameSite[0].siteUrl == siteUrl) {
index = accountsList.sameSite.findIndex((listedSite) => listedSite.id == site.id);
if (index >= 0) {
accountsList.sameSite.splice(index, 1);
accountsList.count--;
}
return;
}
const otherSiteIndex = accountsList.otherSites.findIndex((sites) => sites.length > 0 && sites[0].siteUrl == siteUrl);
if (otherSiteIndex < 0) {
// Site Url not found.
return;
}
index = accountsList.otherSites[otherSiteIndex].findIndex((listedSite) => listedSite.id == site.id);
if (index >= 0) {
accountsList.otherSites[otherSiteIndex].splice(index, 1);
accountsList.count--;
}
if (accountsList.otherSites[otherSiteIndex].length == 0) {
accountsList.otherSites.splice(otherSiteIndex, 1);
}
}
} }
export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider); export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider);
/**
* Accounts list for selecting sites interfaces.
*/
export type CoreAccountsList = {
currentSite?: CoreSiteBasicInfo; // If logged in, current site info.
sameSite: CoreSiteBasicInfo[]; // If logged in, accounts info on the same site.
otherSites: CoreSiteBasicInfo[][]; // Other accounts in other sites.
count: number; // Number of sites.
};
/** /**
* Data related to a SSO authentication. * Data related to a SSO authentication.
*/ */
export interface CoreLoginSSOData { export interface CoreLoginSSOData {
/** siteUrl: string; // The site's URL.
* The site's URL. token?: string; // User's token.
*/ privateToken?: string; // User's private token.
siteUrl: string; pageName?: string; // Name of the page to go after authenticated.
pageOptions?: CoreNavigationOptions; // Options of the navigation to the page.
/** ssoUrlParams?: CoreUrlParams; // Other params added to the login url.
* User's token.
*/
token?: string;
/**
* User's private token.
*/
privateToken?: string;
/**
* Name of the page to go after authenticated.
*/
pageName?: string;
/**
* Options of the navigation to the page.
*/
pageOptions?: CoreNavigationOptions;
/**
* Other params added to the login url.
*/
ssoUrlParams?: CoreUrlParams;
}; };
/** /**

View File

@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button'; import { CoreMainMenuUserButtonComponent } from './user-menu-button/user-menu-button';
import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu'; import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu';
import { CoreLoginComponentsModule } from '@features/login/components/components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -24,6 +25,7 @@ import { CoreMainMenuUserMenuComponent } from './user-menu/user-menu';
], ],
imports: [ imports: [
CoreSharedModule, CoreSharedModule,
CoreLoginComponentsModule,
], ],
exports: [ exports: [
CoreMainMenuUserButtonComponent, CoreMainMenuUserButtonComponent,

View File

@ -8,59 +8,71 @@
<h1> <h1>
{{'core.user.account' | translate}} {{'core.user.account' | translate}}
</h1> </h1>
<ion-buttons slot="end" *ngIf="loaded">
<ion-button fill="clear" *ngIf="moreSites" (click)="switchAccounts($event)"
[attr.aria-label]="'core.mainmenu.changeaccount' | translate">
<ion-icon name="fas-exchange-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button fill="clear" *ngIf="!moreSites" (click)="addAccount($event)"
[attr.aria-label]="'core.login.add' | translate">
<ion-icon name="fas-user-plus" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<core-loading [hideUntil]="true"> <ion-list>
<ion-list> <ion-item class="ion-text-center core-user-profile-maininfo" *ngIf="siteInfo" lines="none">
<ion-item class="ion-text-center core-user-profile-maininfo" *ngIf="siteInfo" lines="none"> <core-user-avatar [user]="siteInfo" [userId]="siteInfo.userid" [linkProfile]="false"></core-user-avatar>
<core-user-avatar [user]="siteInfo" [userId]="siteInfo.userid" [linkProfile]="false"></core-user-avatar> <ion-label>
<ion-label> <h2>{{ siteInfo.fullname }}</h2>
<h2>{{ siteInfo.fullname }}</h2> <p class="core-usermenu-siteinfo core-usermenu-sitename">
<p class="core-usermenu-siteinfo core-usermenu-sitename"> <core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true"> [wsNotFiltered]="true">
</core-format-text> </core-format-text>
</p> </p>
<p class="core-usermenu-siteinfo core-usermenu-siteurl"> <p class="core-usermenu-siteinfo core-usermenu-siteurl">
<a [href]="siteUrl" core-link autoLogin="yes">{{ siteUrl }}</a> <a [href]="siteUrl" core-link autoLogin="yes">{{ siteUrl }}</a>
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item button class="ion-text-wrap core-usermenu-handler" (click)="openUserProfile($event)" <ion-item button class="ion-text-wrap core-usermenu-handler" (click)="openUserProfile($event)"
[attr.aria-label]="'core.user.details' | translate" detail="true" lines="none"> [attr.aria-label]="'core.user.details' | translate" detail="true" lines="none">
<ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon>
<ion-label> <ion-label>
<p class="item-heading">{{ 'core.user.details' | translate }}</p> <p class="item-heading">{{ 'core.user.details' | translate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded" lines="none"> <ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded" lines="none">
<ion-label><ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner></ion-label> <ion-label>
</ion-item> <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)" <ion-item button *ngFor="let handler of handlers" class="ion-text-wrap"
[ngClass]="['core-user-menu-handler', handler.class || '']" [hidden]="handler.hidden" (click)="handlerClicked($event, handler)" [ngClass]="['core-user-menu-handler', handler.class || '']"
[attr.aria-label]="handler.title | translate" detail="true" lines="none"> [hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" detail="true" lines="none">
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon> <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon>
<ion-label> <ion-label>
<p class="item-heading">{{ handler.title | translate }}</p> <p class="item-heading">{{ handler.title | translate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item button (click)="openPreferences($event)" [attr.aria-label]="'core.settings.preferences' | translate" detail="true" <ion-item button (click)="openPreferences($event)" [attr.aria-label]="'core.settings.preferences' | translate"
class="core-user-menu-preferences"> detail="true" class="core-user-menu-preferences">
<ion-icon name="fas-wrench" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-wrench" slot="start" aria-hidden="true"></ion-icon>
<ion-label> <ion-label>
<p class="item-heading">{{ 'core.settings.preferences' | translate }}</p> <p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
</ion-list> </ion-list>
</core-loading>
</ion-content> </ion-content>
<ion-footer class="ion-padding"> <ion-footer class="ion-padding">
<ion-button (click)="logout($event)" expand="block" color="danger" [attr.aria-label]="logoutLabel | translate" class="ion-text-wrap"> <ion-button (click)="logout($event)" expand="block" color="danger"
[attr.aria-label]="'core.mainmenu.logout' | translate" class="ion-text-wrap">
<ion-icon name="fas-sign-out-alt" slot="start" aria-hidden="true"></ion-icon> <ion-icon name="fas-sign-out-alt" slot="start" aria-hidden="true"></ion-icon>
{{ logoutLabel | translate }} {{ 'core.mainmenu.logout' | translate }}
</ion-button> </ion-button>
</ion-footer> </ion-footer>

View File

@ -14,11 +14,13 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { CoreSiteInfo } from '@classes/site'; import { CoreSiteInfo } from '@classes/site';
import { CoreLoginSitesComponent } from '@features/login/components/sites/sites';
import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreUser, CoreUserProfile } from '@features/user/services/user'; import { CoreUser, CoreUserProfile } from '@features/user/services/user';
import { CoreUserProfileHandlerData, CoreUserDelegate, CoreUserDelegateService } from '@features/user/services/user-delegate'; import { CoreUserProfileHandlerData, CoreUserDelegate, CoreUserDelegateService } from '@features/user/services/user-delegate';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { ModalController } from '@singletons'; import { ModalController } from '@singletons';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -34,11 +36,12 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
siteInfo?: CoreSiteInfo; siteInfo?: CoreSiteInfo;
siteName?: string; siteName?: string;
logoutLabel = 'core.mainmenu.changesite';
siteUrl?: string; siteUrl?: string;
handlers: CoreUserProfileHandlerData[] = []; handlers: CoreUserProfileHandlerData[] = [];
handlersLoaded = false; handlersLoaded = false;
loaded = false;
user?: CoreUserProfile; user?: CoreUserProfile;
moreSites = false;
protected subscription!: Subscription; protected subscription!: Subscription;
@ -46,13 +49,16 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
// Check if there are more sites to switch.
const sites = await CoreSites.getSites();
this.moreSites = sites.length > 1;
const currentSite = CoreSites.getRequiredCurrentSite(); const currentSite = CoreSites.getRequiredCurrentSite();
this.siteInfo = currentSite.getInfo(); this.siteInfo = currentSite.getInfo();
this.siteName = currentSite.getSiteName(); this.siteName = currentSite.getSiteName();
this.siteUrl = currentSite.getURL(); this.siteUrl = currentSite.getURL();
this.logoutLabel = CoreLoginHelper.getLogoutLabel(currentSite);
this.loaded = true;
// Load the handlers. // Load the handlers.
if (this.siteInfo) { if (this.siteInfo) {
@ -133,6 +139,38 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
CoreSites.logout(); CoreSites.logout();
} }
/**
* Show account selector.
*
* @param event Click event
*/
async switchAccounts(event: Event): Promise<void> {
const thisModal = await ModalController.getTop();
event.preventDefault();
event.stopPropagation();
const closeAll = await CoreDomUtils.openSideModal<boolean>({
component: CoreLoginSitesComponent,
cssClass: 'core-modal-lateral-sm',
});
if (closeAll) {
await ModalController.dismiss(undefined, undefined, thisModal.id);
}
}
/**
* Add account.
*
* @param event Click event
*/
async addAccount(event: Event): Promise<void> {
await this.close(event);
await CoreLoginHelper.goToAddSite(true, true);
}
/** /**
* Close modal. * Close modal.
*/ */

View File

@ -1,5 +1,5 @@
{ {
"changesite": "Change site",
"home": "Home", "home": "Home",
"logout": "Log out" "logout": "Log out",
"switchaccount": "Switch account"
} }

View File

@ -35,7 +35,7 @@ function buildRoutes(injector: Injector): Routes {
data: { data: {
mainMenuTabRoot: CoreTagMainMenuHandlerService.PAGE_NAME, mainMenuTabRoot: CoreTagMainMenuHandlerService.PAGE_NAME,
}, },
loadChildren: () => import('@features/tag//pages/search/search.page.module').then(m => m.CoreTagSearchPageModule), loadChildren: () => import('@features/tag/pages/search/search.page.module').then(m => m.CoreTagSearchPageModule),
}, },
CoreTagIndexAreaRoute, CoreTagIndexAreaRoute,
...buildTabMainRoutes(injector, { ...buildTabMainRoutes(injector, {

View File

@ -15,12 +15,12 @@
"interests": "Interests", "interests": "Interests",
"lastname": "Surname", "lastname": "Surname",
"manager": "Manager", "manager": "Manager",
"myprofile": "My profile",
"newpicture": "New picture", "newpicture": "New picture",
"noparticipants": "No participants found for this course", "noparticipants": "No participants found for this course",
"participants": "Participants", "participants": "Participants",
"phone1": "Phone", "phone1": "Phone",
"phone2": "Mobile phone", "phone2": "Mobile phone",
"profile": "Profile",
"roles": "Roles", "roles": "Roles",
"sendemail": "Email", "sendemail": "Email",
"student": "Student", "student": "Student",

View File

@ -606,7 +606,7 @@ export class CoreAppProvider {
}; };
localStorage.setItem('CoreRedirect', JSON.stringify(redirect)); localStorage.setItem('CoreRedirect', JSON.stringify(redirect));
} catch (ex) { } catch {
// Ignore errors. // Ignore errors.
} }
} }

View File

@ -1095,7 +1095,7 @@ export class CoreSitesProvider {
async getSortedSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> { async getSortedSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
const sites = await this.getSites(ids); const sites = await this.getSites(ids);
// Sort sites by url and ful lname. // Sort sites by url and fullname.
sites.sort((a, b) => { sites.sort((a, b) => {
// First compare by site url without the protocol. // First compare by site url without the protocol.
const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
@ -1754,40 +1754,13 @@ export type CoreSiteUserTokenResponse = {
* Site's basic info. * Site's basic info.
*/ */
export type CoreSiteBasicInfo = { export type CoreSiteBasicInfo = {
/** id: string; // Site ID.
* Site ID. siteUrl: string; // Site URL.
*/ fullName?: string; // User's full name.
id: string; siteName?: string; // Site's name.
avatar?: string; // User's avatar.
/** badge?: number; // Badge to display in the site.
* Site URL. siteHomeId?: number; // Site home ID.
*/
siteUrl: string;
/**
* User's full name.
*/
fullName?: string;
/**
* Site's name.
*/
siteName?: string;
/**
* User's avatar.
*/
avatar?: string;
/**
* Badge to display in the site.
*/
badge?: number;
/**
* Site home ID.
*/
siteHomeId?: number;
}; };
/** /**