Merge pull request #3859 from NoelDeMartin/MOBILE-3947

MOBILE-3947: Upgrade dependencies for Ionic 7
main
Pau Ferrer Ocaña 2023-11-23 22:43:17 +01:00 committed by GitHub
commit 3760647b21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 24469 additions and 26453 deletions

View File

@ -5,6 +5,7 @@ const appConfig = {
node: true,
},
plugins: [
'@angular-eslint',
'@typescript-eslint',
'header',
'jsdoc',
@ -13,12 +14,12 @@ const appConfig = {
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:@angular-eslint/recommended',
'plugin:promise/recommended',
'plugin:jsdoc/recommended',
"plugin:deprecation/recommended",
'plugin:deprecation/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
@ -61,16 +62,6 @@ const appConfig = {
allowArgumentsExplicitlyTypedAsAny: true,
},
],
'@typescript-eslint/indent': [
'error',
4,
{
SwitchCase: 1,
ignoredNodes: [
'ClassProperty *',
],
},
],
'@typescript-eslint/lines-between-class-members': [
'error',
'always',
@ -103,6 +94,20 @@ const appConfig = {
],
'@typescript-eslint/naming-convention': [
'error',
{
selector: [
'classProperty',
'objectLiteralProperty',
'typeProperty',
'classMethod',
'objectLiteralMethod',
'typeMethod',
'accessor',
'enumMember'
],
modifiers: ['requiresQuotes'],
format: null,
},
{
selector: 'property',
format: ['camelCase'],
@ -200,17 +205,6 @@ const appConfig = {
],
'id-match': 'error',
'jsdoc/check-alignment': 'error',
'jsdoc/newline-after-description': 'error',
'jsdoc/require-param-type': 'off',
'jsdoc/require-returns-type': 'off',
'jsdoc/require-param': 'off',
'jsdoc/check-values': 'off',
'jsdoc/check-tag-names': [
'warn',
{
"definedTags": ["deprecatedonmoodle"]
},
],
'jsdoc/check-param-names': [
'warn',
{
@ -218,6 +212,23 @@ const appConfig = {
enableFixer: true
},
],
'jsdoc/check-tag-names': [
'warn',
{
'definedTags': ['deprecatedonmoodle']
},
],
'jsdoc/check-values': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-param': 'off',
'jsdoc/require-returns-type': 'off',
'jsdoc/tag-lines': [
'error',
'any',
{
startLines: 1,
},
],
'linebreak-style': [
'error',
'unix',
@ -240,7 +251,7 @@ const appConfig = {
'no-fallthrough': 'off',
'no-invalid-this': 'error',
'no-irregular-whitespace': 'error',
'no-multiple-empty-lines': ['error', { "max": 1 }],
'no-multiple-empty-lines': ['error', { max: 1 }],
'no-new-wrappers': 'error',
'no-sequences': 'error',
'no-trailing-spaces': 'error',
@ -318,15 +329,14 @@ module.exports = {
files: ['*.html'],
extends: ['plugin:@angular-eslint/template/recommended'],
rules: {
'max-len': ['warn', { code: 140 }],
'@angular-eslint/template/accessibility-valid-aria': 'warn',
'@angular-eslint/template/accessibility-alt-text': 'error',
'@angular-eslint/template/accessibility-elements-content': 'error',
'@angular-eslint/template/accessibility-label-for': 'error',
'@angular-eslint/template/no-positive-tabindex': 'error',
'@angular-eslint/template/accessibility-table-scope': 'error',
'@angular-eslint/template/accessibility-valid-aria': 'error',
'@angular-eslint/template/alt-text': 'error',
'@angular-eslint/template/elements-content': 'error',
'@angular-eslint/template/label-has-associated-control': 'error',
'@angular-eslint/template/table-scope': 'error',
'@angular-eslint/template/valid-aria': 'error',
'@angular-eslint/template/no-duplicate-attributes': 'error',
'@angular-eslint/template/no-positive-tabindex': 'error',
'max-len': ['warn', { code: 140 }],
},
},
{

View File

@ -8,8 +8,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install npm packages

2
.nvmrc
View File

@ -1 +1 @@
v14.15.0
v18.18.2

View File

@ -1,5 +1,5 @@
## BUILD STAGE
FROM node:14 as build-stage
FROM node:18 as build-stage
WORKDIR /app

View File

@ -1,56 +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.
// Based on the template node_modules/cordova-android/bin/templates/project/Activity.java
package com.moodle.moodlemobile;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import org.apache.cordova.*;
public class MainActivity extends CordovaActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// enable Cordova apps to be started in the background
Bundle extras = getIntent().getExtras();
if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
moveTaskToBack(true);
}
// Set by <content src="index.html" /> in config.xml
loadUrl(launchUrl);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Forward back key events to the web view.
if (this.appView != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
View webview = this.appView.getView();
if (webview != null) {
webview.dispatchKeyEvent(event);
}
return true;
}
return super.dispatchKeyEvent(event);
}
}

View File

@ -1,7 +1,6 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"defaultProject": "app",
"newProjectRoot": "projects",
"projects": {
"app": {
@ -12,13 +11,10 @@
"schematics": {},
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"builder": "@angular-devkit/build-angular:browser",
"options": {
"customWebpackConfig": {
"path": "./webpack.config.js"
},
"allowedCommonJsDependencies":[
"chart.js"
"chart.js"
],
"outputPath": "www",
"index": "src/index.html",
@ -63,12 +59,6 @@
},
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
@ -77,24 +67,25 @@
}
]
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"testing": {
"optimization": {
"scripts": false,
"styles": true
},
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
},
"ci": {
"progress": false
}
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
@ -107,10 +98,11 @@
"production": {
"browserTarget": "app:build:production"
},
"ci": {
"progress": false
"development": {
"browserTarget": "app:build:development"
}
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
@ -122,14 +114,14 @@
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/core/**/*.html",
"src/addons/**/*.html"
"src/**/*.ts",
"src/core/**/*.html",
"src/addons/**/*.html"
]
}
},
"ionic-cordova-build": {
"builder": "@ionic/angular-toolkit:cordova-build",
"builder": "@ionic/cordova-builders:cordova-build",
"options": {
"browserTarget": "app:build"
},
@ -140,7 +132,7 @@
}
},
"ionic-cordova-serve": {
"builder": "@ionic/angular-toolkit:cordova-serve",
"builder": "@ionic/cordova-builders:cordova-serve",
"options": {
"cordovaBuildTarget": "app:ionic-cordova-build",
"devServerTarget": "app:serve"
@ -157,7 +149,9 @@
},
"cli": {
"analytics": false,
"defaultCollection": "@ionic/angular-toolkit"
"schematicCollections": [
"@ionic/angular-toolkit"
]
},
"schematics": {
"@ionic/angular-toolkit:component": {

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<widget android-versionCode="44000" id="com.moodle.moodlemobile" ios-CFBundleVersion="4.4.0.0" version="4.4.0" versionCode="44000" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget id="com.moodle.moodlemobile" version="4.4.0" versionCode="44000" android-versionCode="44000" ios-CFBundleVersion="4.4.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Moodle</name>
<description>Moodle official app</description>
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
@ -60,7 +60,6 @@
<preference name="AndroidWindowSplashScreenAnimatedIcon" value="resources/android/android-splash.xml" />
<preference name="AndroidWindowSplashScreenBackground" value="#FFFFFF" />
<preference name="AndroidWindowSplashScreenIconBackgroundColor" value="#FFFFFF" />
<resource-file src="MainActivity.java" target="app/src/main/java/com/moodle/moodlemobile/MainActivity.java" />
<resource-file src="google-services.json" target="app/google-services.json" />
<resource-file src="resources/android/icon/drawable-ldpi-smallicon.png" target="app/src/main/res/mipmap-ldpi/smallicon.png" />
<resource-file src="resources/android/icon/drawable-mdpi-smallicon.png" target="app/src/main/res/mipmap-mdpi/smallicon.png" />
@ -68,136 +67,16 @@
<resource-file src="resources/android/icon/drawable-xhdpi-smallicon.png" target="app/src/main/res/mipmap-xhdpi/smallicon.png" />
<resource-file src="resources/android/xml/network_security_config.xml" target="app/src/main/res/xml/network_security_config.xml" />
<resource-file src="resources/android/xml/backup_rules.xml" target="app/src/main/res/xml/backup_rules.xml" />
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application/activity[@android:name='MainActivity']">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" android:exported="true" />
</edit-config>
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application">
<application android:allowBackup="true" android:dataExtractionRules="@xml/backup_rules" android:largeHeap="true" android:networkSecurityConfig="@xml/network_security_config" />
</edit-config>
<config-file parent="/manifest/application" target="AndroidManifest.xml">
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="Clipboard">
<param name="android-package" value="com.verso.cordova.clipboard.Clipboard" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="CordovaHttpPlugin">
<param name="android-package" value="com.silkimen.cordovahttp.CordovaHttpPlugin" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="Camera">
<param name="android-package" value="org.apache.cordova.camera.CameraLauncher" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="LaunchMyApp">
<param name="android-package" value="nl.xservices.plugins.LaunchMyApp" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="Device">
<param name="android-package" value="org.apache.cordova.device.Device" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="File">
<param name="android-package" value="org.apache.cordova.file.FileUtils" />
<param name="onload" value="true" />
</feature>
<allow-navigation href="cdvfile:*" />
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="FileOpener2">
<param name="android-package" value="io.github.pwlin.cordova.plugins.fileopener2.FileOpener2" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="FileTransfer">
<param name="android-package" value="org.apache.cordova.filetransfer.FileTransfer" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="Geolocation">
<param name="android-package" value="org.apache.cordova.geolocation.Geolocation" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="InAppBrowser">
<param name="android-package" value="org.apache.cordova.inappbrowser.InAppBrowser" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="LocalNotification">
<param name="android-package" value="de.appplant.cordova.plugin.localnotification.LocalNotification" />
</feature>
</config-file>
<config-file parent="/manifest/application" target="AndroidManifest.xml">
<provider android:authorities="${applicationId}.localnotifications.provider" android:exported="false" android:grantUriPermissions="true" android:name="de.appplant.cordova.plugin.notification.util.AssetProvider">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/localnotification_provider_paths" />
</provider>
<receiver android:exported="false" android:name="de.appplant.cordova.plugin.localnotification.TriggerReceiver" />
<receiver android:exported="false" android:name="de.appplant.cordova.plugin.localnotification.ClearReceiver" />
<service android:exported="false" android:name="de.appplant.cordova.plugin.localnotification.ClickReceiver" />
<receiver android:directBootAware="true" android:exported="false" android:name="de.appplant.cordova.plugin.localnotification.RestoreReceiver">
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="Capture">
<param name="android-package" value="org.apache.cordova.mediacapture.Capture" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="NetworkStatus">
<param name="android-package" value="org.apache.cordova.networkinformation.NetworkManager" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="QRScanner">
<param name="android-package" value="com.bitpay.cordova.qrscanner.QRScanner" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="CDVOrientation">
<param name="android-package" value="cordova.plugins.screenorientation.CDVOrientation" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="StatusBar">
<param name="android-package" value="org.apache.cordova.statusbar.StatusBar" />
<param name="onload" value="true" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="SQLitePlugin">
<param name="android-package" value="io.sqlc.SQLitePlugin" />
</feature>
</config-file>
<config-file parent="/*" target="res/xml/config.xml">
<feature name="PushNotification">
<param name="android-package" value="com.adobe.phonegap.push.PushPlugin" />
</feature>
</config-file>
<config-file parent="/*" target="AndroidManifest.xml">
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
</config-file>
<config-file parent="/*" target="AndroidManifest.xml">
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
<intent>
<action android:name="android.intent.action.GET_CONTENT" />
</intent>
</queries>
</config-file>
</platform>
<platform name="ios">
<resource-file src="GoogleService-Info.plist" />

File diff suppressed because it is too large Load Diff

48672
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -45,39 +45,17 @@
"lang:create-langindex": "./scripts/create_langindex.sh"
},
"dependencies": {
"@angular/animations": "~10.0.14",
"@angular/common": "~10.0.14",
"@angular/core": "~10.0.14",
"@angular/forms": "~10.0.14",
"@angular/platform-browser": "~10.0.14",
"@angular/platform-browser-dynamic": "~10.0.14",
"@angular/router": "~10.0.14",
"@ionic-native/badge": "^5.36.0",
"@ionic-native/camera": "^5.36.0",
"@ionic-native/chooser": "^5.36.0",
"@ionic-native/clipboard": "^5.36.0",
"@ionic-native/core": "^5.36.0",
"@ionic-native/device": "^5.36.0",
"@ionic-native/diagnostic": "^5.36.0",
"@ionic-native/file": "^5.36.0",
"@ionic-native/file-opener": "^5.36.0",
"@ionic-native/file-transfer": "^5.36.0",
"@ionic-native/geolocation": "^5.36.0",
"@ionic-native/http": "^5.36.0",
"@ionic-native/in-app-browser": "^5.36.0",
"@ionic-native/ionic-webview": "^5.36.0",
"@ionic-native/keyboard": "^5.36.0",
"@ionic-native/local-notifications": "^5.36.0",
"@ionic-native/media-capture": "^5.36.0",
"@ionic-native/network": "^5.36.0",
"@ionic-native/qr-scanner": "^5.36.0",
"@ionic-native/splash-screen": "^5.36.0",
"@ionic-native/sqlite": "^5.36.0",
"@ionic-native/status-bar": "^5.36.0",
"@ionic-native/web-intent": "^5.36.0",
"@ionic-native/zip": "^5.36.0",
"@ionic/angular": "^5.9.4",
"@moodlehq/cordova-plugin-advanced-http": "^3.3.1-moodle.1",
"@angular/animations": "^16.2.0",
"@angular/common": "^16.2.0",
"@angular/compiler": "^16.2.0",
"@angular/core": "^16.2.0",
"@angular/forms": "^16.2.0",
"@angular/platform-browser": "^16.2.0",
"@angular/platform-browser-dynamic": "^16.2.0",
"@angular/router": "^16.2.0",
"@ionic/angular": "^7.0.0",
"@ionic/cordova-builders": "^10.0.0",
"@moodlehq/cordova-plugin-advanced-http": "3.3.1-moodle.1",
"@moodlehq/cordova-plugin-camera": "6.0.0-moodle.2",
"@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5",
"@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3",
@ -89,8 +67,8 @@
"@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1",
"@moodlehq/ionic-native-push": "5.36.0-moodle.2",
"@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@ngx-translate/core": "^15.0.0",
"@ngx-translate/http-loader": "^8.0.0",
"@types/chart.js": "^2.9.31",
"@types/cordova": "0.0.34",
"@types/dom-mediacapture-record": "1.0.7",
@ -119,7 +97,7 @@
"cordova.plugins.diagnostic": "^7.1.1",
"core-js": "^3.9.1",
"es6-promise-plugin": "^4.2.2",
"hammerjs": "^2.0.8",
"ionicons": "^7.0.0",
"jszip": "^3.7.1",
"mathjax": "2.7.9",
"moment": "^2.29.4",
@ -127,52 +105,42 @@
"mp3-mediarecorder": "4.0.5",
"nl.kingsquare.cordova.background-audio": "^1.0.1",
"ogv": "^1.8.9",
"rxjs": "~6.5.5",
"rxjs": "~7.8.0",
"ts-md5": "^1.2.7",
"tslib": "^2.3.1",
"tslib": "^2.3.0",
"video.js": "^7.21.1",
"zone.js": "~0.10.3"
"zone.js": "~0.13.0"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^10.0.1",
"@angular-devkit/architect": "^0.1202.7",
"@angular-devkit/build-angular": "~0.1000.8",
"@angular-eslint/builder": "^4.2.0",
"@angular-eslint/eslint-plugin": "^4.2.0",
"@angular-eslint/eslint-plugin-template": "^4.2.0",
"@angular-eslint/schematics": "^4.2.0",
"@angular-eslint/template-parser": "^4.2.0",
"@angular/cli": "~10.0.8",
"@angular/compiler": "~10.0.14",
"@angular/compiler-cli": "~10.0.14",
"@angular/language-service": "~10.0.14",
"@ionic/angular-toolkit": "^2.3.3",
"@ionic/cli": "^6.19.0",
"@storybook/addon-controls": "~6.1.21",
"@storybook/addon-viewport": "~6.1.21",
"@storybook/angular": "~6.1.21",
"@angular-devkit/build-angular": "^16.2.10",
"@angular-eslint/builder": "^16.2.0",
"@angular-eslint/eslint-plugin": "^16.2.0",
"@angular-eslint/eslint-plugin-template": "^16.2.0",
"@angular-eslint/schematics": "^16.2.0",
"@angular-eslint/template-parser": "^16.2.0",
"@angular/cli": "^16.2.10",
"@angular/compiler-cli": "^16.2.0",
"@angular/language-service": "^16.2.0",
"@ionic/angular-toolkit": "^10.0.0",
"@ionic/cli": "^7.1.5",
"@types/faker": "^5.1.3",
"@types/jest": "^26.0.24",
"@types/marked": "^4.3.1",
"@types/node": "^12.12.64",
"@types/node": "^18.0.0",
"@types/resize-observer-browser": "^0.1.5",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"check-es-compat": "^1.1.1",
"compare-versions": "^4.1.4",
"@types/webpack-env": "^1.18.4",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"check-es-compat": "^3.1.0",
"concurrently": "^8.2.0",
"cordova-plugin-moodleapp": "file:cordova-plugin-moodleapp",
"cross-env": "^7.0.3",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-deprecation": "^1.5.0",
"eslint": "^8.0.0",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-jsdoc": "^32.3.3",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-jsdoc": "^46.9.0",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-promise": "^6.1.1",
"faker": "^5.1.0",
"fs-extra": "^9.1.0",
"gulp": "4.0.2",
@ -182,24 +150,17 @@
"gulp-htmlmin": "^5.0.1",
"gulp-rename": "^2.0.0",
"gulp-slash": "^1.1.3",
"jest": "^26.5.2",
"jest-preset-angular": "^8.3.1",
"jest-raw-loader": "^1.0.1",
"jest": "^29.7.0",
"jsonc-parser": "^2.3.1",
"marked": "^4.3.0",
"keytar": "^7.2.0",
"minimatch": "^5.1.0",
"native-run": "^1.4.0",
"native-run": "^2.0.0",
"patch-package": "^6.5.0",
"storybook-addon-designs": "~6.1.0",
"storybook-addon-rtl-direction": "0.0.19",
"storybook-dark-mode": "^3.0.0",
"terser-webpack-plugin": "^4.2.3",
"ts-jest": "^26.4.1",
"ts-node": "~8.3.0",
"typescript": "^3.9.9"
"ts-node": "^8.3.0",
"typescript": "~5.1.3"
},
"engines": {
"node": ">=14.15.0 <15"
"node": ">=18.18.2 <19"
},
"cordova": {
"platforms": [
@ -210,11 +171,26 @@
"@moodlehq/cordova-plugin-advanced-http": {
"ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1"
},
"cordova-clipboard": {},
"cordova-plugin-badge": {},
"@moodlehq/cordova-plugin-camera": {
"ANDROIDX_CORE_VERSION": "1.6.+"
},
"@moodlehq/cordova-plugin-file-transfer": {},
"@moodlehq/cordova-plugin-inappbrowser": {},
"@moodlehq/cordova-plugin-intent": {},
"@moodlehq/cordova-plugin-ionic-webview": {},
"@moodlehq/cordova-plugin-local-notification": {
"ANDROID_SUPPORT_V4_VERSION": "26.+"
},
"@moodlehq/cordova-plugin-qrscanner": {},
"@moodlehq/cordova-plugin-statusbar": {},
"@moodlehq/cordova-plugin-zip": {},
"@moodlehq/phonegap-plugin-push": {
"ANDROIDX_CORE_VERSION": "1.6.+",
"FCM_VERSION": "23.+"
},
"cordova-clipboard": {},
"cordova-plugin-androidx-adapter": {},
"cordova-plugin-badge": {},
"cordova-plugin-chooser": {},
"cordova-plugin-customurlscheme": {
"URL_SCHEME": "moodlemobile",
@ -227,39 +203,20 @@
"cordova-plugin-geolocation": {
"GPS_REQUIRED": "false"
},
"@moodlehq/cordova-plugin-inappbrowser": {},
"cordova-plugin-ionic-keyboard": {},
"@moodlehq/cordova-plugin-ionic-webview": {},
"@moodlehq/cordova-plugin-local-notification": {
"ANDROID_SUPPORT_V4_VERSION": "26.+"
},
"cordova-plugin-media-capture": {},
"cordova-plugin-network-information": {},
"@moodlehq/cordova-plugin-qrscanner": {},
"@moodlehq/cordova-plugin-statusbar": {},
"cordova-plugin-prevent-override": {},
"cordova-plugin-screen-orientation": {},
"cordova-plugin-wkuserscript": {},
"cordova-plugin-wkwebview-cookies": {},
"@moodlehq/cordova-plugin-zip": {},
"cordova-sqlite-storage": {},
"@moodlehq/phonegap-plugin-push": {
"ANDROIDX_CORE_VERSION": "1.6.+",
"FCM_VERSION": "23.+"
},
"@moodlehq/cordova-plugin-intent": {},
"nl.kingsquare.cordova.background-audio": {},
"cordova.plugins.diagnostic": {
"ANDROID_SUPPORT_VERSION": "28.+",
"ANDROIDX_VERSION": "1.0.0",
"ANDROIDX_APPCOMPAT_VERSION": "1.3.1"
},
"@moodlehq/cordova-plugin-file-transfer": {},
"cordova-plugin-prevent-override": {},
"cordova-plugin-androidx-adapter": {},
"cordova-plugin-screen-orientation": {},
"cordova-plugin-moodleapp": {}
"nl.kingsquare.cordova.background-audio": {}
}
},
"optionalDependencies": {
"keytar": "^7.2.0"
}
}

View File

@ -1,13 +0,0 @@
diff --git a/node_modules/@ionic/core/dist/types/components.d.ts b/node_modules/@ionic/core/dist/types/components.d.ts
index fd9b7ad..4d29d1e 100644
--- a/node_modules/@ionic/core/dist/types/components.d.ts
+++ b/node_modules/@ionic/core/dist/types/components.d.ts
@@ -972,7 +972,7 @@ export namespace Components {
/**
* If `true`, a button tag will be rendered and the item will be tappable.
*/
- "button": boolean;
+ "button": boolean | '';
/**
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/

View File

@ -1,21 +0,0 @@
diff --git a/node_modules/eslint-plugin-ecmascript-compat/lib/compatibility.js b/node_modules/eslint-plugin-ecmascript-compat/lib/compatibility.js
index 57772cd..f3667fd 100644
--- a/node_modules/eslint-plugin-ecmascript-compat/lib/compatibility.js
+++ b/node_modules/eslint-plugin-ecmascript-compat/lib/compatibility.js
@@ -1,5 +1,7 @@
/* eslint-disable camelcase, no-underscore-dangle */
+const compareVersions = require('compare-versions').compare;
+
function forbiddenFeatures(features, targets) {
return features.filter(feature => !isFeatureSupportedByTargets(feature, targets));
}
@@ -30,7 +32,7 @@ function isCompatFeatureSupportedByTarget(compatFeature, target) {
return true;
}
- return !support.isNone && target.version >= versionAdded;
+ return !support.isNone && target.version.split('-').every(version => compareVersions(version, versionAdded, '>='));
}
function getSimpleSupportStatement(compatFeature, target) {

View File

@ -1,30 +0,0 @@
diff --git a/node_modules/event-target-shim/index.d.ts b/node_modules/event-target-shim/index.d.ts
index 7a5bfc7..ba5e7d8 100644
--- a/node_modules/event-target-shim/index.d.ts
+++ b/node_modules/event-target-shim/index.d.ts
@@ -359,7 +359,7 @@ export declare namespace defineCustomEventTarget {
/**
* The interface of CustomEventTarget.
*/
- type CustomEventTarget<TEventMap extends Record<string, Event>, TMode extends "standard" | "strict"> = EventTarget<TEventMap, TMode> & defineEventAttribute.EventAttributes<any, TEventMap>;
+ type CustomEventTarget<TEventMap extends Record<string, Event>, TMode extends "standard" | "strict"> = EventTarget<TEventMap, TMode> & defineEventAttribute.EventAttributes<any>;
}
/**
* Define an event attribute.
@@ -368,14 +368,12 @@ export declare namespace defineCustomEventTarget {
* @param _eventClass Unused, but to infer `Event` class type.
* @deprecated Use `getEventAttributeValue`/`setEventAttributeValue` pair on your derived class instead because of static analysis friendly.
*/
-export declare function defineEventAttribute<TEventTarget extends EventTarget, TEventType extends string, TEventConstrucor extends typeof Event>(target: TEventTarget, type: TEventType, _eventClass?: TEventConstrucor): asserts target is TEventTarget & defineEventAttribute.EventAttributes<TEventTarget, Record<TEventType, InstanceType<TEventConstrucor>>>;
+export declare function defineEventAttribute<TEventTarget extends EventTarget, TEventType extends string, TEventConstrucor extends typeof Event>(target: TEventTarget, type: TEventType, _eventClass?: TEventConstrucor): asserts target is TEventTarget & defineEventAttribute.EventAttributes<TEventTarget>;
export declare namespace defineEventAttribute {
/**
* Definition of event attributes.
*/
- type EventAttributes<TEventTarget extends EventTarget<any, any>, TEventMap extends Record<string, Event>> = {
- [P in string & keyof TEventMap as `on${P}`]: EventTarget.CallbackFunction<TEventTarget, TEventMap[P]> | null;
- };
+ type EventAttributes<TEventTarget extends EventTarget<any, any>> = Record<string, EventTarget.CallbackFunction<TEventTarget, any> | null>;
}
/**
* Set the warning handler.

View File

@ -217,7 +217,7 @@
<p class="item-heading">{{ relatedBadge.name }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="badge.relatedbadges.length == 0">
<ion-item class="ion-text-wrap" *ngIf="badge.relatedbadges.length === 0">
<ion-label>
<p class="item-heading">{{ 'addon.badges.norelated' | translate}}</p>
</ion-label>
@ -237,7 +237,7 @@
<p class="item-heading">{{ alignment.targetname }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="badge.alignment.length == 0">
<ion-item class="ion-text-wrap" *ngIf="badge.alignment.length === 0">
<ion-label>
<p class="item-heading">{{ 'addon.badges.noalignment' | translate}}</p>
</ion-label>

View File

@ -99,7 +99,7 @@
</ion-col>
</ion-row>
<core-empty-box *ngIf="filteredCourses.length == 0" image="assets/img/icons/courses.svg">
<core-empty-box *ngIf="filteredCourses.length === 0" image="assets/img/icons/courses.svg">
<p *ngIf="hasCourses" class="item-heading">
{{'addon.block_myoverview.noresult' | translate}}
</p>

View File

@ -8,7 +8,7 @@
</div>
</ion-item-divider>
<core-loading [hideUntil]="loaded">
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg"
<core-empty-box *ngIf="courses.length === 0" image="assets/img/icons/courses.svg"
[message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box>
<!-- List of courses. -->
<div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll"

View File

@ -8,7 +8,7 @@
</div>
</ion-item-divider>
<core-loading [hideUntil]="loaded">
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg"
<core-empty-box *ngIf="courses.length === 0" image="assets/img/icons/courses.svg"
[message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box>
<!-- List of courses. -->
<div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll"

View File

@ -20,10 +20,10 @@
<ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label>
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)" slot="end"></ion-toggle>
</ion-item>
<core-empty-box *ngIf="entries && entries.length == 0" icon="far-newspaper" [message]="'addon.blog.noentriesyet' | translate">
<core-empty-box *ngIf="entries && entries.length === 0" icon="far-newspaper" [message]="'addon.blog.noentriesyet' | translate">
</core-empty-box>
<ng-container *ngFor="let entry of entries">
<ion-card *ngIf="!onlyMyEntries || entry.userid == currentUserId">
<ion-card *ngIf="!onlyMyEntries || entry.userid === currentUserId">
<ion-item class="ion-text-wrap">
<core-user-avatar [user]="entry.user" slot="start" [courseId]="entry.courseid"></core-user-avatar>
<ion-label>

View File

@ -71,7 +71,7 @@
<!-- In tablet, display list of events. -->
<div class="ion-hide-md-down addon-calendar-day-events" *ngIf="day.filteredEvents">
<ng-container *ngFor="let event of day.filteredEvents | slice:0:4; let index = index">
<div *ngIf="index < 3 || day.filteredEvents.length == 4" class="addon-calendar-event"
<div *ngIf="index < 3 || day.filteredEvents.length === 4" class="addon-calendar-event"
[class.addon-calendar-event-past]="event.ispast" (ariaButtonClick)="eventClicked(event, $event)"
[tabindex]="activeView ? 0 : -1">
<span class="calendar_event_type calendar_event_{{event.formattedType}}"></span>

View File

@ -41,7 +41,7 @@
<ion-label>
<p class="item-heading" [core-mark-required]="true">{{ 'addon.calendar.eventkind' | translate }}</p>
</ion-label>
<p *ngIf="eventTypes.length == 1" slot="end">{{eventTypes[0].name | translate }}</p>
<p *ngIf="eventTypes.length === 1" slot="end">{{eventTypes[0].name | translate }}</p>
<ion-select *ngIf="eventTypes.length > 1" formControlName="eventtype" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'addon.calendar.eventkind' | translate}">
<ion-select-option *ngFor="let type of eventTypes" [value]="type.value">

View File

@ -86,7 +86,7 @@
<ion-item class="ion-text-wrap" *ngIf="coursemodules">
<ion-label>
<p class="item-heading">{{ 'addon.competency.activities' | translate }}</p>
<p *ngIf="coursemodules.length == 0">
<p *ngIf="coursemodules.length === 0">
{{ 'addon.competency.noactivities' | translate }}
</p>
<ion-item class="ion-text-wrap" *ngFor="let activity of coursemodules" [href]="activity.url"
@ -130,7 +130,7 @@
<div *ngIf="competency">
<h2 class="ion-margin-horizontal">{{ 'addon.competency.evidence' | translate }}</h2>
<p class="ion-margin-horizontal" *ngIf="competency.evidence.length == 0">
<p class="ion-margin-horizontal" *ngIf="competency.evidence.length === 0">
{{ 'addon.competency.noevidence' | translate }}
</p>
<ion-card *ngFor="let evidence of competency.evidence">

View File

@ -58,7 +58,7 @@
</ion-label>
</ion-item>
</ion-card>
<core-empty-box *ngIf="courseCompetencies && courseCompetencies.statistics.competencycount == 0" icon="fas-award"
<core-empty-box *ngIf="courseCompetencies && courseCompetencies.statistics.competencycount === 0" icon="fas-award"
message="{{ 'addon.competency.nocompetenciesincourse' | translate }}">
</core-empty-box>
@ -121,7 +121,7 @@
</div>
<div>
<p class="item-heading">{{ 'addon.competency.activities' | translate }}</p>
<p *ngIf="competency.coursemodules.length == 0">
<p *ngIf="competency.coursemodules.length === 0">
{{ 'addon.competency.noactivities' | translate }}
</p>
<ion-item class="ion-text-wrap core-course-module-handler" [attr.aria-label]="activity.name" core-link
@ -137,7 +137,7 @@
</div>
<div *ngIf="competency.plans">
<p class="item-heading">{{ 'addon.competency.userplans' | translate }}</p>
<p *ngIf="competency.plans.length == 0">
<p *ngIf="competency.plans.length === 0">
{{ 'addon.competency.nouserplanswithcompetency' | translate }}
</p>
<ion-item class="ion-text-wrap" *ngFor="let plan of competency.plans" [href]="plan.url"

View File

@ -75,7 +75,7 @@
<ion-card-title>{{ 'addon.competency.learningplancompetencies' | translate }}</ion-card-title>
</ion-card-header>
<ion-list>
<ion-item class="ion-text-wrap" *ngIf="plan.competencycount == 0">
<ion-item class="ion-text-wrap" *ngIf="plan.competencycount === 0">
<ion-label>
<p>{{ 'addon.competency.nocompetencies' | translate }}</p>
</ion-label>

View File

@ -21,9 +21,10 @@ import { CoreSite } from '@classes/sites/site';
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
import { makeSingleton } from '@singletons';
import { CoreError } from '@classes/errors/error';
import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs';
import { asyncObservable } from '@/core/utils/rxjs';
import { map } from 'rxjs/operators';
import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
import { firstValueFrom } from 'rxjs';
const ROOT_CACHE_KEY = 'mmaCourseCompletion:';

View File

@ -147,7 +147,7 @@ export class VideoJSOgvJS extends Tech {
if (el.hasOwnProperty(name)) {
el[name] = value;
}
};
}
/**
* Check if browser/device is supported by Ogv.JS.
@ -156,7 +156,7 @@ export class VideoJSOgvJS extends Tech {
*/
static isSupported(): boolean {
return OGVCompat.supported('OGVPlayer');
};
}
/**
* Check if the tech can support the given type.
@ -166,7 +166,7 @@ export class VideoJSOgvJS extends Tech {
*/
static canPlayType(type: string): string {
return (type.indexOf('/ogg') !== -1 || type.indexOf('/webm')) ? 'maybe' : '';
};
}
/**
* Check if the tech can support the given source.
@ -176,7 +176,7 @@ export class VideoJSOgvJS extends Tech {
*/
static canPlaySource(srcObj: TechSourceObject): string {
return VideoJSOgvJS.canPlayType(srcObj.type);
};
}
/**
* Check if the volume can be changed in this browser/device.
@ -194,7 +194,7 @@ export class VideoJSOgvJS extends Tech {
// eslint-disable-next-line no-prototype-builtins
return player.hasOwnProperty('volume');
};
}
/**
* Check if the volume can be muted in this browser/device.
@ -203,7 +203,7 @@ export class VideoJSOgvJS extends Tech {
*/
static canMuteVolume(): boolean {
return true;
};
}
/**
* Check if the playback rate can be changed in this browser/device.
@ -212,7 +212,7 @@ export class VideoJSOgvJS extends Tech {
*/
static canControlPlaybackRate(): boolean {
return true;
};
}
/**
* Check to see if native 'TextTracks' are supported by this browser/device.
@ -221,7 +221,7 @@ export class VideoJSOgvJS extends Tech {
*/
static supportsNativeTextTracks(): boolean {
return false;
};
}
/**
* Check if the fullscreen resize is supported by this browser/device.
@ -230,7 +230,7 @@ export class VideoJSOgvJS extends Tech {
*/
static supportsFullscreenResize(): boolean {
return true;
};
}
/**
* Check if the progress events is supported by this browser/device.
@ -239,7 +239,7 @@ export class VideoJSOgvJS extends Tech {
*/
static supportsProgressEvents(): boolean {
return true;
};
}
/**
* Check if the time update events is supported by this browser/device.
@ -248,7 +248,7 @@ export class VideoJSOgvJS extends Tech {
*/
static supportsTimeupdateEvents(): boolean {
return true;
};
}
/**
* Create the 'OgvJS' Tech's DOM element.

View File

@ -43,7 +43,7 @@
<ion-item class="ion-text-wrap addon-messages-conversation-item"
*ngIf="contact.profileimageurl || contact.profileimageurlsmall" [attr.aria-label]="contact.fullname"
(click)="gotoDiscussion(contact.id)" [detail]="true" button
[attr.aria-current]="contact.id == discussionUserId ? 'page' : 'false'">
[attr.aria-current]="contact.id === discussionUserId ? 'page' : 'false'">
<core-user-avatar [user]="contact" slot="start" [checkOnline]="contact.showonlinestatus"></core-user-avatar>
<ion-label>
<p class="item-heading">{{ contact.fullname }}</p>

View File

@ -29,7 +29,7 @@
<ion-list class="ion-no-margin" *ngIf="confirmedContacts.length">
<ion-item class="ion-text-wrap addon-messages-conversation-item" (click)="selectUser(contact.id)" button
*ngFor="let contact of confirmedContacts" [attr.aria-label]="contact.fullname" [detail]="true"
[attr.aria-current]="contact.id == selectedUserId ? 'page' : 'false'">
[attr.aria-current]="contact.id === selectedUserId ? 'page' : 'false'">
<core-user-avatar slot="start" [user]="contact" [checkOnline]="contact.showonlinestatus"
[linkProfile]="false">
</core-user-avatar>
@ -67,7 +67,7 @@
<ion-list class="ion-no-margin" *ngIf="requests.length">
<ion-item class="ion-text-wrap addon-messages-conversation-item" *ngFor="let request of requests"
[attr.aria-label]="request.fullname" (click)="selectUser(request.id)" button
[attr.aria-current]="request.id == selectedUserId ? 'page' : 'false'" [detail]="true">
[attr.aria-current]="request.id === selectedUserId ? 'page' : 'false'" [detail]="true">
<core-user-avatar slot="start" [user]="request" [linkProfile]="false"></core-user-avatar>
<ion-label>
<core-format-text [text]="request.fullname" contextLevel="system" [contextInstanceId]="0">

View File

@ -76,7 +76,7 @@
{{ message.timecreated | coreFormatDate: "strftimedayshort" }}
</h3>
<ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom > 0 && message.id == unreadMessageFrom" color="light">
<ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom > 0 && message.id === unreadMessageFrom" color="light">
<ion-label>{{ 'addon.messages.newmessages' | translate }}</ion-label>
<ion-icon name="fas-arrow-down" aria-hidden="true"></ion-icon>
</ion-chip>

View File

@ -46,7 +46,7 @@
</ion-item-divider>
<ion-item class="ion-text-wrap addon-message-discussion" *ngFor="let result of search.results" button
[attr.aria-label]="result.fullname" (click)="gotoDiscussion(result.userid, result.messageid)"
[attr.aria-current]="result.userid == discussionUserId ? 'page' : 'false'" [detail]="false">
[attr.aria-current]="result.userid === discussionUserId ? 'page' : 'false'" [detail]="false">
<core-user-avatar [user]="result" slot="start" [checkOnline]="result.showonlinestatus"></core-user-avatar>
<ion-label>
<p class="item-heading">{{ result.fullname }}</p>
@ -60,7 +60,7 @@
<ng-container *ngIf="!search.showResults">
<ion-item class="ion-text-wrap addon-message-discussion" *ngFor="let discussion of discussions" button
[attr.aria-label]="discussion.fullname" (click)="gotoDiscussion(discussion.message!.user)"
[attr.aria-current]="discussion.message!.user == discussionUserId ? 'page' : 'false'" [detail]="false">
[attr.aria-current]="discussion.message!.user === discussionUserId ? 'page' : 'false'" [detail]="false">
<core-user-avatar [user]="discussion" slot="start" checkOnline="false"></core-user-avatar>
<ion-label>
<div class="flex-row ion-justify-content-between">

View File

@ -60,7 +60,7 @@
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
<core-infinite-loading [enabled]="favourites.canLoadMore" (action)="loadMoreConversations(favourites, $event)"
[error]="favourites.loadMoreError"></core-infinite-loading>
<ion-item class="ion-text-wrap" *ngIf="favourites.conversations && favourites.conversations.length == 0">
<ion-item class="ion-text-wrap" *ngIf="favourites.conversations && favourites.conversations.length === 0">
<ion-label>
<p>{{ 'addon.messages.nofavourites' | translate }}</p>
</ion-label>
@ -94,7 +94,7 @@
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
<core-infinite-loading [enabled]="group.canLoadMore" (action)="loadMoreConversations(group, $event)"
[error]="group.loadMoreError"></core-infinite-loading>
<ion-item class="ion-text-wrap" *ngIf="group.conversations && group.conversations.length == 0">
<ion-item class="ion-text-wrap" *ngIf="group.conversations && group.conversations.length === 0">
<ion-label>
<p>{{ 'addon.messages.nogroupconversations' | translate }}</p>
</ion-label>
@ -128,7 +128,7 @@
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
<core-infinite-loading [enabled]="individual.canLoadMore" (action)="loadMoreConversations(individual, $event)"
[error]="individual.loadMoreError"></core-infinite-loading>
<ion-item class="ion-text-wrap" *ngIf="individual.conversations && individual.conversations.length == 0">
<ion-item class="ion-text-wrap" *ngIf="individual.conversations && individual.conversations.length === 0">
<ion-label>
<p>{{ 'addon.messages.noindividualconversations' | translate }}</p>
</ion-label>
@ -150,17 +150,17 @@
<ion-item class="ion-text-wrap addon-message-discussion" *ngFor="let conversation of conversations" button [detail]="false"
(click)="gotoConversation(conversation.id, conversation.userid)"
[attr.aria-current]="((conversation.id &&
conversation.id == selectedConversationId) || (conversation.userid && conversation.userid == selectedUserId)) ? 'page': 'false'"
conversation.id === selectedConversationId) || (conversation.userid && conversation.userid === selectedUserId)) ? 'page': 'false'"
id="addon-message-conversation-{{ conversation.id ? conversation.id : 'user-' + conversation.userid }}"
[attr.aria-label]="conversation.name">
<!-- Group conversation image. -->
<ion-avatar slot="start" *ngIf="conversation.type == typeGroup">
<ion-avatar slot="start" *ngIf="conversation.type === typeGroup">
<img [src]="conversation.imageurl" [alt]="conversation.name" core-external-content
onError="this.src='assets/img/group-avatar.svg'">
</ion-avatar>
<!-- Avatar for individual conversations. -->
<core-user-avatar *ngIf="conversation.type != typeGroup" core-user-avatar [user]="conversation.otherUser" [linkProfile]="false"
<core-user-avatar *ngIf="conversation.type !== typeGroup" core-user-avatar [user]="conversation.otherUser" [linkProfile]="false"
[checkOnline]="conversation.showonlinestatus" slot="start"></core-user-avatar>
<ion-label>
@ -189,7 +189,7 @@
<span *ngIf="conversation.sentfromcurrentuser" class="addon-message-last-message-user">
{{ 'addon.messages.you' | translate }}
</span>
<span *ngIf="!conversation.sentfromcurrentuser && conversation.type == typeGroup && conversation.members[0]"
<span *ngIf="!conversation.sentfromcurrentuser && conversation.type === typeGroup && conversation.members[0]"
class="addon-message-last-message-user">{{ conversation.members[0].fullname + ':' }}</span>
<core-format-text clean="true" singleLine="true" [text]="conversation.lastmessage" class="addon-message-last-message-text"
contextLevel="system" [contextInstanceId]="0"></core-format-text>

View File

@ -45,7 +45,7 @@
<!-- List of results -->
<ion-item class="addon-message-discussion ion-text-wrap" *ngFor="let result of item.results" [attr.aria-label]="result.fullname"
(click)="openConversation(result)" [attr.aria-current]="result == selectedResult ? 'page' : 'false'" [detail]="true" button>
(click)="openConversation(result)" [attr.aria-current]="result === selectedResult ? 'page' : 'false'" [detail]="true" button>
<core-user-avatar slot="start" [user]="result" [checkOnline]="true" [linkProfile]="false"></core-user-avatar>
<ion-label>
<p class="item-heading">

View File

@ -45,11 +45,11 @@
<ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading">
<ion-label>
<p class="item-heading">{{ 'addon.mod_assign.attemptnumber' | translate }}</p>
<p *ngIf="assign!.maxattempts == unlimitedAttempts">
<p *ngIf="assign!.maxattempts === unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
</p>
<p *ngIf="assign!.maxattempts != unlimitedAttempts">
<p *ngIf="assign!.maxattempts !== unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
</p>
@ -128,7 +128,7 @@
<!-- Last modified. -->
<ion-item class="ion-text-wrap"
*ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">
*ngIf="userSubmission && userSubmission!.status !== statusNew && userSubmission!.timemodified">
<ion-label>
<p class="item-heading">{{ 'addon.mod_assign.timemodified' | translate }}</p>
<p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p>
@ -175,7 +175,7 @@
</ion-button>
<!-- If no submission or is new, show add submission. -->
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline &&
(!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)">
(!userSubmission || !userSubmission!.status || userSubmission!.status === statusNew)">
<ng-container *ngIf="!assign?.timelimit || userSubmission?.timestarted">
{{ 'addon.mod_assign.addsubmission' | translate }}
</ng-container>
@ -184,7 +184,7 @@
</ng-container>
</ion-button>
<!-- If reopened, show addfromprevious and addnewattempt. -->
<ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened">
<ng-container *ngIf="!hasOffline && userSubmission?.status === statusReopened">
<ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap"
(click)="copyPrevious()">
{{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}
@ -195,8 +195,8 @@
</ng-container>
<!-- Else show editsubmission. -->
<ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline && userSubmission &&
userSubmission!.status && userSubmission!.status != statusNew &&
userSubmission!.status != statusReopened" (click)="goToEdit()">
userSubmission!.status && userSubmission!.status !== statusNew &&
userSubmission!.status !== statusReopened" (click)="goToEdit()">
{{ 'addon.mod_assign.editsubmission' | translate }}
</ion-button>
</ng-container>
@ -367,15 +367,15 @@
</ion-item>
<!-- Attempt status. -->
<ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone">
<ng-container *ngIf="isGrading && assign!.attemptreopenmethod !== attemptReopenMethodNone">
<ion-item class="ion-text-wrap">
<ion-label>
<p class="item-heading">{{ 'addon.mod_assign.attemptsettings' | translate }}</p>
<p *ngIf="assign!.maxattempts == unlimitedAttempts">
<p *ngIf="assign!.maxattempts === unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
</p>
<p *ngIf="assign!.maxattempts != unlimitedAttempts">
<p *ngIf="assign!.maxattempts !== unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
</p>

View File

@ -23,7 +23,7 @@
<ion-item class="ion-text-wrap" *ngFor="let chapter of chapters" [class.item-dimmed]="chapter.hidden" button [detail]="true"
(click)="openBook(chapter.id)">
<ion-label>
<p [class.ion-padding-start]="addPadding && chapter.level == 1 ? true : null">
<p [class.ion-padding-start]="addPadding && chapter.level === 1 ? true : null">
<span *ngIf="showNumbers" class="addon-mod-book-number">{{chapter.indexNumber}} </span>
<span *ngIf="showBullets" class="addon-mod-book-bullet">&bull; </span>
<core-format-text [text]="chapter.title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">

View File

@ -14,9 +14,10 @@
<nav>
<ion-list>
<ion-item class="ion-text-wrap" *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)"
[attr.aria-current]="selected == chapter.id ? 'page' : 'false'" button [class.item-dimmed]="chapter.hidden" [detail]="false">
[attr.aria-current]="selected === chapter.id ? 'page' : 'false'" button [class.item-dimmed]="chapter.hidden"
[detail]="false">
<ion-label>
<p [class.ion-padding-start]="addPadding && chapter.level == 1 ? true : null" class="item-heading">
<p [class.ion-padding-start]="addPadding && chapter.level === 1 ? true : null" class="item-heading">
<span *ngIf="showNumbers" class="addon-mod-book-number">{{chapter.indexNumber}} </span>
<span *ngIf="showBullets" class="addon-mod-book-bullet">&bull; </span>
<core-format-text [text]="chapter.title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId">

View File

@ -12,12 +12,12 @@
</ion-header>
<ion-content>
<core-loading [hideUntil]="usersLoaded">
<ion-item class="ion-text-wrap" *ngFor="let user of users" [class.addon-mod-chat-user]="currentUserId != user.id && isOnline">
<ion-item class="ion-text-wrap" *ngFor="let user of users" [class.addon-mod-chat-user]="currentUserId !== user.id && isOnline">
<core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar>
<ion-label>
<p class="item-heading">{{ user.fullname }}</p>
<ng-container *ngIf="currentUserId != user.id && isOnline">
<ng-container *ngIf="currentUserId !== user.id && isOnline">
<ion-button fill="clear" (click)="talkTo(user)">
<ion-icon name="fas-comments" slot="start" aria-hidden="true"></ion-icon>
{{ 'addon.mod_chat.talk' | translate }}

View File

@ -51,7 +51,7 @@
</ion-badge>
<ion-badge class="ion-text-wrap" color="primary"
*ngIf="message.userid != currentUserId && message.beep == currentUserId">
*ngIf="message.userid !== currentUserId && message.beep === currentUserId">
<span>
<ion-icon name="fas-bell" aria-hidden="true"></ion-icon>
{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }}

View File

@ -46,7 +46,7 @@
</ion-badge>
<ion-badge class="ion-text-wrap" color="primary"
*ngIf="message.userid != currentUserId && message.beep == currentUserId">
*ngIf="message.userid !== currentUserId && message.beep === currentUserId">
<span>
<ion-icon name="fas-bell" aria-hidden="true"></ion-icon>
{{ message.timestamp * 1000 | coreFormatDate:"strftimetime" }}

View File

@ -42,4 +42,4 @@ export interface AddonModDataActionsMenuItem {
text: string;
icon: string;
action: () => void;
};
}

View File

@ -27,7 +27,7 @@
</ng-container>
</ion-list>
<core-empty-box *ngIf="!contents || (contents.files.length + contents.folders.length == 0)" icon="far-folder-open"
<core-empty-box *ngIf="!contents || (contents.files.length + contents.folders.length === 0)" icon="far-folder-open"
[message]=" 'addon.mod_folder.emptyfilelist' | translate"></core-empty-box>
</core-loading>

View File

@ -61,7 +61,7 @@
</ion-label>
</ion-item>
</ion-card-header>
<ion-card-content [class]="post.parentid == 0 ? 'ion-padding-top' : ''">
<ion-card-content [class]="post.parentid === 0 ? 'ion-padding-top' : ''">
<div class="ion-padding-bottom" *ngIf="post.isprivatereply">
<ion-note color="danger">{{ 'addon.mod_forum.postisprivatereply' | translate }}</ion-note>
</div>

View File

@ -14,7 +14,7 @@
<ion-list id="addon-mod-forum-sort-selector" role="listbox" aria-labelledby="addon-mod-forum-sort-order-label">
<ng-container *ngFor="let sortOrder of sortOrders">
<ion-item class="ion-text-wrap" [detail]="false" role="combobox"
[attr.aria-current]="selected == sortOrder.value ? 'page' : 'false'" [attr.aria-label]="sortOrder.label | translate"
[attr.aria-current]="selected === sortOrder.value ? 'page' : 'false'" [attr.aria-label]="sortOrder.label | translate"
(click)="selectSortOrder(sortOrder)" button aria-haspopup="dialog">
<ion-label>
<p class="item-heading">{{ sortOrder.label | translate }}</p>

View File

@ -14,7 +14,7 @@
<nav>
<ion-list>
<ion-item *ngFor="let item of items" (click)="loadItem(item.href)"
[attr.aria-current]="selected == item.href ? 'page' : 'false'" button [detail]="false">
[attr.aria-current]="selected === item.href ? 'page' : 'false'" button [detail]="false">
<ion-label [class.core-bold]="!item.href">
<p class="item-heading">
<span class="ion-padding-start" *ngFor="let i of getNumberForPadding(item.level)"></span>

View File

@ -88,39 +88,39 @@
<ion-col class="ion-text-center">
<p class="item-heading">{{ 'addon.mod_lesson.highscore' | translate }}</p>
<p *ngIf="overview.highscore != null">
<p *ngIf="overview.highscore !== null">
{{ 'core.percentagenumber' | translate:{$a: overview.highscore} }}
</p>
<p *ngIf="overview.highscore == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
<p *ngIf="overview.highscore === null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
</ion-col>
<ion-col class="ion-text-center">
<p class="item-heading">{{ 'addon.mod_lesson.lowscore' | translate }}</p>
<p *ngIf="overview.lowscore != null">
<p *ngIf="overview.lowscore !== null">
{{ 'core.percentagenumber' | translate:{$a: overview.lowscore} }}
</p>
<p *ngIf="overview.lowscore == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
<p *ngIf="overview.lowscore === null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
</ion-col>
</ion-row>
<ion-row>
<ion-col class="ion-text-center">
<p class="item-heading">{{ 'addon.mod_lesson.averagetime' | translate }}</p>
<p *ngIf="overview.avetime != null && overview.numofattempts">{{ avetimeReadable }}</p>
<p *ngIf="overview.avetime == null || !overview.numofattempts">
<p *ngIf="overview.avetime !== null && overview.numofattempts">{{ avetimeReadable }}</p>
<p *ngIf="overview.avetime === null || !overview.numofattempts">
{{ 'addon.mod_lesson.notcompleted' | translate }}
</p>
</ion-col>
<ion-col class="ion-text-center">
<p class="item-heading">{{ 'addon.mod_lesson.hightime' | translate }}</p>
<p *ngIf="overview.hightime != null">{{ hightimeReadable }}</p>
<p *ngIf="overview.hightime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
<p *ngIf="overview.hightime !== null">{{ hightimeReadable }}</p>
<p *ngIf="overview.hightime === null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
</ion-col>
<ion-col class="ion-text-center">
<p class="item-heading">{{ 'addon.mod_lesson.lowtime' | translate }}</p>
<p *ngIf="overview.lowtime != null">{{ lowtimeReadable }}</p>
<p *ngIf="overview.lowtime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
<p *ngIf="overview.lowtime !== null">{{ lowtimeReadable }}</p>
<p *ngIf="overview.lowtime === null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
</ion-col>
</ion-row>
</ion-grid>
@ -138,8 +138,8 @@
<ion-col [ngClass]="{'ion-text-center': overview.lessonscored}">
<p class="item-heading">{{ 'addon.mod_lesson.averagetime' | translate }}</p>
<p *ngIf="overview.avetime != null && overview.numofattempts">{{ avetimeReadable }}</p>
<p *ngIf="overview.avetime == null || !overview.numofattempts">
<p *ngIf="overview.avetime !== null && overview.numofattempts">{{ avetimeReadable }}</p>
<p *ngIf="overview.avetime === null || !overview.numofattempts">
{{ 'addon.mod_lesson.notcompleted' | translate }}
</p>
</ion-col>
@ -147,31 +147,31 @@
<ion-row>
<ion-col class="ion-text-center" *ngIf="overview.lessonscored">
<p class="item-heading">{{ 'addon.mod_lesson.highscore' | translate }}</p>
<p *ngIf="overview.highscore != null">
<p *ngIf="overview.highscore !== null">
{{ 'core.percentagenumber' | translate:{$a: overview.highscore} }}
</p>
<p *ngIf="overview.highscore == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
<p *ngIf="overview.highscore === null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
</ion-col>
<ion-col [ngClass]="{'ion-text-center': overview.lessonscored}">
<p class="item-heading">{{ 'addon.mod_lesson.hightime' | translate }}</p>
<p *ngIf="overview.hightime != null">{{ hightimeReadable }}</p>
<p *ngIf="overview.hightime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
<p *ngIf="overview.hightime !== null">{{ hightimeReadable }}</p>
<p *ngIf="overview.hightime === null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
</ion-col>
</ion-row>
<ion-row>
<ion-col class="ion-text-center" *ngIf="overview.lessonscored">
<p class="item-heading">{{ 'addon.mod_lesson.lowscore' | translate }}</p>
<p *ngIf="overview.lowscore != null">
<p *ngIf="overview.lowscore !== null">
{{ 'core.percentagenumber' | translate:{$a: overview.lowscore} }}
</p>
<p *ngIf="overview.lowscore == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
<p *ngIf="overview.lowscore === null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
</ion-col>
<ion-col [ngClass]="{'ion-text-center': overview.lessonscored}">
<p class="item-heading">{{ 'addon.mod_lesson.lowtime' | translate }}</p>
<p *ngIf="overview.lowtime != null">{{ lowtimeReadable }}</p>
<p *ngIf="overview.lowtime == null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
<p *ngIf="overview.lowtime !== null">{{ lowtimeReadable }}</p>
<p *ngIf="overview.lowtime === null">{{ 'addon.mod_lesson.notcompleted' | translate }}</p>
</ion-col>
</ion-row>
</ion-grid>

View File

@ -40,7 +40,7 @@
<div *ngIf="!pageInstance.loadingMenu">
<ng-container *ngFor="let page of pageInstance.lessonPages">
<ion-item class="ion-text-wrap" *ngIf="page.display && page.displayinmenublock" (click)="loadPage(page.id)"
[attr.aria-current]="!pageInstance.eolData && pageInstance.currentPage == page.id ? 'page' : 'false'" button
[attr.aria-current]="!pageInstance.eolData && pageInstance.currentPage === page.id ? 'page' : 'false'" button
[detail]="true">
<ion-label>
<core-format-text [text]="page.title" contextLevel="module" [courseId]="pageInstance.courseId"

View File

@ -14,7 +14,7 @@
<nav>
<ion-list>
<ion-item button class="ion-text-wrap {{question.stateClass}}" *ngFor="let question of navigation"
[attr.aria-current]="!summaryShown && currentPage == question.page ? 'page' : 'false'"
[attr.aria-current]="!summaryShown && currentPage === question.page ? 'page' : 'false'"
(click)="loadPage(question.page, question.slot)" [detail]="false">
<ion-label class="ion-text-wrap">
@ -38,8 +38,8 @@
[attr.aria-label]="question.status" slot="end">
</ion-icon>
<ion-icon *ngIf="question.stateClass === 'core-question-incorrect' ||
question.stateClass === 'core-question-notanswered'" name="fas-xmark" color="danger" [attr.aria-label]="question.status"
slot="end">
question.stateClass === 'core-question-notanswered'" name="fas-xmark" color="danger"
[attr.aria-label]="question.status" slot="end">
</ion-icon>
</ion-item>
</ion-list>

View File

@ -77,7 +77,7 @@
{{ 'core.next' | translate }}
<ion-icon name="fas-chevron-right" slot="end" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button expand="block" (click)="changePage(nextPage)" class="ion-text-wrap" *ngIf="nextPage == -1">
<ion-button expand="block" (click)="changePage(nextPage)" class="ion-text-wrap" *ngIf="nextPage === -1">
{{ 'core.submit' | translate }}
</ion-button>
</ion-col>

View File

@ -34,7 +34,7 @@
<p class="item-heading">{{ 'addon.mod_scorm.noattemptsallowed' | translate }}</p>
</ion-label>
<p slot="end">
<span *ngIf="scorm.maxattempt == 0">{{ 'core.unlimited' | translate }}</span>
<span *ngIf="scorm.maxattempt === 0">{{ 'core.unlimited' | translate }}</span>
<span *ngIf="scorm.maxattempt! > 0">{{ scorm.maxattempt }}</span>
</p>
</ion-item>
@ -59,8 +59,8 @@
<p class="item-heading">{{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.num}}</p>
</ion-label>
<p slot="end">
<span *ngIf="attempt.grade != -1">{{ attempt.gradeFormatted }}</span>
<span *ngIf="attempt.grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
<span *ngIf="attempt.grade !== -1">{{ attempt.gradeFormatted }}</span>
<span *ngIf="attempt.grade === -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
</p>
</ion-item>
</ng-container>
@ -76,8 +76,8 @@
</p>
</ion-label>
<p slot="end">
<span *ngIf="attempt.grade != -1">{{ attempt.gradeFormatted }}</span>
<span *ngIf="attempt.grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
<span *ngIf="attempt.grade !== -1">{{ attempt.gradeFormatted }}</span>
<span *ngIf="attempt.grade === -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
</p>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="scorm.displayattemptstatus && gradeMethodReadable">
@ -91,8 +91,8 @@
<p class="item-heading">{{ 'addon.mod_scorm.gradereported' | translate }}</p>
</ion-label>
<p slot="end">
<span *ngIf="grade != -1">{{ gradeFormatted }}</span>
<span *ngIf="grade == -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
<span *ngIf="grade !== -1">{{ gradeFormatted }}</span>
<span *ngIf="grade === -1">{{ 'addon.mod_scorm.cannotcalculategrade' | translate }}</span>
</p>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="syncTime">
@ -174,7 +174,7 @@
</ng-container>
<!-- Warning that user doesn't have any more attempts. -->
<ion-card *ngIf="!errorMessage && attemptsLeft == 0" class="core-danger-card">
<ion-card *ngIf="!errorMessage && attemptsLeft === 0" class="core-danger-card">
<ion-item class="ion-text-wrap">
<ion-label>
<p>{{ 'addon.mod_scorm.exceededmaxattempts' | translate }}</p>

View File

@ -33,7 +33,7 @@
<ng-container *ngFor="let sco of toc">
<ion-item *ngIf="sco.isvisible" class="ion-text-wrap" [detail]="sco.prereq && sco.launch"
[ngClass]="'core-padding-' + sco.level + ' addon-mod_scorm-type-' + sco.scormtype"
[attr.aria-current]="selected == sco.id ? 'page' : 'false'" (click)="loadSco(sco)"
[attr.aria-current]="selected === sco.id ? 'page' : 'false'" (click)="loadSco(sco)"
[disabled]="!sco.prereq || !sco.launch ? true : null" [button]="sco.prereq && sco.launch">
<ion-icon *ngIf="sco.icon" [name]="sco.icon.icon" [attr.aria-label]="sco.icon.description" slot="start">
</ion-icon>

View File

@ -32,7 +32,7 @@
class="ion-no-padding ion-text-wrap">
<!-- Parent question (Category header) -->
<ng-container *ngIf="question.multiArray?.length">
<h3 class="ion-padding-horizontal" [class.ion-padding-top]="questionIndex == 1">{{ question.text }}</h3>
<h3 class="ion-padding-horizontal" [class.ion-padding-top]="questionIndex === 1">{{ question.text }}</h3>
<ion-row class="ion-align-items-center ion-hide-md-down ion-padding">
<ion-col size="7" class="ion-padding">{{ 'addon.mod_survey.responses' | translate }}</ion-col>
<ion-col size="1" class="ion-text-center option-name"
@ -81,7 +81,7 @@
</ng-container>
<!-- Single question (don't belong to a category) -->
<ng-container *ngIf="(!question.multiArray || question.multiArray.length == 0) && question.parent === 0">
<ng-container *ngIf="(!question.multiArray || question.multiArray.length === 0) && question.parent === 0">
<ion-row class="ion-align-items-center ion-padding" *ngIf="question.type > 0" [class.even]="isEven">
<ion-col size="7">
<span id="addon-mod_survey-{{question.name}}" [core-mark-required]="question.required">

View File

@ -25,7 +25,7 @@
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngFor="let page of letter.pages" (click)="goToPage(page)"
[attr.aria-current]="selectedTitle == page.title ? 'page' : 'false'" button [detail]="false">
[attr.aria-current]="selectedTitle === page.title ? 'page' : 'false'" button [detail]="false">
<ion-icon name="fas-house" slot="start" *ngIf="page.firstpage" aria-hidden="true"></ion-icon>
<ion-label>
<core-format-text [text]="page.title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId">

View File

@ -13,7 +13,7 @@
<p *ngIf="access.canviewallsubmissions && showGrade(assessment.gradinggradeover)" class="core-overriden-grade">
{{ 'addon.mod_workshop.gradinggradeof' | translate:{$a: workshop.gradinggrade } }}: {{assessment.gradinggradeover}}
</p>
<p *ngIf="assessment.weight && assessment.weight != 1">
<p *ngIf="assessment.weight && assessment.weight !== 1">
{{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }}
</p>
<ion-badge *ngIf="!assessment.grade" color="danger">{{ 'addon.mod_workshop.notassessed' | translate }}</ion-badge>

View File

@ -31,7 +31,7 @@
<ion-icon slot="start" name="fas-circle-info" color="info" *ngIf="task.completed === 'info'"
[attr.aria-label]="'addon.mod_workshop.taskinfo' | translate">
</ion-icon>
<ion-icon slot="start" name="fas-circle-check" color="success" *ngIf="task.completed == '1'"
<ion-icon slot="start" name="fas-circle-check" color="success" *ngIf="task.completed === '1'"
[attr.aria-label]="'addon.mod_workshop.taskdone' | translate">
</ion-icon>
<ion-label>
@ -45,7 +45,7 @@
</ion-card>
<!-- Description (setup phase only) -->
<ion-card *ngIf="description && workshop && workshop.phase == PHASE_SETUP">
<ion-card *ngIf="description && workshop && workshop.phase === PHASE_SETUP">
<ion-item class="ion-text-wrap">
<ion-label>
<h3 class="item-heading">{{ 'core.description' | translate }}</h3>
@ -92,7 +92,7 @@
</ng-container>
<!-- SUBMISSION PHASE -->
<ion-card *ngIf="workshop.phase == PHASE_SUBMISSION && workshop.instructauthors">
<ion-card *ngIf="workshop.phase === PHASE_SUBMISSION && workshop.instructauthors">
<ion-item class="ion-text-wrap">
<ion-label>
<h3 class="item-heading">{{ 'addon.mod_workshop.areainstructauthors' | translate }}</h3>
@ -106,10 +106,10 @@
<ion-card *ngIf="canSubmit">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h3 class="item-heading" *ngIf="workshop.phase != PHASE_CLOSED || !submission">
<h3 class="item-heading" *ngIf="workshop.phase !== PHASE_CLOSED || !submission">
{{ 'addon.mod_workshop.yoursubmission' | translate }}
</h3>
<h3 class="item-heading" *ngIf="workshop.phase == PHASE_CLOSED && submission">
<h3 class="item-heading" *ngIf="workshop.phase === PHASE_CLOSED && submission">
{{ 'addon.mod_workshop.yoursubmissionwithassessments' | translate }}
</h3>
</ion-label>
@ -142,7 +142,7 @@
<!-- ASSESSMENT PHASE -->
<ng-container *ngIf="workshop.phase >= PHASE_ASSESSMENT">
<ion-card *ngIf="workshop.phase == PHASE_ASSESSMENT && workshop.instructreviewers">
<ion-card *ngIf="workshop.phase === PHASE_ASSESSMENT && workshop.instructreviewers">
<ion-item class="ion-text-wrap">
<ion-label>
<h3 class="item-heading">{{ 'addon.mod_workshop.areainstructreviewers' | translate }}</h3>
@ -178,7 +178,7 @@
((grades && grades.length) || (groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)))">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h3 class="item-heading" *ngIf="workshop.phase == PHASE_SUBMISSION">{{ 'addon.mod_workshop.submissionsreport' |
<h3 class="item-heading" *ngIf="workshop.phase === PHASE_SUBMISSION">{{ 'addon.mod_workshop.submissionsreport' |
translate }}</h3>
<h3 class="item-heading" *ngIf="workshop.phase > PHASE_SUBMISSION">{{ 'addon.mod_workshop.gradesreport' | translate }}
</h3>

View File

@ -13,10 +13,10 @@
<ion-content>
<ion-list>
<ng-container *ngFor="let phase of phases">
<ion-item-divider [attr.aria-current]="workshopPhase == phase.code ? 'page' : 'false'">
<ion-item-divider [attr.aria-current]="workshopPhase === phase.code ? 'page' : 'false'">
<ion-label>
<h2>{{ phase.title }}</h2>
<p class="ion-text-wrap" *ngIf="workshopPhase == phase.code">
<p class="ion-text-wrap" *ngIf="workshopPhase === phase.code">
{{ 'addon.mod_workshop.userplancurrentphase' | translate }}
</p>
</ion-label>
@ -31,13 +31,13 @@
<ion-item class="ion-text-wrap" *ngFor="let task of phase.tasks"
[class.item-dimmed]="phase.code !== workshopPhase || (task.code === 'submit' && !showSubmit)" (click)="runTask(task)"
[detail]="false" button>
<ion-icon slot="start" name="far-circle" *ngIf="task.completed == null"
<ion-icon slot="start" name="far-circle" *ngIf="task.completed === null"
[attr.aria-label]="'addon.mod_workshop.tasktodo' | translate"></ion-icon>
<ion-icon slot="start" name="fas-circle-xmark" color="danger" *ngIf="task.completed === ''"
[attr.aria-label]="'addon.mod_workshop.taskfail' | translate"></ion-icon>
<ion-icon slot="start" name="fas-circle-info" color="info" *ngIf="task.completed === 'info'"
[attr.aria-label]="'addon.mod_workshop.taskinfo' | translate"></ion-icon>
<ion-icon slot="start" name="fas-circle-check" color="success" *ngIf="task.completed == '1'"
<ion-icon slot="start" name="fas-circle-check" color="success" *ngIf="task.completed === '1'"
[attr.aria-label]="'addon.mod_workshop.taskdone' | translate"></ion-icon>
<ion-label>
<p class="item-heading ion-text-wrap">{{task.title}}</p>

View File

@ -41,7 +41,7 @@
class="core-overriden-grade">
{{ 'addon.mod_workshop.gradinggradeover' | translate }}: {{assessment.gradinggradeover}}
</p>
<p *ngIf="assessment && assessment.weight && assessment.weight != 1">
<p *ngIf="assessment && assessment.weight && assessment.weight !== 1">
{{ 'addon.mod_workshop.weightinfo' | translate:{$a: assessment.weight } }}
</p>
<ion-badge *ngIf="!assessment || !showGrade(assessment.grade)" color="danger">

View File

@ -59,7 +59,7 @@
</ion-item>
</ion-card>
<core-empty-box *ngIf="notes && notes.length == 0" icon="fas-receipt" [message]="'addon.notes.nonotes' | translate">
<core-empty-box *ngIf="notes && notes.length === 0" icon="fas-receipt" [message]="'addon.notes.nonotes' | translate">
</core-empty-box>
<ng-container *ngIf="notes && notes.length > 0">

View File

@ -63,9 +63,9 @@
[adaptImg]="false">
</core-format-text>
</p>
<ion-badge [color]="section.downloadStatus == statusDownloaded ? 'success' : 'light'"
<ion-badge [color]="section.downloadStatus === statusDownloaded ? 'success' : 'light'"
*ngIf="!section.calculatingSize && section.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus == statusDownloaded"
<ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus === statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate">
</ion-icon>{{ section.totalSize | coreBytesToSize }}
</ion-badge>
@ -74,14 +74,14 @@
</ion-badge>
<!-- Download progress. -->
<p *ngIf="downloadEnabled && section.isDownloading">
<core-progress-bar [progress]="section.total == 0 ? -1 : section.count / section.total">
<core-progress-bar [progress]="section.total === 0 ? -1 : section.count / section.total">
</core-progress-bar>
</p>
</ion-label>
<div class="storage-buttons" slot="end"
*ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
<div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
<core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus != statusDownloaded"
<core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus !== statusDownloaded"
[status]="section.downloadStatus" [enabled]="true" (action)="prefecthSection(section)"
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="true">
</core-download-refresh>
@ -116,9 +116,9 @@
[contextInstanceId]="module.id" [adaptImg]="false">
</core-format-text>
</p>
<ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
<ion-badge [color]="module.downloadStatus === statusDownloaded ? 'success' : 'light'"
*ngIf="!module.calculatingSize && module.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus === statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate">
</ion-icon>{{ module.totalSize | coreBytesToSize }}
</ion-badge>
@ -129,7 +129,7 @@
<div class="storage-buttons" slot="end">
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
module.downloadStatus !== statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
(action)="prefetchModule(module)">
</core-download-refresh>

View File

@ -6,10 +6,10 @@
[courseId]="courseId" [wsNotFiltered]="true">
</core-format-text>
</p>
<p *ngIf="value != '0'">
<p *ngIf="value !== '0'">
{{ 'core.yes' | translate }}
</p>
<p *ngIf="value == '0'">
<p *ngIf="value === '0'">
{{ 'core.no' | translate }}
</p>
</ion-label>

View File

@ -194,10 +194,7 @@ export const APP_ROUTES = new InjectionToken('APP_ROUTES');
@NgModule({
imports: [
RouterModule.forRoot([], {
preloadingStrategy: PreloadAllModules,
relativeLinkResolution: 'corrected',
}),
RouterModule.forRoot([], { preloadingStrategy: PreloadAllModules }),
],
providers: [
{ provide: ROUTES, multi: true, useFactory: buildAppRoutes, deps: [Injector] },

View File

@ -19,7 +19,7 @@ import { ElementController } from './ElementController';
*
* @todo Remove frame TAG support.
*/
export type FrameElement = HTMLIFrameElement | HTMLFrameElement | HTMLObjectElement | HTMLEmbedElement;
export type FrameElement = HTMLIFrameElement | HTMLObjectElement | HTMLEmbedElement;
/**
* Wrapper class to control the interactivity of a frame element.

View File

@ -33,9 +33,8 @@ import { CoreLang, CoreLangFormat } from '@services/lang';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSilentError } from '../errors/silenterror';
import { CorePromisedValue } from '@classes/promised-value';
import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject } from 'rxjs';
import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject, firstValueFrom } from 'rxjs';
import { finalize, map, mergeMap } from 'rxjs/operators';
import { firstValueFrom } from '../../utils/rxjs';
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config';
import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse, CoreUnauthenticatedSite } from './unauthenticated-site';
@ -1279,7 +1278,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
const ignoreCache = CoreSitesReadingStrategy.ONLY_NETWORK || CoreSitesReadingStrategy.PREFER_NETWORK;
if (!ignoreCache && this.publicConfig) {
return this.publicConfig;
};
}
const method = 'tool_mobile_get_public_config';
const cacheId = this.getCacheId(method, {});

View File

@ -48,10 +48,10 @@ import {
WS_CACHE_TABLE,
} from '@services/database/sites';
import { map } from 'rxjs/operators';
import { firstValueFrom } from '../../utils/rxjs';
import { CoreFilepool } from '@services/filepool';
import { CoreSiteInfo } from './unauthenticated-site';
import { CoreAuthenticatedSite, CoreAuthenticatedSiteOptionalData, CoreSiteWSPreSets, WSObservable } from './authenticated-site';
import { firstValueFrom } from 'rxjs';
/**
* Class that represents a site (combination of site + user).

View File

@ -174,7 +174,7 @@ export class CoreUnauthenticatedSite {
options.readingStrategy === CoreSitesReadingStrategy.PREFER_NETWORK;
if (!ignoreCache && this.publicConfig) {
return this.publicConfig;
};
}
if (options.readingStrategy === CoreSitesReadingStrategy.ONLY_CACHE) {
throw new CoreError('Cache not available to read public config');

View File

@ -1,18 +1,18 @@
<ng-container *ngIf="enabled && !loading">
<!-- Download button. -->
<ion-button *ngIf="status == statusNotDownloaded" fill="clear" (click)="download($event, false)" @coreShowHideAnimation
<ion-button *ngIf="status === statusNotDownloaded" fill="clear" (click)="download($event, false)" @coreShowHideAnimation
[attr.aria-label]="(statusTranslatable || 'core.download') | translate">
<ion-icon slot="icon-only" name="fas-cloud-arrow-down" aria-hidden="true"></ion-icon>
</ion-button>
<!-- Refresh button. -->
<ion-button *ngIf="status == statusOutdated || (status == statusDownloaded && !canTrustDownload)" fill="clear"
<ion-button *ngIf="status === statusOutdated || (status === statusDownloaded && !canTrustDownload)" fill="clear"
(click)="download($event, true)" @coreShowHideAnimation [attr.aria-label]="(statusTranslatable || 'core.refresh') | translate">
<ion-icon slot="icon-only" name="fam-cloud-refresh" aria-hidden="true"></ion-icon>
</ion-button>
<!-- Downloaded status icon. -->
<ion-icon *ngIf="status == statusDownloaded && canTrustDownload" class="core-icon-downloaded ion-padding-horizontal" color="success"
<ion-icon *ngIf="status === statusDownloaded && canTrustDownload" class="core-icon-downloaded ion-padding-horizontal" color="success"
name="fam-cloud-done" [attr.aria-label]="(statusTranslatable || 'core.downloaded') | translate" role="status"></ion-icon>
<ion-spinner *ngIf="status === statusDownloading" @coreShowHideAnimation

View File

@ -8,12 +8,12 @@
</ion-button>
<ion-slides [options]="slidesOpts" [dir]="direction" role="tablist" [attr.aria-label]="description">
<ng-container *ngFor="let tab of tabs">
<ion-slide *ngIf="tab.id" role="presentation" [id]="tab.id + '-tab'" tabindex="-1" [class.selected]="selected == tab.id"
class="{{tab.class}}">
<ion-slide *ngIf="tab.id" role="presentation" [id]="tab.id + '-tab'" tabindex="-1"
[class.selected]="selected === tab.id" class="{{tab.class}}">
<ion-tab-button (ionTabButtonClick)="selectTab(tab.id, $event)" (keydown)="tabAction.keyDown(tab.id, $event)"
(keyup)="tabAction.keyUp(tab.id, $event)" [tab]="tab.page" [layout]="layout" role="tab"
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected == tab.id"
[tabindex]="selected == tab.id ? 0 : -1">
[attr.aria-controls]="tab.id" [attr.aria-selected]="selected === tab.id"
[tabindex]="selected === tab.id ? 0 : -1">
<ion-icon *ngIf="tab.icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ tab.title | translate}}</h2>

View File

@ -267,7 +267,7 @@ export enum CoreAutoLogoutType {
* then, the user must login again.
*/
CUSTOM = 2,
};
}
export type CoreAutoLogoutConfig = CoreAutoLogoutSessionConfig | CoreAutoLogoutOtherConfig;

View File

@ -18,7 +18,7 @@
</ng-container>
</ion-list>
<core-empty-box *ngIf="blocks.length == 0" icon="fas-table-cells-large" [message]="'core.block.noblocks' | translate">
<core-empty-box *ngIf="blocks.length === 0" icon="fas-table-cells-large" [message]="'core.block.noblocks' | translate">
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -8,7 +8,7 @@
<core-loading [hideUntil]="loaded">
<!-- Single section. -->
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
<div *ngIf="selectedSection && selectedSection.id !== allSectionsId">
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-table-cells-large"
@ -18,7 +18,7 @@
</div>
<!-- Multiple sections. -->
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
<div *ngIf="selectedSection && selectedSection.id === allSectionsId">
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
<ng-container *ngFor="let section of sections; index as i">
<ng-container *ngIf="i <= lastShownSectionIndex">
@ -64,7 +64,7 @@
<!-- Template to render a section. -->
<ng-template #sectionTemplate let-section="section">
<section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId"
<section *ngIf="!section.hiddenbynumsections && section.id !== allSectionsId && section.id !== stealthModulesSectionId"
class="core-course-module-list-wrapper" [id]="section.id"
[attr.aria-labelledby]="section.name ? 'core-section-name-' + section.id : null">
<ion-item-divider class="course-section ion-text-wrap" [class.item-dimmed]="section.visible === 0 || section.uservisible === false">

View File

@ -14,7 +14,7 @@
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="loaded" id="core-course-section-selector" role="listbox" aria-labelledby="core-course-section-selector-label">
<ng-container *ngFor="let section of sectionsToRender">
<ion-item *ngIf="allSectionId == section.id" class="divider core-course-index-all"
<ion-item *ngIf="allSectionId === section.id" class="divider core-course-index-all"
(click)="selectSectionOrModule($event, section.id)" button [class.item-current]="selectedId === section.id"
[detail]="false">
<ion-label>
@ -24,7 +24,7 @@
</h2>
</ion-label>
</ion-item>
<ng-container *ngIf="allSectionId != section.id">
<ng-container *ngIf="allSectionId !== section.id">
<ion-item class="divider section" (click)="selectSectionOrModule($event, section.id)" button
[class.item-current]="selectedId === section.id" [class.item-dimmed]="!section.visible"
[class.item-hightlighted]="section.highlighted" [detail]="false">

View File

@ -49,6 +49,6 @@ export class CoreCourseModuleDescriptionComponent {
@HostBinding('class.deprecated') get isDeprecated(): boolean {
return true;
};
}
}

View File

@ -26,7 +26,7 @@
<div class="list-item-limited-width">
<ion-item class="ion-text-wrap course-name">
<ion-label>
<p *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
<p *ngIf="course.displayname && course.shortname && course.fullname !== course.displayname"
class="core-course-shortname">
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>

View File

@ -1,4 +1,4 @@
<ion-header [collapsible]="(tabsComponent?.selectedIndex == 0 || tabsComponent?.selectedIndex === undefined) && !fullScreenEnabled">
<ion-header [collapsible]="(tabsComponent?.selectedIndex === 0 || tabsComponent?.selectedIndex === undefined) && !fullScreenEnabled">
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>

View File

@ -53,7 +53,8 @@ import { CoreDatabaseTable } from '@classes/database/database-table';
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
import { SQLiteDB } from '@classes/sqlitedb';
import { CorePlatform } from '@services/platform';
import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs';
import { asyncObservable } from '@/core/utils/rxjs';
import { firstValueFrom } from 'rxjs';
import { map } from 'rxjs/operators';
import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';

View File

@ -40,7 +40,7 @@
<ion-label>
<div class="core-course-maininfo">
<p *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
<p *ngIf="course.displayname && course.shortname && course.fullname !== course.displayname"
class="core-course-shortname core-course-additional-info">
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
@ -74,7 +74,7 @@
</ion-label>
</ion-chip>
<ion-chip color="info" *ngIf="course.visible == 0"
<ion-chip color="info" *ngIf="course.visible === 0"
class="core-course-additional-info ion-text-wrap core-course-hidden-message">
<ion-label>
{{ 'core.course.hiddenfromstudents' | translate }}

View File

@ -37,6 +37,6 @@ export class CoreCoursesCourseProgressComponent {
@HostBinding('class.deprecated') get isDeprecated(): boolean {
return true;
};
}
}

View File

@ -26,8 +26,8 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreWSExternalFile } from '@services/ws';
import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
import moment from 'moment-timezone';
import { of } from 'rxjs';
import { firstValueFrom, zipIncludingComplete } from '@/core/utils/rxjs';
import { of, firstValueFrom } from 'rxjs';
import { zipIncludingComplete } from '@/core/utils/rxjs';
import { catchError, map } from 'rxjs/operators';
import { chainRequests, WSObservable } from '@classes/sites/authenticated-site';

View File

@ -19,8 +19,8 @@ import { makeSingleton } from '@singletons';
import { CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { CoreEvents } from '@singletons/events';
import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper';
import { asyncObservable, firstValueFrom, ignoreErrors, zipIncludingComplete } from '@/core/utils/rxjs';
import { of } from 'rxjs';
import { asyncObservable, ignoreErrors, zipIncludingComplete } from '@/core/utils/rxjs';
import { of, firstValueFrom } from 'rxjs';
import { map } from 'rxjs/operators';
import { AddonEnrolGuest, AddonEnrolGuestInfo } from '@addons/enrol/guest/services/guest';
import { AddonEnrolSelf } from '@addons/enrol/self/services/self';

View File

@ -20,7 +20,8 @@ import { CoreStatusWithWarningsWSResponse } from '@services/ws';
import { makeSingleton } from '@singletons';
import { CoreError } from '@classes/errors/error';
import { map } from 'rxjs/operators';
import { asyncObservable, firstValueFrom } from '@/core/utils/rxjs';
import { firstValueFrom } from 'rxjs';
import { asyncObservable } from '@/core/utils/rxjs';
import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
const ROOT_CACHE_KEY = 'CoreCoursesDashboard:';

View File

@ -17,10 +17,6 @@ import { Camera, CameraOptions } from '@ionic-native/camera/ngx';
import { CoreEmulatorCaptureHelper } from './capture-helper';
// @todo remove android.media.action.IMAGE_CAPTURE and android.intent.action.GET_CONTENT entries
// from config.xml once https://github.com/apache/cordova-plugin-camera/issues/673 is resolved.
// (this is written here because comments get stripped out from config.xml)
/**
* Emulates the Cordova Camera plugin in browser.
*/

View File

@ -31,7 +31,7 @@ import { CoreEvents, CoreEventSiteData } from '@singletons/events';
import { CoreLogger } from '@singletons/logger';
import { CoreSite } from '@classes/sites/site';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { firstValueFrom } from '@/core/utils/rxjs';
import { firstValueFrom } from 'rxjs';
/**
* Helper service to provide filter functionalities.

View File

@ -23,7 +23,7 @@
{{'core.login.onboardingwelcome' | translate}}
</h1>
<div *ngIf="step == 0" class="core-login-onboarding-step">
<div *ngIf="step === 0" class="core-login-onboarding-step">
<ion-button expand="block" (click)="skip($event)" class="ion-margin-bottom" fill="outline">
{{'core.login.onboardingimalearner' | translate}}
</ion-button>
@ -32,7 +32,7 @@
</ion-button>
</div>
<div *ngIf="step == 1" class="core-login-onboarding-step">
<div *ngIf="step === 1" class="core-login-onboarding-step">
<p class="core-login-onboarding-text">
{{ 'core.login.onboardingtoconnect' | translate }}
</p>
@ -44,7 +44,7 @@
</ion-button>
</div>
<div *ngIf="step == 2" class="core-login-onboarding-step">
<div *ngIf="step === 2" class="core-login-onboarding-step">
<ul class="core-login-onboarding-text ion-text-start">
<li>
<ion-icon name="far-circle-check" aria-hidden="true">

View File

@ -16,7 +16,7 @@
</ion-toolbar>
</ion-header>
<core-loading [hideUntil]="loaded">
<core-empty-box *ngIf="tabs.length == 0" icon="fas-house" [message]="'core.courses.nocourses' | translate"></core-empty-box>
<core-empty-box *ngIf="tabs.length === 0" icon="fas-house" [message]="'core.courses.nocourses' | translate"></core-empty-box>
</core-loading>
<core-tabs-outlet *ngIf="tabs.length > 0" [hideUntil]="loaded" [tabs]="tabs" (ionChange)="tabSelected()">
</core-tabs-outlet>

View File

@ -7,7 +7,7 @@
<ion-tab-button *ngFor="let tab of tabs" (keydown)="tabAction.keyDown(tab.page, $event)" (keyup)="tabAction.keyUp(tab.page, $event)"
[hidden]="!loaded && tab.hide" [tab]="tab.page" [disabled]="tab.hide" layout="label-hide" class="{{tab.class}}"
[selected]="tab.page === selectedTab" [tabindex]="selectedTab == tab.page ? 0 : -1" [attr.aria-controls]="tab.id">
[selected]="tab.page === selectedTab" [tabindex]="selectedTab === tab.page ? 0 : -1" [attr.aria-controls]="tab.id">
<ion-icon class="core-tab-icon" [name]="tab.icon" aria-hidden="true"></ion-icon>
<ion-label aria-hidden="true">{{ tab.title | translate }}</ion-label>
<ion-badge class="core-tab-badge" *ngIf="tab.badge" aria-hidden="true">{{ tab.badge }}</ion-badge>
@ -18,7 +18,7 @@
</ion-tab-button>
<ion-tab-button (keydown)="tabAction.keyDown(morePageName, $event)" (keyup)="tabAction.keyUp(morePageName, $event)"
[hidden]="!loaded" [tab]="morePageName" layout="label-hide" [tabindex]="selectedTab == morePageName ? 0 : -1"
[hidden]="!loaded" [tab]="morePageName" layout="label-hide" [tabindex]="selectedTab === morePageName ? 0 : -1"
[attr.aria-controls]="morePageName">
<ion-icon class="core-tab-icon" name="ellipsis-horizontal" aria-hidden="true"></ion-icon>
<ion-label aria-hidden="true">{{ 'core.more' | translate }}</ion-label>

View File

@ -1,4 +1,4 @@
<ion-item class="ion-text-wrap" *ngIf="item && (item!.canrate || item!.rating != null) && !disabled">
<ion-item class="ion-text-wrap" *ngIf="item && (item!.canrate || item!.rating !== null) && !disabled">
<ion-label>{{ 'core.rating.rating' | translate }}</ion-label>
<ion-select class="ion-text-start" [(ngModel)]="rating" (ngModelChange)="userRatingChanged()" interface="action-sheet"
[cancelText]="'core.cancel' | translate" [disabled]="!item!.canrate"

View File

@ -24,7 +24,7 @@
</ion-note>
</ion-item>
</ion-list>
<core-empty-box *ngIf="ratings.length == 0" icon="fas-star-half-stroke" [message]="'core.rating.noratings' | translate">
<core-empty-box *ngIf="ratings.length === 0" icon="fas-star-half-stroke" [message]="'core.rating.noratings' | translate">
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -264,4 +264,4 @@ export type CoreReportBuilderReportDetailSettingsData = {
cardviewVisibleColumns: number;
};
export interface CoreReportBuilderReport extends CoreReportBuilderReportWSResponse {};
export interface CoreReportBuilderReport extends CoreReportBuilderReportWSResponse {}

View File

@ -16,6 +16,7 @@ import { Component, OnInit } from '@angular/core';
import { CoreConstants } from '@/core/constants';
import { Http } from '@singletons';
import { IonSearchbar } from '@ionic/angular';
import { firstValueFrom } from 'rxjs';
/**
* Defines license info
@ -63,7 +64,7 @@ export class CoreSettingsLicensesPage implements OnInit {
*/
async ngOnInit(): Promise<void> {
try {
const licenses = await Http.get(this.licensesUrl).toPromise();
const licenses = await firstValueFrom(Http.get(this.licensesUrl));
this.allLicenses = Object.keys(licenses).map((name) => {
const license = licenses[name];

View File

@ -26,6 +26,7 @@ import { CoreSite } from '../classes/sites/site';
import { CorePlatform } from '@services/platform';
import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang';
import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2';
import { firstValueFrom } from 'rxjs';
/*
* Service to handle language features, like changing the current language.
@ -501,7 +502,7 @@ export class CoreLangProvider {
responseType: 'json',
});
return <Record<string, string>> await observable.toPromise();
return <Record<string, string>> await firstValueFrom(observable);
}
/**

View File

@ -65,6 +65,7 @@ import { CoreAutoLogoutType, CoreAutoLogout } from '@features/autologout/service
import { CoreCacheManager } from '@services/cache-manager';
import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from '@classes/sites/unauthenticated-site';
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
import { firstValueFrom } from 'rxjs';
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id';
@ -441,7 +442,7 @@ export class CoreSitesProvider {
let data: CoreSitesLoginTokenResponse;
try {
data = await Http.post(loginUrl, params).pipe(timeout(CoreWS.getRequestTimeout())).toPromise();
data = await firstValueFrom(Http.post(loginUrl, params).pipe(timeout(CoreWS.getRequestTimeout())));
} catch (error) {
throw new CoreError(
this.isLoggedIn()

View File

@ -18,7 +18,7 @@ import { HttpResponse, HttpParams, HttpErrorResponse } from '@angular/common/htt
import { FileEntry } from '@ionic-native/file/ngx';
import { FileUploadOptions, FileUploadResult } from '@ionic-native/file-transfer/ngx';
import { Md5 } from 'ts-md5/dist/md5';
import { Observable } from 'rxjs';
import { Observable, firstValueFrom } from 'rxjs';
import { timeout } from 'rxjs/operators';
import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http';
@ -691,7 +691,7 @@ export class CoreWSProvider {
const requestUrl = siteUrl + '&wsfunction=' + method;
// Perform the post request.
const promise = Http.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())).toPromise();
const promise = firstValueFrom(Http.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return promise.then(async (data: any) => {
@ -1178,7 +1178,7 @@ export class CoreWSProvider {
observable = observable.pipe(timeout(angularOptions.timeout));
}
return observable.toPromise();
return firstValueFrom(observable);
}
}

View File

@ -16,6 +16,7 @@ import { Http } from '@singletons';
import { CoreConstants } from '../constants';
import { CoreLogger } from './logger';
import aliases from '@/assets/fonts/font-awesome/aliases.json';
import { firstValueFrom } from 'rxjs';
/**
* Singleton with helper functions for icon management.
@ -65,8 +66,7 @@ export class CoreIcons {
}
if (!(src in CoreIcons.DEV_ICONS_STATUS)) {
CoreIcons.DEV_ICONS_STATUS[src] = Http.get(src, { responseType: 'text' })
.toPromise()
CoreIcons.DEV_ICONS_STATUS[src] = firstValueFrom(Http.get(src, { responseType: 'text' }))
.then(() => true)
.catch(() => false);
}

View File

@ -57,20 +57,20 @@ export class CoreSubscriptions {
subscription?.unsubscribe();
};
subscription = subscribable.subscribe(
value => {
subscription = subscribable.subscribe({
next: value => {
unsubscribe();
runCallback(() => onSuccess(value));
},
error => {
error: error => {
unsubscribe();
runCallback(() => onError?.(error));
},
() => {
complete: () => {
unsubscribe();
runCallback(() => onComplete?.());
},
);
});
return () => subscription?.unsubscribe();
}

View File

@ -13,9 +13,7 @@
// limitations under the License.
import { FormControl } from '@angular/forms';
import { CoreError } from '@classes/errors/error';
import { CoreSubscriptions } from '@singletons/subscriptions';
import { BehaviorSubject, Observable, of, OperatorFunction, Subscription } from 'rxjs';
import { Observable, of, OperatorFunction, Subscription } from 'rxjs';
import { catchError, filter } from 'rxjs/operators';
/**
@ -79,27 +77,6 @@ export function asyncObservable<T>(createObservable: () => Promise<Observable<T>
});
}
/**
* Create a Promise resolved with the first value returned from an observable. The difference with toPromise is that
* this function returns the value as soon as it's emitted, it doesn't wait until the observable completes.
* This function can be removed when the app starts using rxjs v7.
*
* @param observable Observable.
* @returns Promise resolved with the first value returned.
*/
export function firstValueFrom<T>(observable: Observable<T>): Promise<T> {
return new Promise((resolve, reject) => {
CoreSubscriptions.once(observable, resolve, reject, () => {
// Subscription is completed, check if we can get its value.
if (observable instanceof BehaviorSubject) {
resolve(observable.getValue());
}
reject(new CoreError('Couldn\'t get first value from observable because it\'s already completed'));
});
});
}
/**
* Ignore errors from an observable, returning a certain value instead.
*

View File

@ -14,7 +14,6 @@
import {
asyncObservable,
firstValueFrom,
formControlValue,
ignoreErrors,
resolved,
@ -23,7 +22,7 @@ import {
} from '@/core/utils/rxjs';
import { mock } from '@/testing/utils';
import { FormControl } from '@angular/forms';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { Observable, of, Subject } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
describe('RXJS Utils', () => {
@ -156,35 +155,6 @@ describe('RXJS Utils', () => {
});
});
it('firstValueFrom returns first value emitted by an observable', async () => {
const subject = new Subject();
setTimeout(() => subject.next('foo'), 10);
await expect(firstValueFrom(subject)).resolves.toEqual('foo');
// Check that running it again doesn't get last value, it gets the new one.
setTimeout(() => subject.next('bar'), 10);
await expect(firstValueFrom(subject)).resolves.toEqual('bar');
// Check we cannot get first value if a subject is already completed.
subject.complete();
await expect(firstValueFrom(subject)).rejects.toThrow();
// Check that we get last value when using BehaviourSubject.
const behaviorSubject = new BehaviorSubject('baz');
await expect(firstValueFrom(behaviorSubject)).resolves.toEqual('baz');
// Check we get last value even if behaviour subject is completed.
behaviorSubject.complete();
await expect(firstValueFrom(behaviorSubject)).resolves.toEqual('baz');
// Check that Promise is rejected if the observable emits an error.
const errorSubject = new Subject();
setTimeout(() => errorSubject.error('foo error'), 10);
await expect(firstValueFrom(errorSubject)).rejects.toMatch('foo error');
});
it('ignoreErrors ignores observable errors', (done) => {
const subject = new Subject();

View File

@ -16,7 +16,7 @@
window.__Zone_disable_customElements = true;
// Zone JS is required by default for Angular itself.
import 'zone.js/dist/zone';
import 'zone.js';
// Platform polyfills
import 'core-js/es/array/includes';

View File

@ -32,17 +32,17 @@
@import "bootstrap.scss";
/* Core CSS required for Ionic components to work properly */
@import "~@ionic/angular/css/core.css";
@import "@ionic/angular/css/core.css";
/* Basic CSS for apps built with Ionic */
@import "~@ionic/angular/css/normalize.css";
@import "~@ionic/angular/css/structure.css";
@import "~@ionic/angular/css/typography.css";
@import "~@ionic/angular/css/display.css";
@import "@ionic/angular/css/normalize.css";
@import "@ionic/angular/css/structure.css";
@import "@ionic/angular/css/typography.css";
@import "@ionic/angular/css/display.css";
/* Optional CSS utils that can be commented out */
@import "~@ionic/angular/css/padding.css";
@import "~@ionic/angular/css/float-elements.css";
@import "~@ionic/angular/css/text-alignment.css";
@import "~@ionic/angular/css/text-transformation.css";
@import "~@ionic/angular/css/flex-utils.css";
@import "@ionic/angular/css/padding.css";
@import "@ionic/angular/css/float-elements.css";
@import "@ionic/angular/css/text-alignment.css";
@import "@ionic/angular/css/text-transformation.css";
@import "@ionic/angular/css/flex-utils.css";

View File

@ -8,7 +8,6 @@
"cordova-plugin-moodleapp",
"cordova",
"dom-mediacapture-record",
"node",
"resize-observer-browser",
"webpack-env"
]