MOBILE-4059 core: Separate error details in alerts

main
Noel De Martin 2022-10-04 11:10:14 +02:00
parent 238dc458fc
commit 11fea266e9
20 changed files with 385 additions and 15 deletions

View File

@ -1,4 +1,5 @@
module.exports = {
framework: '@storybook/angular',
addons: ['@storybook/addon-controls'],
stories: ['../src/**/*.stories.ts'],
}

View File

@ -0,0 +1,6 @@
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',
};

View File

@ -0,0 +1,3 @@
.core-error-info {
max-width: 300px;
}

View File

@ -13,7 +13,10 @@ module.exports = {
'^.+\\.(ts|html)$': 'ts-jest',
},
transformIgnorePatterns: ['node_modules/(?!@ionic-native|@ionic)'],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src/' }),
moduleNameMapper: {
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src/' }),
'^!raw-loader!.*': 'jest-raw-loader',
},
globals: {
'ts-jest': {
tsConfig: './tsconfig.test.json',

22
package-lock.json generated
View File

@ -5677,6 +5677,22 @@
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.11.0.tgz",
"integrity": "sha512-/IubCWhVXCguyMUp/3zGrg3c882+RJNg/zpiKfyfJL3kRCOwe+/MD8OoAXVGdd+xAohZKIi1Ik+EHFlsptsjLg=="
},
"@storybook/addon-controls": {
"version": "6.1.21",
"resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.1.21.tgz",
"integrity": "sha512-IJgZWD2E9eLKj8DJLA9lT63N4jPfVneFJ05gnPco01ZJCEiDAo7babP5Ns2UTJDUaQEtX0m04UoIkidcteWKsA==",
"dev": true,
"requires": {
"@storybook/addons": "6.1.21",
"@storybook/api": "6.1.21",
"@storybook/client-api": "6.1.21",
"@storybook/components": "6.1.21",
"@storybook/node-logger": "6.1.21",
"@storybook/theming": "6.1.21",
"core-js": "^3.0.1",
"ts-dedent": "^2.0.0"
}
},
"@storybook/addons": {
"version": "6.1.21",
"resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.21.tgz",
@ -21473,6 +21489,12 @@
"ts-jest": "26.x"
}
},
"jest-raw-loader": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jest-raw-loader/-/jest-raw-loader-1.0.1.tgz",
"integrity": "sha512-g9oaAjeC4/rIJk1Wd3RxVbOfMizowM7LSjEJqa4R9qDX0OjQNABXOhH+GaznUp+DjTGVPi2vPPbQXyX87DOnYg==",
"dev": true
},
"jest-regex-util": {
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz",

View File

@ -141,6 +141,7 @@
"@angular/language-service": "~10.0.14",
"@ionic/angular-toolkit": "^2.3.3",
"@ionic/cli": "^6.19.0",
"@storybook/addon-controls": "~6.1",
"@storybook/angular": "~6.1",
"@types/faker": "^5.1.3",
"@types/node": "^12.12.64",
@ -172,6 +173,7 @@
"gulp-slash": "^1.1.3",
"jest": "^26.5.2",
"jest-preset-angular": "^8.3.1",
"jest-raw-loader": "^1.0.1",
"jsonc-parser": "^2.3.1",
"minimatch": "^5.1.0",
"native-run": "^1.4.0",

View File

@ -1472,6 +1472,7 @@
"core.cancel": "moodle",
"core.cannotconnect": "local_moodlemobileapp",
"core.cannotconnecttrouble": "local_moodlemobileapp",
"core.cannotconnecttroublewithoutsupport": "local_moodlemobileapp",
"core.cannotconnectverify": "local_moodlemobileapp",
"core.cannotdownloadfiles": "local_moodlemobileapp",
"core.cannotinstallapk": "local_moodlemobileapp",
@ -1697,7 +1698,10 @@
"core.endonesteptour": "tool_usertours",
"core.error": "moodle",
"core.errorchangecompletion": "local_moodlemobileapp",
"core.errorcode": "local_moodlemobileapp",
"core.errordeletefile": "local_moodlemobileapp",
"core.errordetailshide": "local_moodlemobileapp",
"core.errordetailsshow": "local_moodlemobileapp",
"core.errordownloading": "local_moodlemobileapp",
"core.errordownloadingsomefiles": "local_moodlemobileapp",
"core.errorfileexistssamename": "local_moodlemobileapp",

View File

@ -29,7 +29,7 @@ export class CoreSiteError extends CoreError {
siteConfig?: CoreSitePublicConfigResponse;
constructor(options: CoreSiteErrorOptions) {
super(options.message);
super(getErrorMessage(options));
this.errorcode = options.errorcode;
this.errorDetails = options.errorDetails;
@ -67,8 +67,26 @@ export class CoreSiteError extends CoreError {
}
/**
* Get message to use in the error.
*
* @param options Error options.
* @returns Error message.
*/
function getErrorMessage(options: CoreSiteErrorOptions): string {
if (
options.contactSupport &&
(!options.siteConfig || !CoreUserSupport.canContactSupport(options.siteConfig))
) {
return options.fallbackMessage ?? options.message;
}
return options.message;
}
export type CoreSiteErrorOptions = {
message: string;
fallbackMessage?: string; // Message to use if contacting support was intended but isn't possible.
errorcode?: string;
errorDetails?: string;
critical?: boolean; // Whether the error is important enough to abort the operation.

View File

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

@ -0,0 +1,88 @@
.core-error-info {
background: var(--gray-200);
border-radius: var(--small-radius);
font-size: var(--font-size-sm);
color: var(--gray-900);
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
.core-error-info--content {
padding: var(--spacing-2) var(--spacing-2) 0 var(--spacing-2);
.core-error-info--code {
font-size: var(--font-size-normal);
}
.core-error-info--details {
color: var(--gray-500);
}
}
.core-error-info--checkbox {
display: none;
& + .core-error-info--content {
max-height: calc(var(--font-size-sm) + 2 * var(--spacing-2));
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--content {
max-height: 150px;
& + .core-error-info--toggle .core-error-info--hide-content {
display: flex;
}
& + .core-error-info--toggle .core-error-info--show-content {
display: none;
}
}
}
&.has-error-code .core-error-info--checkbox {
& + .core-error-info--content {
max-height: calc(var(--font-size-normal) + 2 * var(--spacing-2));
}
&:checked + .core-error-info--content {
max-height: 170px;
}
}
}

View File

@ -0,0 +1,94 @@
// (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';
import ChevronUpSVG from '!raw-loader!ionicons/dist/svg/chevron-up.svg';
import ChevronDownSVG from '!raw-loader!ionicons/dist/svg/chevron-down.svg';
/**
* 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');
const hideDetailsLabel = Translate.instant('core.errordetailshide');
const showDetailsLabel = Translate.instant('core.errordetailsshow');
return `
<div class="core-error-info ${errorCode ? 'has-error-code' : ''}">
<input id="${toggleId}" type="checkbox" class="core-error-info--checkbox" />
<div class="core-error-info--content">
${errorCode ? `<p class="core-error-info--code"><strong>${errorCodeLabel}: ${errorCode}</strong></p>` : ''}
<p class="core-error-info--details">${errorDetails}</p>
</div>
<label for="${toggleId}" class="core-error-info--toggle" aria-hidden="true">
<span class="core-error-info--hide-content">
${hideDetailsLabel}
${ChevronUpSVG}
</span>
<span class="core-error-info--show-content">
${showDetailsLabel}
${ChevronDownSVG}
</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

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

@ -42,6 +42,7 @@ import { CoreForms } from '@singletons/form';
import { AlertButton } from '@ionic/core';
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreUserSupport } from '@features/user/services/support';
import { CoreErrorInfoComponent } from '@components/error-info/error-info';
/**
* Site (url) chooser when adding a new site.
@ -382,7 +383,7 @@ export class CoreLoginSitePage implements OnInit {
* @param url The URL the user was trying to connect to.
* @param error Error to display.
*/
protected showLoginIssue(url: string | null, error: CoreError): void {
protected async showLoginIssue(url: string | null, error: CoreError): Promise<void> {
let errorMessage = CoreDomUtils.getErrorMessage(error);
let siteExists = false;
let supportPageUrl: string | null = null;
@ -396,7 +397,12 @@ export class CoreLoginSitePage implements OnInit {
errorCode = error.errorcode;
}
if (errorMessage == Translate.instant('core.cannotconnecttrouble')) {
if (
!siteExists && (
errorMessage === Translate.instant('core.cannotconnecttrouble') ||
errorMessage === Translate.instant('core.cannotconnecttroublewithoutsupport')
)
) {
const found = this.sites.find((site) => site.url == url);
if (!found) {
@ -404,10 +410,14 @@ export class CoreLoginSitePage implements OnInit {
}
}
let message = '<p>' + errorMessage + '</p>';
errorMessage = '<p>' + errorMessage + '</p>';
if (!siteExists && url) {
const fullUrl = CoreUrlUtils.isAbsoluteURL(url) ? url : 'https://' + url;
message += '<p padding><a href="' + fullUrl + '" core-link>' + url + '</a></p>';
errorMessage += '<p padding><a href="' + fullUrl + '" core-link>' + url + '</a></p>';
}
if (errorDetails) {
errorMessage += '<div class="core-error-info-container"></div>';
}
const buttons: AlertButton[] = [
@ -432,11 +442,19 @@ export class CoreLoginSitePage implements OnInit {
];
// @TODO: Remove CoreSite.MINIMUM_MOODLE_VERSION, not used on translations since 3.9.0.
CoreDomUtils.showAlertWithOptions({
const alertElement = await CoreDomUtils.showAlertWithOptions({
header: Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
message,
message: errorMessage,
buttons,
});
if (errorDetails) {
const containerElement = alertElement.querySelector('.core-error-info-container');
if (containerElement) {
containerElement.innerHTML = CoreErrorInfoComponent.render(errorDetails, errorCode);
}
}
}
/**

View File

@ -16,6 +16,7 @@
"cancel": "Cancel",
"cannotconnect": "Cannot connect",
"cannotconnecttrouble": "We're having trouble connecting to your site.",
"cannotconnecttroublewithoutsupport": "We're having trouble connecting to your site, please contact your institution.",
"cannotconnectverify": "<strong>Please check the address is correct.</strong>",
"cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.",
"cannotinstallapk": "For security reasons, you can't install unknown apps on your device from this app. Please open the file using a browser.",
@ -101,7 +102,10 @@
"endonesteptour": "Got it",
"error": "Error",
"errorchangecompletion": "An error occurred while changing the completion status. Please try again.",
"errorcode": "Error code",
"errordeletefile": "Error deleting the file. Please try again.",
"errordetailshide": "Hide error details",
"errordetailsshow": "Show error details",
"errordownloading": "Error downloading file.",
"errordownloadingsomefiles": "Error downloading files. Some files might be missing.",
"errorfileexistssamename": "A file with this name already exists.",

View File

@ -342,6 +342,7 @@ export class CoreSitesProvider {
errorDetails,
siteConfig,
message: Translate.instant('core.cannotconnecttrouble'),
fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'),
critical: true,
contactSupport: true,
});

View File

@ -20,6 +20,8 @@ import { CoreEventFormAction, CoreEvents } from '@singletons/events';
*/
export class CoreForms {
private static formIds: Record<string, number> = {};
/**
* Get the data from a form. It will only collect elements that have a name.
*
@ -93,6 +95,18 @@ export class CoreForms {
}, siteId);
}
/**
* Generate a unique id for a form input using the given name.
*
* @param name Form input name.
* @returns Unique id.
*/
static uniqueId(name: string): string {
const count = this.formIds[name] ?? 0;
return `${name}-${this.formIds[name] = count + 1}`;
}
}
export type CoreFormFields<T = unknown> = Record<string, T>;

View File

@ -0,0 +1,21 @@
html {
// Spacing
@for $i from 0 to 13 {
--spacing-#{$i}: #{$i*4}px;
}
// Font sizes
--font-size-sm: 12px;
--font-size-normal: 14px;
// Radiuses
--small-radius: 4px;
--medium-radius: 8px;
--big-radius: 16px;
--huge-radius: 24px;
// A11y
--a11y-min-target-size: 44px;
}

View File

@ -48,18 +48,12 @@ html {
}
// Accessibility vars.
--a11y-min-target-size: 44px;
--a11y-focus-color: var(--primary);
--a11y-focus-width: 2px;
--zoom-level: 100%;
--small-radius: 4px;
--medium-radius: 8px;
--big-radius: 16px;
--huge-radius: 24px;
--text-color: #{$text-color};
--text-size: 14px;
--text-size: var(--font-size-normal);
--background-color: #{$background-color};
--stroke: var(--gray-300);

View File

@ -16,6 +16,7 @@
@import "./theme.light.scss";
@import "./theme.dark.scss";
@import "./theme.custom.scss";
@import "./theme.design-system.scss";
@import "./theme.base.scss";
/* Components */
@ -24,6 +25,7 @@
@import "./components/format-text.scss";
@import "./components/rubrics.scss";
@import "./components/mod-label.scss";
@import "../core/components/error-info/error-info.scss";
/* Some styles from 3rd party libraries. */
@import "./bootstrap.scss";

19
src/types/raw.d.ts vendored 100644
View File

@ -0,0 +1,19 @@
// (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.
declare module '!raw-loader!*' {
const contents: string;
export = contents;
}