Merge pull request #3982 from NoelDeMartin/MOBILE-4268

MOBILE-4268: Apply design system to Error Accordion
main
Pau Ferrer Ocaña 2024-03-19 13:20:12 +01:00 committed by GitHub
commit 776e3fa481
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 554 additions and 1881 deletions

View File

@ -1,11 +0,0 @@
module.exports = {
framework: '@storybook/angular',
addons: [
'@storybook/addon-controls',
'@storybook/addon-viewport',
'storybook-addon-designs',
'storybook-addon-rtl-direction',
'storybook-dark-mode',
],
stories: ['../src/**/*.stories.ts'],
}

View File

@ -1,11 +0,0 @@
import '!style-loader!css-loader!sass-loader!../src/theme/theme.design-system.scss';
import '!style-loader!css-loader!sass-loader!./styles.scss';
export const parameters = {
layout: 'centered',
darkMode: {
darkClass: 'dark',
classTarget: 'html',
stylePreview: true,
},
};

View File

@ -1,7 +0,0 @@
storybook-dynamic-app-root {
color: var(--ion-text-color);
}
.core-error-info {
max-width: 300px;
}

View File

@ -1,11 +0,0 @@
{
"extends": "../tsconfig.json",
"include": [
"../src/**/*"
],
"exclude": [
"../src/**/tests/**",
"../src/testing/**",
"../src/**/*.test.ts"
]
}

View File

@ -30,7 +30,6 @@
"dev:ios": "ionic cordova run ios",
"prod:android": "npm run prod --prefix cordova-plugin-moodleapp && NODE_ENV=production ionic cordova run android --prod",
"prod:ios": "NODE_ENV=production ionic cordova run ios --prod",
"storybook": "start-storybook -p 6006",
"test": "NODE_ENV=testing gulp && jest --verbose",
"test:ci": "NODE_ENV=testing gulp && jest -ci --runInBand --verbose",
"test:watch": "NODE_ENV=testing gulp watch & jest --watch",

View File

@ -1 +0,0 @@
[{"id":1,"courseimage":"https://picsum.photos/500/500","shortname":"Moodle and Mountaineering","summary":"This course will introduce you to the basics of Alpine Mountaineering, while at the same time highlighting some of the great features of Moodle."},{"id":2,"courseimage":"assets/storybook/geopattern.svg","shortname":"Digital Literacy","summary":"This course explores Digital Literacy and its importance for teachers and students. The course is optimised for the Moodle App. Please try it out!"},{"id":3,"shortname":"Class and Conflict in World Cinema","summary":"In this module we will analyse two very significant films - City of God and La Haine, both of which depict violent lives in poor conditions, the former in the favelas of Brazil and the latter in a Parisian banlieue. We will look at how conflict and class are portrayed, focusing particularly on the use of mise en scène."}]

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,32 +0,0 @@
// (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,32 +0,0 @@
// (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

@ -1,32 +0,0 @@
// (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

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreSiteError, CoreSiteErrorOptions } from '@classes/errors/siteerror';
/**
* Error returned by WS.
@ -29,10 +29,7 @@ export class CoreAjaxWSError extends CoreSiteError {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(error: any, available?: number) {
super({
message: error.message || error.error,
errorcode: error.errorcode,
});
super(getErrorOptions(error));
this.exception = error.exception;
this.warningcode = error.warningcode;
@ -40,15 +37,37 @@ export class CoreAjaxWSError extends CoreSiteError {
this.moreinfourl = error.moreinfourl;
this.debuginfo = error.debuginfo;
this.backtrace = error.backtrace;
this.available = available;
if (this.available === undefined) {
if (this.errorcode) {
this.available = this.errorcode == 'invalidrecord' ? -1 : 1;
} else {
this.available = 0;
}
}
this.available = available ?? (
this.debug
? (this.debug.code == 'invalidrecord' ? -1 : 1)
: 0
);
}
}
/**
* Get error options from unknown error instance.
*
* @param error The error.
* @returns Options
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getErrorOptions(error: any): CoreSiteErrorOptions {
const options: CoreSiteErrorOptions = {
message: error.message || error.error,
};
if ('debug' in error) {
options.debug = error.debug;
}
if ('errorcode' in error) {
options.debug = {
code: error.errorcode,
details: error.message || error.error,
};
}
return options;
}

View File

@ -20,24 +20,39 @@ import { CoreUserSupportConfig } from '@features/user/classes/support/support-co
*/
export class CoreSiteError extends CoreError {
errorcode?: string;
errorDetails?: string;
debug?: CoreSiteErrorDebug;
supportConfig?: CoreUserSupportConfig;
constructor(options: CoreSiteErrorOptions) {
super(options.message);
this.errorcode = options.errorcode;
this.errorDetails = options.errorDetails;
this.debug = options.debug;
this.supportConfig = options.supportConfig;
}
/**
* @deprecated This getter should not be called directly, but it's defined for backwards compatibility with many
* parts of the code that type errors as any and use it. We cannot rename those because the errors could also be
* CoreWSError instances which do have an "errorcode" property.
*
* @returns error code.
*/
get errorcode(): string | undefined {
return this.debug?.code;
}
}
export type CoreSiteErrorDebug = {
code: string; // Technical error code useful for technical assistance.
details: string; // Technical error details useful for technical assistance.
};
export type CoreSiteErrorOptions = {
message: string;
errorcode?: string; // Technical error code useful for technical assistance.
errorDetails?: string; // Technical error details useful for technical assistance.
// Debugging information.
debug?: CoreSiteErrorDebug;
// Configuration to use to contact site support. If this attribute is present, it means
// that the error warrants contacting support.

View File

@ -943,8 +943,10 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
throw new CoreSiteError({
supportConfig: new CoreUserAuthenticatedSupportConfig(this),
message: Translate.instant('core.siteunavailablehelp', { site: this.siteUrl }),
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'tool_mobile_call_external_functions' }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method: 'tool_mobile_call_external_functions' }),
},
});
}

View File

@ -1,6 +0,0 @@
<!--
The markup for this component is rendered dynamically using the static render() method
instead of using Angular's engine. The reason for using this approach is that this
allows injecting this component into HTML directly, rather than requiring Angular
to control its lifecycle.
-->

View File

@ -1,75 +0,0 @@
.core-error-info {
background: var(--gray-200);
border-radius: var(--radius-xs);
font-size: var(--body-font-size-sm);
color: var(--gray-900);
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
.core-error-info--code {
padding: var(--spacing-2) var(--spacing-2) 0 var(--spacing-2);
font-size: var(--body-font-size-md);
}
.core-error-info--details p {
padding: var(--spacing-2) var(--spacing-2) 0 var(--spacing-2);
color: var(--gray-500);
}
.core-error-info--checkbox {
display: none;
& + .core-error-info--details,
& + .core-error-info--code + .core-error-info--details {
max-height: 0;
overflow: hidden;
transition: max-height 600ms ease-in-out;
& + .core-error-info--toggle {
display: flex;
padding: var(--spacing-2);
min-height: var(--a11y-min-target-size);
align-items: center;
span {
width: 100%;
display: flex;
justify-content: space-between;
}
svg {
fill: currentColor;
width: 11px;
}
.core-error-info--hide-content {
display: none;
}
}
}
&:checked + .core-error-info--details,
&:checked + .core-error-info--code + .core-error-info--details {
max-height: 110px;
& + .core-error-info--toggle .core-error-info--hide-content {
display: flex;
}
& + .core-error-info--toggle .core-error-info--show-content {
display: none;
}
}
}
}

View File

@ -1,92 +0,0 @@
// (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, ElementRef, Input, OnChanges, OnInit } from '@angular/core';
import { Translate } from '@singletons';
import { CoreForms } from '@singletons/form';
/**
* Component to show error details.
*
* Given that this component has to be injected dynamically in some situations (for example, error alerts),
* it can be rendered using the static render() method to get the raw HTML.
*/
@Component({
selector: 'core-error-info',
templateUrl: 'core-error-info.html',
styleUrls: ['error-info.scss'],
})
export class CoreErrorInfoComponent implements OnInit, OnChanges {
/**
* Render an instance of the component into an HTML string.
*
* @param errorDetails Error details.
* @param errorCode Error code.
* @returns Component HTML.
*/
static render(errorDetails: string, errorCode?: string): string {
const toggleId = CoreForms.uniqueId('error-info-toggle');
const errorCodeLabel = Translate.instant('core.errorcode', { errorCode });
const hideDetailsLabel = Translate.instant('core.errordetailshide');
const showDetailsLabel = Translate.instant('core.errordetailsshow');
return `
<div class="core-error-info">
<input id="${toggleId}" type="checkbox" class="core-error-info--checkbox" />
${errorCode ? `<div class="core-error-info--code"><strong>${errorCodeLabel}</strong></div>` : ''}
<div class="core-error-info--details">
<p>${errorDetails}</p>
</div>
<label for="${toggleId}" class="core-error-info--toggle" aria-hidden="true">
<span class="core-error-info--hide-content">
${hideDetailsLabel}
<ion-icon name="chevron-up" />
</span>
<span class="core-error-info--show-content">
${showDetailsLabel}
<ion-icon name="chevron-down" />
</span>
</label>
</div>
`;
}
@Input() errorDetails!: string;
@Input() errorCode?: string;
constructor(private element: ElementRef) {}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.render();
}
/**
* @inheritdoc
*/
ngOnChanges(): void {
this.render();
}
/**
* Render component html in the element created by Angular.
*/
private render(): void {
this.element.nativeElement.innerHTML = CoreErrorInfoComponent.render(this.errorDetails, this.errorCode);
}
}

View File

@ -1,43 +0,0 @@
// (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 { CoreEmptyBoxPageComponent } from './empty-box-page/empty-box-page';
import { CoreEmptyBoxWrapperComponent } from './empty-box-wrapper/empty-box-wrapper';
import { StorybookModule } from '@/storybook/storybook.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
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: [
CoreCourseImageCardsPageComponent,
CoreCourseImageListPageComponent,
CoreEmptyBoxPageComponent,
CoreEmptyBoxWrapperComponent,
CoreSitesListWrapperComponent,
],
imports: [
CommonModule,
StorybookModule,
CoreDirectivesModule,
CoreComponentsModule,
CoreSearchComponentsModule,
],
})
export class CoreComponentsStorybookModule {}

View File

@ -1,22 +0,0 @@
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>
<h1>Course Cards</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-card *ngFor="let course of courses">
<div class="course-image-wrapper">
<core-course-image [course]="course" [fill]="true" />
</div>
<ion-card-header>
<ion-card-title>{{ course.shortname }}</ion-card-title>
</ion-card-header>
<ion-card-content>
{{ course.summary }}
</ion-card-content>
</ion-card>
</ion-content>
</ion-app>

View File

@ -1,16 +0,0 @@
:host {
ion-card {
max-width: 350px;
margin-right: auto;
margin-left: auto;
}
.course-image-wrapper {
width: 100%;
height: 0;
padding-top: 40%;
position: relative;
}
}

View File

@ -1,28 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { CoreCourseListItem } from '@features/courses/services/courses';
import courses from '@/assets/storybook/courses.json';
@Component({
selector: 'core-course-image-cards-page',
templateUrl: 'course-image-cards-page.html',
styleUrls: ['./course-image-cards-page.scss'],
})
export class CoreCourseImageCardsPageComponent {
courses: Partial<CoreCourseListItem>[] = courses;
}

View File

@ -1,19 +0,0 @@
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>
<h1>Courses List</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item class="ion-text-wrap" *ngFor="let course of courses">
<core-course-image [course]="course" slot="start" />
<ion-label>
{{ course.shortname }}
</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-app>

View File

@ -1,27 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { CoreCourseListItem } from '@features/courses/services/courses';
import courses from '@/assets/storybook/courses.json';
@Component({
selector: 'core-course-image-list-page',
templateUrl: 'course-image-list-page.html',
})
export class CoreCourseImageListPageComponent {
courses: Partial<CoreCourseListItem>[] = courses;
}

View File

@ -1,15 +0,0 @@
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>
<h1>Search</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="core-flex-fill">
<core-search-box />
<core-empty-box-wrapper [icon]="icon" [content]="content" [dimmed]="dimmed" class="core-flex-fill" />
</div>
</ion-content>
</ion-app>

View File

@ -1,27 +0,0 @@
// (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 } from '@angular/core';
@Component({
selector: 'core-empty-box-page',
templateUrl: 'empty-box-page.html',
})
export class CoreEmptyBoxPageComponent {
@Input() icon!: string;
@Input() content!: string;
@Input() dimmed!: boolean;
}

View File

@ -1,3 +0,0 @@
<core-empty-box [icon]="icon" [dimmed]="dimmed">
<div [innerHTML]="html"></div>
</core-empty-box>

View File

@ -1,38 +0,0 @@
// (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 } from '@angular/core';
import { DomSanitizer } from '@singletons';
import { SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'core-empty-box-wrapper',
templateUrl: 'empty-box-wrapper.html',
})
export class CoreEmptyBoxWrapperComponent implements OnChanges {
@Input() icon!: string;
@Input() content!: string;
@Input() dimmed!: boolean;
html?: SafeHtml;
/**
* @inheritdoc
*/
ngOnChanges(): void {
this.html = DomSanitizer.bypassSecurityTrustHtml(this.content);
}
}

View File

@ -1,23 +0,0 @@
<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-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

@ -1,60 +0,0 @@
// (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

@ -1,104 +0,0 @@
// (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 { CoreCourseImageComponent } from '@components/course-image/course-image';
import { APP_INITIALIZER } from '@angular/core';
import { CoreSitesStub } from '@/storybook/stubs/services/sites';
import { CoreCourseImageListPageComponent } from '@components/stories/components/course-image-list-page/course-image-list-page';
import { CoreComponentsStorybookModule } from '@components/stories/components/components.module';
import { CoreCourseImageCardsPageComponent } from '@components/stories/components/course-image-cards-page/course-image-cards-page';
interface Args {
type: 'image' | 'geopattern' | 'color';
fill: boolean;
}
export default <Meta> {
title: 'Core/Course Image',
component: CoreCourseImageComponent,
decorators: [
moduleMetadata({
imports: [CoreComponentsStorybookModule],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
const site = CoreSitesStub.getRequiredCurrentSite();
site.stubWSResponse('tool_mobile_get_config', {
settings: [
{ name: 'core_admin_coursecolor1', value: '#F9B000' },
{ name: 'core_admin_coursecolor2', value: '#EF4B00' },
{ name: 'core_admin_coursecolor3', value: '#4338FB' },
{ name: 'core_admin_coursecolor4', value: '#E142FB' },
{ name: 'core_admin_coursecolor5', value: '#FF0064' },
{ name: 'core_admin_coursecolor6', value: '#FF0F18' },
{ name: 'core_admin_coursecolor7', value: '#039B06' },
{ name: 'core_admin_coursecolor8', value: '#039B88' },
{ name: 'core_admin_coursecolor9', value: '#EF009B' },
{ name: 'core_admin_coursecolor10', value: '#020B6E' },
],
warnings: [],
});
},
},
],
}),
],
argTypes: {
type: {
control: {
type: 'select',
options: ['image', 'geopattern', 'color'],
},
},
},
args: {
type: 'image',
fill: false,
},
};
const Template = story<Args>(({ type, ...args }) => {
const getImageSource = () => {
switch (type) {
case 'image':
return 'https://picsum.photos/500/500';
case 'geopattern':
return 'assets/storybook/geopattern.svg';
case 'color':
return undefined;
}
};
return {
component: CoreCourseImageComponent,
props: {
...args,
course: {
id: 1,
courseimage: getImageSource(),
},
},
};
});
export const Primary = story(Template);
export const ListPage = story(() => ({ component: CoreCourseImageListPageComponent }));
export const CardsPage = story(() => ({ component: CoreCourseImageCardsPageComponent }));

View File

@ -1,79 +0,0 @@
// (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 { marked } from 'marked';
import { story } from '@/storybook/utils/helpers';
import { CoreEmptyBoxComponent } from '@components/empty-box/empty-box';
import { CoreEmptyBoxWrapperComponent } from './components/empty-box-wrapper/empty-box-wrapper';
import { CoreEmptyBoxPageComponent } from './components/empty-box-page/empty-box-page';
import { CoreComponentsStorybookModule } from './components/components.module';
interface Args {
icon: string;
content: string;
dimmed: boolean;
}
export default <Meta<Args>> {
title: 'Core/Empty Box',
component: CoreEmptyBoxComponent,
decorators: [
moduleMetadata({ imports: [CoreComponentsStorybookModule] }),
],
argTypes: {
icon: {
control: {
type: 'select',
options: ['fas-magnifying-glass', 'fas-user', 'fas-check'],
},
},
},
args: {
icon: 'fas-user',
content: 'No users',
dimmed: false,
},
};
const WrapperTemplate = story<Args>((args) => ({
component: CoreEmptyBoxWrapperComponent,
props: {
...args,
content: marked(args.content),
},
}));
const PageTemplate = story<Args>((args) => ({
component: CoreEmptyBoxPageComponent,
props: {
...args,
content: marked(args.content),
},
}));
export const Primary = story<Args>(WrapperTemplate);
export const Example = story<Args>(PageTemplate, {
icon: 'fas-magnifying-glass',
content: '**No results for "Test Search"**\n\n<small>Check for typos or try using different keywords</small>',
});
export const DimmedExample = story<Args>(PageTemplate, {
icon: 'fas-magnifying-glass',
content: 'What are you searching for?',
dimmed: true,
});

View File

@ -1,50 +0,0 @@
// (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, Story } from '@storybook/angular';
import { story } from '@/storybook/utils/helpers';
import { StorybookModule } from '@/storybook/storybook.module';
import { CoreErrorInfoComponent } from '@components/error-info/error-info';
interface Args {
errorCode: string;
errorDetails: string;
}
export default <Meta<Args>> {
title: 'Core/Error Info',
component: CoreErrorInfoComponent,
decorators: [
moduleMetadata({
declarations: [CoreErrorInfoComponent],
imports: [StorybookModule],
}),
],
args: {
errorCode: '',
errorDetails:
'AJAX endpoint not found. ' +
'This can happen if the Moodle site is too old or it blocks access to this endpoint. ' +
'The Moodle app only supports Moodle systems 3.5 onwards.',
},
};
const Template: Story<Args> = (args) => ({
component: CoreErrorInfoComponent,
props: args,
});
export const Primary = story<Args>(Template);

View File

@ -1,78 +0,0 @@
// (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

@ -1,37 +0,0 @@
// (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, Story } from '@storybook/angular';
import { story } from '@/storybook/utils/helpers';
import { StorybookModule } from '@/storybook/storybook.module';
import { CoreUserAvatarComponent } from '@components/user-avatar/user-avatar';
export default <Meta> {
title: 'Core/User Avatar',
component: CoreUserAvatarComponent,
decorators: [
moduleMetadata({
declarations: [CoreUserAvatarComponent],
imports: [StorybookModule],
}),
],
};
const Template: Story = () => ({
component: CoreUserAvatarComponent,
});
export const Primary = story(Template);

View File

@ -38,9 +38,9 @@ import { CoreCustomURLSchemes, CoreCustomURLSchemesHandleError } from '@services
import { CoreTextUtils } from '@services/utils/text';
import { CoreForms } from '@singletons/form';
import { AlertButton } from '@ionic/core';
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreSiteError, CoreSiteErrorDebug } from '@classes/errors/siteerror';
import { CoreUserSupport } from '@features/user/services/support';
import { CoreErrorInfoComponent } from '@components/error-info/error-info';
import { CoreErrorAccordion } from '@services/error-accordion';
import { CoreUserSupportConfig } from '@features/user/classes/support/support-config';
import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config';
import { CoreLoginError } from '@classes/errors/loginerror';
@ -408,22 +408,20 @@ export class CoreLoginSitePage implements OnInit {
let siteExists = false;
let supportConfig: CoreUserSupportConfig | undefined = undefined;
let errorTitle: string | undefined;
let errorDetails: string | undefined;
let errorCode: string | undefined;
let debug: CoreSiteErrorDebug | undefined;
if (error instanceof CoreSiteError) {
supportConfig = error.supportConfig;
errorDetails = error.errorDetails;
errorCode = error.errorcode;
siteExists = supportConfig instanceof CoreUserGuestSupportConfig;
debug = error.debug;
}
if (error instanceof CoreLoginError) {
errorTitle = error.title;
}
if (errorDetails) {
errorMessage = `<p>${errorMessage}</p><div class="core-error-info-container"></div>`;
if (debug) {
errorMessage = `<p>${errorMessage}</p><div class="core-error-accordion-container"></div>`;
}
const alertSupportConfig = supportConfig;
@ -438,7 +436,7 @@ export class CoreLoginSitePage implements OnInit {
handler: () => CoreUserSupport.contact({
supportConfig: alertSupportConfig,
subject: Translate.instant('core.cannotconnect'),
message: `Error: ${errorCode}\n\n${errorDetails}`,
message: `Error: ${debug?.code}\n\n${debug?.details}`,
}),
}
: (
@ -458,11 +456,11 @@ export class CoreLoginSitePage implements OnInit {
buttons: buttons as AlertButton[],
});
if (errorDetails) {
// Avoid sanitizing JS.
const containerElement = alertElement.querySelector('.core-error-info-container');
if (debug) {
const containerElement = alertElement.querySelector('.core-error-accordion-container');
if (containerElement) {
containerElement.innerHTML = CoreErrorInfoComponent.render(errorDetails, errorCode);
await CoreErrorAccordion.render(containerElement, debug.code, debug.details);
}
}
}

View File

@ -39,7 +39,6 @@ import { CorePushNotifications } from '@features/pushnotifications/services/push
import { CorePath } from '@singletons/path';
import { CorePromisedValue } from '@classes/promised-value';
import { SafeHtml } from '@angular/platform-browser';
import { CoreLoginError } from '@classes/errors/loginerror';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import {
CoreSiteIdentityProvider,
@ -916,8 +915,8 @@ export class CoreLoginHelperProvider {
/**
* Show a modal warning that the credentials introduced were not correct.
*/
protected showInvalidLoginModal(error: CoreLoginError): void {
CoreDomUtils.showErrorModal(error.errorDetails ?? error.message);
protected showInvalidLoginModal(error: CoreWSError): void {
CoreDomUtils.showErrorModal(error.message);
}
/**

View File

@ -124,7 +124,10 @@ describe('Credentials page', () => {
getUserToken: () => {
throw new CoreLoginError({
message: '',
errorcode: 'invalidlogin',
debug: {
code: 'invalidlogin',
details: 'Invalid login',
},
});
},
checkSite: async () => (siteCheck),

View File

@ -1,37 +0,0 @@
// (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 { StorybookModule } from '@/storybook/storybook.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
import { CoreComponentsModule } from '@components/components.module';
import { CommonModule } from '@angular/common';
import {
CoreSearchGlobalSearchResultsPageComponent,
} from '@features/search/stories/components/global-search-results-page/global-search-results-page';
import { CoreSharedModule } from '@/core/shared.module';
@NgModule({
declarations: [
CoreSearchGlobalSearchResultsPageComponent,
],
imports: [
CoreSharedModule,
CommonModule,
StorybookModule,
CoreComponentsModule,
CoreSearchComponentsModule,
],
})
export class CoreSearchComponentsStorybookModule {}

View File

@ -1,18 +0,0 @@
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>
<h1>Search Results</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="limited-width">
<div>
<core-search-box />
<ion-list>
<core-search-global-search-result *ngFor="let result of results" [result]="result"
(onClick)="resultClicked(result.title)" />
</ion-list>
</div>
</ion-content>
</ion-app>

View File

@ -1,141 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { CoreCourseListItem } from '@features/courses/services/courses';
import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar';
import { CoreSearchGlobalSearchResult } from '@features/search/services/global-search';
import courses from '@/assets/storybook/courses.json';
@Component({
selector: 'core-search-global-search-results-page',
templateUrl: 'global-search-results-page.html',
})
export class CoreSearchGlobalSearchResultsPageComponent {
results: CoreSearchGlobalSearchResult[] = [
{
id: 1,
url: '',
title: 'Activity forum test',
content: 'this is a content test for a forum to see in the search result.',
context: {
courseName: 'Course 102',
userName: 'Stephania Krovalenko',
},
module: {
name: 'forum',
iconurl: 'assets/img/mod/forum.svg',
area: 'activity',
},
},
{
id: 2,
url: '',
title: 'Activity assignment test',
content: 'this is a content test for a forum to see in the search result.',
context: {
courseName: 'Course 102',
},
module: {
name: 'assign',
iconurl: 'assets/img/mod/assign.svg',
area: '',
},
},
{
id: 3,
url: '',
title: 'Course 101',
course: courses[0] as CoreCourseListItem,
},
{
id: 4,
url: '',
title: 'John the Tester',
user: {
fullname: 'John Doe',
profileimageurl: 'https://placekitten.com/300/300',
} as CoreUserWithAvatar,
},
{
id: 5,
url: '',
title: 'Search result title',
content: 'this is a content test for a forum to see in the search result.',
context: {
userName: 'Stephania Krovalenko',
},
module: {
name: 'forum',
iconurl: 'assets/img/mod/forum.svg',
area: 'post',
},
},
{
id: 6,
url: '',
title: 'Side block',
context: {
courseName: 'Moodle Site',
},
component: {
name: 'block_html',
iconurl: 'https://master.mm.moodledemo.net/theme/image.php?theme=boost&component=core&image=e%2Fanchor',
},
},
{
id: 7,
url: '',
title: 'Course section',
context: {
courseName: 'Course 101',
},
component: {
name: 'core_course',
iconurl: 'https://master.mm.moodledemo.net/theme/image.php?theme=boost&component=core&image=i%2Fsection',
},
},
{
id: 8,
url: '',
title: 'This item has long text everywhere, so make sure that it looks good anyways. ' +
'Even if the screen you\'re using is also big, this should still be a problem because this text is *really* long.',
content: 'You would normally see lorem ipsum here, but we decided to just write some gibberish here to make it more ' +
'real. We all know that lorem ipsum is fabricated text, and even though it serves its purpose, it isn\'t as ' +
'engaging as some real, hand-crafted text (not sure why this should be engaging, anyways).',
context: {
courseName: 'And it\'s not just the title, either. Other things like the Course title also take more than ' +
'you would expect in a normal site (or even not so normal).',
userName: 'To top it off, it has a user name as well! What is this madness? Well, at some point you just have to ' +
'get creative. Honestly, I\'m surprised if you\'re even reading this. Kudos to you for being thorough.',
},
module: {
name: 'book',
iconurl: 'assets/img/mod/book.svg',
area: '',
},
},
];
/**
* Result clicked.
*
* @param title Result title.
*/
resultClicked(title: string): void {
alert(`clicked on ${title}`);
}
}

View File

@ -1,136 +0,0 @@
// (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 { CoreSearchGlobalSearchResultComponent } from '@features/search/components/global-search-result/global-search-result';
import { CoreSearchComponentsStorybookModule } from '@features/search/stories/components/components.module';
import {
CoreSearchGlobalSearchResultsPageComponent,
} from '@features/search/stories/components/global-search-results-page/global-search-results-page';
import { APP_INITIALIZER } from '@angular/core';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { AddonModForumModuleHandler } from '@addons/mod/forum/services/handlers/module';
import { AddonModAssignModuleHandler } from '@addons/mod/assign/services/handlers/module';
import { CoreSearchGlobalSearchResult } from '@features/search/services/global-search';
import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar';
import { CoreCourseListItem } from '@features/courses/services/courses';
import courses from '@/assets/storybook/courses.json';
interface Args {
title: string;
content: string;
image: 'course' | 'user' | 'none';
module: 'forum-activity' | 'forum-post' | 'assign' | 'none';
courseContext: boolean;
userContext: boolean;
showCourse: boolean;
}
export default <Meta<Args>> {
title: 'Core/Search/Global Search Result',
component: CoreSearchGlobalSearchResultComponent,
decorators: [
moduleMetadata({
imports: [CoreSearchComponentsStorybookModule],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue() {
CoreCourseModuleDelegate.registerHandler(AddonModForumModuleHandler.instance);
CoreCourseModuleDelegate.registerHandler(AddonModAssignModuleHandler.instance);
CoreCourseModuleDelegate.updateHandlers();
},
},
],
}),
],
argTypes: {
image: {
control: {
type: 'select',
options: ['course', 'user', 'none'],
},
},
module: {
control: {
type: 'select',
options: ['forum-activity', 'forum-post', 'assign', 'none'],
},
},
},
args: {
title: 'Result #1',
content: 'This item seems really interesting, maybe you should click through',
image: 'none',
module: 'none',
courseContext: false,
userContext: false,
showCourse: true,
},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/h3E7pkfgyImJPaYmTfnwuF/Global-Search?node-id=118%3A4610',
},
},
};
const Template = story<Args>(({ image, courseContext, userContext, module, showCourse, ...args }) => {
const result: CoreSearchGlobalSearchResult = {
...args,
id: 1,
url: '',
};
if (courseContext || userContext) {
result.context = {
courseName: courseContext ? 'Course 101' : undefined,
userName: userContext ? 'John Doe' : undefined,
};
}
if (module !== 'none') {
const name = module.startsWith('forum') ? 'forum' : module;
result.module = {
name,
iconurl: `assets/img/mod/${name}.svg`,
area: module.startsWith('forum') ? module.substring(6) : '',
};
}
switch (image) {
case 'course':
result.course = courses[0] as CoreCourseListItem;
break;
case 'user':
result.user = {
fullname: 'John Doe',
profileimageurl: 'https://placekitten.com/300/300',
} as CoreUserWithAvatar;
break;
}
return {
component: CoreSearchGlobalSearchResultComponent,
props: { result, showCourse },
};
});
export const Primary = story<Args>(Template);
export const ResultsPage = story<Args>(() => ({ component: CoreSearchGlobalSearchResultsPageComponent }));

View File

@ -0,0 +1,123 @@
// (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 { Injectable } from '@angular/core';
import { Translate, makeSingleton } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreDom } from '@singletons/dom';
import { CoreForms } from '@singletons/form';
import { CoreLogger } from '@singletons/logger';
/**
* Service used to render an Error Accordion component.
*
* This is declared as a service instead of an Angular Component because the HTML
* has to be injected dynamically in alerts (only HTML and Ionic components work).
*/
@Injectable({ providedIn: 'root' })
export class CoreErrorAccordionService {
private logger: CoreLogger;
constructor() {
this.logger = CoreLogger.getInstance('CoreErrorAccordion');
}
/**
* Render an instance of the component into an HTML string.
*
* @param element Root element.
* @param errorCode Error code.
* @param errorDetails Error details.
*/
async render(element: Element, errorCode: string, errorDetails: string): Promise<void> {
const html = this.html(errorCode, errorDetails);
element.innerHTML = html;
await this.hydrate(element);
}
/**
* Get component html.
*
* @param errorCode Error code.
* @param errorDetails Error details.
* @returns HTML.
*/
private html(errorCode: string, errorDetails: string): string {
const contentId = CoreForms.uniqueId('error-accordion-content');
const errorCodeLabel = Translate.instant('core.errorcode', { errorCode });
const hideDetailsLabel = Translate.instant('core.errordetailshide');
const showDetailsLabel = Translate.instant('core.errordetailsshow');
return `
<div class="core-error-accordion">
<h3 class="core-error-accordion--code">${errorCodeLabel}</h3>
<div id="${contentId}" class="core-error-accordion--details" role="region" aria-hidden="true">
<p>${errorDetails}</p>
</div>
<button type="button" class="core-error-accordion--toggle" aria-expanded="false" aria-controls="${contentId}">
<div class="core-error-accordion--toggle-text">
<span class="core-error-accordion--show-details">
${showDetailsLabel}
</span>
<span class="core-error-accordion--hide-details">
${hideDetailsLabel}
</span>
</div>
<ion-icon name="chevron-down" />
</button>
</div>
`;
}
/**
* Hydrate component.
*
* @param element Root element.
*/
private async hydrate(element: Element): Promise<void> {
const wrapper = element.querySelector<HTMLDivElement>('.core-error-accordion');
const description = element.querySelector<HTMLParagraphElement>('.core-error-accordion--details');
const button = element.querySelector<HTMLButtonElement>('.core-error-accordion--toggle');
const hideText = element.querySelector<HTMLSpanElement>('.core-error-accordion--hide-details');
if (!wrapper || !description || !button || !hideText) {
this.logger.error('Couldn\'t render error-accordion, one of the child elements is missing');
return;
}
await CoreDom.waitToBeVisible(wrapper);
button.onclick = () => {
wrapper.classList.toggle('expanded');
description.setAttribute('aria-hidden', description.getAttribute('aria-hidden') === 'true' ? 'false' : 'true');
button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');
};
hideText.style.display = 'none';
wrapper.style.setProperty('--width', `${wrapper.clientWidth}px`);
wrapper.style.setProperty('--description-height', `${description.clientHeight}px`);
wrapper.classList.add('hydrated');
await CoreUtils.nextTick();
hideText.style.display = 'revert';
}
}
export const CoreErrorAccordion = makeSingleton(CoreErrorAccordionService);

View File

@ -66,6 +66,7 @@ import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
import { firstValueFrom } from 'rxjs';
import { CoreHTMLClasses } from '@singletons/html-classes';
import { CoreSiteErrorDebug } from '@classes/errors/siteerror';
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id';
@ -362,18 +363,22 @@ export class CoreSitesProvider {
if (!config.enablewebservices) {
throw this.createCannotConnectLoginError(config.httpswwwroot || config.wwwroot, {
supportConfig: new CoreUserGuestSupportConfig(temporarySite, config),
errorcode: 'webservicesnotenabled',
errorDetails: Translate.instant('core.login.webservicesnotenabled'),
critical: true,
debug: {
code: 'webservicesnotenabled',
details: Translate.instant('core.login.webservicesnotenabled'),
},
});
}
if (!config.enablemobilewebservice) {
throw this.createCannotConnectLoginError(config.httpswwwroot || config.wwwroot, {
supportConfig: new CoreUserGuestSupportConfig(temporarySite, config),
errorcode: 'mobileservicesnotenabled',
errorDetails: Translate.instant('core.login.mobileservicesnotenabled'),
critical: true,
debug: {
code: 'mobileservicesnotenabled',
details: Translate.instant('core.login.mobileservicesnotenabled'),
},
});
}
@ -421,14 +426,16 @@ export class CoreSitesProvider {
siteUrl: string,
error: CoreError | CoreAjaxError | CoreAjaxWSError,
): Promise<CoreLoginError> {
if (error instanceof CoreAjaxError || !('errorcode' in error)) {
if (error instanceof CoreAjaxError || (!('debug' in error) && !('errorcode' in error))) {
// The WS didn't return data, probably cannot connect.
return new CoreLoginError({
title: Translate.instant('core.cannotconnect'),
message: Translate.instant('core.siteunavailablehelp', { site: siteUrl }),
errorcode: 'publicconfigfailed',
errorDetails: error.message || '',
critical: false, // Allow fallback to http if siteUrl uses https.
debug: {
code: 'publicconfigfailed',
details: error.message || 'Failed getting public config',
},
});
}
@ -437,28 +444,31 @@ export class CoreSitesProvider {
critical: true,
title: Translate.instant('core.cannotconnect'),
message: Translate.instant('core.siteunavailablehelp', { site: siteUrl }),
errorcode: error.errorcode,
supportConfig: error.supportConfig,
errorDetails: error.errorDetails ?? error.message,
debug: error.debug,
};
if (error.errorcode === 'codingerror') {
if (error.debug?.code === 'codingerror') {
// This could be caused by a redirect. Check if it's the case.
const redirect = await CoreUtils.checkRedirect(siteUrl);
options.message = Translate.instant('core.siteunavailablehelp', { site: siteUrl });
if (redirect) {
options.errorcode = 'sitehasredirect';
options.errorDetails = Translate.instant('core.login.sitehasredirect');
options.critical = false; // Keep checking fallback URLs.
options.debug = {
code: 'sitehasredirect',
details: Translate.instant('core.login.sitehasredirect'),
};
}
} else if (error.errorcode === 'invalidrecord') {
} else if (error.debug?.code === 'invalidrecord') {
// WebService not found, site not supported.
options.message = Translate.instant('core.siteunavailablehelp', { site: siteUrl });
options.errorcode = 'invalidmoodleversion';
options.errorDetails = Translate.instant('core.login.invalidmoodleversion', { $a: CoreSite.MINIMUM_MOODLE_VERSION });
} else if (error.errorcode === 'redirecterrordetected') {
options.debug = {
code: 'invalidmoodleversion',
details: Translate.instant('core.login.invalidmoodleversion', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
};
} else if (error.debug?.code === 'redirecterrordetected') {
options.critical = false; // Keep checking fallback URLs.
}
@ -499,19 +509,21 @@ export class CoreSitesProvider {
try {
data = await firstValueFrom(Http.post(loginUrl, params).pipe(timeout(CoreWS.getRequestTimeout())));
} catch (error) {
throw new CoreError(
this.isLoggedIn()
? Translate.instant('core.siteunavailablehelp', { site: this.currentSite?.siteUrl })
: Translate.instant('core.sitenotfoundhelp'),
);
throw this.createCannotConnectLoginError(siteUrl, {
debug: {
code: 'logintokenerror',
details: error.message,
},
});
}
if (data === undefined) {
throw new CoreError(
this.isLoggedIn()
? Translate.instant('core.siteunavailablehelp', { site: this.currentSite?.siteUrl })
: Translate.instant('core.sitenotfoundhelp'),
);
throw this.createCannotConnectLoginError(siteUrl, {
debug: {
code: 'logintokenempty',
details: 'The request to /login/token.php returned an empty response',
},
});
}
if (data.token !== undefined) {
@ -536,16 +548,20 @@ export class CoreSitesProvider {
if (redirect) {
throw this.createCannotConnectLoginError(siteUrl, {
supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl),
errorcode: 'sitehasredirect',
errorDetails: Translate.instant('core.login.sitehasredirect'),
debug: {
code: 'sitehasredirect',
details: Translate.instant('core.login.sitehasredirect'),
},
});
}
}
throw this.createCannotConnectLoginError(siteUrl, {
supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl),
errorcode: data.errorcode,
errorDetails: data.error,
debug: {
code: data.errorcode ?? 'loginfailed',
details: data.error ?? 'Could not get a user token in /login/token.php',
},
});
}
@ -657,23 +673,33 @@ export class CoreSitesProvider {
* @returns A promise rejected with the error info.
*/
protected async treatInvalidAppVersion(result: number, siteId?: string): Promise<never> {
let errorCode: string | undefined;
let debug: CoreSiteErrorDebug | undefined;
let errorKey: string | undefined;
let translateParams = {};
switch (result) {
case CoreSitesProvider.MOODLE_APP:
errorKey = 'core.login.connecttomoodleapp';
errorCode = 'connecttomoodleapp';
debug = {
code: 'connecttomoodleapp',
details: 'Cannot connect to app',
};
break;
case CoreSitesProvider.WORKPLACE_APP:
errorKey = 'core.login.connecttoworkplaceapp';
errorCode = 'connecttoworkplaceapp';
debug = {
code: 'connecttoworkplaceapp',
details: 'Cannot connect to app',
};
break;
default:
errorCode = 'invalidmoodleversion';
errorKey = 'core.login.invalidmoodleversion';
translateParams = { $a: CoreSite.MINIMUM_MOODLE_VERSION };
debug = {
code: 'invalidmoodleversion',
details: 'Cannot connect to app',
};
break;
}
if (siteId) {
@ -681,8 +707,8 @@ export class CoreSitesProvider {
}
throw new CoreLoginError({
debug,
message: Translate.instant(errorKey, translateParams),
errorcode: errorCode,
loggedOut: true,
});
}

View File

@ -52,7 +52,7 @@ import { Subscription } from 'rxjs';
import { CoreNetwork } from '@services/network';
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreUserSupport } from '@features/user/services/support';
import { CoreErrorInfoComponent } from '@components/error-info/error-info';
import { CoreErrorAccordion } from '@services/error-accordion';
import { CorePlatform } from '@services/platform';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { CoreLang } from '@services/lang';
@ -1037,8 +1037,8 @@ export class CoreDomUtilsProvider {
if (typeof error !== 'string' && 'buttons' in error && typeof error.buttons !== 'undefined') {
alertOptions.buttons = error.buttons;
} else if (error instanceof CoreSiteError) {
if (error.errorDetails) {
alertOptions.message = `<p>${alertOptions.message}</p><div class="core-error-info-container"></div>`;
if (error.debug) {
alertOptions.message = `<p>${alertOptions.message}</p><div class="core-error-accordion-container"></div>`;
}
const supportConfig = error.supportConfig;
@ -1051,7 +1051,7 @@ export class CoreDomUtilsProvider {
handler: () => CoreUserSupport.contact({
supportConfig,
subject: alertOptions.header,
message: `${error.errorcode}\n\n${error.errorDetails}`,
message: `${error.debug?.code}\n\n${error.debug?.details}`,
}),
});
}
@ -1061,11 +1061,11 @@ export class CoreDomUtilsProvider {
const alertElement = await this.showAlertWithOptions(alertOptions, autocloseTime);
if (error instanceof CoreSiteError && error.errorDetails) {
const containerElement = alertElement.querySelector('.core-error-info-container');
if (error instanceof CoreSiteError && error.debug) {
const containerElement = alertElement.querySelector('.core-error-accordion-container');
if (containerElement) {
containerElement.innerHTML = CoreErrorInfoComponent.render(error.errorDetails, error.errorcode);
await CoreErrorAccordion.render(containerElement, error.debug.code, error.debug.details);
}
}

View File

@ -497,10 +497,12 @@ export class CoreWSProvider {
throw new CoreAjaxError({
message,
supportConfig: await CoreUserGuestSupportConfig.forSite(preSets.siteUrl),
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}),
},
});
} else if (data.error) {
throw new CoreAjaxWSError(data);
@ -527,54 +529,72 @@ export class CoreWSProvider {
if (CorePlatform.isMobile()) {
switch (data.status) {
case NativeHttp.ErrorCode.SSL_EXCEPTION:
options.errorcode = 'invalidcertificate';
options.errorDetails = Translate.instant('core.certificaterror', {
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Invalid certificate',
});
options.debug = {
code: 'invalidcertificate',
details: Translate.instant('core.certificaterror', {
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Invalid certificate',
}),
};
break;
case NativeHttp.ErrorCode.SERVER_NOT_FOUND:
options.errorcode = 'servernotfound';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Server could not be found';
options.debug = {
code: 'servernotfound',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Server could not be found',
};
break;
case NativeHttp.ErrorCode.TIMEOUT:
options.errorcode = 'requesttimeout';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request timed out';
options.debug = {
code: 'requesttimeout',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request timed out',
};
break;
case NativeHttp.ErrorCode.UNSUPPORTED_URL:
options.errorcode = 'unsupportedurl';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Url not supported';
options.debug = {
code: 'unsupportedurl',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Url not supported',
};
break;
case NativeHttp.ErrorCode.NOT_CONNECTED:
options.errorcode = 'connectionerror';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error)
?? 'Connection error, is network available?';
options.debug = {
code: 'connectionerror',
details: CoreTextUtils.getErrorMessageFromError(data.error)
?? 'Connection error, is network available?',
};
break;
case NativeHttp.ErrorCode.ABORTED:
options.errorcode = 'requestaborted';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request aborted';
options.debug = {
code: 'requestaborted',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request aborted',
};
break;
case NativeHttp.ErrorCode.POST_PROCESSING_FAILED:
options.errorcode = 'requestprocessingfailed';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request processing failed';
options.debug = {
code: 'requestprocessingfailed',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request processing failed',
};
break;
}
}
if (!options.errorcode) {
if (!options.debug) {
switch (data.status) {
case 404:
options.errorcode = 'endpointnotfound';
options.errorDetails = Translate.instant('core.ajaxendpointnotfound', {
$a: CoreSite.MINIMUM_MOODLE_VERSION,
});
options.debug = {
code: 'endpointnotfound',
details: Translate.instant('core.ajaxendpointnotfound', {
$a: CoreSite.MINIMUM_MOODLE_VERSION,
}),
};
break;
default: {
const details = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Unknown error';
options.errorcode = 'serverconnectionajax';
options.errorDetails = Translate.instant('core.serverconnection', {
details: `[Response status code: ${data.status}] ${details}`,
});
options.debug = {
code: 'serverconnectionajax',
details: Translate.instant('core.serverconnection', {
details: `[Response status code: ${data.status}] ${details}`,
}),
};
}
break;
}
@ -716,10 +736,12 @@ export class CoreWSProvider {
if (!data) {
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'serverconnectionpost',
errorDetails: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}),
debug: {
code: 'serverconnectionpost',
details: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}),
},
});
} else if (typeof data !== typeExpected) {
// If responseType is text an string will be returned, parse before returning.
@ -730,8 +752,10 @@ export class CoreWSProvider {
this.logger.warn(`Response expected type "${typeExpected}" cannot be parsed to number`);
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method }),
},
});
}
} else if (typeExpected === 'boolean') {
@ -743,24 +767,30 @@ export class CoreWSProvider {
this.logger.warn(`Response expected type "${typeExpected}" is not true or false`);
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method }),
},
});
}
} else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${typeExpected}"`);
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method }),
},
});
}
} else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${typeExpected}"`);
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method }),
},
});
}
}
@ -803,10 +833,12 @@ export class CoreWSProvider {
return retryPromise;
} else if (error.status === -2) {
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidcertificate',
errorDetails: Translate.instant('core.certificaterror', {
details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error',
}),
debug: {
code: 'invalidcertificate',
details: Translate.instant('core.certificaterror', {
details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error',
}),
},
});
} else if (error.status > 0) {
throw this.createHttpError(error, error.status);
@ -1033,24 +1065,30 @@ export class CoreWSProvider {
if (data === null) {
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
},
});
}
if (!data) {
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'serverconnectionupload',
errorDetails: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
}),
debug: {
code: 'serverconnectionupload',
details: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
}),
},
});
} else if (typeof data != 'object') {
this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"');
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
},
});
}

View File

@ -1,78 +0,0 @@
// (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 { IonicModule } from '@ionic/angular';
import { NgModule, ApplicationInitStatus, APP_INITIALIZER } from '@angular/core';
import { Observable, of } from 'rxjs';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import englishTranslations from '@/assets/lang/en.json';
import { CoreApplicationInitStatus } from '@classes/application-init-status';
import { Translate } from '@singletons';
import { CoreSitesProviderStub, CoreSitesStub } from '@/storybook/stubs/services/sites';
import { CoreSitesProvider } from '@services/sites';
import { CoreDbProviderStub } from '@/storybook/stubs/services/db';
import { CoreDbProvider } from '@services/db';
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 {
getTranslation(): Observable<typeof englishTranslations> {
return of(englishTranslations);
}
}
/**
* Module declaring dependencies for Storybook components.
*/
@NgModule({
imports: [
IonicModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: StaticTranslateLoader,
},
}),
],
providers: [
{ provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus },
{ provide: CoreSitesProvider, useClass: CoreSitesProviderStub },
{ provide: CoreDbProvider, useClass: CoreDbProviderStub },
{ provide: CoreFilepoolProvider, useClass: CoreFilepoolProviderStub },
{ provide: CorePushNotificationsProvider, useClass: CorePushNotificationsProviderStub },
{ provide: HttpClient, useClass: HttpClientStub },
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
Translate.setDefaultLang('en');
Translate.use('en');
CoreSitesStub.stubCurrentSite();
},
},
],
exports: [
IonicModule,
TranslateModule,
],
})
export class StorybookModule {}

View File

@ -1,59 +0,0 @@
// (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 { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
import { CoreSite, CoreSiteConfigResponse } from '@classes/sites/site';
import { CoreSiteInfo } from '@classes/sites/unauthenticated-site';
import { of } from 'rxjs';
export interface CoreSiteFixture {
id: string;
info: CoreSiteInfo;
}
export class CoreSiteStub extends CoreSite {
protected wsStubs: Record<string, unknown> = {};
constructor (fixture: CoreSiteFixture) {
super(fixture.id, fixture.info.siteurl, '', { info: fixture.info });
this.stubWSResponse<CoreSiteConfigResponse>('tool_mobile_get_config', {
settings: [],
warnings: [],
});
}
/**
* @inheritdoc
*/
readObservable<T = unknown>(wsFunction: string, data: unknown, preSets?: CoreSiteWSPreSets): WSObservable<T> {
if (wsFunction in this.wsStubs) {
return of(this.wsStubs[wsFunction] as T);
}
return super.readObservable<T>(wsFunction, data, preSets);
}
/**
* Prepare as stubbed response for a given WS.
*
* @param wsFunction WS function.
* @param response Response.
*/
stubWSResponse<T=unknown>(wsFunction: string, response: T): void {
this.wsStubs[wsFunction] = response;
}
}

View File

@ -1,32 +0,0 @@
// (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 { SQLiteDB } from '@classes/sqlitedb';
import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx';
/**
* SQlite database stub.
*/
export class SQLiteDBStub extends SQLiteDB {
/**
* @inheritdoc
*/
async createDatabase(): Promise<SQLiteObject> {
return new Proxy({
executeSql: () => Promise.resolve({ insertId: Math.random().toString() }),
}, {}) as unknown as SQLiteObject;
}
}

View File

@ -1,35 +0,0 @@
// (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 { SQLiteDBStub } from '@/storybook/stubs/classes/sqlitedb';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreDbProvider } from '@services/db';
/**
* Database provider stub.
*/
export class CoreDbProviderStub extends CoreDbProvider {
/**
* @inheritdoc
*/
getDB(name: string, forceNew?: boolean): SQLiteDB {
if (this.dbInstances[name] === undefined || forceNew) {
this.dbInstances[name] = new SQLiteDBStub(name);
}
return this.dbInstances[name];
}
}

View File

@ -1,32 +0,0 @@
// (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 { makeSingleton } from '@singletons';
import { CoreFilepoolProvider } from '@services/filepool';
/**
* Filepool provider stub.
*/
export class CoreFilepoolProviderStub extends CoreFilepoolProvider {
/**
* @inheritdoc
*/
async getSrcByUrl(siteId: string, fileUrl: string): Promise<string> {
return fileUrl;
}
}
export const CoreFilepoolStub = makeSingleton<CoreFilepoolProviderStub>(CoreFilepoolProvider);

View File

@ -1,38 +0,0 @@
// (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 { makeSingleton } from '@singletons';
import { HttpClient, HttpHandler } from '@angular/common/http';
import { from, Observable } from 'rxjs';
/**
* Http service stub.
*/
export class HttpClientStub extends HttpClient {
constructor() {
super(null as unknown as HttpHandler);
}
/**
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(url: string): Observable<any> {
return from(fetch(url).then(response => response.text()));
}
}
export const HttpStub = makeSingleton<HttpClientStub>(HttpClient);

View File

@ -1,32 +0,0 @@
// (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

@ -1,86 +0,0 @@
// (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 { 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 { CoreError } from '@classes/errors/error';
import { CoreSite } from '@classes/sites/site';
import { SiteDBEntry } from '@services/database/sites';
import { CoreSiteBasicInfo, CoreSitesProvider } from '@services/sites';
import { makeSingleton } from '@singletons';
/**
* Sites provider stub.
*/
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 ?? schoolBarbaraSite);
}
return this.getRequiredCurrentSite();
}
}
export const CoreSitesStub = makeSingleton<CoreSitesProviderStub>(CoreSitesProvider);

View File

@ -1,30 +0,0 @@
// (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 { Story } from '@storybook/angular';
/**
* Define story.
*
* @param template Story template.
* @param defaultArgs Default arguments.
* @returns Story.
*/
export function story<T>(template: Story<T>, defaultArgs: Partial<T> = {}): Story<T> {
const story = template.bind({});
story.args = defaultArgs;
return story;
}

View File

@ -0,0 +1,151 @@
.core-error-accordion {
--toggle-icon-animation-duration: 300ms;
--toggle-icon-animation-function: ease-in;
--background-color: var(--gray-300);
--toggle-icon-size: 16px;
background: var(--background-color);
border-radius: var(--radius-xs);
.core-error-accordion--code {
margin: 0;
text-align: start;
color: var(--text-color-main);
font: var(--subtitle-md-font);
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
padding-inline-start: var(--spacing-3);
padding-inline-end: var(--spacing-3);
}
.core-error-accordion--details {
opacity: 0;
display: flex;
overflow: hidden;
flex-direction: column;
justify-content: center;
transition-property: opacity, height;
transition-duration: var(--toggle-icon-animation-duration);
transition-timing-function: var(--toggle-icon-animation-function);
p {
margin: 0;
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
padding-inline-start: var(--spacing-3);
padding-inline-end: var(--spacing-3);
text-align: start;
font: var(--body-md-font);
color: var(--text-color-secondary);
}
}
.core-error-accordion--toggle {
display: flex;
width: 100%;
align-items: center;
background: transparent;
justify-content: space-between;
color: var(--text-color-secondary);
font: var(--label-lg-font);
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
padding-inline-start: var(--spacing-3);
padding-inline-end: var(--spacing-3);
.core-error-accordion--toggle-text {
display: flex;
flex-direction: column;
}
.core-error-accordion--show-details,
.core-error-accordion--hide-details {
text-align: start;
transition: opacity var(--toggle-icon-animation-duration) var(--toggle-icon-animation-function);
}
ion-icon {
width: var(--toggle-icon-size);
margin-inline-start: var(--spacing-4);
transition: transform var(--toggle-icon-animation-duration) var(--toggle-icon-animation-function);
transform: rotate(0);
}
&:hover {
background: var(--state-color-hover);
}
&:focus {
box-shadow: none;
background: var(--state-color-focused);
}
&:focus-visible {
box-shadow: none;
outline: 2px solid var(--state-color-keyboard-focus);
}
&:active {
background: var(--state-color-pressed);
}
}
&.hydrated {
width: var(--width);
.core-error-accordion--details {
height: 0;
}
.core-error-accordion--toggle {
.core-error-accordion--toggle-text {
flex-grow: 1;
position: relative;
}
.core-error-accordion--hide-details {
position: absolute;
opacity: 0;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
}
&.expanded {
.core-error-accordion--details {
opacity: 1;
height: var(--description-height);
}
.core-error-accordion--toggle {
.core-error-accordion--hide-details {
opacity: 1;
}
.core-error-accordion--show-details {
opacity: 0;
}
ion-icon {
transform: rotate(180deg);
}
}
}
}
html.dark .core-error-accordion {
--background-color: var(--gray-700);
}

View File

@ -5,9 +5,7 @@ html {
--spacing-#{$i}: #{$i*4}px;
}
// Font sizes
// Body font size
// Typography
--font-size-sm: 12px;
--font-size-md: 14px;
--font-size-lg: 16px;
@ -15,19 +13,29 @@ html {
--font-weight-normal: 400;
--font-weight-medium: 500;
// Typography - Body
--body-font-size-sm: var(--font-size-sm);
--body-font-size-md: var(--font-size-md);
--body-font-size-lg: var(--font-size-lg);
--body-font-weight: var(--font-weight-normal);
--body-line-height: 150%;
--body-sm-font: normal normal var(--body-font-weight) var(--body-font-size-sm)/var(--body-line-height) var(--ion-font-family);
--body-md-font: normal normal var(--body-font-weight) var(--body-font-size-md)/var(--body-line-height) var(--ion-font-family);
--body-lg-font: normal normal var(--body-font-weight) var(--body-font-size-lg)/var(--body-line-height) var(--ion-font-family);
// Typography - Links
--link-sm-font-size: var(--font-size-sm);
--link-md-font-size: var(--font-size-md);
--link-lg-font-size: var(--font-size-lg);
--link-font-weight: var(--font-weight-normal);
--link-line-height: 150%;
// Labels
--link-sm-font: normal normal var(--link-font-weight) var(--link-sm-font-size)/var(--link-line-height) var(--ion-font-family);
--link-md-font: normal normal var(--link-font-weight) var(--link-md-font-size)/var(--link-line-height) var(--ion-font-family);
--link-lg-font: normal normal var(--link-font-weight) var(--link-lg-font-size)/var(--link-line-height) var(--ion-font-family);
// Typography - Labels
--label-sm-font-size: 10px;
--label-md-font-size: 12px;
--label-lg-font-size: 14px;
@ -37,7 +45,11 @@ html {
--label-md-line-height: 16px;
--label-lg-line-height: 20px;
// Subtitles
--label-sm-font: normal normal var(--label-font-weight) var(--label-sm-font-size)/var(--label-sm-line-height) var(--ion-font-family);
--label-md-font: normal normal var(--label-font-weight) var(--label-md-font-size)/var(--label-md-line-height) var(--ion-font-family);
--label-lg-font: normal normal var(--label-font-weight) var(--label-lg-font-size)/var(--label-lg-line-height) var(--ion-font-family);
// Typography - Subtitles
--subtitle-sm-font-size: 14px;
--subtitle-md-font-size: 16px;
--subtitle-lg-font-size: 20px;
@ -45,7 +57,11 @@ html {
--subtitle-font-weight: var(--font-weight-medium);
--subtitle-line-height: 150%;
// Headings
--subtitle-sm-font: normal normal var(--subtitle-font-weight) var(--subtitle-sm-font-size)/var(--subtitle-line-height) var(--ion-font-family);
--subtitle-md-font: normal normal var(--subtitle-font-weight) var(--subtitle-md-font-size)/var(--subtitle-line-height) var(--ion-font-family);
--subtitle-lg-font: normal normal var(--subtitle-font-weight) var(--subtitle-lg-font-size)/var(--subtitle-line-height) var(--ion-font-family);
// Typography - Headings
--heading-1-font-size: 28px;
--heading-2-font-size: 24px;
--heading-3-font-size: 22px;
@ -94,6 +110,26 @@ html {
// A11y
--a11y-min-target-size: 44px;
// Colors
--blue: #0f6cbf;
--text-color-main: var(--gray-900);
--text-color-secondary: var(--gray-800);
--state-color-hover: rgb(40 40 40, 4%); // --gray-900 4%
--state-color-pressed: rgb(40 40 40, 12%); // --gray-900 12%
--state-color-focused: rgb(40 40 40, 12%); // --gray-900 12%
--state-color-keyboard-focus: var(--blue);
}
html.dark {
// Colors
--text-color-main: var(--gray-200);
--text-color-secondary: var(--gray-300);
}
/** @deprecated since 4.3 **/

View File

@ -22,11 +22,11 @@
/* Components */
@import "components/collapsible-header.scss";
@import "components/collapsible-item.scss";
@import "components/error-accordion.scss";
@import "components/format-text.scss";
@import "components/rubrics.scss";
@import "components/mod-label.scss";
@import "components/ion-icon.scss";
@import "../core/components/error-info/error-info.scss";
@import "components/mod-label.scss";
@import "components/rubrics.scss";
@import "components/videojs.scss";
/* Some styles from 3rd party libraries. */

View File

@ -25,7 +25,6 @@
"src/**/tests/**",
"src/**/stories/**",
"src/testing/**",
"src/storybook/**",
"src/**/*.test.ts",
"src/**/*.stories.*"
]