Merge pull request #3844 from dpalou/MOBILE-4459

Mobile 4459
main
Noel De Martin 2023-11-15 15:25:03 +01:00 committed by GitHub
commit ee9daad783
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 735 additions and 322 deletions

View File

@ -0,0 +1,32 @@
// (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 { CoreSiteFixture } from '@/storybook/stubs/classes/site';
export const companyLisaSite: CoreSiteFixture = {
id: 'companylisasite',
info: {
version: '2022041900',
sitename: 'Company',
username: 'lisa',
firstname: 'Lisa',
lastname: 'Díaz',
fullname: 'Lisa Díaz',
lang: 'en',
userid: 1,
siteurl: 'https://company.example.edu',
userpictureurl: 'https://i.pravatar.cc/300?user=companylisa',
functions: [],
},
};

View File

@ -1 +0,0 @@
{"id":"123456","info":{"version":"2022041900","sitename":"School","username":"barbara","firstname":"Barbara","lastname":"Gardner","fullname":"Barbara Gardner","lang":"en","userid":1,"siteurl":"https://campus.example.edu","userpictureurl":"","functions":[]}}

View File

@ -0,0 +1,32 @@
// (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 { CoreSiteFixture } from '@/storybook/stubs/classes/site';
export const schoolBarbaraSite: CoreSiteFixture = {
id: 'schoolbarbarasite',
info: {
version: '2022041900',
sitename: 'School',
username: 'barbara',
firstname: 'Barbara',
lastname: 'Gardner',
fullname: 'Barbara Gardner',
lang: 'en',
userid: 1,
siteurl: 'https://campus.example.edu',
userpictureurl: 'https://i.pravatar.cc/300?user=schoolbarbara',
functions: [],
},
};

View File

@ -0,0 +1,32 @@
// (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 { CoreSiteFixture } from '@/storybook/stubs/classes/site';
export const schoolJefferySite: CoreSiteFixture = {
id: 'schooljefferysite',
info: {
version: '2022041900',
sitename: 'School',
username: 'jeffery',
firstname: 'Jeffery',
lastname: 'Sanders',
fullname: 'Jeffery Sanders',
lang: 'en',
userid: 2,
siteurl: 'https://campus.example.edu',
userpictureurl: 'https://i.pravatar.cc/300?user=schooljeffery',
functions: [],
},
};

View File

@ -296,14 +296,18 @@ export class CoreSite {
* @returns Site name.
*/
async getSiteName(): Promise<string> {
if (this.isDemoModeSite()) {
return CoreConstants.CONFIG.appname;
}
if (this.infos?.sitename) {
return this.infos?.sitename;
}
// Fallback.
const isSigleFixedSite = await CoreLoginHelper.isSingleFixedSite();
const isSingleFixedSite = await CoreLoginHelper.isSingleFixedSite();
if (isSigleFixedSite) {
if (isSingleFixedSite) {
const sites = await CoreLoginHelper.getAvailableSites();
return sites[0].name;
@ -2459,6 +2463,17 @@ export class CoreSite {
});
}
/**
* Check if the site is a demo mode site.
*
* @returns Whether the site is a demo mode site.
*/
isDemoModeSite(): boolean {
const demoSiteData = CoreLoginHelper.getDemoModeSiteInfo();
return this.containsUrl(demoSiteData?.url);
}
}
/**

View File

@ -65,6 +65,7 @@ import { CoreGroupSelectorComponent } from './group-selector/group-selector';
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
import { CoreCourseImageComponent } from '@components/course-image/course-image';
import { CoreSitesListComponent } from './sites-list/sites-list';
@NgModule({
declarations: [
@ -112,6 +113,7 @@ import { CoreCourseImageComponent } from '@components/course-image/course-image'
CoreSwipeNavigationTourComponent,
CoreRefreshButtonModalComponent,
CoreSheetModalComponent,
CoreSitesListComponent,
],
imports: [
CommonModule,
@ -166,6 +168,7 @@ import { CoreCourseImageComponent } from '@components/course-image/course-image'
CoreSwipeNavigationTourComponent,
CoreRefreshButtonModalComponent,
CoreSheetModalComponent,
CoreSitesListComponent,
],
})
export class CoreComponentsModule {}

View File

@ -0,0 +1,56 @@
<ion-list class="core-sites-list" *ngIf="accountsList">
<ion-card *ngIf="accountsList.currentSite">
<ng-container *ngTemplateOutlet="siteCardHeader; context: {site: accountsList.currentSite, isCurrentSite: true}"></ng-container>
<ng-container *ngTemplateOutlet="siteItem; context: {site: accountsList.currentSite, isCurrentSite: true}"></ng-container>
<ng-container *ngFor="let site of accountsList.sameSite">
<ng-container *ngTemplateOutlet="siteItem; context: {site: site, isCurrentSite: false}"></ng-container>
</ng-container>
</ion-card>
<ion-card *ngFor="let sites of accountsList.otherSites">
<ng-container *ngTemplateOutlet="siteCardHeader; context: {site: sites[0], isCurrentSite: false}"></ng-container>
<ng-container *ngFor="let site of sites">
<ng-container *ngTemplateOutlet="siteItem; context: {site: site, isCurrentSite: false}"></ng-container>
</ng-container>
</ion-card>
</ion-list>
<!-- Template to render the header of a site card. -->
<ng-template #siteCardHeader let-site="site" let-isCurrentSite="isCurrentSite">
<ion-item-divider sticky="true" *ngIf="site" class="core-sites-list-sitename">
<ion-label>
<h2>
<core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text>
</h2>
<p *ngIf="!site.isDemoModeSite">
<a [href]="site.siteUrl" core-link [autoLogin]="isCurrentSite ? 'yes' : 'no'">
{{ site.siteUrlWithoutProtocol }}
</a>
</p>
</ion-label>
</ion-item-divider>
</ng-template>
<!-- Template to render a site item. -->
<ng-template #siteItem let-site="site" let-isCurrentSite="isCurrentSite">
<ion-item [attr.button]="isSiteClickable(isCurrentSite) ? true : null" (click)="siteClicked($event, site, isCurrentSite)"
[attr.detail]="isSiteClickable(isCurrentSite) ? 'true' : 'false'" [class.item-current]="isCurrentSite">
<core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<ion-label>
<p class="item-heading">{{site.fullname}}</p>
<ng-container *ngIf="siteLabelTemplate" [ngTemplateOutlet]="siteLabelTemplate"
[ngTemplateOutletContext]="{site: site, isCurrentSite: isCurrentSite}">
</ng-container>
</ion-label>
<ng-container *ngIf="siteItemTemplate" [ngTemplateOutlet]="siteItemTemplate"
[ngTemplateOutletContext]="{site: site, isCurrentSite: isCurrentSite}">
</ng-container>
</ion-item>
</ng-template>

View File

@ -1,5 +1,5 @@
ion-list.core-sitelist {
.core-sitelist-sitename {
ion-list.core-sites-list {
.core-sites-list-sitename {
ion-label {
margin-top: 8px;
margin-bottom: 8px;

View File

@ -0,0 +1,80 @@
// (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, ContentChild, Input, Output, TemplateRef, EventEmitter } from '@angular/core';
import { CoreSiteBasicInfo } from '@services/sites';
import { CoreAccountsList } from '@features/login/services/login-helper';
/**
* Component to display a list of sites (accounts).
*
* By default this component will display the avatar and user fullname for each site, but it allows adding more information
* in the item and in the label for each site, using #siteItem and #siteLabel ng-templates. These templates will receive the
* site being rendered and whether it's the current site or not. Example:
*
* <core-sites-list [accountsList]="accountsList">
* <ng-template #siteLabel let-site="site" let-isCurrentSite="isCurrentSite">
* <!-- Content to be placed in the label, after the user full name.
* </ng-template>
*
* <ng-template #siteItem let-site="site" let-isCurrentSite="isCurrentSite">
* <!-- Content to be placed in the item.
* </ng-template>
* </core-sites-list>
*/
@Component({
selector: 'core-sites-list',
templateUrl: 'sites-list.html',
styleUrls: ['sites-list.scss'],
})
export class CoreSitesListComponent<T extends CoreSiteBasicInfo> {
@Input() accountsList!: CoreAccountsList<T>;
@Input() sitesClickable = false; // Whether the sites are clickable.
@Input() currentSiteClickable?: boolean; // If set, specify a different clickable value for current site.
@Output() onSiteClicked = new EventEmitter<T>();
@ContentChild('siteItem') siteItemTemplate?: TemplateRef<unknown>;
@ContentChild('siteLabel') siteLabelTemplate?: TemplateRef<unknown>;
/**
* Check whether a site is clickable.
*
* @param isCurrentSite Whether the site is current site.
* @returns Whether it's clickable.
*/
isSiteClickable(isCurrentSite: boolean): boolean {
return isCurrentSite ? this.currentSiteClickable ?? this.sitesClickable : this.sitesClickable;
}
/**
* A site was clicked.
*
* @param ev Event.
* @param site Site clicked.
* @param isCurrentSite Whether the site is current site.
*/
siteClicked(ev: Event, site: T, isCurrentSite: boolean): void {
if (!this.isSiteClickable(isCurrentSite)) {
return;
}
ev.preventDefault();
ev.stopPropagation();
this.onSiteClicked.emit(site);
}
}

View File

@ -21,6 +21,8 @@ import { CoreComponentsModule } from '@components/components.module';
import { CommonModule } from '@angular/common';
import { CoreCourseImageCardsPageComponent } from '@components/stories/components/course-image-cards-page/course-image-cards-page';
import { CoreCourseImageListPageComponent } from '@components/stories/components/course-image-list-page/course-image-list-page';
import { CoreSitesListWrapperComponent } from './sites-list-wrapper/sites-list-wrapper';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
@ -28,10 +30,12 @@ import { CoreCourseImageListPageComponent } from '@components/stories/components
CoreCourseImageListPageComponent,
CoreEmptyBoxPageComponent,
CoreEmptyBoxWrapperComponent,
CoreSitesListWrapperComponent,
],
imports: [
CommonModule,
StorybookModule,
CoreDirectivesModule,
CoreComponentsModule,
CoreSearchComponentsModule,
],

View File

@ -0,0 +1,23 @@
<ion-app>
<ion-content class="limited-width">
<core-sites-list *ngIf="accountsList" [accountsList]="accountsList" [sitesClickable]="sitesClickable"
[currentSiteClickable]="currentSiteClickable" (onSiteClicked)="siteClicked($event)">
<ng-template *ngIf="extraText !== 'none'" #siteLabel let-site="site">
<p *ngIf="extraText === 'text'">Extra text for user {{ site.fullname }}</p>
<ion-badge *ngIf="extraText === 'badge'" color="light">{{ site.badge }} MB</ion-badge>
</ng-template>
<ng-template #siteItem let-site="site">
<ion-button *ngIf="extraDetails === 'delete-button'" fill="clear" color="danger" slot="end">
<ion-icon name="fas-trash" slot="icon-only"></ion-icon>
</ion-button>
<ion-badge *ngIf="extraDetails === 'badge'" slot="end">
<span>{{site.badge}}</span>
</ion-badge>
</ng-template>
</core-sites-list>
</ion-content>
</ion-app>

View File

@ -0,0 +1,60 @@
// (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, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreSiteBasicInfo } from '@services/sites';
@Component({
selector: 'core-sites-list-wrapper',
templateUrl: 'sites-list-wrapper.html',
})
export class CoreSitesListWrapperComponent implements OnInit, OnChanges {
@Input() sitesClickable = false;
@Input() currentSiteClickableSelect = 'undefined';
@Input() extraText: 'text' | 'badge' | 'none' = 'none';
@Input() extraDetails: 'delete-button' | 'badge' | 'none' = 'none';
accountsList?: CoreAccountsList;
currentSiteClickable?: boolean;
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.accountsList = await CoreLoginHelper.getAccountsList();
}
/**
* @inheritdoc
*/
async ngOnChanges(changes: SimpleChanges): Promise<void> {
if (changes.currentSiteClickableSelect) {
this.currentSiteClickable = this.currentSiteClickableSelect === 'undefined' ?
undefined :
this.currentSiteClickableSelect === 'true';
}
}
/**
* Site clicked.
*
* @param site Site.
*/
siteClicked(site: CoreSiteBasicInfo): void {
alert(`clicked on ${site.id} - ${site.fullname}`);
}
}

View File

@ -0,0 +1,78 @@
// (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 { Meta, moduleMetadata } from '@storybook/angular';
import { story } from '@/storybook/utils/helpers';
import { CoreSitesListComponent } from '@components/sites-list/sites-list';
import { CoreSitesListWrapperComponent } from './components/sites-list-wrapper/sites-list-wrapper';
import { CoreComponentsStorybookModule } from './components/components.module';
interface Args {
sitesClickable: boolean;
currentSiteClickable: 'true' | 'false' | 'undefined';
extraText: 'text' | 'badge' | 'none';
extraDetails: 'delete-button' | 'badge' | 'none';
}
export default <Meta<Args>> {
title: 'Core/Sites List',
component: CoreSitesListComponent,
decorators: [
moduleMetadata({ imports: [CoreComponentsStorybookModule] }),
],
argTypes: {
sitesClickable: {
control: {
type: 'boolean',
},
},
currentSiteClickable: {
control: {
type: 'select',
options: ['true', 'false', 'undefined'],
},
},
extraText: {
control: {
type: 'select',
options: ['text', 'badge', 'none'],
},
},
extraDetails: {
control: {
type: 'select',
options: ['delete-button', 'badge', 'none'],
},
},
},
args: {
sitesClickable: false,
currentSiteClickable: 'undefined',
extraText: 'none',
extraDetails: 'none',
},
};
const Template = story<Args>(({ sitesClickable, currentSiteClickable, extraText, extraDetails }) => ({
component: CoreSitesListWrapperComponent,
props: {
sitesClickable,
currentSiteClickableSelect: currentSiteClickable,
extraText,
extraDetails,
},
}));
export const Primary = story<Args>(Template);

View File

@ -27,7 +27,7 @@
<p>
<core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text>
</p>
<p>{{site.siteUrl}}</p>
<p *ngIf="!site.isDemoModeSite">{{site.siteUrl}}</p>
</ion-label>
</ion-item>
</ion-list>

View File

@ -16,7 +16,7 @@ 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';
import { CoreLoginSitesModalComponent } from './sites-modal/sites-modal';
import { CoreLoginMethodsComponent } from './login-methods/login-methods';
import { CoreLoginExceededAttemptsComponent } from '@features/login/components/exceeded-attempts/exceeded-attempts';
@ -25,7 +25,7 @@ import { CoreLoginExceededAttemptsComponent } from '@features/login/components/e
CoreLoginExceededAttemptsComponent,
CoreLoginSiteOnboardingComponent,
CoreLoginSiteHelpComponent,
CoreLoginSitesComponent,
CoreLoginSitesModalComponent,
CoreLoginMethodsComponent,
],
imports: [
@ -35,7 +35,7 @@ import { CoreLoginExceededAttemptsComponent } from '@features/login/components/e
CoreLoginExceededAttemptsComponent,
CoreLoginSiteOnboardingComponent,
CoreLoginSiteHelpComponent,
CoreLoginSitesComponent,
CoreLoginSitesModalComponent,
CoreLoginMethodsComponent,
],
})

View File

@ -0,0 +1,48 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button fill="clear" (click)="close($event)" [attr.aria-label]="'core.back' | translate" class="ion-back-button">
<ion-icon ios="chevron-back" md="arrow-back-sharp" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>
<h1>{{ 'core.mainmenu.switchaccount' | translate }}</h1>
</ion-title>
<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">
<core-sites-list [accountsList]="accountsList" [sitesClickable]="true" [currentSiteClickable]="false"
(onSiteClicked)="login($event)">
<ng-template #siteItem let-site="site" let-isCurrentSite="isCurrentSite">
<ion-icon *ngIf="isCurrentSite" color="success" name="fas-check"></ion-icon>
<ng-container *ngIf="!isCurrentSite">
<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>
</ng-container>
</ng-template>
</core-sites-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>

View File

@ -23,15 +23,14 @@ import { CoreAnimations } from '@components/animations';
import { ModalController } from '@singletons';
/**
* Component that displays a "splash screen" while the app is being initialized.
* Modal that displays a list of sites to be able to enter or delete a site.
*/
@Component({
selector: 'core-login-sites',
templateUrl: 'sites.html',
styleUrls: ['../../sitelist.scss'],
selector: 'core-login-sites-modal',
templateUrl: 'sites-modal.html',
animations: [CoreAnimations.SLIDE_IN_OUT, CoreAnimations.SHOW_HIDE],
})
export class CoreLoginSitesComponent implements OnInit {
export class CoreLoginSitesModalComponent implements OnInit {
accountsList: CoreAccountsList = {
sameSite: [],
@ -51,7 +50,7 @@ export class CoreLoginSitesComponent implements OnInit {
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.accountsList = await CoreLoginHelper.getAccountsList(this.currentSiteId);
this.accountsList = await CoreLoginHelper.getAccountsList();
this.loaded = true;
}
@ -99,15 +98,14 @@ export class CoreLoginSitesComponent implements OnInit {
/**
* Login in a site.
*
* @param event Click event.
* @param siteId The site ID.
* @param site The site.
* @returns Promise resolved when done.
*/
async login(event: Event, siteId: string): Promise<void> {
await this.close(event, true);
async login(site: CoreSiteBasicInfo): Promise<void> {
await this.close(undefined, true);
// This navigation will logout and navigate to the site home.
await CoreNavigator.navigateToSiteHome({ preferCurrentTab: false , siteId });
await CoreNavigator.navigateToSiteHome({ preferCurrentTab: false , siteId: site.id });
}
/**
@ -122,9 +120,9 @@ export class CoreLoginSitesComponent implements OnInit {
*
* @param event Click event.
*/
async close(event: Event, closeAll = false): Promise<void> {
event.preventDefault();
event.stopPropagation();
async close(event?: Event, closeAll = false): Promise<void> {
event?.preventDefault();
event?.stopPropagation();
await ModalController.dismiss(closeAll);
}

View File

@ -1,91 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button fill="clear" (click)="close($event)" [attr.aria-label]="'core.back' | translate" class="ion-back-button">
<ion-icon ios="chevron-back" md="arrow-back-sharp" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>
<h1>{{ 'core.mainmenu.switchaccount' | translate }}</h1>
</ion-title>
<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 class="core-sitelist">
<ion-card *ngIf="accountsList.currentSite">
<ion-item-divider sticky="true" class="core-sitelist-sitename">
<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.siteUrlWithoutProtocol }}</a>
</p>
</ion-label>
</ion-item-divider>
<ion-item detail="false">
<core-user-avatar [user]="accountsList.currentSite" slot="start" [linkProfile]="false"
[siteId]="accountsList.currentSite.id"></core-user-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>
</ion-card>
<ion-card *ngFor="let sites of accountsList.otherSites">
<ion-item-divider sticky="true" *ngIf="sites[0]" class="core-sitelist-sitename">
<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].siteUrlWithoutProtocol }}</a></p>
</ion-label>
</ion-item-divider>
<ng-container *ngTemplateOutlet="siteList; context: {sites: sites}"></ng-container>
</ion-card>
</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">
<core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-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

@ -32,7 +32,7 @@
<h2 *ngIf="siteName" class="ion-margin-top ion-no-padding core-sitename">
<core-format-text [text]="siteName" [filter]="false"></core-format-text>
</h2>
<p class="core-siteurl">{{siteUrl}}</p>
<p class="core-siteurl" *ngIf="!isDemoModeSite">{{siteUrl}}</p>
</div>
<core-login-exceeded-attempts *ngIf="exceededAttemptsHTML && supportConfig && loginAttempts >= 3"

View File

@ -61,6 +61,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
exceededAttemptsHTML?: SafeHtml | string | null;
siteConfig?: CoreSitePublicConfigResponse;
siteCheckError = '';
isDemoModeSite = false;
protected siteCheck?: CoreSiteCheckResponse;
protected eventThrown = false;
@ -89,8 +90,11 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
this.siteConfig = this.siteCheck.config;
}
this.siteName = CoreNavigator.getRouteParam('siteName');
this.logoUrl = !CoreConstants.CONFIG.forceLoginLogo && CoreNavigator.getRouteParam('logoUrl') || undefined;
this.isDemoModeSite = CoreLoginHelper.isDemoModeSite(this.siteUrl);
this.siteName = this.isDemoModeSite ? CoreConstants.CONFIG.appname : CoreNavigator.getRouteParam('siteName');
this.logoUrl = !CoreConstants.CONFIG.forceLoginLogo && !this.isDemoModeSite ?
CoreNavigator.getRouteParam('logoUrl') :
undefined;
this.urlToOpen = CoreNavigator.getRouteParam('urlToOpen');
this.supportConfig = this.siteConfig && new CoreUserGuestSupportConfig(this.siteConfig);
} catch (error) {
@ -191,9 +195,13 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
return;
}
if (this.isDemoModeSite) {
this.showScanQR = false;
} else {
this.siteName = this.siteConfig.sitename;
this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig);
this.showScanQR = await CoreLoginHelper.displayQRInCredentialsScreen(this.siteConfig.tool_mobile_qrcodetype);
}
const disabledFeatures = CoreLoginHelper.getDisabledFeatures(this.siteConfig);
this.canSignup = this.siteConfig.registerauth == 'email' &&

View File

@ -82,7 +82,7 @@
<ion-item class="ion-text-wrap ion-text-center">
<ion-label>
<!-- If no sitename show big siteurl. -->
<p *ngIf="!siteName" class="ion-padding item-heading">{{siteUrl}}</p>
<p *ngIf="!siteName && !isDemoModeSite" class="ion-padding item-heading">{{siteUrl}}</p>
<!-- If sitename, show big sitename and small siteurl. -->
<p *ngIf="siteName" class="ion-padding item-heading">
<core-format-text [text]="siteName" [filter]="false"></core-format-text>

View File

@ -34,6 +34,7 @@ import { CoreForms } from '@singletons/form';
import { CoreRecaptchaComponent } from '@components/recaptcha/recaptcha';
import { CorePath } from '@singletons/path';
import { CoreDom } from '@singletons/dom';
import { CoreConstants } from '@/core/constants';
/**
* Page to signup using email.
@ -51,6 +52,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
signupForm: FormGroup;
siteUrl!: string;
isDemoModeSite = false;
siteConfig?: CoreSitePublicConfigResponse;
siteName?: string;
authInstructions = '';
@ -127,6 +129,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
}
this.siteUrl = siteUrl;
this.isDemoModeSite = CoreLoginHelper.isDemoModeSite(this.siteUrl);
// Fetch the data.
this.fetchData().finally(() => {
@ -235,7 +238,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
*/
protected treatSiteConfig(): boolean {
if (this.siteConfig?.registerauth == 'email' && !CoreLoginHelper.isEmailSignupDisabled(this.siteConfig)) {
this.siteName = this.siteConfig.sitename;
this.siteName = this.isDemoModeSite ? CoreConstants.CONFIG.appname : this.siteConfig.sitename;
this.authInstructions = this.siteConfig.authinstructions;
this.ageDigitalConsentVerification = this.siteConfig.agedigitalconsentverification;
this.supportName = this.siteConfig.supportname;

View File

@ -40,7 +40,7 @@
<p *ngIf="siteInfo?.siteName" class="ion-no-margin ion-no-padding core-sitename">
<core-format-text [text]="siteInfo?.siteName" [filter]="false"></core-format-text>
</p>
<p class="core-siteurl">{{siteUrl}}</p>
<p class="core-siteurl" *ngIf="!isDemoModeSite">{{siteUrl}}</p>
</div>
<div class="core-login-user">

View File

@ -46,6 +46,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
credForm: FormGroup;
siteUrl!: string;
isDemoModeSite = false;
logoUrl?: string;
showForgottenPassword = true;
showUserAvatar = false;
@ -101,6 +102,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
}
this.siteUrl = site.getURL();
this.isDemoModeSite = site.isDemoModeSite();
this.siteInfo = {
id: this.siteId,
@ -112,6 +114,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
siteName: await site.getSiteName(),
userpictureurl: site.infos.userpictureurl,
loggedOut: true, // Not used.
isDemoModeSite: this.isDemoModeSite,
};
this.username = site.infos.username;
@ -185,7 +188,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
await CoreSites.checkApplication(this.siteConfig);
this.logoUrl = CoreLoginHelper.getLogoUrl(this.siteConfig);
this.logoUrl = this.isDemoModeSite ? undefined : CoreLoginHelper.getLogoUrl(this.siteConfig);
}
/**

View File

@ -21,23 +21,8 @@
</ion-header>
<ion-content class="limited-width">
<core-loading [hideUntil]="loaded">
<ion-list class="core-sitelist">
<ion-card *ngFor="let sites of accountsList.otherSites">
<ion-item-divider sticky="true" *ngIf="sites[0]" class="core-sitelist-sitename">
<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].siteUrlWithoutProtocol }}</a></p>
</ion-label>
</ion-item-divider>
<ion-item button *ngFor="let site of sites" (click)="login($event, site.id)" detail="true">
<core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<ion-label>
<p class="item-heading">{{site.fullname}}</p>
</ion-label>
<core-sites-list [accountsList]="accountsList" [sitesClickable]="true" (onSiteClicked)="login($event)">
<ng-template #siteItem let-site="site" let-isCurrentSite="isCurrentSite">
<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 }
@ -47,9 +32,8 @@
[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>
</ion-card>
</ion-list>
</ng-template>
</core-sites-list>
</core-loading>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
<ion-fab-button (click)="add()" [attr.aria-label]="'core.login.add' | translate">

View File

@ -22,12 +22,11 @@ import { CoreFilter } from '@features/filter/services/filter';
import { CoreAnimations } from '@components/animations';
/**
* Page that displays a "splash screen" while the app is being initialized.
* Page that displays the list of sites stored in the device.
*/
@Component({
selector: 'page-core-login-sites',
templateUrl: 'sites.html',
styleUrls: ['../../sitelist.scss'],
animations: [CoreAnimations.SLIDE_IN_OUT, CoreAnimations.SHOW_HIDE],
})
export class CoreLoginSitesPage implements OnInit {
@ -102,18 +101,14 @@ export class CoreLoginSitesPage implements OnInit {
/**
* Login in a site.
*
* @param event Click event.
* @param siteId The site ID.
* @param site The site.
* @returns Promise resolved when done.
*/
async login(event: Event, siteId: string): Promise<void> {
event.preventDefault();
event.stopPropagation();
async login(site: CoreSiteBasicInfo): Promise<void> {
const modal = await CoreDomUtils.showModalLoading();
try {
const loggedIn = await CoreSites.loadSite(siteId);
const loggedIn = await CoreSites.loadSite(site.id);
if (loggedIn) {
await CoreNavigator.navigateToSiteHome();

View File

@ -374,14 +374,23 @@ export class CoreLoginHelperProvider {
}
/**
* Get Available sites (includes staging sites if are enabled).
* Get Available sites (includes staging sites if are enabled). It doesn't include demo mode site.
*
* @returns Available sites.
*/
async getAvailableSites(): Promise<CoreLoginSiteInfo[]> {
const hasEnabledStagingSites = await CoreSettingsHelper.hasEnabledStagingSites();
return hasEnabledStagingSites ? CoreConstants.CONFIG.sites : CoreConstants.CONFIG.sites.filter(site => !site.staging);
return CoreConstants.CONFIG.sites.filter(site => (!site.staging || hasEnabledStagingSites) && !site.demoMode);
}
/**
* Get demo mode site info. This function doesn't check if demo mode is enabled.
*
* @returns Demo mode site info, undefined if no demo mode site.
*/
getDemoModeSiteInfo(): CoreLoginSiteInfo | undefined {
return CoreConstants.CONFIG.sites.find(site => site.demoMode);
}
/**
@ -453,6 +462,14 @@ export class CoreLoginHelperProvider {
* @returns Path and params.
*/
async getAddSiteRouteInfo(showKeyboard?: boolean): Promise<[string, Params]> {
if (CoreConstants.CONFIG.demoMode) {
const demoModeSite = this.getDemoModeSiteInfo();
if (demoModeSite) {
return ['/login/credentials', { siteUrl: demoModeSite.url }];
}
}
const sites = await this.getAvailableSites();
if (sites.length === 1) {
@ -583,7 +600,10 @@ export class CoreLoginHelperProvider {
const sites = await this.getAvailableSites();
if (sites.length) {
return sites.some((site) => CoreUrl.sameDomainAndPath(siteUrl, site.url));
const demoModeSite = this.getDemoModeSiteInfo();
return sites.some((site) => CoreUrl.sameDomainAndPath(siteUrl, site.url)) ||
(!!demoModeSite && CoreUrl.sameDomainAndPath(siteUrl, demoModeSite.url));
} else if (CoreConstants.CONFIG.multisitesdisplay == 'sitefinder' && CoreConstants.CONFIG.onlyallowlistedsites &&
checkSiteFinder) {
// Call the sites finder to validate the site.
@ -1281,10 +1301,9 @@ export class CoreLoginHelperProvider {
/**
* Get the accounts list classified per site.
*
* @param currentSiteId If loggedin, current Site Id.
* @returns Promise resolved with account list.
*/
async getAccountsList(currentSiteId?: string): Promise<CoreAccountsList> {
async getAccountsList(): Promise<CoreAccountsList> {
const sites = await CoreUtils.ignoreErrors(CoreSites.getSortedSites(), [] as CoreSiteBasicInfo[]);
const accountsList: CoreAccountsList = {
@ -1292,14 +1311,11 @@ export class CoreLoginHelperProvider {
otherSites: [],
count: sites.length,
};
const currentSiteId = CoreSites.getCurrentSiteId();
let siteUrl = '';
if (currentSiteId) {
const index = sites.findIndex((site) => site.id == currentSiteId);
accountsList.currentSite = sites.splice(index, 1)[0];
siteUrl = accountsList.currentSite.siteUrlWithoutProtocol;
siteUrl = sites.find((site) => site.id == currentSiteId)?.siteUrlWithoutProtocol ?? '';
}
const otherSites: Record<string, CoreSiteBasicInfo[]> = {};
@ -1308,7 +1324,9 @@ export class CoreLoginHelperProvider {
await Promise.all(sites.map(async (site) => {
site.badge = await CoreUtils.ignoreErrors(CorePushNotifications.getSiteCounter(site.id)) || 0;
if (site.siteUrlWithoutProtocol == siteUrl) {
if (site.id === currentSiteId) {
accountsList.currentSite = site;
} else if (site.siteUrlWithoutProtocol == siteUrl) {
accountsList.sameSite.push(site);
} else {
if (!otherSites[site.siteUrlWithoutProtocol]) {
@ -1497,6 +1515,23 @@ export class CoreLoginHelperProvider {
return CoreTextUtils.parseJSON<Record<string, number>>(passwordResetsJson, {});
}
/**
* Check if a URL belongs to the demo mode site.
*
* @returns Whether the URL belongs to the demo mode site.
*/
isDemoModeSite(url: string): boolean {
const demoSiteData = CoreLoginHelper.getDemoModeSiteInfo();
if (!demoSiteData) {
return false;
}
const demoSiteUrl = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(demoSiteData.url));
url = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url));
return demoSiteUrl.indexOf(url) === 0;
}
}
export const CoreLoginHelper = makeSingleton(CoreLoginHelperProvider);
@ -1504,10 +1539,10 @@ 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.
export type CoreAccountsList<T extends CoreSiteBasicInfo = CoreSiteBasicInfo> = {
currentSite?: T; // If logged in, current site info.
sameSite: T[]; // If logged in, accounts info on the same site.
otherSites: T[][]; // Other accounts in other sites.
count: number; // Number of sites.
};

View File

@ -16,7 +16,7 @@ import { CoreConstants } from '@/core/constants';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { CoreSite, CoreSiteInfo } from '@classes/site';
import { CoreFilter } from '@features/filter/services/filter';
import { CoreLoginSitesComponent } from '@features/login/components/sites/sites';
import { CoreLoginSitesModalComponent } from '@features/login/components/sites-modal/sites-modal';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config';
import { CoreUserSupport } from '@features/user/services/support';
@ -113,7 +113,7 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
* @returns Promise resolved when done.
*/
protected async loadSiteLogo(currentSite: CoreSite): Promise<void> {
if (CoreConstants.CONFIG.forceLoginLogo) {
if (CoreConstants.CONFIG.forceLoginLogo || currentSite.isDemoModeSite()) {
this.siteLogo = 'assets/img/login_logo.png';
this.siteLogoLoaded = true;
@ -239,7 +239,7 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
event.stopPropagation();
const closeAll = await CoreDomUtils.openSideModal<boolean>({
component: CoreLoginSitesComponent,
component: CoreLoginSitesModalComponent,
cssClass: 'core-modal-lateral core-modal-lateral-sm',
});

View File

@ -17,43 +17,21 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-list class="core-sitelist limited-width">
<ion-card *ngIf="accountsList.currentSite">
<ion-item-divider sticky="true" class="core-sitelist-sitename">
<ion-label>
<p class="item-heading">
<core-format-text [text]="accountsList.currentSite.siteName" clean="true"
[siteId]="accountsList.currentSite.id"></core-format-text>
</p>
<p><a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes">{{
accountsList.currentSite.siteUrlWithoutProtocol }}</a>
</p>
</ion-label>
</ion-item-divider>
<ion-list class="limited-width">
<core-sites-list [accountsList]="accountsList">
<ng-template #siteLabel let-site="site">
<ion-badge color="light" *ngIf="site.spaceUsage !== undefined">{{ site.spaceUsage | coreBytesToSize }}</ion-badge>
</ng-template>
<ion-item class="item-current">
<ng-container *ngTemplateOutlet="siteUsage; context: {site: accountsList.currentSite}"></ng-container>
</ion-item>
<ion-item *ngFor="let site of accountsList.sameSite">
<ng-container *ngTemplateOutlet="siteUsage; context: {site: site}"></ng-container>
</ion-item>
</ion-card>
<ion-card *ngFor="let sites of accountsList.otherSites">
<ion-item-divider sticky="true" *ngIf="sites[0]" class="core-sitelist-sitename">
<ion-label>
<p class="item-heading">
<core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
</p>
<p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a></p>
</ion-label>
</ion-item-divider>
<ion-item *ngFor="let site of sites">
<ng-container *ngTemplateOutlet="siteUsage; context: {site: site}"></ng-container>
</ion-item>
</ion-card>
<ng-template #siteItem let-site="site">
<ion-button fill="clear" color="danger" slot="end" (click)="deleteSiteStorage(site)"
[hidden]="site.spaceUsage <= 0 && !site.hasCacheEntries">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: site.siteName }">
</ion-icon>
</ion-button>
</ng-template>
</core-sites-list>
<ion-item-divider>
<ion-label>
@ -66,19 +44,3 @@
</ion-list>
</core-loading>
</ion-content>
<!-- Template to render a site space usage. -->
<ng-template #siteUsage let-site="site">
<core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<ion-label class="ion-text-wrap">
<p class="item-heading">{{site.fullname}}</p>
<ion-badge color="light" *ngIf="site.spaceUsage !== undefined">{{ site.spaceUsage | coreBytesToSize }}</ion-badge>
</ion-label>
<ion-button fill="clear" color="danger" slot="end" (click)="deleteSiteStorage(site)"
[hidden]="site.spaceUsage <= 0 && !site.hasCacheEntries">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: site.siteName }">
</ion-icon>
</ion-button>
</ng-template>

View File

@ -20,6 +20,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreSettingsHelper } from '../../services/settings-helper';
import { CoreAccountsList } from '@features/login/services/login-helper';
/**
* Page that displays the space usage settings.
@ -33,9 +34,10 @@ export class CoreSettingsSpaceUsagePage implements OnInit, OnDestroy {
loaded = false;
totalSpaceUsage = 0;
accountsList: CoreAccountsListWithUsage = {
accountsList: CoreAccountsList<CoreSiteBasicInfoWithUsage> = {
sameSite: [],
otherSites: [],
count: 0,
};
protected sitesObserver: CoreEventObserver;
@ -128,6 +130,7 @@ export class CoreSettingsSpaceUsagePage implements OnInit, OnDestroy {
});
this.accountsList.otherSites = CoreUtils.objectToArray(otherSites);
this.accountsList.count = sites.length;
this.totalSpaceUsage = totalSize;
}
@ -192,12 +195,3 @@ interface CoreSiteBasicInfoWithUsage extends CoreSiteBasicInfo {
hasCacheEntries: boolean; // If has cached entries that can be cleared.
spaceUsage: number; // Space used in this site.
}
/**
* Accounts list for selecting sites interfaces.
*/
type CoreAccountsListWithUsage = {
currentSite?: CoreSiteBasicInfoWithUsage; // If logged in, current site info.
sameSite: CoreSiteBasicInfoWithUsage[]; // If logged in, accounts info on the same site.
otherSites: CoreSiteBasicInfoWithUsage[][]; // Other accounts in other sites.
};

View File

@ -17,7 +17,7 @@
</ion-header>
<ion-content class="limited-width">
<core-loading [hideUntil]="sitesLoaded">
<ion-list class="core-sitelist limited-width">
<ion-list class="limited-width">
<ion-item-divider>
<ion-label>
<h2>{{ 'core.settings.syncsettings' | translate }}</h2>
@ -51,61 +51,25 @@
</ion-label>
</ion-item-divider>
<ion-card *ngIf="accountsList.currentSite">
<ion-item-divider sticky="true" class="core-sitelist-sitename">
<ion-label>
<p class="item-heading">
<core-format-text [text]="accountsList.currentSite.siteName" clean="true"
[siteId]="accountsList.currentSite.id"></core-format-text>
</p>
<p><a [href]="accountsList.currentSite.siteUrl" core-link autoLogin="yes">{{
accountsList.currentSite.siteUrlWithoutProtocol }}</a>
</p>
</ion-label>
</ion-item-divider>
<core-sites-list [accountsList]="accountsList">
<ng-template #siteLabel let-site="site">
<p class="text-danger" *ngIf="site.loggedOut">{{ 'core.settings.logintosync' | translate }}</p>
</ng-template>
<ion-item class="item-current">
<ng-container *ngTemplateOutlet="siteSync; context: {site: accountsList.currentSite}"></ng-container>
</ion-item>
<ion-item *ngFor="let site of accountsList.sameSite">
<ng-container *ngTemplateOutlet="siteSync; context: {site: site}"></ng-container>
</ion-item>
</ion-card>
<ion-card *ngFor="let sites of accountsList.otherSites">
<ion-item-divider sticky="true" *ngIf="sites[0]" class="core-sitelist-sitename">
<ion-label>
<p class="item-heading">
<core-format-text [text]="sites[0].siteName" clean="true" [siteId]="sites[0].id"></core-format-text>
</p>
<p><a [href]="sites[0].siteUrl" core-link autoLogin="no">{{ sites[0].siteUrlWithoutProtocol }}</a></p>
</ion-label>
</ion-item-divider>
<ion-item *ngFor="let site of sites">
<ng-container *ngTemplateOutlet="siteSync; context: {site: site}"></ng-container>
</ion-item>
</ion-card>
<ng-template #siteItem let-site="site">
<core-button-with-spinner [loading]="isSynchronizing(site.id)" slot="end" *ngIf="!site.loggedOut">
<ion-button fill="clear" (click)="synchronize(site.id)"
[attr.aria-label]="'core.settings.synchronizenow' | translate">
<ion-icon name="fas-rotate" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-button-with-spinner>
<ion-button fill="clear" (click)="login(site.id)" [attr.aria-label]="'core.login.login' | translate"
*ngIf="site.loggedOut" slot="end">
<ion-icon name="fas-right-to-bracket" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ng-template>
</core-sites-list>
</ng-container>
</ion-list>
</core-loading>
</ion-content>
<!-- Template to render a site to sync. -->
<ng-template #siteSync let-site="site">
<core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<ion-label>
<p class="item-heading">{{site.fullname}}</p>
<p class="text-danger" *ngIf="site.loggedOut">{{ 'core.settings.logintosync' | translate }}</p>
</ion-label>
<core-button-with-spinner [loading]="isSynchronizing(site.id)" slot="end" *ngIf="!site.loggedOut">
<ion-button fill="clear" (click)="synchronize(site.id)" [attr.aria-label]="'core.settings.synchronizenow' | translate">
<ion-icon name="fas-rotate" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-button-with-spinner>
<ion-button fill="clear" (click)="login(site.id)" [attr.aria-label]="'core.login.login' | translate" *ngIf="site.loggedOut" slot="end">
<ion-icon name="fas-right-to-bracket" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ng-template>

View File

@ -32,7 +32,6 @@ import { CoreNavigator } from '@services/navigator';
@Component({
selector: 'page-core-app-settings-synchronization',
templateUrl: 'synchronization.html',
styleUrls: ['../../../login/sitelist.scss'],
})
export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy {
@ -105,10 +104,8 @@ export class CoreSettingsSynchronizationPage implements OnInit, OnDestroy {
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
const currentSiteId = CoreSites.getCurrentSiteId();
try {
this.accountsList = await CoreLoginHelper.getAccountsList(currentSiteId);
this.accountsList = await CoreLoginHelper.getAccountsList();
} catch {
// Ignore errors.
}

View File

@ -17,17 +17,8 @@
<p>{{fileName}}</p>
</ion-label>
</ion-item>
<ion-item *ngFor="let site of sites" (click)="storeInSite(site.id)" detail="false" button>
<core-user-avatar [user]="site" slot="start" [linkProfile]="false" [siteId]="site.id"></core-user-avatar>
<ion-label>
<p class="item-heading">{{site.fullname}}</p>
<p>
<core-format-text clean="true" [text]="site.siteName" [siteId]="site.id"></core-format-text>
</p>
<p>{{site.siteUrl}}</p>
</ion-label>
</ion-item>
<core-sites-list [accountsList]="accountsList" [sitesClickable]="true" (onSiteClicked)="storeInSite($event)">
</core-sites-list>
</ion-list>
</core-loading>
</ion-content>

View File

@ -13,11 +13,12 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { CoreAccountsList, CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreSharedFilesHelper } from '@features/sharedfiles/services/sharedfiles-helper';
import { FileEntry } from '@ionic-native/file/ngx';
import { CoreFile } from '@services/file';
import { CoreNavigator } from '@services/navigator';
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
import { CoreSiteBasicInfo } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
/**
@ -30,8 +31,12 @@ import { CoreDomUtils } from '@services/utils/dom';
export class CoreSharedFilesChooseSitePage implements OnInit {
fileName?: string;
sites?: CoreSiteBasicInfo[];
loaded = false;
accountsList: CoreAccountsList = {
sameSite: [],
otherSites: [],
count: 0,
};
protected filePath?: string;
protected fileEntry?: FileEntry;
@ -89,15 +94,15 @@ export class CoreSharedFilesChooseSitePage implements OnInit {
* @returns Promise resolved when done.
*/
protected async loadSites(): Promise<void> {
this.sites = await CoreSites.getSites();
this.accountsList = await CoreLoginHelper.getAccountsList();
}
/**
* Store the file in a certain site.
*
* @param siteId Site ID.
* @param site Site.
*/
async storeInSite(siteId: string): Promise<void> {
async storeInSite(site: CoreSiteBasicInfo): Promise<void> {
if (!this.fileEntry) {
return;
}
@ -105,7 +110,7 @@ export class CoreSharedFilesChooseSitePage implements OnInit {
this.loaded = false;
try {
await CoreSharedFilesHelper.storeSharedFileInSite(this.fileEntry, siteId, this.isInbox);
await CoreSharedFilesHelper.storeSharedFileInSite(this.fileEntry, site.id, this.isInbox);
CoreNavigator.back();
} finally {

View File

@ -1300,11 +1300,24 @@ export class CoreSitesProvider {
async getSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
const sites = await this.sitesTable.getMany();
return this.siteDBRecordsToBasicInfo(sites, ids);
}
/**
* Convert sites DB records to site basic info.
*
* @param sites DB records.
* @param ids IDs of sites to return, undefined to return them all.
* @returns Sites basic info.
*/
protected siteDBRecordsToBasicInfo(sites: SiteDBEntry[], ids?: string[]): CoreSiteBasicInfo[] {
const formattedSites: CoreSiteBasicInfo[] = [];
sites.forEach((site) => {
if (!ids || ids.indexOf(site.id) > -1) {
// Parse info.
const isDemoModeSite = CoreLoginHelper.isDemoModeSite(site.siteUrl);
const siteInfo = site.info ? <CoreSiteInfo> CoreTextUtils.parseJSON(site.info) : undefined;
const basicInfo: CoreSiteBasicInfo = {
id: site.id,
siteUrl: site.siteUrl,
@ -1312,10 +1325,11 @@ export class CoreSitesProvider {
fullname: siteInfo?.fullname,
firstname: siteInfo?.firstname,
lastname: siteInfo?.lastname,
siteName: siteInfo?.sitename,
siteName: isDemoModeSite ? CoreConstants.CONFIG.appname : siteInfo?.sitename,
userpictureurl: siteInfo?.userpictureurl,
siteHomeId: siteInfo?.siteid || 1,
loggedOut: !!site.loggedOut,
isDemoModeSite,
};
formattedSites.push(basicInfo);
}
@ -2195,6 +2209,7 @@ export type CoreSiteBasicInfo = {
badge?: number; // Badge to display in the site.
siteHomeId?: number; // Site home ID.
loggedOut: boolean; // If Site is logged out.
isDemoModeSite: boolean;
};
/**
@ -2288,6 +2303,11 @@ export type CoreLoginSiteInfo = {
* Class to apply to site item.
*/
className?: string;
/**
* Whether the site is for demo mode usage.
*/
demoMode?: boolean;
};
/**

View File

@ -28,6 +28,8 @@ import { CoreFilepoolProviderStub } from '@/storybook/stubs/services/filepool';
import { CoreFilepoolProvider } from '@services/filepool';
import { HttpClientStub } from '@/storybook/stubs/services/http';
import { HttpClient } from '@angular/common/http';
import { CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications';
import { CorePushNotificationsProviderStub } from './stubs/services/pushnotifications';
// For translate loader. AoT requires an exported function for factories.
export class StaticTranslateLoader extends TranslateLoader {
@ -56,6 +58,7 @@ export class StaticTranslateLoader extends TranslateLoader {
{ provide: CoreSitesProvider, useClass: CoreSitesProviderStub },
{ provide: CoreDbProvider, useClass: CoreDbProviderStub },
{ provide: CoreFilepoolProvider, useClass: CoreFilepoolProviderStub },
{ provide: CorePushNotificationsProvider, useClass: CorePushNotificationsProviderStub },
{ provide: HttpClient, useClass: HttpClientStub },
{
provide: APP_INITIALIZER,

View File

@ -0,0 +1,32 @@
// (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 { CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications';
import { makeSingleton } from '@singletons';
/**
* Sites provider stub.
*/
export class CorePushNotificationsProviderStub extends CorePushNotificationsProvider {
/**
* @inheritdoc
*/
async getSiteCounter(): Promise<number> {
return Math.round(Math.random() * 100);
}
}
export const CorePushNotificationsStub = makeSingleton<CorePushNotificationsProviderStub>(CorePushNotificationsProvider);

View File

@ -12,9 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import school from '@/assets/storybook/sites/school.json';
import { companyLisaSite } from '@/assets/storybook/sites/companylisa';
import { schoolBarbaraSite } from '@/assets/storybook/sites/schoolbarbara';
import { schoolJefferySite } from '@/assets/storybook/sites/schooljeffery';
import { CoreSiteFixture, CoreSiteStub } from '@/storybook/stubs/classes/site';
import { CoreSitesProvider } from '@services/sites';
import { CoreError } from '@classes/errors/error';
import { CoreSite } from '@classes/site';
import { SiteDBEntry } from '@services/database/sites';
import { CoreSiteBasicInfo, CoreSitesProvider } from '@services/sites';
import { makeSingleton } from '@singletons';
/**
@ -22,17 +27,55 @@ import { makeSingleton } from '@singletons';
*/
export class CoreSitesProviderStub extends CoreSitesProvider {
protected static readonly SITES_FIXTURES = [schoolBarbaraSite, schoolJefferySite, companyLisaSite];
/**
* @inheritdoc
*/
getRequiredCurrentSite!: () => CoreSiteStub;
/**
* @inheritdoc
*/
async getSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
const sites = CoreSitesProviderStub.SITES_FIXTURES.map(site => (<SiteDBEntry> {
id: site.id,
siteUrl: site.info.siteurl,
info: JSON.stringify(site.info),
token: '',
privateToken: '',
loggedOut: 0,
}));
return this.siteDBRecordsToBasicInfo(sites, ids);
}
/**
* @inheritdoc
*/
async getSite(siteId?: string): Promise<CoreSite> {
if (!siteId) {
if (this.currentSite) {
return this.currentSite;
}
throw new CoreError('No current site found.');
}
const siteFixture = CoreSitesProviderStub.SITES_FIXTURES.find(site => site.id === siteId);
if (!siteFixture) {
throw new CoreError('SiteId not found.');
}
return new CoreSiteStub(siteFixture);
}
/**
* @inheritdoc
*/
stubCurrentSite(fixture?: CoreSiteFixture): CoreSiteStub {
if (!this.currentSite) {
this.currentSite = new CoreSiteStub(fixture ?? school);
this.currentSite = new CoreSiteStub(fixture ?? schoolBarbaraSite);
}
return this.getRequiredCurrentSite();

View File

@ -73,4 +73,5 @@ export interface EnvironmentConfig {
disableCallWSInBackground?: boolean; // If true, disable calling WS in background.
callWSInBackgroundExpirationTime?: number; // Ms to consider an entry expired when calling WS in background. Default: 1 week.
disableTokenFile: boolean; // Disable the use of tokenpluginfile.php for downloading files (so it fallbacks to pluginfile.php)
demoMode?: boolean; // Whether to run the app in "demo mode".
}

View File

@ -7,6 +7,10 @@ For more information about upgrading, read the official documentation: https://m
- Starting with this release, this file will only document breaking changes for APIs exposed to site plugins. Internal changes will no longer be documented.
- CoreCache has been deprecated, use plain object as in-memory stores instead.
=== 4.4.0 ===
- Renamed CoreLoginSitesComponent to CoreLoginSitesModalComponent to make it clear that it's a modal and to avoid confusing it with the new CoreSitesListComponent.
=== 4.3.0 ===
- CoreSiteBasicInfo fullName attribute has changed to fullname and avatar to userpictureurl to match user fields.