forked from EVOgeek/Vmeda.Online
		
	Merge pull request #3897 from moodlehq/ionic7
MOBILE-3947: Upgrade to Ionic 7
This commit is contained in:
		
						commit
						a9f6d2ca94
					
				
							
								
								
									
										77
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										77
									
								
								.eslintrc.js
									
									
									
									
									
								
							| @ -5,6 +5,7 @@ const appConfig = { | |||||||
|         node: true, |         node: true, | ||||||
|     }, |     }, | ||||||
|     plugins: [ |     plugins: [ | ||||||
|  |         '@angular-eslint', | ||||||
|         '@typescript-eslint', |         '@typescript-eslint', | ||||||
|         'header', |         'header', | ||||||
|         'jsdoc', |         'jsdoc', | ||||||
| @ -13,12 +14,13 @@ const appConfig = { | |||||||
|     ], |     ], | ||||||
|     extends: [ |     extends: [ | ||||||
|         'eslint:recommended', |         'eslint:recommended', | ||||||
|  |         'plugin:@typescript-eslint/eslint-recommended', | ||||||
|         'plugin:@typescript-eslint/recommended', |         'plugin:@typescript-eslint/recommended', | ||||||
|         'prettier', |  | ||||||
|         'plugin:@angular-eslint/recommended', |         'plugin:@angular-eslint/recommended', | ||||||
|  |         'plugin:@angular-eslint/template/process-inline-templates', | ||||||
|         'plugin:promise/recommended', |         'plugin:promise/recommended', | ||||||
|         'plugin:jsdoc/recommended', |         'plugin:jsdoc/recommended', | ||||||
|         "plugin:deprecation/recommended", |         'plugin:deprecation/recommended', | ||||||
|     ], |     ], | ||||||
|     parser: '@typescript-eslint/parser', |     parser: '@typescript-eslint/parser', | ||||||
|     parserOptions: { |     parserOptions: { | ||||||
| @ -46,6 +48,7 @@ const appConfig = { | |||||||
|                     Object: { |                     Object: { | ||||||
|                         message: 'Use {} instead.', |                         message: 'Use {} instead.', | ||||||
|                     }, |                     }, | ||||||
|  |                     Function: false, | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
| @ -61,16 +64,6 @@ const appConfig = { | |||||||
|                 allowArgumentsExplicitlyTypedAsAny: true, |                 allowArgumentsExplicitlyTypedAsAny: true, | ||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
|         '@typescript-eslint/indent': [ |  | ||||||
|             'error', |  | ||||||
|             4, |  | ||||||
|             { |  | ||||||
|                 SwitchCase: 1, |  | ||||||
|                 ignoredNodes: [ |  | ||||||
|                     'ClassProperty *', |  | ||||||
|                 ], |  | ||||||
|             }, |  | ||||||
|         ], |  | ||||||
|         '@typescript-eslint/lines-between-class-members': [ |         '@typescript-eslint/lines-between-class-members': [ | ||||||
|             'error', |             'error', | ||||||
|             'always', |             'always', | ||||||
| @ -103,6 +96,20 @@ const appConfig = { | |||||||
|         ], |         ], | ||||||
|         '@typescript-eslint/naming-convention': [ |         '@typescript-eslint/naming-convention': [ | ||||||
|             'error', |             'error', | ||||||
|  |             { | ||||||
|  |                 selector: [ | ||||||
|  |                     'classProperty', | ||||||
|  |                     'objectLiteralProperty', | ||||||
|  |                     'typeProperty', | ||||||
|  |                     'classMethod', | ||||||
|  |                     'objectLiteralMethod', | ||||||
|  |                     'typeMethod', | ||||||
|  |                     'accessor', | ||||||
|  |                     'enumMember' | ||||||
|  |                 ], | ||||||
|  |                 modifiers: ['requiresQuotes'], | ||||||
|  |                 format: null, | ||||||
|  |             }, | ||||||
|             { |             { | ||||||
|                 selector: 'property', |                 selector: 'property', | ||||||
|                 format: ['camelCase'], |                 format: ['camelCase'], | ||||||
| @ -200,17 +207,6 @@ const appConfig = { | |||||||
|         ], |         ], | ||||||
|         'id-match': 'error', |         'id-match': 'error', | ||||||
|         'jsdoc/check-alignment': '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': [ |         'jsdoc/check-param-names': [ | ||||||
|             'warn', |             'warn', | ||||||
|             { |             { | ||||||
| @ -218,6 +214,23 @@ const appConfig = { | |||||||
|                 enableFixer: true |                 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': [ |         'linebreak-style': [ | ||||||
|             'error', |             'error', | ||||||
|             'unix', |             'unix', | ||||||
| @ -240,7 +253,7 @@ const appConfig = { | |||||||
|         'no-fallthrough': 'off', |         'no-fallthrough': 'off', | ||||||
|         'no-invalid-this': 'error', |         'no-invalid-this': 'error', | ||||||
|         'no-irregular-whitespace': 'error', |         'no-irregular-whitespace': 'error', | ||||||
|         'no-multiple-empty-lines': ['error', { "max": 1 }], |         'no-multiple-empty-lines': ['error', { max: 1 }], | ||||||
|         'no-new-wrappers': 'error', |         'no-new-wrappers': 'error', | ||||||
|         'no-sequences': 'error', |         'no-sequences': 'error', | ||||||
|         'no-trailing-spaces': 'error', |         'no-trailing-spaces': 'error', | ||||||
| @ -318,15 +331,15 @@ module.exports = { | |||||||
|             files: ['*.html'], |             files: ['*.html'], | ||||||
|             extends: ['plugin:@angular-eslint/template/recommended'], |             extends: ['plugin:@angular-eslint/template/recommended'], | ||||||
|             rules: { |             rules: { | ||||||
|                 'max-len': ['warn', { code: 140 }], |                 '@angular-eslint/template/alt-text': 'error', | ||||||
|                 '@angular-eslint/template/accessibility-valid-aria': 'warn', |                 '@angular-eslint/template/elements-content': 'error', | ||||||
|                 '@angular-eslint/template/accessibility-alt-text': 'error', |                 '@angular-eslint/template/label-has-associated-control': '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/no-duplicate-attributes': 'error', |                 '@angular-eslint/template/no-duplicate-attributes': 'error', | ||||||
|  |                 '@angular-eslint/template/no-positive-tabindex': 'error', | ||||||
|  |                 '@angular-eslint/template/prefer-self-closing-tags': 'error', | ||||||
|  |                 '@angular-eslint/template/table-scope': 'error', | ||||||
|  |                 '@angular-eslint/template/valid-aria': 'error', | ||||||
|  |                 'max-len': ['warn', { code: 140 }], | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							| @ -10,13 +10,13 @@ on: | |||||||
|       moodle_branch: |       moodle_branch: | ||||||
|         description: 'Moodle branch' |         description: 'Moodle branch' | ||||||
|         required: true |         required: true | ||||||
|         default: 'master' |         default: 'main' | ||||||
|       moodle_repository: |       moodle_repository: | ||||||
|         description: 'Moodle repository' |         description: 'Moodle repository' | ||||||
|         required: true |         required: true | ||||||
|         default: 'https://github.com/moodle/moodle' |         default: 'https://github.com/moodle/moodle' | ||||||
|   pull_request: |   pull_request: | ||||||
|     branches: [ main, v*.x ] |     branches: [ main, ionic7, v*.x ] | ||||||
| 
 | 
 | ||||||
| jobs: | jobs: | ||||||
|   behat: |   behat: | ||||||
| @ -24,8 +24,8 @@ jobs: | |||||||
|     env: |     env: | ||||||
|       MOODLE_DOCKER_DB: pgsql |       MOODLE_DOCKER_DB: pgsql | ||||||
|       MOODLE_DOCKER_BROWSER: chrome |       MOODLE_DOCKER_BROWSER: chrome | ||||||
|       MOODLE_DOCKER_PHP_VERSION: '8.0' |       MOODLE_DOCKER_PHP_VERSION: '8.1' | ||||||
|       MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'master' }} |       MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }} | ||||||
|       MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }} |       MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }} | ||||||
|       BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }} |       BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }} | ||||||
| 
 | 
 | ||||||
| @ -37,7 +37,7 @@ jobs: | |||||||
|     - name: Additional checkouts |     - name: Additional checkouts | ||||||
|       run: | |       run: | | ||||||
|         git clone --branch $MOODLE_BRANCH --depth 1 $MOODLE_REPOSITORY $GITHUB_WORKSPACE/moodle |         git clone --branch $MOODLE_BRANCH --depth 1 $MOODLE_REPOSITORY $GITHUB_WORKSPACE/moodle | ||||||
|         git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker |         git clone --branch main --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker | ||||||
|     - name: Install npm packages |     - name: Install npm packages | ||||||
|       run: npm ci --no-audit |       run: npm ci --no-audit | ||||||
|     - name: Create Behat faildumps folder |     - name: Create Behat faildumps folder | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							| @ -16,8 +16,8 @@ jobs: | |||||||
|         node-version-file: '.nvmrc' |         node-version-file: '.nvmrc' | ||||||
|     - name: Additional checkouts |     - name: Additional checkouts | ||||||
|       run: | |       run: | | ||||||
|         git clone --branch master --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle |         git clone --branch main --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle | ||||||
|         git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker |         git clone --branch main --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker | ||||||
|     - name: Install npm packages |     - name: Install npm packages | ||||||
|       run: npm ci --no-audit |       run: npm ci --no-audit | ||||||
|     - name: Generate Behat tests plugin |     - name: Generate Behat tests plugin | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								.github/workflows/testing.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/testing.yml
									
									
									
									
										vendored
									
									
								
							| @ -8,8 +8,8 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|     - uses: actions/checkout@v2 |     - uses: actions/checkout@v4 | ||||||
|     - uses: actions/setup-node@v3 |     - uses: actions/setup-node@v4 | ||||||
|       with: |       with: | ||||||
|         node-version-file: '.nvmrc' |         node-version-file: '.nvmrc' | ||||||
|     - name: Install npm packages |     - name: Install npm packages | ||||||
| @ -59,4 +59,8 @@ jobs: | |||||||
|         npm run build:prod |         npm run build:prod | ||||||
|         npm run prod --prefix cordova-plugin-moodleapp |         npm run prod --prefix cordova-plugin-moodleapp | ||||||
|     - name: JavaScript code compatibility |     - name: JavaScript code compatibility | ||||||
|       run: result=$(npx check-es-compat www/*.js 2> /dev/null | grep -v -E "Array\.prototype\.includes|Promise\.prototype\.finally|String\.prototype\.(matchAll|trimRight)|globalThis" | grep -Po "(?<=error).*?(?=\s+ecmascript)" | wc -l); test $result -eq 1 |       # 6 BigInt usage errors are expected, they are fine without polyfill because they are only used if available. | ||||||
|  |       # See https://github.com/videojs/mpd-parser/blob/v0.22.1/src/segment/urlType.js | ||||||
|  |       run: | | ||||||
|  |         result=$(npx check-es-compat www/*.js --polyfills="{Array,String,TypedArray}.prototype.at,Array.prototype.flatMap,Array.prototype.flat,Array.prototype.includes,globalThis,Object.fromEntries,Object.hasOwn,Promise.prototype.finally,String.prototype.matchAll,String.prototype.trimRight" | grep "6 problems (6 errors, 0 warnings)" | wc -l); test $result -eq 1 | ||||||
|  |         npx check-es-compat cordova-plugin-moodleapp/www/*.js | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -70,6 +70,7 @@ Thumbs.db | |||||||
| /src/assets/lib | /src/assets/lib | ||||||
| /src/assets/lang/* | /src/assets/lang/* | ||||||
| /src/assets/env.json | /src/assets/env.json | ||||||
|  | /src/assets/fonts/icons.json | ||||||
| 
 | 
 | ||||||
| /moodle.config.*.json | /moodle.config.*.json | ||||||
| !/moodle.config.example.json | !/moodle.config.example.json | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -9,12 +9,12 @@ | |||||||
|     }, |     }, | ||||||
|     "editor.formatOnSave": true, |     "editor.formatOnSave": true, | ||||||
|     "eslint.format.enable": true, |     "eslint.format.enable": true, | ||||||
|     "html.format.endWithNewline": true, |  | ||||||
|     "html.format.wrapLineLength": 140, |     "html.format.wrapLineLength": 140, | ||||||
|     "files.eol": "\n", |     "files.eol": "\n", | ||||||
|     "files.trimFinalNewlines": true, |     "files.trimFinalNewlines": true, | ||||||
|     "files.insertFinalNewline": true, |     "files.insertFinalNewline": true, | ||||||
|     "files.trimTrailingWhitespace": true, |     "files.trimTrailingWhitespace": true, | ||||||
|  |     "typescript.tsdk": "./node_modules/typescript/lib", | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Config files. |      * Config files. | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| ## BUILD STAGE | ## BUILD STAGE | ||||||
| FROM node:14 as build-stage | FROM node:18 as build-stage | ||||||
| 
 | 
 | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										43
									
								
								angular.json
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								angular.json
									
									
									
									
									
								
							| @ -1,7 +1,6 @@ | |||||||
| { | { | ||||||
|   "$schema": "./node_modules/@angular/cli/lib/config/schema.json", |   "$schema": "./node_modules/@angular/cli/lib/config/schema.json", | ||||||
|   "version": 1, |   "version": 1, | ||||||
|   "defaultProject": "app", |  | ||||||
|   "newProjectRoot": "projects", |   "newProjectRoot": "projects", | ||||||
|   "projects": { |   "projects": { | ||||||
|     "app": { |     "app": { | ||||||
| @ -63,12 +62,6 @@ | |||||||
|               }, |               }, | ||||||
|               "outputHashing": "all", |               "outputHashing": "all", | ||||||
|               "sourceMap": false, |               "sourceMap": false, | ||||||
|               "extractCss": true, |  | ||||||
|               "namedChunks": false, |  | ||||||
|               "aot": true, |  | ||||||
|               "extractLicenses": true, |  | ||||||
|               "vendorChunk": false, |  | ||||||
|               "buildOptimizer": true, |  | ||||||
|               "budgets": [ |               "budgets": [ | ||||||
|                 { |                 { | ||||||
|                   "type": "initial", |                   "type": "initial", | ||||||
| @ -77,24 +70,25 @@ | |||||||
|                 } |                 } | ||||||
|               ] |               ] | ||||||
|             }, |             }, | ||||||
|  |             "development": { | ||||||
|  |               "buildOptimizer": false, | ||||||
|  |               "optimization": false, | ||||||
|  |               "vendorChunk": true, | ||||||
|  |               "extractLicenses": false, | ||||||
|  |               "sourceMap": true, | ||||||
|  |               "namedChunks": true | ||||||
|  |             }, | ||||||
|             "testing": { |             "testing": { | ||||||
|               "optimization": { |               "optimization": { | ||||||
|                 "scripts": false, |                 "scripts": false, | ||||||
|                 "styles": true |                 "styles": true | ||||||
|               }, |               } | ||||||
|               "outputHashing": "all", |  | ||||||
|               "sourceMap": false, |  | ||||||
|               "extractCss": true, |  | ||||||
|               "namedChunks": false, |  | ||||||
|               "aot": true, |  | ||||||
|               "extractLicenses": true, |  | ||||||
|               "vendorChunk": false, |  | ||||||
|               "buildOptimizer": true |  | ||||||
|             }, |             }, | ||||||
|             "ci": { |             "ci": { | ||||||
|               "progress": false |               "progress": false | ||||||
|             } |             } | ||||||
|           } |           }, | ||||||
|  |           "defaultConfiguration": "production" | ||||||
|         }, |         }, | ||||||
|         "serve": { |         "serve": { | ||||||
|           "builder": "@angular-devkit/build-angular:dev-server", |           "builder": "@angular-devkit/build-angular:dev-server", | ||||||
| @ -107,11 +101,12 @@ | |||||||
|             "production": { |             "production": { | ||||||
|               "browserTarget": "app:build:production" |               "browserTarget": "app:build:production" | ||||||
|             }, |             }, | ||||||
|             "ci": { |             "development": { | ||||||
|               "progress": false |               "browserTarget": "app:build:development" | ||||||
|             } |  | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|  |           "defaultConfiguration": "development" | ||||||
|  |         }, | ||||||
|         "extract-i18n": { |         "extract-i18n": { | ||||||
|           "builder": "@angular-devkit/build-angular:extract-i18n", |           "builder": "@angular-devkit/build-angular:extract-i18n", | ||||||
|           "options": { |           "options": { | ||||||
| @ -129,7 +124,7 @@ | |||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "ionic-cordova-build": { |         "ionic-cordova-build": { | ||||||
|           "builder": "@ionic/angular-toolkit:cordova-build", |           "builder": "@ionic/cordova-builders:cordova-build", | ||||||
|           "options": { |           "options": { | ||||||
|             "browserTarget": "app:build" |             "browserTarget": "app:build" | ||||||
|           }, |           }, | ||||||
| @ -140,7 +135,7 @@ | |||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "ionic-cordova-serve": { |         "ionic-cordova-serve": { | ||||||
|           "builder": "@ionic/angular-toolkit:cordova-serve", |           "builder": "@ionic/cordova-builders:cordova-serve", | ||||||
|           "options": { |           "options": { | ||||||
|             "cordovaBuildTarget": "app:ionic-cordova-build", |             "cordovaBuildTarget": "app:ionic-cordova-build", | ||||||
|             "devServerTarget": "app:serve" |             "devServerTarget": "app:serve" | ||||||
| @ -157,7 +152,9 @@ | |||||||
|   }, |   }, | ||||||
|   "cli": { |   "cli": { | ||||||
|     "analytics": false, |     "analytics": false, | ||||||
|     "defaultCollection": "@ionic/angular-toolkit" |     "schematicCollections": [ | ||||||
|  |       "@ionic/angular-toolkit" | ||||||
|  |     ] | ||||||
|   }, |   }, | ||||||
|   "schematics": { |   "schematics": { | ||||||
|     "@ionic/angular-toolkit:component": { |     "@ionic/angular-toolkit:component": { | ||||||
|  | |||||||
							
								
								
									
										123
									
								
								config.xml
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								config.xml
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | |||||||
| <?xml version='1.0' encoding='utf-8'?> | <?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> |     <name>Moodle</name> | ||||||
|     <description>Moodle official app</description> |     <description>Moodle official app</description> | ||||||
|     <author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author> |     <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="AndroidWindowSplashScreenAnimatedIcon" value="resources/android/android-splash.xml" /> | ||||||
|         <preference name="AndroidWindowSplashScreenBackground" value="#FFFFFF" /> |         <preference name="AndroidWindowSplashScreenBackground" value="#FFFFFF" /> | ||||||
|         <preference name="AndroidWindowSplashScreenIconBackgroundColor" 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="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-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" /> |         <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/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/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" /> |         <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"> |         <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" /> |             <application android:allowBackup="true" android:dataExtractionRules="@xml/backup_rules" android:largeHeap="true" android:networkSecurityConfig="@xml/network_security_config" /> | ||||||
|         </edit-config> |         </edit-config> | ||||||
|         <config-file parent="/manifest/application" target="AndroidManifest.xml"> |         <config-file parent="/manifest/application" target="AndroidManifest.xml"> | ||||||
|             <meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" /> |             <meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" /> | ||||||
|         </config-file> |         </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"> |         <config-file parent="/*" target="AndroidManifest.xml"> | ||||||
|             <uses-feature android:name="android.hardware.bluetooth" android:required="false" /> |             <uses-feature android:name="android.hardware.bluetooth" android:required="false" /> | ||||||
|             <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> |             <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> | ||||||
|         </config-file> |         </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> | ||||||
|     <platform name="ios"> |     <platform name="ios"> | ||||||
|         <resource-file src="GoogleService-Info.plist" /> |         <resource-file src="GoogleService-Info.plist" /> | ||||||
|  | |||||||
							
								
								
									
										1135
									
								
								cordova-plugin-moodleapp/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1135
									
								
								cordova-plugin-moodleapp/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,69 +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.
 |  | ||||||
| 
 |  | ||||||
| const fs = require('fs'); |  | ||||||
| 
 |  | ||||||
| const DEV_CONFIG_FILE = '.moodleapp-dev-config'; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Class to read and write dev-config data from a file. |  | ||||||
|  */ |  | ||||||
| class DevConfig { |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         this.loadFileData(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get a setting. |  | ||||||
|      * |  | ||||||
|      * @param name Name of the setting to get. |  | ||||||
|      * @param defaultValue Value to use if not found. |  | ||||||
|      */ |  | ||||||
|     get(name, defaultValue) { |  | ||||||
|         return typeof this.config[name] != 'undefined' ? this.config[name] : defaultValue; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Load file data to memory. |  | ||||||
|      */ |  | ||||||
|     loadFileData() { |  | ||||||
|         if (!fs.existsSync(DEV_CONFIG_FILE)) { |  | ||||||
|             this.config = {}; |  | ||||||
| 
 |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         try { |  | ||||||
|             this.config = JSON.parse(fs.readFileSync(DEV_CONFIG_FILE)); |  | ||||||
|         } catch (error) { |  | ||||||
|             console.error('Error reading dev config file.', error); |  | ||||||
|             this.config = {}; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Save some settings. |  | ||||||
|      * |  | ||||||
|      * @param settings Object with the settings to save. |  | ||||||
|      */ |  | ||||||
|     save(settings) { |  | ||||||
|         this.config = Object.assign(this.config, settings); |  | ||||||
| 
 |  | ||||||
|         // Save the data in the dev file.
 |  | ||||||
|         fs.writeFileSync(DEV_CONFIG_FILE, JSON.stringify(this.config, null, 4)); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = new DevConfig(); |  | ||||||
							
								
								
									
										237
									
								
								gulp/git.js
									
									
									
									
									
								
							
							
						
						
									
										237
									
								
								gulp/git.js
									
									
									
									
									
								
							| @ -1,237 +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.
 |  | ||||||
| 
 |  | ||||||
| const exec = require('child_process').exec; |  | ||||||
| const fs = require('fs'); |  | ||||||
| const DevConfig = require('./dev-config'); |  | ||||||
| const Utils = require('./utils'); |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Class to run git commands. |  | ||||||
|  */ |  | ||||||
| class Git { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Create a patch. |  | ||||||
|      * |  | ||||||
|      * @param range Show only commits in the specified revision range. |  | ||||||
|      * @param saveTo Path to the file to save the patch to. If not defined, the patch contents will be returned. |  | ||||||
|      * @return Promise resolved when done. If saveTo not provided, it will return the patch contents. |  | ||||||
|      */ |  | ||||||
|     createPatch(range, saveTo) { |  | ||||||
|         return new Promise((resolve, reject) => { |  | ||||||
|             exec(`git format-patch ${range} --stdout`, (err, result) => { |  | ||||||
|                 if (err) { |  | ||||||
|                     reject(err); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (!saveTo) { |  | ||||||
|                     resolve(result); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 // Save it to a file.
 |  | ||||||
|                 const directory = saveTo.substring(0, saveTo.lastIndexOf('/')); |  | ||||||
|                 if (directory && directory != '.' && directory != '..' && !fs.existsSync(directory)) { |  | ||||||
|                     fs.mkdirSync(directory); |  | ||||||
|                 } |  | ||||||
|                 fs.writeFileSync(saveTo, result); |  | ||||||
| 
 |  | ||||||
|                 resolve(); |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get current branch. |  | ||||||
|      * |  | ||||||
|      * @return Promise resolved with the branch name. |  | ||||||
|      */ |  | ||||||
|     getCurrentBranch() { |  | ||||||
|         return new Promise((resolve, reject) => { |  | ||||||
|             exec('git branch --show-current', (err, branch) => { |  | ||||||
|                 if (branch) { |  | ||||||
|                     resolve(branch.replace('\n', '')); |  | ||||||
|                 } else { |  | ||||||
|                     reject (err || 'Current branch not found.'); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get the HEAD commit for a certain branch. |  | ||||||
|      * |  | ||||||
|      * @param branch Name of the branch. |  | ||||||
|      * @param branchData Parsed branch data. If not provided it will be calculated. |  | ||||||
|      * @return HEAD commit. |  | ||||||
|      */ |  | ||||||
|     async getHeadCommit(branch, branchData) { |  | ||||||
|         if (!branchData) { |  | ||||||
|             // Parse the branch to get the project and issue number.
 |  | ||||||
|             branchData = Utils.parseBranch(branch); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Loop over the last commits to find the first commit messages that doesn't belong to the issue.
 |  | ||||||
|         const commitsString = await this.log(50, branch, '%s_____%H'); |  | ||||||
|         const commits = commitsString.split('\n'); |  | ||||||
|         commits.pop(); // Remove last element, it's an empty string.
 |  | ||||||
| 
 |  | ||||||
|         for (let i = 0; i < commits.length; i++) { |  | ||||||
|             const commit = commits[i]; |  | ||||||
|             const match = Utils.getIssueFromCommitMessage(commit) == branchData.issue; |  | ||||||
| 
 |  | ||||||
|             if (i === 0 && !match) { |  | ||||||
|                 // Most recent commit doesn't belong to the issue. Stop looking.
 |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (!match) { |  | ||||||
|                 // The commit does not match any more, we found it!
 |  | ||||||
|                 return commit.split('_____')[1]; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Couldn't find the commit using the commit names, get the last commit in the integration branch.
 |  | ||||||
|         const remote = DevConfig.get('upstreamRemote', 'origin'); |  | ||||||
|         console.log(`Head commit not found using commit messages. Get last commit from ${remote}/integration`); |  | ||||||
|         const hashes = await this.hashes(1, `${remote}/integration`); |  | ||||||
| 
 |  | ||||||
|         return hashes[0]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get the URL of a certain remote. |  | ||||||
|      * |  | ||||||
|      * @param remote Remote name. |  | ||||||
|      * @return Promise resolved with the remote URL. |  | ||||||
|      */ |  | ||||||
|     getRemoteUrl(remote) { |  | ||||||
|         return new Promise((resolve, reject) => { |  | ||||||
|             exec(`git remote get-url ${remote}`, (err, url) => { |  | ||||||
|                 if (url) { |  | ||||||
|                     resolve(url.replace('\n', '')); |  | ||||||
|                 } else { |  | ||||||
|                     reject (err || 'Remote not found.'); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Return the latest hashes from git log. |  | ||||||
|      * |  | ||||||
|      * @param count Number of commits to display. |  | ||||||
|      * @param range Show only commits in the specified revision range. |  | ||||||
|      * @param format Pretty-print the contents of the commit logs in a given format. |  | ||||||
|      * @return Promise resolved with the list of hashes. |  | ||||||
|      */ |  | ||||||
|     async hashes(count, range, format) { |  | ||||||
|         format = format || '%H'; |  | ||||||
| 
 |  | ||||||
|         const hashList = await this.log(count, range, format); |  | ||||||
| 
 |  | ||||||
|         const hashes = hashList.split('\n'); |  | ||||||
|         hashes.pop(); // Remove last element, it's an empty string.
 |  | ||||||
| 
 |  | ||||||
|         return hashes; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Calls the log command and returns the raw output. |  | ||||||
|      * |  | ||||||
|      * @param count Number of commits to display. |  | ||||||
|      * @param range Show only commits in the specified revision range. |  | ||||||
|      * @param format Pretty-print the contents of the commit logs in a given format. |  | ||||||
|      * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. |  | ||||||
|      * @return Promise resolved with the result. |  | ||||||
|      */ |  | ||||||
|     log(count, range, format, path) { |  | ||||||
|         if (typeof count == 'undefined') { |  | ||||||
|             count = 10; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let command = 'git log'; |  | ||||||
| 
 |  | ||||||
|         if (count > 0) { |  | ||||||
|             command += ` -n ${count} `; |  | ||||||
|         } |  | ||||||
|         if (format) { |  | ||||||
|             command += ` --format=${format} `; |  | ||||||
|         } |  | ||||||
|         if (range){ |  | ||||||
|             command += ` ${range} `; |  | ||||||
|         } |  | ||||||
|         if (path) { |  | ||||||
|             command += ` -- ${path}`; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return new Promise((resolve, reject) => { |  | ||||||
|             exec(command, (err, result, stderr) => { |  | ||||||
|                 if (err) { |  | ||||||
|                     reject(err); |  | ||||||
|                 } else { |  | ||||||
|                     resolve(result); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Return the latest titles of the commit messages. |  | ||||||
|      * |  | ||||||
|      * @param count Number of commits to display. |  | ||||||
|      * @param range Show only commits in the specified revision range. |  | ||||||
|      * @param path Show only commits that are enough to explain how the files that match the specified paths came to be. |  | ||||||
|      * @return Promise resolved with the list of titles. |  | ||||||
|      */ |  | ||||||
|     async messages(count, range, path) { |  | ||||||
|         count = typeof count != 'undefined' ? count : 10; |  | ||||||
| 
 |  | ||||||
|         const messageList = await this.log(count, range, '%s', path); |  | ||||||
| 
 |  | ||||||
|         const messages = messageList.split('\n'); |  | ||||||
|         messages.pop(); // Remove last element, it's an empty string.
 |  | ||||||
| 
 |  | ||||||
|         return messages; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Push a branch. |  | ||||||
|      * |  | ||||||
|      * @param remote Remote to use. |  | ||||||
|      * @param branch Branch to push. |  | ||||||
|      * @param force Whether to force the push. |  | ||||||
|      * @return Promise resolved when done. |  | ||||||
|      */ |  | ||||||
|     push(remote, branch, force) { |  | ||||||
|         return new Promise((resolve, reject) => { |  | ||||||
|             let command = `git push ${remote} ${branch}`; |  | ||||||
|             if (force) { |  | ||||||
|                 command += ' -f'; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             exec(command, (err, result, stderr) => { |  | ||||||
|                 if (err) { |  | ||||||
|                     reject(err); |  | ||||||
|                 } else { |  | ||||||
|                     resolve(); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = new Git(); |  | ||||||
							
								
								
									
										476
									
								
								gulp/jira.js
									
									
									
									
									
								
							
							
						
						
									
										476
									
								
								gulp/jira.js
									
									
									
									
									
								
							| @ -1,476 +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.
 |  | ||||||
| 
 |  | ||||||
| const exec = require('child_process').exec; |  | ||||||
| const https = require('https'); |  | ||||||
| const inquirer = require('inquirer'); |  | ||||||
| const fs = require('fs'); |  | ||||||
| const request = require('request'); // This lib is deprecated, but it was the only way I found to make upload files work.
 |  | ||||||
| const DevConfig = require('./dev-config'); |  | ||||||
| const Git = require('./git'); |  | ||||||
| const Url = require('./url'); |  | ||||||
| const Utils = require('./utils'); |  | ||||||
| 
 |  | ||||||
| const apiVersion = 2; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Class to interact with Jira. |  | ||||||
|  */ |  | ||||||
| class Jira { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Ask the password to the user. |  | ||||||
|      * |  | ||||||
|      * @return Promise resolved with the password. |  | ||||||
|      */ |  | ||||||
|     async askPassword() { |  | ||||||
|         const data = await inquirer.prompt([ |  | ||||||
|             { |  | ||||||
|                 type: 'password', |  | ||||||
|                 name: 'password', |  | ||||||
|                 message: `Please enter the password for the username ${this.username}.`, |  | ||||||
|             }, |  | ||||||
|         ]); |  | ||||||
| 
 |  | ||||||
|         return data.password; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Ask the user the tracker data. |  | ||||||
|      * |  | ||||||
|      * @return Promise resolved with the data, rejected if cannot get. |  | ||||||
|      */ |  | ||||||
|     async askTrackerData() { |  | ||||||
|         const data = await inquirer.prompt([ |  | ||||||
|             { |  | ||||||
|                 type: 'input', |  | ||||||
|                 name: 'url', |  | ||||||
|                 message: 'Please enter the tracker URL.', |  | ||||||
|                 default: 'https://tracker.moodle.org/', |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 type: 'input', |  | ||||||
|                 name: 'username', |  | ||||||
|                 message: 'Please enter your tracker username.', |  | ||||||
|             }, |  | ||||||
|         ]); |  | ||||||
| 
 |  | ||||||
|         DevConfig.save({ |  | ||||||
|             'tracker.url': data.url, |  | ||||||
|             'tracker.username': data.username, |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         return data; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Build URL to perform requests to Jira. |  | ||||||
|      * |  | ||||||
|      * @param uri URI to add the the Jira URL. |  | ||||||
|      * @return URL. |  | ||||||
|      */ |  | ||||||
|     buildRequestUrl(uri) { |  | ||||||
|         return Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Delete an attachment. |  | ||||||
|      * |  | ||||||
|      * @param attachmentId Attachment ID. |  | ||||||
|      * @return Promise resolved when done. |  | ||||||
|      */ |  | ||||||
|     async deleteAttachment(attachmentId) { |  | ||||||
|         const response = await this.request(`attachment/${attachmentId}`, 'DELETE'); |  | ||||||
| 
 |  | ||||||
|         if (response.status != 204) { |  | ||||||
|             throw new Error('Could not delete the attachment'); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Load the issue info from jira server using a REST API call. |  | ||||||
|      * |  | ||||||
|      * @param key Key to identify the issue. E.g. MOBILE-1234. |  | ||||||
|      * @param fields Fields to get. |  | ||||||
|      * @return Promise resolved with the issue data. |  | ||||||
|      */ |  | ||||||
|     async getIssue(key, fields) { |  | ||||||
|         fields = fields || '*all,-comment'; |  | ||||||
| 
 |  | ||||||
|         await this.init(); // Initialize data if needed.
 |  | ||||||
| 
 |  | ||||||
|         const response = await this.request(`issue/${key}`, 'GET', {'fields': fields, 'expand': 'names'}); |  | ||||||
| 
 |  | ||||||
|         if (response.status == 404) { |  | ||||||
|             throw new Error('Issue could not be found.'); |  | ||||||
|         } else if (response.status != 200) { |  | ||||||
|             throw new Error('The tracker is not available.') |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const issue = response.data; |  | ||||||
|         issue.named = {}; |  | ||||||
| 
 |  | ||||||
|         // Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID.
 |  | ||||||
|         const nameList = issue.names || {}; |  | ||||||
|         for (const fieldKey in issue.fields) { |  | ||||||
|             if (nameList[fieldKey]) { |  | ||||||
|                 issue.named[nameList[fieldKey]] = issue.fields[fieldKey]; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return issue |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Load the version info from the jira server using a rest api call. |  | ||||||
|      * |  | ||||||
|      * @return Promise resolved when done. |  | ||||||
|      */ |  | ||||||
|     async getServerInfo() { |  | ||||||
|         const response = await this.request('serverInfo'); |  | ||||||
| 
 |  | ||||||
|         if (response.status != 200) { |  | ||||||
|             throw new Error(`Unexpected response code: ${response.status}`, response); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.version = response.data; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get tracker data to push an issue. |  | ||||||
|      * |  | ||||||
|      * @return Promise resolved with the data. |  | ||||||
|      */ |  | ||||||
|     async getTrackerData() { |  | ||||||
|         // Check dev-config file first.
 |  | ||||||
|         let data = this.getTrackerDataFromDevConfig(); |  | ||||||
| 
 |  | ||||||
|         if (data) { |  | ||||||
|             console.log('Using tracker data from dev-config file'); |  | ||||||
|             return data; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Try to use mdk now.
 |  | ||||||
|         try { |  | ||||||
|             data = await this.getTrackerDataFromMdk(); |  | ||||||
| 
 |  | ||||||
|             console.log('Using tracker data from mdk'); |  | ||||||
| 
 |  | ||||||
|             return data; |  | ||||||
|         } catch (error) { |  | ||||||
|             // MDK not available or not configured. Ask for the data.
 |  | ||||||
|             const trackerData = await this.askTrackerData(); |  | ||||||
| 
 |  | ||||||
|             trackerData.fromInput = true; |  | ||||||
| 
 |  | ||||||
|             return trackerData; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get tracker data from dev config file. |  | ||||||
|      * |  | ||||||
|      * @return Data, undefined if cannot get. |  | ||||||
|      */ |  | ||||||
|     getTrackerDataFromDevConfig() { |  | ||||||
|         const url = DevConfig.get('tracker.url'); |  | ||||||
|         const username = DevConfig.get('tracker.username'); |  | ||||||
| 
 |  | ||||||
|         if (url && username) { |  | ||||||
|             return { |  | ||||||
|                 url, |  | ||||||
|                 username, |  | ||||||
|             }; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get tracker URL and username from mdk. |  | ||||||
|      * |  | ||||||
|      * @return Promise resolved with the data, rejected if cannot get. |  | ||||||
|      */ |  | ||||||
|     getTrackerDataFromMdk() { |  | ||||||
|         return new Promise((resolve, reject) => { |  | ||||||
|             exec('mdk config show tracker.url', (err, url) => { |  | ||||||
|                 if (!url) { |  | ||||||
|                     reject(err || 'URL not found.'); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 exec('mdk config show tracker.username', (error, username) => { |  | ||||||
|                     if (username) { |  | ||||||
|                         resolve({ |  | ||||||
|                             url: url.replace('\n', ''), |  | ||||||
|                             username: username.replace('\n', ''), |  | ||||||
|                         }); |  | ||||||
|                     } else { |  | ||||||
|                         reject(error || 'Username not found.'); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Initialize some data. |  | ||||||
|      * |  | ||||||
|      * @return Promise resolved when done. |  | ||||||
|      */ |  | ||||||
|     async init() { |  | ||||||
|         if (this.initialized) { |  | ||||||
|             // Already initialized.
 |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Get tracker URL and username.
 |  | ||||||
|         let trackerData = await this.getTrackerData(); |  | ||||||
| 
 |  | ||||||
|         this.url = trackerData.url; |  | ||||||
|         this.username = trackerData.username; |  | ||||||
| 
 |  | ||||||
|         const parsed = Url.parse(this.url); |  | ||||||
|         this.ssl = parsed.protocol == 'https'; |  | ||||||
|         this.host = parsed.domain; |  | ||||||
|         this.uri = parsed.path; |  | ||||||
| 
 |  | ||||||
|         // Get the password.
 |  | ||||||
|         const keytar = require('keytar'); |  | ||||||
| 
 |  | ||||||
|         this.password = await keytar.getPassword('mdk-jira-password', this.username); // Use same service name as mdk.
 |  | ||||||
| 
 |  | ||||||
|         if (!this.password) { |  | ||||||
|             // Ask the user.
 |  | ||||||
|             this.password = await this.askPassword(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         while (!this.initialized) { |  | ||||||
|             try { |  | ||||||
|                 await this.getServerInfo(); |  | ||||||
| 
 |  | ||||||
|                 this.initialized = true; |  | ||||||
|                 keytar.setPassword('mdk-jira-password', this.username, this.password); |  | ||||||
|             } catch (error) { |  | ||||||
|                 console.log('Error connecting to the server. Please make sure you entered the data correctly.', error); |  | ||||||
|                 if (trackerData.fromInput) { |  | ||||||
|                     // User entered the data manually, ask him again.
 |  | ||||||
|                     trackerData = await this.askTrackerData(); |  | ||||||
| 
 |  | ||||||
|                     this.url = trackerData.url; |  | ||||||
|                     this.username = trackerData.username; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 this.password = await this.askPassword(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Check if a certain issue could be a security issue. |  | ||||||
|      * |  | ||||||
|      * @param key Key to identify the issue. E.g. MOBILE-1234. |  | ||||||
|      * @return Promise resolved with boolean: whether it's a security issue. |  | ||||||
|      */ |  | ||||||
|     async isSecurityIssue(key) { |  | ||||||
|         const issue = await this.getIssue(key, 'security'); |  | ||||||
| 
 |  | ||||||
|         return issue.fields && !!issue.fields.security; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Sends a request to the server and returns the data. |  | ||||||
|      * |  | ||||||
|      * @param uri URI to add the the Jira URL. |  | ||||||
|      * @param method Method to use. Defaults to 'GET'. |  | ||||||
|      * @param params Params to send as GET params (in the URL). |  | ||||||
|      * @param data JSON string with the data to send as POST/PUT params. |  | ||||||
|      * @param headers Headers to send. |  | ||||||
|      * @return Promise resolved with the result. |  | ||||||
|      */ |  | ||||||
|     request(uri, method, params, data, headers) { |  | ||||||
|         uri = uri || ''; |  | ||||||
|         method = (method || 'GET').toUpperCase(); |  | ||||||
|         data = data || ''; |  | ||||||
|         params = params || {}; |  | ||||||
|         headers = headers || {}; |  | ||||||
|         headers['Content-Type'] = 'application/json'; |  | ||||||
| 
 |  | ||||||
|         return new Promise((resolve, reject) => { |  | ||||||
| 
 |  | ||||||
|             // Build the request URL.
 |  | ||||||
|             const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params); |  | ||||||
| 
 |  | ||||||
|             // Initialize the request.
 |  | ||||||
|             const options = { |  | ||||||
|                 method: method, |  | ||||||
|                 auth: `${this.username}:${this.password}`, |  | ||||||
|                 headers: headers, |  | ||||||
|             }; |  | ||||||
|             const buildRequest = https.request(url, options); |  | ||||||
| 
 |  | ||||||
|             // Add data.
 |  | ||||||
|             if (data) { |  | ||||||
|                 buildRequest.write(data); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Treat response.
 |  | ||||||
|             buildRequest.on('response', (response) => { |  | ||||||
|                 // Read the result.
 |  | ||||||
|                 let result = ''; |  | ||||||
|                 response.on('data', (chunk) => { |  | ||||||
|                     result += chunk; |  | ||||||
|                 }); |  | ||||||
|                 response.on('end', () => { |  | ||||||
|                     try { |  | ||||||
|                         result = JSON.parse(result); |  | ||||||
|                     } catch (error) { |  | ||||||
|                         // Leave it as text.
 |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     resolve({ |  | ||||||
|                         status: response.statusCode, |  | ||||||
|                         data: result, |  | ||||||
|                     }); |  | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             buildRequest.on('error', (e) => { |  | ||||||
|                 reject(e); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             // Send the request.
 |  | ||||||
|             buildRequest.end(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Sets a set of fields for a certain issue in Jira. |  | ||||||
|      * |  | ||||||
|      * @param issueId Key to identify the issue. E.g. MOBILE-1234. |  | ||||||
|      * @param updates Object with the fields to update. |  | ||||||
|      * @return Promise resolved when done. |  | ||||||
|      */ |  | ||||||
|     async setCustomFields(issueId, updates) { |  | ||||||
|         const issue = await this.getIssue(issueId); |  | ||||||
|         const update = {'fields': {}}; |  | ||||||
| 
 |  | ||||||
|         // Detect which fields have changed.
 |  | ||||||
|         for (const updateName in updates) { |  | ||||||
|             const updateValue = updates[updateName]; |  | ||||||
|             const remoteValue = issue.named[updateName]; |  | ||||||
| 
 |  | ||||||
|             if (!remoteValue || remoteValue != updateValue) { |  | ||||||
|                 // Map the label of the field with the field code.
 |  | ||||||
|                 let fieldKey; |  | ||||||
| 
 |  | ||||||
|                 for (const id in issue.names) { |  | ||||||
|                     if (issue.names[id] == updateName) { |  | ||||||
|                         fieldKey = id; |  | ||||||
|                         break; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (!fieldKey) { |  | ||||||
|                     throw new Error(`Could not find the field named ${updateName}.`); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 update.fields[fieldKey] = updateValue; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (!Object.keys(update.fields).length) { |  | ||||||
|             // No fields to update.
 |  | ||||||
|             console.log('No updates required.') |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const response = await this.request(`issue/${key}`, 'PUT', null, JSON.stringify(update)); |  | ||||||
| 
 |  | ||||||
|         if (response.status != 204) { |  | ||||||
|             throw new Error(`Issue was not updated: ${response.status}`, response.data); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         console.log('Issue updated successfully.'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Upload a new attachment to an issue. |  | ||||||
|      * |  | ||||||
|      * @param key Key to identify the issue. E.g. MOBILE-1234. |  | ||||||
|      * @param filePath Path to the file to upload. |  | ||||||
|      * @return Promise resolved when done. |  | ||||||
|      */ |  | ||||||
|     async upload(key, filePath) { |  | ||||||
| 
 |  | ||||||
|         const uri = `issue/${key}/attachments`; |  | ||||||
|         const headers = { |  | ||||||
|             'X-Atlassian-Token': 'nocheck', |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const response = await this.uploadFile(uri, 'file', filePath, headers); |  | ||||||
| 
 |  | ||||||
|         if (response.status != 200) { |  | ||||||
|             throw new Error('Could not upload file to Jira issue'); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         console.log('File successfully uploaded.') |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Upload a file to Jira. |  | ||||||
|      * |  | ||||||
|      * @param uri URI to add the the Jira URL. |  | ||||||
|      * @param fieldName Name of the form field where to put the file. |  | ||||||
|      * @param filePath Path to the file. |  | ||||||
|      * @param headers Headers. |  | ||||||
|      * @return Promise resolved with the result. |  | ||||||
|      */ |  | ||||||
|     async uploadFile(uri, fieldName, filePath, headers) { |  | ||||||
|         uri = uri || ''; |  | ||||||
|         headers = headers || {}; |  | ||||||
|         headers['Content-Type'] = 'multipart/form-data'; |  | ||||||
| 
 |  | ||||||
|         return new Promise((resolve) => { |  | ||||||
|             // Add the file to the form data.
 |  | ||||||
|             const formData = {}; |  | ||||||
|             formData[fieldName] = { |  | ||||||
|                 value: fs.createReadStream(filePath), |  | ||||||
|                 options: { |  | ||||||
|                     filename: filePath.substr(filePath.lastIndexOf('/') + 1), |  | ||||||
|                     contentType: 'multipart/form-data', |  | ||||||
|                 }, |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             // Perform the request.
 |  | ||||||
|             const options = { |  | ||||||
|                 url: this.buildRequestUrl(uri), |  | ||||||
|                 method: 'POST', |  | ||||||
|                 headers: headers, |  | ||||||
|                 auth: { |  | ||||||
|                     user: this.username, |  | ||||||
|                     pass: this.password, |  | ||||||
|                 }, |  | ||||||
|                 formData: formData, |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             request(options, (_err, httpResponse, body) => { |  | ||||||
|                 resolve({ |  | ||||||
|                     status: httpResponse.statusCode, |  | ||||||
|                     data: body, |  | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = new Jira(); |  | ||||||
							
								
								
									
										118
									
								
								gulp/task-build-icons-json.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								gulp/task-build-icons-json.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | |||||||
|  | // (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.
 | ||||||
|  | 
 | ||||||
|  | const { writeFile, readdirSync, statSync, readFileSync } = require('fs'); | ||||||
|  | 
 | ||||||
|  | const FONTS_PATH = 'src/assets/fonts'; | ||||||
|  | const ICONS_JSON_FILE_PATH = 'src/assets/fonts/icons.json'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Get object with the map of icons for all fonts. | ||||||
|  |  * | ||||||
|  |  * @returns Icons map. | ||||||
|  |  */ | ||||||
|  | function getIconsMap() { | ||||||
|  |     const config = JSON.parse(readFileSync('moodle.config.json')); | ||||||
|  |     let icons = {}; | ||||||
|  | 
 | ||||||
|  |     const fonts = readdirSync(FONTS_PATH); | ||||||
|  |     fonts.forEach(font => { | ||||||
|  |         const fontPath = `${FONTS_PATH}/${font}`; | ||||||
|  |         if (statSync(fontPath).isFile()) { | ||||||
|  |             // Not a font, ignore.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         icons = { | ||||||
|  |             ...icons, | ||||||
|  |             ...getFontIconsMap(config.iconsPrefixes, font, fontPath), | ||||||
|  |         }; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return icons; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Get object with the map of icons for a certain font. | ||||||
|  |  * | ||||||
|  |  * @param prefixes Prefixes to add to the icons. | ||||||
|  |  * @param fontName Font name. | ||||||
|  |  * @param fontPath Font path. | ||||||
|  |  * @returns Icons map. | ||||||
|  |  */ | ||||||
|  | function getFontIconsMap(prefixes, fontName, fontPath) { | ||||||
|  |     const icons = {}; | ||||||
|  |     const fontLibraries = readdirSync(fontPath); | ||||||
|  | 
 | ||||||
|  |     fontLibraries.forEach(libraryName => { | ||||||
|  |         const libraryPath = `${fontPath}/${libraryName}`; | ||||||
|  |         if (statSync(libraryPath).isFile()) { | ||||||
|  |             // Not a font library, ignore.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const libraryPrefixes = prefixes?.[fontName]?.[libraryName]; | ||||||
|  |         if (!libraryPrefixes || !libraryPrefixes.length) { | ||||||
|  |             console.warn(`WARNING: There is no prefix for the library ${fontName}/${libraryName}. Adding icons without prefix is ` + | ||||||
|  |                 'discouraged, please add a prefix for your library in moodle.config.json file, in the iconsPrefixes property.'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const libraryIcons = readdirSync(libraryPath); | ||||||
|  |         libraryIcons.forEach(iconFileName => { | ||||||
|  |             if (!iconFileName.endsWith('.svg')) { | ||||||
|  |                 // Only accept svg files.
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (iconFileName.includes('_')) { | ||||||
|  |                 throw Error(`Icon ${libraryPath}/${iconFileName} has an invalid name, it cannot contain '_'. ` | ||||||
|  |                     + 'Please rename it to use \'-\' instead.'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const iconName = iconFileName.replace('.svg', ''); | ||||||
|  |             const iconPath = `${libraryPath}/${iconFileName}`.replace('src/', ''); | ||||||
|  | 
 | ||||||
|  |             if (!libraryPrefixes || !libraryPrefixes.length) { | ||||||
|  |                 icons[iconName] = iconPath; | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             libraryPrefixes.forEach(prefix => { | ||||||
|  |                 icons[`${prefix}-${iconName}`] = iconPath; | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return icons; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Task to build a JSON file with the list of icons to add to Ionicons. | ||||||
|  |  */ | ||||||
|  | class BuildIconsJsonTask { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Run the task. | ||||||
|  |      * | ||||||
|  |      * @param done Function to call when done. | ||||||
|  |      */ | ||||||
|  |     run(done) { | ||||||
|  |         const icons = getIconsMap(); | ||||||
|  | 
 | ||||||
|  |         writeFile(ICONS_JSON_FILE_PATH, JSON.stringify(icons), done); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = BuildIconsJsonTask; | ||||||
| @ -1,280 +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.
 |  | ||||||
| 
 |  | ||||||
| const gulp = require('gulp'); |  | ||||||
| const inquirer = require('inquirer'); |  | ||||||
| const DevConfig = require('./dev-config'); |  | ||||||
| const Git = require('./git'); |  | ||||||
| const Jira = require('./jira'); |  | ||||||
| const Utils = require('./utils'); |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Task to push a git branch and update tracker issue. |  | ||||||
|  */ |  | ||||||
| class PushTask { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Ask the user whether he wants to continue. |  | ||||||
|      * |  | ||||||
|      * @return Promise resolved with boolean: true if he wants to continue. |  | ||||||
|      */ |  | ||||||
|     async askConfirmContinue() { |  | ||||||
|         const answer = await inquirer.prompt([ |  | ||||||
|             { |  | ||||||
|                 type: 'input', |  | ||||||
|                 name: 'confirm', |  | ||||||
|                 message: 'Are you sure you want to continue?', |  | ||||||
|                 default: 'n', |  | ||||||
|             }, |  | ||||||
|         ]); |  | ||||||
| 
 |  | ||||||
|         return answer.confirm == 'y'; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Push a patch to the tracker and remove the previous one. |  | ||||||
|      * |  | ||||||
|      * @param branch Branch name. |  | ||||||
|      * @param branchData Parsed branch data. |  | ||||||
|      * @param remote Remote used. |  | ||||||
|      * @return Promise resolved when done. |  | ||||||
|      */ |  | ||||||
|     async pushPatch(branch, branchData, remote) { |  | ||||||
|         const headCommit = await Git.getHeadCommit(branch, branchData); |  | ||||||
| 
 |  | ||||||
|         if (!headCommit) { |  | ||||||
|             throw new Error('Head commit not resolved, abort pushing patch.'); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Create the patch file.
 |  | ||||||
|         const fileName = branch + '.patch'; |  | ||||||
|         const tmpPatchPath = `./tmp/${fileName}`; |  | ||||||
| 
 |  | ||||||
|         await Git.createPatch(`${headCommit}...${branch}`, tmpPatchPath); |  | ||||||
|         console.log('Git patch created'); |  | ||||||
| 
 |  | ||||||
|         // Check if there is an attachment with same name in the issue.
 |  | ||||||
|         const issue = await Jira.getIssue(branchData.issue, 'attachment'); |  | ||||||
| 
 |  | ||||||
|         let existingAttachmentId; |  | ||||||
|         const attachments = (issue.fields && issue.fields.attachment) || []; |  | ||||||
|         for (const i in attachments) { |  | ||||||
|             if (attachments[i].filename == fileName) { |  | ||||||
|                 // Found an existing attachment with the same name, we keep track of it.
 |  | ||||||
|                 existingAttachmentId = attachments[i].id; |  | ||||||
|                 break |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Push the patch to the tracker.
 |  | ||||||
|         console.log(`Uploading patch ${fileName} to the tracker...`); |  | ||||||
|         await Jira.upload(branchData.issue, tmpPatchPath); |  | ||||||
| 
 |  | ||||||
|         if (existingAttachmentId) { |  | ||||||
|             // On success, deleting file that was there before.
 |  | ||||||
|             try { |  | ||||||
|                 console.log('Deleting older patch...') |  | ||||||
|                 await Jira.deleteAttachment(existingAttachmentId); |  | ||||||
|             } catch (error) { |  | ||||||
|                 console.log('Could not delete older attachment.'); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Run the task. |  | ||||||
|      * |  | ||||||
|      * @param args Command line arguments. |  | ||||||
|      * @param done Function to call when done. |  | ||||||
|      */ |  | ||||||
|     async run(args, done) { |  | ||||||
|         try { |  | ||||||
|             const remote = args.remote || DevConfig.get('upstreamRemote', 'origin'); |  | ||||||
|             let branch = args.branch; |  | ||||||
|             const force = !!args.force; |  | ||||||
| 
 |  | ||||||
|             if (!branch) { |  | ||||||
|                 branch = await Git.getCurrentBranch(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (!branch) { |  | ||||||
|                 throw new Error('Cannot determine the current branch. Please make sure youu aren\'t in detached HEAD state'); |  | ||||||
|             } else if (branch == 'HEAD') { |  | ||||||
|                 throw new Error('Cannot push HEAD branch'); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Parse the branch to get the project and issue number.
 |  | ||||||
|             const branchData = Utils.parseBranch(branch); |  | ||||||
|             const keepRunning = await this.validateCommitMessages(branchData); |  | ||||||
| 
 |  | ||||||
|             if (!keepRunning) { |  | ||||||
|                 // Last commit not valid, stop.
 |  | ||||||
|                 console.log('Exiting...'); |  | ||||||
|                 done(); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (!args.patch) { |  | ||||||
|                 // Check if it's a security issue to force patch mode.
 |  | ||||||
|                 try { |  | ||||||
|                     args.patch = await Jira.isSecurityIssue(branchData.issue); |  | ||||||
| 
 |  | ||||||
|                     if (args.patch) { |  | ||||||
|                         console.log(`${branchData.issue} appears to be a security issue, switching to patch mode...`); |  | ||||||
|                     } |  | ||||||
|                 } catch (error) { |  | ||||||
|                     console.log(`Could not check if ${branchData.issue} is a security issue.`); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (args.patch) { |  | ||||||
|                 // Create and upload a patch file.
 |  | ||||||
|                 await this.pushPatch(branch, branchData, remote); |  | ||||||
|             } else { |  | ||||||
|                 // Push the branch.
 |  | ||||||
|                 console.log(`Pushing branch ${branch} to remote ${remote}...`); |  | ||||||
|                 await Git.push(remote, branch, force); |  | ||||||
| 
 |  | ||||||
|                 // Update tracker info.
 |  | ||||||
|                 console.log(`Branch pushed, update tracker info...`); |  | ||||||
|                 await this.updateTrackerGitInfo(branch, branchData, remote); |  | ||||||
|             } |  | ||||||
|         } catch (error) { |  | ||||||
|             console.error(error); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         done(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Update git info in the tracker issue. |  | ||||||
|      * |  | ||||||
|      * @param branch Branch name. |  | ||||||
|      * @param branchData Parsed branch data. |  | ||||||
|      * @param remote Remote used. |  | ||||||
|      * @return Promise resolved when done. |  | ||||||
|      */ |  | ||||||
|     async updateTrackerGitInfo(branch, branchData, remote) { |  | ||||||
|         // Get the repository data for the project.
 |  | ||||||
|         let repositoryUrl = DevConfig.get(branchData.project + '.repositoryUrl'); |  | ||||||
|         let diffUrlTemplate = DevConfig.get(branchData.project + '.diffUrlTemplate', ''); |  | ||||||
| 
 |  | ||||||
|         if (!repositoryUrl) { |  | ||||||
|             // Calculate the repositoryUrl based on the remote URL.
 |  | ||||||
|             repositoryUrl = await Git.getRemoteUrl(remote); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Make sure the repository URL uses the regular format.
 |  | ||||||
|         repositoryUrl = repositoryUrl.replace(/^(git@|git:\/\/)/, 'https://') |  | ||||||
|                                      .replace(/\.git$/, '') |  | ||||||
|                                      .replace('github.com:', 'github.com/'); |  | ||||||
| 
 |  | ||||||
|         if (!diffUrlTemplate) { |  | ||||||
|             diffUrlTemplate = Utils.concatenatePaths([repositoryUrl, 'compare/%headcommit%...%branch%']); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Now create the git URL for the repository.
 |  | ||||||
|         const repositoryGitUrl = repositoryUrl.replace(/^https?:\/\//, 'git://') + '.git'; |  | ||||||
| 
 |  | ||||||
|         // Search HEAD commit to put in the diff URL.
 |  | ||||||
|         console.log ('Searching for head commit...'); |  | ||||||
|         let headCommit = await Git.getHeadCommit(branch, branchData); |  | ||||||
| 
 |  | ||||||
|         if (!headCommit) { |  | ||||||
|             throw new Error('Head commit not resolved, aborting update of tracker fields'); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         headCommit = headCommit.substr(0, 10); |  | ||||||
|         console.log(`Head commit resolved to ${headCommit}`); |  | ||||||
| 
 |  | ||||||
|         // Calculate last properties needed.
 |  | ||||||
|         const diffUrl = diffUrlTemplate.replace('%branch%', branch).replace('%headcommit%', headCommit); |  | ||||||
|         const fieldRepositoryUrl = DevConfig.get('tracker.fieldnames.repositoryurl', 'Pull  from Repository'); |  | ||||||
|         const fieldBranch = DevConfig.get('tracker.fieldnames.branch', 'Pull Master Branch'); |  | ||||||
|         const fieldDiffUrl = DevConfig.get('tracker.fieldnames.diffurl', 'Pull Master Diff URL'); |  | ||||||
| 
 |  | ||||||
|         // Update tracker fields.
 |  | ||||||
|         const updates = {}; |  | ||||||
|         updates[fieldRepositoryUrl] = repositoryGitUrl; |  | ||||||
|         updates[fieldBranch] = branch; |  | ||||||
|         updates[fieldDiffUrl] = diffUrl; |  | ||||||
| 
 |  | ||||||
|         console.log('Setting tracker fields...'); |  | ||||||
|         await Jira.setCustomFields(branchData.issue, updates); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Validate commit messages comparing them with the branch name. |  | ||||||
|      * |  | ||||||
|      * @param branchData Parsed branch data. |  | ||||||
|      * @return True if value is ok or the user wants to continue anyway, false to stop. |  | ||||||
|      */ |  | ||||||
|     async validateCommitMessages(branchData) { |  | ||||||
|         const messages = await Git.messages(30); |  | ||||||
| 
 |  | ||||||
|         let numConsecutive = 0; |  | ||||||
|         let wrongCommitCandidate = null; |  | ||||||
| 
 |  | ||||||
|         for (let i = 0; i < messages.length; i++) { |  | ||||||
|             const message = messages[i]; |  | ||||||
|             const issue = Utils.getIssueFromCommitMessage(message); |  | ||||||
| 
 |  | ||||||
|             if (!issue || issue != branchData.issue) { |  | ||||||
|                 if (i === 0) { |  | ||||||
|                     // Last commit is wrong, it shouldn't happen. Ask the user if he wants to continue.
 |  | ||||||
|                     if (!issue) { |  | ||||||
|                         console.log('The issue number could not be found in the last commit message.'); |  | ||||||
|                         console.log(`Commit: ${message}`); |  | ||||||
|                     } else if (issue != branchData.issue) { |  | ||||||
|                         console.log('The issue number in the last commit does not match the branch being pushed to.'); |  | ||||||
|                         console.log(`Branch: ${branchData.issue} vs. commit: ${issue}`); |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     return this.askConfirmContinue(); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 numConsecutive++; |  | ||||||
|                 if (numConsecutive > 2) { |  | ||||||
|                     // 3 consecutive commits with different branch, probably the branch commits are over. Everything OK.
 |  | ||||||
|                     return true; |  | ||||||
| 
 |  | ||||||
|                 // Don't treat a merge pull request commit as a wrong commit between right commits.
 |  | ||||||
|                 // The current push could be a quick fix after a merge.
 |  | ||||||
|                 } else if (!wrongCommitCandidate && message.indexOf('Merge pull request') == -1) { |  | ||||||
|                     wrongCommitCandidate = { |  | ||||||
|                         message: message, |  | ||||||
|                         issue: issue, |  | ||||||
|                         index: i, |  | ||||||
|                     }; |  | ||||||
|                 } |  | ||||||
|             } else if (wrongCommitCandidate) { |  | ||||||
|                 // We've found a commit with the branch name after a commit with a different branch. Probably wrong commit.
 |  | ||||||
|                 if (!wrongCommitCandidate.issue) { |  | ||||||
|                     console.log('The issue number could not be found in one of the commit messages.'); |  | ||||||
|                     console.log(`Commit: ${wrongCommitCandidate.message}`); |  | ||||||
|                 } else { |  | ||||||
|                     console.log('The issue number in a certain commit does not match the branch being pushed to.'); |  | ||||||
|                     console.log(`Branch: ${branchData.issue} vs. commit: ${wrongCommitCandidate.issue}`); |  | ||||||
|                     console.log(`Commit message: ${wrongCommitCandidate.message}`); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 return this.askConfirmContinue(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = PushTask; |  | ||||||
							
								
								
									
										79
									
								
								gulp/url.js
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								gulp/url.js
									
									
									
									
									
								
							| @ -1,79 +0,0 @@ | |||||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 |  | ||||||
| //
 |  | ||||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 |  | ||||||
| // you may not use this file except in compliance with the License.
 |  | ||||||
| // You may obtain a copy of the License at
 |  | ||||||
| //
 |  | ||||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 |  | ||||||
| //
 |  | ||||||
| // Unless required by applicable law or agreed to in writing, software
 |  | ||||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 |  | ||||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 |  | ||||||
| // See the License for the specific language governing permissions and
 |  | ||||||
| // limitations under the License.
 |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Class with helper functions for urls. |  | ||||||
|  */ |  | ||||||
| class Url { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Add params to a URL. |  | ||||||
|      * |  | ||||||
|      * @param url URL to add the params to. |  | ||||||
|      * @param params Object with the params to add. |  | ||||||
|      * @return URL with params. |  | ||||||
|      */ |  | ||||||
|     static addParamsToUrl(url, params) { |  | ||||||
|         let separator = url.indexOf('?') != -1 ? '&' : '?'; |  | ||||||
| 
 |  | ||||||
|         for (const key in params) { |  | ||||||
|             let value = params[key]; |  | ||||||
| 
 |  | ||||||
|             // Ignore objects.
 |  | ||||||
|             if (typeof value != 'object') { |  | ||||||
|                 url += separator + key + '=' + value; |  | ||||||
|                 separator = '&'; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return url; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Parse parts of a url, using an implicit protocol if it is missing from the url. |  | ||||||
|      * |  | ||||||
|      * @param url Url. |  | ||||||
|      * @return Url parts. |  | ||||||
|      */ |  | ||||||
|     static parse(url) { |  | ||||||
|         // Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B.
 |  | ||||||
|         const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/); |  | ||||||
| 
 |  | ||||||
|         if (!match) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const host = match[4] || ''; |  | ||||||
| 
 |  | ||||||
|         // Get the credentials and the port from the host.
 |  | ||||||
|         const [domainAndPort, credentials] = host.split('@').reverse(); |  | ||||||
|         const [domain, port] = domainAndPort.split(':'); |  | ||||||
|         const [username, password] = credentials ? credentials.split(':') : []; |  | ||||||
| 
 |  | ||||||
|         // Prepare parts replacing empty strings with undefined.
 |  | ||||||
|         return { |  | ||||||
|             protocol: match[2] || undefined, |  | ||||||
|             domain: domain || undefined, |  | ||||||
|             port: port || undefined, |  | ||||||
|             credentials: credentials || undefined, |  | ||||||
|             username: username || undefined, |  | ||||||
|             password: password || undefined, |  | ||||||
|             path: match[5] || undefined, |  | ||||||
|             query: match[7] || undefined, |  | ||||||
|             fragment: match[9] || undefined, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = Url; |  | ||||||
							
								
								
									
										120
									
								
								gulp/utils.js
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								gulp/utils.js
									
									
									
									
									
								
							| @ -1,120 +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.
 |  | ||||||
| 
 |  | ||||||
| const DevConfig = require('./dev-config'); |  | ||||||
| const DEFAULT_ISSUE_REGEX = '^(MOBILE)[-_]([0-9]+)'; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Class with some utility functions. |  | ||||||
|  */ |  | ||||||
| class Utils { |  | ||||||
|     /** |  | ||||||
|      * Concatenate several paths, adding a slash between them if needed. |  | ||||||
|      * |  | ||||||
|      * @param paths List of paths. |  | ||||||
|      * @return Concatenated path. |  | ||||||
|      */ |  | ||||||
|     static concatenatePaths(paths) { |  | ||||||
|         if (!paths.length) { |  | ||||||
|             return ''; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Remove all slashes between paths.
 |  | ||||||
|         for (let i = 0; i < paths.length; i++) { |  | ||||||
|             if (!paths[i]) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (i === 0) { |  | ||||||
|                 paths[i] = String(paths[i]).replace(/\/+$/g, ''); |  | ||||||
|             } else if (i === paths.length - 1) { |  | ||||||
|                 paths[i] = String(paths[i]).replace(/^\/+/g, ''); |  | ||||||
|             } else { |  | ||||||
|                 paths[i] = String(paths[i]).replace(/^\/+|\/+$/g, ''); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Remove empty paths.
 |  | ||||||
|         paths = paths.filter(path => !!path); |  | ||||||
| 
 |  | ||||||
|         return paths.join('/'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Get command line arguments. |  | ||||||
|      * |  | ||||||
|      * @return Object with command line arguments. |  | ||||||
|      */ |  | ||||||
|     static getCommandLineArguments() { |  | ||||||
| 
 |  | ||||||
|         let args = {}; |  | ||||||
|         let curOpt; |  | ||||||
| 
 |  | ||||||
|         for (const argument of process.argv) { |  | ||||||
|             const thisOpt = argument.trim(); |  | ||||||
|             const option = thisOpt.replace(/^\-+/, ''); |  | ||||||
| 
 |  | ||||||
|             if (option === thisOpt) { |  | ||||||
|                 // argument value
 |  | ||||||
|                 if (curOpt) { |  | ||||||
|                     args[curOpt] = option; |  | ||||||
|                 } |  | ||||||
|                 curOpt = null; |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 // Argument name.
 |  | ||||||
|                 curOpt = option; |  | ||||||
|                 args[curOpt] = true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return args; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Given a commit message, return the issue name (e.g. MOBILE-1234). |  | ||||||
|      * |  | ||||||
|      * @param commit Commit message. |  | ||||||
|      * @return Issue name. |  | ||||||
|      */ |  | ||||||
|     static getIssueFromCommitMessage(commit) { |  | ||||||
|         const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); |  | ||||||
|         const matches = commit.match(regex); |  | ||||||
| 
 |  | ||||||
|         return matches && matches[0]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Parse a branch name to extract some data. |  | ||||||
|      * |  | ||||||
|      * @param branch Branch name to parse. |  | ||||||
|      * @return Data. |  | ||||||
|      */ |  | ||||||
|     static parseBranch(branch) { |  | ||||||
|         const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i'); |  | ||||||
| 
 |  | ||||||
|         const matches = branch.match(regex); |  | ||||||
|         if (!matches || matches.length < 3) { |  | ||||||
|             throw new Error(`Error parsing branch ${branch}`); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return { |  | ||||||
|             issue: matches[0], |  | ||||||
|             project: matches[1], |  | ||||||
|             issueNumber: matches[2], |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| module.exports = Utils; |  | ||||||
							
								
								
									
										14
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								gulpfile.js
									
									
									
									
									
								
							| @ -15,9 +15,8 @@ | |||||||
| const BuildLangTask = require('./gulp/task-build-lang'); | const BuildLangTask = require('./gulp/task-build-lang'); | ||||||
| const BuildBehatPluginTask = require('./gulp/task-build-behat-plugin'); | const BuildBehatPluginTask = require('./gulp/task-build-behat-plugin'); | ||||||
| const BuildEnvTask = require('./gulp/task-build-env'); | const BuildEnvTask = require('./gulp/task-build-env'); | ||||||
| const PushTask = require('./gulp/task-push'); | const BuildIconsJsonTask = require('./gulp/task-build-icons-json'); | ||||||
| const OverrideLangTask = require('./gulp/task-override-lang'); | const OverrideLangTask = require('./gulp/task-override-lang'); | ||||||
| const Utils = require('./gulp/utils'); |  | ||||||
| const gulp = require('gulp'); | const gulp = require('gulp'); | ||||||
| 
 | 
 | ||||||
| const paths = { | const paths = { | ||||||
| @ -30,8 +29,6 @@ const paths = { | |||||||
|     ], |     ], | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const args = Utils.getCommandLineArguments(); |  | ||||||
| 
 |  | ||||||
| // Build the language files into a single file per language.
 | // Build the language files into a single file per language.
 | ||||||
| gulp.task('lang', (done) => { | gulp.task('lang', (done) => { | ||||||
|     new BuildLangTask().run(paths.lang, done); |     new BuildLangTask().run(paths.lang, done); | ||||||
| @ -47,6 +44,10 @@ gulp.task('env', (done) => { | |||||||
|     new BuildEnvTask().run(done); |     new BuildEnvTask().run(done); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | gulp.task('icons', (done) => { | ||||||
|  |     new BuildIconsJsonTask().run(done); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| // Build a Moodle plugin to run Behat tests.
 | // Build a Moodle plugin to run Behat tests.
 | ||||||
| if (BuildBehatPluginTask.isBehatConfigured()) { | if (BuildBehatPluginTask.isBehatConfigured()) { | ||||||
|     gulp.task('behat', (done) => { |     gulp.task('behat', (done) => { | ||||||
| @ -54,15 +55,12 @@ if (BuildBehatPluginTask.isBehatConfigured()) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| gulp.task('push', (done) => { |  | ||||||
|     new PushTask().run(args, done); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| gulp.task( | gulp.task( | ||||||
|     'default', |     'default', | ||||||
|     gulp.parallel([ |     gulp.parallel([ | ||||||
|         'lang', |         'lang', | ||||||
|         'env', |         'env', | ||||||
|  |         'icons', | ||||||
|         ...(BuildBehatPluginTask.isBehatConfigured() ? ['behat'] : []) |         ...(BuildBehatPluginTask.isBehatConfigured() ? ['behat'] : []) | ||||||
|     ]), |     ]), | ||||||
| ); | ); | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| const { pathsToModuleNameMapper } = require('ts-jest/utils'); | const { pathsToModuleNameMapper } = require('ts-jest'); | ||||||
| const { compilerOptions } = require('./tsconfig'); | const { compilerOptions } = require('./tsconfig'); | ||||||
| 
 | 
 | ||||||
| module.exports = { | module.exports = { | ||||||
| @ -9,17 +9,6 @@ module.exports = { | |||||||
|         'src/**/*.{ts,html}', |         'src/**/*.{ts,html}', | ||||||
|         '!src/testing/**/*', |         '!src/testing/**/*', | ||||||
|     ], |     ], | ||||||
|     transform: { |     transformIgnorePatterns: ['node_modules/(?!@stencil|@angular|@ionic|@moodlehq|@ngx-translate|swiper)'], | ||||||
|         '^.+\\.(ts|html)$': 'ts-jest', |     moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src/' }), | ||||||
|     }, |  | ||||||
|     transformIgnorePatterns: ['node_modules/(?!@ionic-native|@ionic|@moodlehq/ionic-native-push)'], |  | ||||||
|     moduleNameMapper: { |  | ||||||
|         ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src/' }), |  | ||||||
|         '^!raw-loader!.*': 'jest-raw-loader', |  | ||||||
|     }, |  | ||||||
|     globals: { |  | ||||||
|         'ts-jest': { |  | ||||||
|             tsconfig: './tsconfig.test.json', |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -214,10 +214,7 @@ class behat_app extends behat_app_helper { | |||||||
|             return true; |             return true; | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         $this->wait_for_pending_js(); |         $this->wait_animations_done(); | ||||||
| 
 |  | ||||||
|         // Wait scroll animation to finish.
 |  | ||||||
|         $this->getSession()->wait(300); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -263,10 +260,7 @@ class behat_app extends behat_app_helper { | |||||||
|             throw new DriverException('Error when swiping - ' . $result); |             throw new DriverException('Error when swiping - ' . $result); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         $this->wait_for_pending_js(); |         $this->wait_animations_done(); | ||||||
| 
 |  | ||||||
|         // Wait swipe animation to finish.
 |  | ||||||
|         $this->getSession()->wait(300); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -689,10 +683,7 @@ class behat_app extends behat_app_helper { | |||||||
|                 return true; |                 return true; | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             $this->wait_for_pending_js(); |             $this->wait_animations_done(); | ||||||
| 
 |  | ||||||
|             // Wait for UI to settle after refreshing.
 |  | ||||||
|             $this->getSession()->wait(300); |  | ||||||
| 
 | 
 | ||||||
|             if (is_null($locator)) { |             if (is_null($locator)) { | ||||||
|                 return; |                 return; | ||||||
| @ -790,13 +781,10 @@ class behat_app extends behat_app_helper { | |||||||
|     /** |     /** | ||||||
|      * Sets a field to the given text value in the app. |      * Sets a field to the given text value in the app. | ||||||
|      * |      * | ||||||
|      * Currently this only works for input fields which must be identified using a partial or |  | ||||||
|      * exact match on the placeholder text. |  | ||||||
|      * |  | ||||||
|      * @Given /^I set the field "((?:[^"]|\\")+)" to "((?:[^"]|\\")*)" in the app$/ |      * @Given /^I set the field "((?:[^"]|\\")+)" to "((?:[^"]|\\")*)" in the app$/ | ||||||
|      * @param string $field Text identifying field |      * @param string $field Text identifying the field. | ||||||
|      * @param string $value Value for field |      * @param string $value Value to set. In select fields, this can be either the value or text included in the select option. | ||||||
|      * @throws DriverException If the field set doesn't work |      * @throws DriverException If the field set doesn't work. | ||||||
|      */ |      */ | ||||||
|     public function i_set_the_field_in_the_app(string $field, string $value) { |     public function i_set_the_field_in_the_app(string $field, string $value) { | ||||||
|         $field = addslashes_js($field); |         $field = addslashes_js($field); | ||||||
|  | |||||||
| @ -641,4 +641,15 @@ EOF; | |||||||
|             return $text; |             return $text; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Wait until animations have finished. | ||||||
|  |      */ | ||||||
|  |     protected function wait_animations_done() { | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  | 
 | ||||||
|  |         // Ideally, we wouldn't wait a fixed amount of time. But it is not straightforward to wait for animations
 | ||||||
|  |         // to finish, so for now we'll just wait 300ms.
 | ||||||
|  |         usleep(300000); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -111,5 +111,16 @@ | |||||||
|         "long": 3500, |         "long": 3500, | ||||||
|         "sticky": 0 |         "sticky": 0 | ||||||
|     }, |     }, | ||||||
|     "disableTokenFile": false |     "disableTokenFile": false, | ||||||
|  |     "iconsPrefixes": { | ||||||
|  |         "font-awesome": { | ||||||
|  |             "brands": ["fab"], | ||||||
|  |             "regular": ["far"], | ||||||
|  |             "solid": ["fa", "fas"] | ||||||
|  |         }, | ||||||
|  |         "moodle": { | ||||||
|  |             "font-awesome": ["fam"], | ||||||
|  |             "moodle": ["moodle"] | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										46262
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										46262
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										203
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										203
									
								
								package.json
									
									
									
									
									
								
							| @ -45,39 +45,39 @@ | |||||||
|     "lang:create-langindex": "./scripts/create_langindex.sh" |     "lang:create-langindex": "./scripts/create_langindex.sh" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@angular/animations": "~10.0.14", |     "@angular/animations": "^16.2.0", | ||||||
|     "@angular/common": "~10.0.14", |     "@angular/common": "^16.2.0", | ||||||
|     "@angular/core": "~10.0.14", |     "@angular/compiler": "^16.2.0", | ||||||
|     "@angular/forms": "~10.0.14", |     "@angular/core": "^16.2.0", | ||||||
|     "@angular/platform-browser": "~10.0.14", |     "@angular/forms": "^16.2.0", | ||||||
|     "@angular/platform-browser-dynamic": "~10.0.14", |     "@angular/platform-browser": "^16.2.0", | ||||||
|     "@angular/router": "~10.0.14", |     "@angular/platform-browser-dynamic": "^16.2.0", | ||||||
|     "@ionic-native/badge": "^5.36.0", |     "@angular/router": "^16.2.0", | ||||||
|     "@ionic-native/camera": "^5.36.0", |     "@awesome-cordova-plugins/badge": "^6.3.0", | ||||||
|     "@ionic-native/chooser": "^5.36.0", |     "@awesome-cordova-plugins/camera": "^6.3.0", | ||||||
|     "@ionic-native/clipboard": "^5.36.0", |     "@awesome-cordova-plugins/clipboard": "^6.3.0", | ||||||
|     "@ionic-native/core": "^5.36.0", |     "@awesome-cordova-plugins/core": "^6.3.0", | ||||||
|     "@ionic-native/device": "^5.36.0", |     "@awesome-cordova-plugins/device": "^6.3.0", | ||||||
|     "@ionic-native/diagnostic": "^5.36.0", |     "@awesome-cordova-plugins/diagnostic": "^6.3.0", | ||||||
|     "@ionic-native/file": "^5.36.0", |     "@awesome-cordova-plugins/file": "^6.3.0", | ||||||
|     "@ionic-native/file-opener": "^5.36.0", |     "@awesome-cordova-plugins/file-opener": "^6.3.0", | ||||||
|     "@ionic-native/file-transfer": "^5.36.0", |     "@awesome-cordova-plugins/file-transfer": "^6.3.0", | ||||||
|     "@ionic-native/geolocation": "^5.36.0", |     "@awesome-cordova-plugins/geolocation": "^6.3.0", | ||||||
|     "@ionic-native/http": "^5.36.0", |     "@awesome-cordova-plugins/http": "^6.3.0", | ||||||
|     "@ionic-native/in-app-browser": "^5.36.0", |     "@awesome-cordova-plugins/in-app-browser": "^6.3.0", | ||||||
|     "@ionic-native/ionic-webview": "^5.36.0", |     "@awesome-cordova-plugins/ionic-webview": "^6.3.0", | ||||||
|     "@ionic-native/keyboard": "^5.36.0", |     "@awesome-cordova-plugins/keyboard": "^6.3.0", | ||||||
|     "@ionic-native/local-notifications": "^5.36.0", |     "@awesome-cordova-plugins/local-notifications": "^6.3.0", | ||||||
|     "@ionic-native/media-capture": "^5.36.0", |     "@awesome-cordova-plugins/media-capture": "^6.3.0", | ||||||
|     "@ionic-native/network": "^5.36.0", |     "@awesome-cordova-plugins/network": "^6.3.0", | ||||||
|     "@ionic-native/qr-scanner": "^5.36.0", |     "@awesome-cordova-plugins/push": "^6.3.0", | ||||||
|     "@ionic-native/splash-screen": "^5.36.0", |     "@awesome-cordova-plugins/splash-screen": "^6.3.0", | ||||||
|     "@ionic-native/sqlite": "^5.36.0", |     "@awesome-cordova-plugins/sqlite": "^6.3.0", | ||||||
|     "@ionic-native/status-bar": "^5.36.0", |     "@awesome-cordova-plugins/status-bar": "^6.3.0", | ||||||
|     "@ionic-native/web-intent": "^5.36.0", |     "@awesome-cordova-plugins/web-intent": "^6.3.0", | ||||||
|     "@ionic-native/zip": "^5.36.0", |     "@ionic/angular": "^7.6.1", | ||||||
|     "@ionic/angular": "^5.9.4", |     "@ionic/cordova-builders": "^10.0.0", | ||||||
|     "@moodlehq/cordova-plugin-advanced-http": "^3.3.1-moodle.1", |     "@moodlehq/cordova-plugin-advanced-http": "3.3.1-moodle.1", | ||||||
|     "@moodlehq/cordova-plugin-camera": "6.0.0-moodle.2", |     "@moodlehq/cordova-plugin-camera": "6.0.0-moodle.2", | ||||||
|     "@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5", |     "@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5", | ||||||
|     "@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3", |     "@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3", | ||||||
| @ -85,12 +85,11 @@ | |||||||
|     "@moodlehq/cordova-plugin-ionic-webview": "5.0.0-moodle.2", |     "@moodlehq/cordova-plugin-ionic-webview": "5.0.0-moodle.2", | ||||||
|     "@moodlehq/cordova-plugin-local-notification": "0.9.0-moodle.11", |     "@moodlehq/cordova-plugin-local-notification": "0.9.0-moodle.11", | ||||||
|     "@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.5", |     "@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.5", | ||||||
|     "@moodlehq/cordova-plugin-statusbar": "4.0.0-moodle.2", |     "@moodlehq/cordova-plugin-statusbar": "4.0.0-moodle.3", | ||||||
|     "@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1", |     "@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", |     "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7", | ||||||
|     "@ngx-translate/core": "^13.0.0", |     "@ngx-translate/core": "^15.0.0", | ||||||
|     "@ngx-translate/http-loader": "^6.0.0", |     "@ngx-translate/http-loader": "^8.0.0", | ||||||
|     "@types/chart.js": "^2.9.31", |     "@types/chart.js": "^2.9.31", | ||||||
|     "@types/cordova": "0.0.34", |     "@types/cordova": "0.0.34", | ||||||
|     "@types/dom-mediacapture-record": "1.0.7", |     "@types/dom-mediacapture-record": "1.0.7", | ||||||
| @ -119,7 +118,7 @@ | |||||||
|     "cordova.plugins.diagnostic": "^7.1.1", |     "cordova.plugins.diagnostic": "^7.1.1", | ||||||
|     "core-js": "^3.9.1", |     "core-js": "^3.9.1", | ||||||
|     "es6-promise-plugin": "^4.2.2", |     "es6-promise-plugin": "^4.2.2", | ||||||
|     "hammerjs": "^2.0.8", |     "ionicons": "^7.0.0", | ||||||
|     "jszip": "^3.7.1", |     "jszip": "^3.7.1", | ||||||
|     "mathjax": "2.7.9", |     "mathjax": "2.7.9", | ||||||
|     "moment": "^2.29.4", |     "moment": "^2.29.4", | ||||||
| @ -127,52 +126,44 @@ | |||||||
|     "mp3-mediarecorder": "4.0.5", |     "mp3-mediarecorder": "4.0.5", | ||||||
|     "nl.kingsquare.cordova.background-audio": "^1.0.1", |     "nl.kingsquare.cordova.background-audio": "^1.0.1", | ||||||
|     "ogv": "^1.8.9", |     "ogv": "^1.8.9", | ||||||
|     "rxjs": "~6.5.5", |     "rxjs": "~7.8.0", | ||||||
|  |     "swiper": "^11.0.3", | ||||||
|     "ts-md5": "^1.2.7", |     "ts-md5": "^1.2.7", | ||||||
|     "tslib": "^2.3.1", |     "tslib": "^2.3.0", | ||||||
|     "video.js": "^7.21.1", |     "video.js": "^7.21.1", | ||||||
|     "zone.js": "~0.10.3" |     "zone.js": "~0.13.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@angular-builders/custom-webpack": "^10.0.1", |     "@angular-builders/custom-webpack": "^16.0.1", | ||||||
|     "@angular-devkit/architect": "^0.1202.7", |     "@angular-devkit/build-angular": "^16.2.10", | ||||||
|     "@angular-devkit/build-angular": "~0.1000.8", |     "@angular-eslint/builder": "^16.2.0", | ||||||
|     "@angular-eslint/builder": "^4.2.0", |     "@angular-eslint/eslint-plugin": "^16.2.0", | ||||||
|     "@angular-eslint/eslint-plugin": "^4.2.0", |     "@angular-eslint/eslint-plugin-template": "^16.2.0", | ||||||
|     "@angular-eslint/eslint-plugin-template": "^4.2.0", |     "@angular-eslint/schematics": "^16.2.0", | ||||||
|     "@angular-eslint/schematics": "^4.2.0", |     "@angular-eslint/template-parser": "^16.2.0", | ||||||
|     "@angular-eslint/template-parser": "^4.2.0", |     "@angular/cli": "^16.2.10", | ||||||
|     "@angular/cli": "~10.0.8", |     "@angular/compiler-cli": "^16.2.0", | ||||||
|     "@angular/compiler": "~10.0.14", |     "@angular/language-service": "^16.2.0", | ||||||
|     "@angular/compiler-cli": "~10.0.14", |     "@ionic/angular-toolkit": "^10.0.0", | ||||||
|     "@angular/language-service": "~10.0.14", |     "@ionic/cli": "^7.1.5", | ||||||
|     "@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", |  | ||||||
|     "@types/faker": "^5.1.3", |     "@types/faker": "^5.1.3", | ||||||
|     "@types/jest": "^26.0.24", |     "@types/jest": "^26.0.24", | ||||||
|     "@types/marked": "^4.3.1", |     "@types/node": "^18.0.0", | ||||||
|     "@types/node": "^12.12.64", |  | ||||||
|     "@types/resize-observer-browser": "^0.1.5", |     "@types/resize-observer-browser": "^0.1.5", | ||||||
|     "@types/webpack-env": "^1.16.0", |     "@types/webpack-env": "^1.18.4", | ||||||
|     "@typescript-eslint/eslint-plugin": "^4.22.0", |     "@typescript-eslint/eslint-plugin": "^6.0.0", | ||||||
|     "@typescript-eslint/parser": "^4.22.0", |     "@typescript-eslint/parser": "^6.0.0", | ||||||
|     "check-es-compat": "^1.1.1", |     "check-es-compat": "^3.1.0", | ||||||
|     "compare-versions": "^4.1.4", |  | ||||||
|     "concurrently": "^8.2.0", |     "concurrently": "^8.2.0", | ||||||
|     "cordova-plugin-moodleapp": "file:cordova-plugin-moodleapp", |     "cordova-plugin-moodleapp": "file:cordova-plugin-moodleapp", | ||||||
|     "cross-env": "^7.0.3", |     "cross-env": "^7.0.3", | ||||||
|     "eslint": "^7.25.0", |     "eslint": "^8.0.0", | ||||||
|     "eslint-config-prettier": "^8.3.0", |     "eslint-plugin-deprecation": "^2.0.0", | ||||||
|     "eslint-plugin-deprecation": "^1.5.0", |  | ||||||
|     "eslint-plugin-header": "^3.1.1", |     "eslint-plugin-header": "^3.1.1", | ||||||
|     "eslint-plugin-import": "^2.22.1", |     "eslint-plugin-jest": "^27.6.0", | ||||||
|     "eslint-plugin-jest": "^24.3.6", |     "eslint-plugin-jsdoc": "^46.9.0", | ||||||
|     "eslint-plugin-jsdoc": "^32.3.3", |  | ||||||
|     "eslint-plugin-prefer-arrow": "^1.2.3", |     "eslint-plugin-prefer-arrow": "^1.2.3", | ||||||
|     "eslint-plugin-promise": "^5.1.0", |     "eslint-plugin-promise": "^6.1.1", | ||||||
|     "faker": "^5.1.0", |     "faker": "^5.1.0", | ||||||
|     "fs-extra": "^9.1.0", |     "fs-extra": "^9.1.0", | ||||||
|     "gulp": "4.0.2", |     "gulp": "4.0.2", | ||||||
| @ -182,24 +173,19 @@ | |||||||
|     "gulp-htmlmin": "^5.0.1", |     "gulp-htmlmin": "^5.0.1", | ||||||
|     "gulp-rename": "^2.0.0", |     "gulp-rename": "^2.0.0", | ||||||
|     "gulp-slash": "^1.1.3", |     "gulp-slash": "^1.1.3", | ||||||
|     "jest": "^26.5.2", |     "jest": "^29.7.0", | ||||||
|     "jest-preset-angular": "^8.3.1", |     "jest-preset-angular": "^13.1.4", | ||||||
|     "jest-raw-loader": "^1.0.1", |  | ||||||
|     "jsonc-parser": "^2.3.1", |     "jsonc-parser": "^2.3.1", | ||||||
|     "marked": "^4.3.0", |     "minimatch": "^9.0.3", | ||||||
|     "minimatch": "^5.1.0", |     "native-run": "^2.0.0", | ||||||
|     "native-run": "^1.4.0", |  | ||||||
|     "patch-package": "^6.5.0", |     "patch-package": "^6.5.0", | ||||||
|     "storybook-addon-designs": "~6.1.0", |     "terser-webpack-plugin": "^5.3.9", | ||||||
|     "storybook-addon-rtl-direction": "0.0.19", |     "ts-jest": "^29.1.1", | ||||||
|     "storybook-dark-mode": "^3.0.0", |     "ts-node": "^8.3.0", | ||||||
|     "terser-webpack-plugin": "^4.2.3", |     "typescript": "~5.1.3" | ||||||
|     "ts-jest": "^26.4.1", |  | ||||||
|     "ts-node": "~8.3.0", |  | ||||||
|     "typescript": "^3.9.9" |  | ||||||
|   }, |   }, | ||||||
|   "engines": { |   "engines": { | ||||||
|     "node": ">=14.15.0 <15" |     "node": ">=18.18.2 <19" | ||||||
|   }, |   }, | ||||||
|   "cordova": { |   "cordova": { | ||||||
|     "platforms": [ |     "platforms": [ | ||||||
| @ -210,11 +196,26 @@ | |||||||
|       "@moodlehq/cordova-plugin-advanced-http": { |       "@moodlehq/cordova-plugin-advanced-http": { | ||||||
|         "ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1" |         "ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1" | ||||||
|       }, |       }, | ||||||
|       "cordova-clipboard": {}, |  | ||||||
|       "cordova-plugin-badge": {}, |  | ||||||
|       "@moodlehq/cordova-plugin-camera": { |       "@moodlehq/cordova-plugin-camera": { | ||||||
|         "ANDROIDX_CORE_VERSION": "1.6.+" |         "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-chooser": {}, | ||||||
|       "cordova-plugin-customurlscheme": { |       "cordova-plugin-customurlscheme": { | ||||||
|         "URL_SCHEME": "moodlemobile", |         "URL_SCHEME": "moodlemobile", | ||||||
| @ -227,39 +228,21 @@ | |||||||
|       "cordova-plugin-geolocation": { |       "cordova-plugin-geolocation": { | ||||||
|         "GPS_REQUIRED": "false" |         "GPS_REQUIRED": "false" | ||||||
|       }, |       }, | ||||||
|       "@moodlehq/cordova-plugin-inappbrowser": {}, |  | ||||||
|       "cordova-plugin-ionic-keyboard": {}, |       "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-media-capture": {}, | ||||||
|  |       "cordova-plugin-moodleapp": {}, | ||||||
|       "cordova-plugin-network-information": {}, |       "cordova-plugin-network-information": {}, | ||||||
|       "@moodlehq/cordova-plugin-qrscanner": {}, |       "cordova-plugin-prevent-override": {}, | ||||||
|       "@moodlehq/cordova-plugin-statusbar": {}, |       "cordova-plugin-screen-orientation": {}, | ||||||
|       "cordova-plugin-wkuserscript": {}, |       "cordova-plugin-wkuserscript": {}, | ||||||
|       "cordova-plugin-wkwebview-cookies": {}, |       "cordova-plugin-wkwebview-cookies": {}, | ||||||
|       "@moodlehq/cordova-plugin-zip": {}, |  | ||||||
|       "cordova-sqlite-storage": {}, |       "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": { |       "cordova.plugins.diagnostic": { | ||||||
|         "ANDROID_SUPPORT_VERSION": "28.+", |         "ANDROID_SUPPORT_VERSION": "28.+", | ||||||
|         "ANDROIDX_VERSION": "1.0.0", |         "ANDROIDX_VERSION": "1.0.0", | ||||||
|         "ANDROIDX_APPCOMPAT_VERSION": "1.3.1" |         "ANDROIDX_APPCOMPAT_VERSION": "1.3.1" | ||||||
|       }, |       }, | ||||||
|       "@moodlehq/cordova-plugin-file-transfer": {}, |       "nl.kingsquare.cordova.background-audio": {} | ||||||
|       "cordova-plugin-prevent-override": {}, |  | ||||||
|       "cordova-plugin-androidx-adapter": {}, |  | ||||||
|       "cordova-plugin-screen-orientation": {}, |  | ||||||
|       "cordova-plugin-moodleapp": {} |  | ||||||
|     } |     } | ||||||
|   }, |  | ||||||
|   "optionalDependencies": { |  | ||||||
|     "keytar": "^7.2.0" |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -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). |  | ||||||
|           */ |  | ||||||
							
								
								
									
										65
									
								
								patches/check-es-compat+3.1.0.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								patches/check-es-compat+3.1.0.patch
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | diff --git a/node_modules/check-es-compat/bin/cli.mjs b/node_modules/check-es-compat/bin/cli.mjs
 | ||||||
|  | index 25c53f5..26ce475 100755
 | ||||||
|  | --- a/node_modules/check-es-compat/bin/cli.mjs
 | ||||||
|  | +++ b/node_modules/check-es-compat/bin/cli.mjs
 | ||||||
|  | @@ -17,7 +17,8 @@ if (args.length === 0) {
 | ||||||
|  |    } | ||||||
|  |  } | ||||||
|  |   | ||||||
|  | -async function execute(files) {
 | ||||||
|  | +async function execute(args) {
 | ||||||
|  | +  const { files, polyfills } = parseArguments(args);
 | ||||||
|  |    const eslint = new ESLint({ | ||||||
|  |      // Ignore any config files | ||||||
|  |      useEslintrc: false, | ||||||
|  | @@ -34,7 +35,7 @@ async function execute(files) {
 | ||||||
|  |          es2021: true, | ||||||
|  |        }, | ||||||
|  |        rules: { | ||||||
|  | -        'ecmascript-compat/compat': 'error',
 | ||||||
|  | +        'ecmascript-compat/compat': ['error', { polyfills }],
 | ||||||
|  |        }, | ||||||
|  |      }, | ||||||
|  |    }); | ||||||
|  | @@ -46,3 +47,41 @@ async function execute(files) {
 | ||||||
|  |   | ||||||
|  |    return { hasErrors: results.some((result) => result.errorCount > 0) }; | ||||||
|  |  } | ||||||
|  | +
 | ||||||
|  | +function parseArguments(args) {
 | ||||||
|  | +    const files = [];
 | ||||||
|  | +    const polyfills = [];
 | ||||||
|  | +    let nextArgIsPolyfills = false;
 | ||||||
|  | +
 | ||||||
|  | +    for (const arg of args) {
 | ||||||
|  | +        if (nextArgIsPolyfills) {
 | ||||||
|  | +            nextArgIsPolyfills = false;
 | ||||||
|  | +            polyfills.push(...splitPolyfillsArgument(arg));
 | ||||||
|  | +            continue;
 | ||||||
|  | +        }
 | ||||||
|  | +
 | ||||||
|  | +        if (arg.startsWith('--polyfills')) {
 | ||||||
|  | +            if (arg.startsWith('--polyfills=')) {
 | ||||||
|  | +                polyfills.push(...splitPolyfillsArgument(arg.slice(12)));
 | ||||||
|  | +            } else {
 | ||||||
|  | +                nextArgIsPolyfills = true;
 | ||||||
|  | +            }
 | ||||||
|  | +
 | ||||||
|  | +            continue;
 | ||||||
|  | +        }
 | ||||||
|  | +
 | ||||||
|  | +        files.push(arg);
 | ||||||
|  | +    }
 | ||||||
|  | +
 | ||||||
|  | +    return { files, polyfills };
 | ||||||
|  | +}
 | ||||||
|  | +
 | ||||||
|  | +function splitPolyfillsArgument(polyfills) {
 | ||||||
|  | +    const prototypeAtPolyfill = '{Array,String,TypedArray}.prototype.at';
 | ||||||
|  | +    const prototypeAtPlaceholder = '{{PROTOTYPEAT}}';
 | ||||||
|  | +
 | ||||||
|  | +    return polyfills
 | ||||||
|  | +        .replace(prototypeAtPolyfill, prototypeAtPlaceholder)
 | ||||||
|  | +        .split(',')
 | ||||||
|  | +        .map(polyfill => polyfill === prototypeAtPlaceholder ? prototypeAtPolyfill : polyfill);
 | ||||||
|  | +}
 | ||||||
| @ -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) { |  | ||||||
| @ -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. |  | ||||||
| @ -14,7 +14,7 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| const minimatch = require('minimatch'); | const { minimatch } = require('minimatch'); | ||||||
| const { existsSync, readFileSync, writeFileSync, statSync, renameSync, rmSync } = require('fs'); | const { existsSync, readFileSync, writeFileSync, statSync, renameSync, rmSync } = require('fs'); | ||||||
| const { readdir } = require('fs').promises; | const { readdir } = require('fs').promises; | ||||||
| const { mkdirSync, copySync } = require('fs-extra'); | const { mkdirSync, copySync } = require('fs-extra'); | ||||||
|  | |||||||
| @ -72,6 +72,7 @@ | |||||||
|   "addon.block_starredcourses.nocourses": "block_starredcourses", |   "addon.block_starredcourses.nocourses": "block_starredcourses", | ||||||
|   "addon.block_starredcourses.pluginname": "block_starredcourses", |   "addon.block_starredcourses.pluginname": "block_starredcourses", | ||||||
|   "addon.block_tags.pluginname": "block_tags", |   "addon.block_tags.pluginname": "block_tags", | ||||||
|  |   "addon.block_timeline.ariadayfilter": "block_timeline", | ||||||
|   "addon.block_timeline.duedate": "block_timeline", |   "addon.block_timeline.duedate": "block_timeline", | ||||||
|   "addon.block_timeline.next30days": "block_timeline", |   "addon.block_timeline.next30days": "block_timeline", | ||||||
|   "addon.block_timeline.next3months": "block_timeline", |   "addon.block_timeline.next3months": "block_timeline", | ||||||
| @ -1792,6 +1793,7 @@ | |||||||
|   "core.fileuploader.uploadingperc": "local_moodlemobileapp", |   "core.fileuploader.uploadingperc": "local_moodlemobileapp", | ||||||
|   "core.fileuploader.video": "local_moodlemobileapp", |   "core.fileuploader.video": "local_moodlemobileapp", | ||||||
|   "core.filter": "moodle", |   "core.filter": "moodle", | ||||||
|  |   "core.firstdayofweek": "langconfig", | ||||||
|   "core.folder": "moodle", |   "core.folder": "moodle", | ||||||
|   "core.forcepasswordchangenotice": "moodle", |   "core.forcepasswordchangenotice": "moodle", | ||||||
|   "core.fulllistofcourses": "moodle", |   "core.fulllistofcourses": "moodle", | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1 *ngIf="badge">{{ badge.name }}</h1> |             <h1 *ngIf="badge">{{ badge.name }}</h1> | ||||||
| @ -11,7 +11,7 @@ | |||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content [core-swipe-navigation]="badges" class="limited-width"> | <ion-content [core-swipe-navigation]="badges" class="limited-width"> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="badgeLoaded"> |     <core-loading [hideUntil]="badgeLoaded"> | ||||||
|         <ion-item-group *ngIf="badge"> |         <ion-item-group *ngIf="badge"> | ||||||
| @ -122,8 +122,7 @@ | |||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading">{{ 'core.course' | translate}}</p> |                         <p class="item-heading">{{ 'core.course' | translate}}</p> | ||||||
|                         <p> |                         <p> | ||||||
|                             <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="courseId"> |                             <core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="courseId" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </p> |                         </p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| @ -217,7 +216,7 @@ | |||||||
|                         <p class="item-heading">{{ relatedBadge.name }}</p> |                         <p class="item-heading">{{ relatedBadge.name }}</p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </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> |                     <ion-label> | ||||||
|                         <p class="item-heading">{{ 'addon.badges.norelated' | translate}}</p> |                         <p class="item-heading">{{ 'addon.badges.norelated' | translate}}</p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
| @ -237,7 +236,7 @@ | |||||||
|                         <p class="item-heading">{{ alignment.targetname }}</p> |                         <p class="item-heading">{{ alignment.targetname }}</p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </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> |                     <ion-label> | ||||||
|                         <p class="item-heading">{{ 'addon.badges.noalignment' | translate}}</p> |                         <p class="item-heading">{{ 'addon.badges.noalignment' | translate}}</p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.badges.badges' | translate }}</h1> |             <h1>{{ 'addon.badges.badges' | translate }}</h1> | ||||||
| @ -11,11 +11,10 @@ | |||||||
| <ion-content> | <ion-content> | ||||||
|     <core-split-view> |     <core-split-view> | ||||||
|         <ion-refresher slot="fixed" [disabled]="!badges.loaded" (ionRefresh)="refreshBadges($event.target)"> |         <ion-refresher slot="fixed" [disabled]="!badges.loaded" (ionRefresh)="refreshBadges($event.target)"> | ||||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|         </ion-refresher> |         </ion-refresher> | ||||||
|         <core-loading [hideUntil]="badges.loaded"> |         <core-loading [hideUntil]="badges.loaded"> | ||||||
|             <core-empty-box *ngIf="badges.empty" icon="fas-trophy" [message]="'addon.badges.nobadges' | translate"> |             <core-empty-box *ngIf="badges.empty" icon="fas-trophy" [message]="'addon.badges.nobadges' | translate" /> | ||||||
|             </core-empty-box> |  | ||||||
| 
 | 
 | ||||||
|             <ion-list *ngIf="!badges.empty" class="ion-no-margin"> |             <ion-list *ngIf="!badges.empty" class="ion-no-margin"> | ||||||
|                 <ion-item button class="ion-text-wrap" *ngFor="let badge of badges.items" [attr.aria-label]="badge.name" |                 <ion-item button class="ion-text-wrap" *ngFor="let badge of badges.items" [attr.aria-label]="badge.name" | ||||||
|  | |||||||
| @ -5,8 +5,7 @@ | |||||||
| </ion-item-divider> | </ion-item-divider> | ||||||
| <core-loading [hideUntil]="loaded"> | <core-loading [hideUntil]="loaded"> | ||||||
|     <ion-item class="ion-text-wrap" *ngFor="let entry of entries" [detail]="true" button (click)="gotoCoureListModType(entry)"> |     <ion-item class="ion-text-wrap" *ngFor="let entry of entries" [detail]="true" button (click)="gotoCoureListModType(entry)"> | ||||||
|         <core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.iconModName" [showAlt]="false"> |         <core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.iconModName" [showAlt]="false" /> | ||||||
|         </core-mod-icon> |  | ||||||
|         <ion-label>{{ entry.name }}</ion-label> |         <ion-label>{{ entry.name }}</ion-label> | ||||||
|     </ion-item> |     </ion-item> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | |||||||
| @ -7,16 +7,14 @@ | |||||||
|         <div *ngIf="downloadCoursesEnabled && filteredCourses.length > 0" class="core-button-spinner"> |         <div *ngIf="downloadCoursesEnabled && filteredCourses.length > 0" class="core-button-spinner"> | ||||||
|             <ion-button *ngIf="!prefetchCoursesData.loading" fill="clear" (click)="prefetchCourses()" |             <ion-button *ngIf="!prefetchCoursesData.loading" fill="clear" (click)="prefetchCourses()" | ||||||
|                 [attr.aria-label]="prefetchCoursesData.statusTranslatable | translate"> |                 [attr.aria-label]="prefetchCoursesData.statusTranslatable | translate"> | ||||||
|                 <ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"> |                 <ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true" /> | ||||||
|                 </ion-icon> |  | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge" role="progressbar" |             <ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge" role="progressbar" | ||||||
|                 [attr.aria-valuemax]="prefetchCoursesData.total" [attr.aria-valuenow]="prefetchCoursesData.count" |                 [attr.aria-valuemax]="prefetchCoursesData.total" [attr.aria-valuenow]="prefetchCoursesData.count" | ||||||
|                 [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText"> |                 [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText"> | ||||||
|                 {{prefetchCoursesData.badge}} |                 {{prefetchCoursesData.badge}} | ||||||
|             </ion-badge> |             </ion-badge> | ||||||
|             <ion-spinner *ngIf="prefetchCoursesData.loading" [attr.aria-label]="'core.loading' | translate"> |             <ion-spinner *ngIf="prefetchCoursesData.loading" [attr.aria-label]="'core.loading' | translate" /> | ||||||
|             </ion-spinner> |  | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| </ion-item-divider> | </ion-item-divider> | ||||||
| @ -26,8 +24,7 @@ | |||||||
|         <ion-col> |         <ion-col> | ||||||
|             <!-- Filter courses. --> |             <!-- Filter courses. --> | ||||||
|             <ion-searchbar [(ngModel)]="textFilter" (ionInput)="filterTextChanged($event.target)" |             <ion-searchbar [(ngModel)]="textFilter" (ionInput)="filterTextChanged($event.target)" | ||||||
|                 (ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.courses.filtermycourses' | translate"> |                 (ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.courses.filtermycourses' | translate" /> | ||||||
|             </ion-searchbar> |  | ||||||
|         </ion-col> |         </ion-col> | ||||||
|     </ion-row> |     </ion-row> | ||||||
|     <ion-row class="ion-justify-content-between ion-align-items-center addon-block-myoverview-filter" *ngIf="hasCourses"> |     <ion-row class="ion-justify-content-between ion-align-items-center addon-block-myoverview-filter" *ngIf="hasCourses"> | ||||||
| @ -70,12 +67,11 @@ | |||||||
|         <ion-col> |         <ion-col> | ||||||
|             <!-- Filter courses. --> |             <!-- Filter courses. --> | ||||||
|             <ion-searchbar class="ion-hide-md-down" [(ngModel)]="textFilter" (ionInput)="filterTextChanged($event.target)" |             <ion-searchbar class="ion-hide-md-down" [(ngModel)]="textFilter" (ionInput)="filterTextChanged($event.target)" | ||||||
|                 (ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.courses.filtermycourses' | translate"> |                 (ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.courses.filtermycourses' | translate" /> | ||||||
|             </ion-searchbar> |  | ||||||
|         </ion-col> |         </ion-col> | ||||||
|         <ion-col size="auto" *ngIf="sort.enabled"> |         <ion-col size="auto" *ngIf="sort.enabled"> | ||||||
|             <core-combobox [label]="'core.sortby' | translate" [selection]="sort.selected" (onChange)="sortCourses($event)" |             <core-combobox [label]="'core.sortby' | translate" [selection]="sort.selected" (onChange)="sortCourses($event)" | ||||||
|                 icon="fas-arrow-down-short-wide"> |                 icon="fas-arrow-down-short-wide" class="no-border"> | ||||||
|                 <ion-select-option class="ion-text-wrap" value="fullname"> |                 <ion-select-option class="ion-text-wrap" value="fullname"> | ||||||
|                     {{'addon.block_myoverview.title' | translate}} |                     {{'addon.block_myoverview.title' | translate}} | ||||||
|                 </ion-select-option> |                 </ion-select-option> | ||||||
| @ -90,16 +86,16 @@ | |||||||
|         <ion-col size="auto" *ngIf="isLayoutSwitcherAvailable"> |         <ion-col size="auto" *ngIf="isLayoutSwitcherAvailable"> | ||||||
|             <ion-button *ngIf="layout === 'card'" fill="outline" (click)="toggleLayout('list')" |             <ion-button *ngIf="layout === 'card'" fill="outline" (click)="toggleLayout('list')" | ||||||
|                 [attr.aria-label]="'addon.block_myoverview.aria:list' | translate"> |                 [attr.aria-label]="'addon.block_myoverview.aria:list' | translate"> | ||||||
|                 <ion-icon slot="icon-only" name="fas-list" aria-hidden="true"></ion-icon> |                 <ion-icon slot="icon-only" name="fas-list" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <ion-button *ngIf="layout === 'list'" fill="outline" (click)="toggleLayout('card')" |             <ion-button *ngIf="layout === 'list'" fill="outline" (click)="toggleLayout('card')" | ||||||
|                 [attr.aria-label]="'addon.block_myoverview.aria:card' | translate"> |                 [attr.aria-label]="'addon.block_myoverview.aria:card' | translate"> | ||||||
|                 <ion-icon slot="icon-only" name="fas-table-cells-large" aria-hidden="true"></ion-icon> |                 <ion-icon slot="icon-only" name="fas-table-cells-large" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ion-col> |         </ion-col> | ||||||
|     </ion-row> |     </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"> |         <p *ngIf="hasCourses" class="item-heading"> | ||||||
|             {{'addon.block_myoverview.noresult' | translate}} |             {{'addon.block_myoverview.noresult' | translate}} | ||||||
|         </p> |         </p> | ||||||
| @ -114,8 +110,7 @@ | |||||||
|                 {{'addon.block_myoverview.nocoursesenrolleddescription' | translate}} |                 {{'addon.block_myoverview.nocoursesenrolleddescription' | translate}} | ||||||
|             </p> |             </p> | ||||||
|             <ion-button (click)="openSearch()" fill="outline"> |             <ion-button (click)="openSearch()" fill="outline"> | ||||||
|                 <ion-icon name="fas-magnifying-glass" slot="start" aria-hidden="true"> |                 <ion-icon name="fas-magnifying-glass" slot="start" aria-hidden="true" /> | ||||||
|                 </ion-icon> |  | ||||||
|                 {{'addon.block_myoverview.browseallcourses' | translate}} |                 {{'addon.block_myoverview.browseallcourses' | translate}} | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ng-container> |         </ng-container> | ||||||
| @ -128,8 +123,7 @@ | |||||||
|                 <ion-col *ngFor="let course of filteredCourses" class="ion-no-padding" size="12" size-sm="6" size-md="6" size-lg="4" |                 <ion-col *ngFor="let course of filteredCourses" class="ion-no-padding" size="12" size-sm="6" size-md="6" size-lg="4" | ||||||
|                     size-xl="3"> |                     size-xl="3"> | ||||||
|                     <core-courses-course-list-item [course]="course" class="core-courseoverview" [showDownload]="downloadCourseEnabled" |                     <core-courses-course-list-item [course]="course" class="core-courseoverview" [showDownload]="downloadCourseEnabled" | ||||||
|                         [layout]="layout"> |                         [layout]="layout" /> | ||||||
|                     </core-courses-course-list-item> |  | ||||||
|                 </ion-col> |                 </ion-col> | ||||||
|             </ion-row> |             </ion-row> | ||||||
|         </ion-grid> |         </ion-grid> | ||||||
|  | |||||||
| @ -10,24 +10,19 @@ | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         ion-button, |         ion-button, | ||||||
|         core-combobox ::ng-deep ion-button { |         core-combobox ::ng-deep ion-select { | ||||||
|             --border-width: 0; |  | ||||||
|             --a11y-min-target-size: 40px; |             --a11y-min-target-size: 40px; | ||||||
|             margin: 0; |             margin: 0; | ||||||
| 
 |  | ||||||
|             .select-icon { |  | ||||||
|                 display: none; |  | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         ion-button { | ||||||
|  |             --border-width: 0; | ||||||
|  | 
 | ||||||
|             ion-icon { |             ion-icon { | ||||||
|                 font-size: 20px; |                 font-size: 20px; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         core-combobox ::ng-deep ion-select { |  | ||||||
|             margin: 0; |  | ||||||
|             --a11y-min-target-size: 40px; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         ion-searchbar { |         ion-searchbar { | ||||||
|             padding: 0; |             padding: 0; | ||||||
|             --height: 40px; |             --height: 40px; | ||||||
|  | |||||||
| @ -3,21 +3,19 @@ | |||||||
|         <h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2> |         <h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2> | ||||||
|     </ion-label> |     </ion-label> | ||||||
|     <div slot="end" class="flex-row"> |     <div slot="end" class="flex-row"> | ||||||
|         <core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId"> |         <core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId" /> | ||||||
|         </core-horizontal-scroll-controls> |  | ||||||
|     </div> |     </div> | ||||||
| </ion-item-divider> | </ion-item-divider> | ||||||
| <core-loading [hideUntil]="loaded"> | <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> |         [message]="'addon.block_recentlyaccessedcourses.nocourses' | translate" /> | ||||||
|     <!-- List of courses. --> |     <!-- List of courses. --> | ||||||
|     <div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll" |     <div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll" | ||||||
|         (scroll)="scrollControls.updateScrollPosition()"> |         (scroll)="scrollControls.updateScrollPosition()"> | ||||||
|         <div (onResize)="scrollControls.updateScrollPosition()" class="flex-row"> |         <div (onResize)="scrollControls.updateScrollPosition()" class="flex-row"> | ||||||
|             <div class="safe-area-pseudo-padding-start"></div> |             <div class="safe-area-pseudo-padding-start"></div> | ||||||
|             <ng-container *ngFor="let course of courses"> |             <ng-container *ngFor="let course of courses"> | ||||||
|                 <core-courses-course-list-item [course]="course" class="core-recentlyaccessedcourses" layout="summarycard"> |                 <core-courses-course-list-item [course]="course" class="core-recentlyaccessedcourses" layout="summarycard" /> | ||||||
|                 </core-courses-course-list-item> |  | ||||||
|             </ng-container> |             </ng-container> | ||||||
|             <div class="safe-area-pseudo-padding-end"></div> |             <div class="safe-area-pseudo-padding-end"></div> | ||||||
|         </div> |         </div> | ||||||
|  | |||||||
| @ -3,8 +3,7 @@ | |||||||
|         <h2>{{ 'addon.block_recentlyaccesseditems.pluginname' | translate }}</h2> |         <h2>{{ 'addon.block_recentlyaccesseditems.pluginname' | translate }}</h2> | ||||||
|     </ion-label> |     </ion-label> | ||||||
|     <div slot="end"> |     <div slot="end"> | ||||||
|         <core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId"> |         <core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId" /> | ||||||
|         </core-horizontal-scroll-controls> |  | ||||||
|     </div> |     </div> | ||||||
| </ion-item-divider> | </ion-item-divider> | ||||||
| <core-loading [hideUntil]="loaded"> | <core-loading [hideUntil]="loaded"> | ||||||
| @ -16,18 +15,16 @@ | |||||||
|                 <ion-card> |                 <ion-card> | ||||||
|                     <ion-item class="core-course-module-handler ion-text-wrap" [detail]="false" (click)="action($event, item)" button> |                     <ion-item class="core-course-module-handler ion-text-wrap" [detail]="false" (click)="action($event, item)" button> | ||||||
|                         <core-mod-icon slot="start" *ngIf="item.iconUrl" [modicon]="item.iconUrl" [modname]="item.modname" |                         <core-mod-icon slot="start" *ngIf="item.iconUrl" [modicon]="item.iconUrl" [modname]="item.modname" | ||||||
|                             [componentId]="item.cmid" [showAlt]="false" [purpose]="item.purpose"> |                             [componentId]="item.cmid" [showAlt]="false" [purpose]="item.purpose" /> | ||||||
|                         </core-mod-icon> |  | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <!-- Add the icon title so accessibility tools read it. --> |                             <!-- Add the icon title so accessibility tools read it. --> | ||||||
|                             <span class="sr-only" *ngIf="item.iconTitle">{{ item.iconTitle }}</span> |                             <span class="sr-only" *ngIf="item.iconTitle">{{ item.iconTitle }}</span> | ||||||
|                             <p class="item-heading"> |                             <p class="item-heading"> | ||||||
|                                 <core-format-text [text]="item.name" contextLevel="module" [contextInstanceId]="item.cmid" |                                 <core-format-text [text]="item.name" contextLevel="module" [contextInstanceId]="item.cmid" | ||||||
|                                     [courseId]="item.courseid"></core-format-text> |                                     [courseId]="item.courseid" /> | ||||||
|                             </p> |                             </p> | ||||||
|                             <p> |                             <p> | ||||||
|                                 <core-format-text [text]="item.coursename" contextLevel="course" [contextInstanceId]="item.courseid"> |                                 <core-format-text [text]="item.coursename" contextLevel="course" [contextInstanceId]="item.courseid" /> | ||||||
|                                 </core-format-text> |  | ||||||
|                             </p> |                             </p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| @ -38,6 +35,6 @@ | |||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <core-empty-box *ngIf="items.length <= 0" image="assets/img/icons/activities.svg" |     <core-empty-box *ngIf="items.length <= 0" image="assets/img/icons/activities.svg" | ||||||
|         [message]="'addon.block_recentlyaccesseditems.noitems' | translate"></core-empty-box> |         [message]="'addon.block_recentlyaccesseditems.noitems' | translate" /> | ||||||
| 
 | 
 | ||||||
| </core-loading> | </core-loading> | ||||||
|  | |||||||
| @ -8,10 +8,10 @@ | |||||||
|         <ion-item class="ion-text-wrap" *ngIf="mainMenuBlock.summary"> |         <ion-item class="ion-text-wrap" *ngIf="mainMenuBlock.summary"> | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <core-format-text [text]="mainMenuBlock.summary" [component]="component" [componentId]="siteHomeId" contextLevel="course" |                 <core-format-text [text]="mainMenuBlock.summary" [component]="component" [componentId]="siteHomeId" contextLevel="course" | ||||||
|                     [contextInstanceId]="siteHomeId"></core-format-text> |                     [contextInstanceId]="siteHomeId" /> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| 
 | 
 | ||||||
|         <core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [section]="mainMenuBlock"></core-course-module> |         <core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [section]="mainMenuBlock" /> | ||||||
|     </ion-list> |     </ion-list> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | |||||||
| @ -3,21 +3,19 @@ | |||||||
|         <h2>{{ 'addon.block_starredcourses.pluginname' | translate }}</h2> |         <h2>{{ 'addon.block_starredcourses.pluginname' | translate }}</h2> | ||||||
|     </ion-label> |     </ion-label> | ||||||
|     <div slot="end" class="flex-row"> |     <div slot="end" class="flex-row"> | ||||||
|         <core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId"> |         <core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId" /> | ||||||
|         </core-horizontal-scroll-controls> |  | ||||||
|     </div> |     </div> | ||||||
| </ion-item-divider> | </ion-item-divider> | ||||||
| <core-loading [hideUntil]="loaded"> | <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> |         [message]="'addon.block_starredcourses.nocourses' | translate" /> | ||||||
|     <!-- List of courses. --> |     <!-- List of courses. --> | ||||||
|     <div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll" |     <div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll" | ||||||
|         (scroll)="scrollControls.updateScrollPosition()"> |         (scroll)="scrollControls.updateScrollPosition()"> | ||||||
|         <div (onResize)="scrollControls.updateScrollPosition()" class="flex-row"> |         <div (onResize)="scrollControls.updateScrollPosition()" class="flex-row"> | ||||||
|             <div class="safe-area-pseudo-padding-start"></div> |             <div class="safe-area-pseudo-padding-start"></div> | ||||||
|             <ng-container *ngFor="let course of courses"> |             <ng-container *ngFor="let course of courses"> | ||||||
|                 <core-courses-course-list-item [course]="course" class="core-block_starredcourses" layout="summarycard"> |                 <core-courses-course-list-item [course]="course" class="core-block_starredcourses" layout="summarycard" /> | ||||||
|                 </core-courses-course-list-item> |  | ||||||
|             </ng-container> |             </ng-container> | ||||||
|             <div class="safe-area-pseudo-padding-end"></div> |             <div class="safe-area-pseudo-padding-end"></div> | ||||||
|         </div> |         </div> | ||||||
|  | |||||||
| @ -1,14 +1,20 @@ | |||||||
| :host .core-block-content ::ng-deep { | :host .core-block-content ::ng-deep { | ||||||
|  |     ion-label { | ||||||
|  |         max-width: 100%; | ||||||
|  |     } | ||||||
|     .tag_cloud { |     .tag_cloud { | ||||||
|         text-align: center; |  | ||||||
|         ul.inline-list { |         ul.inline-list { | ||||||
|             list-style: none; |             list-style: none; | ||||||
|             margin: 0; |             margin: 0; | ||||||
|             -webkit-padding-start: 0; |             -webkit-padding-start: 0; | ||||||
| 
 | 
 | ||||||
|  |             display: flex; | ||||||
|  |             flex-wrap: wrap; | ||||||
|  |             align-items: center; | ||||||
|  |             justify-content: center; | ||||||
|  | 
 | ||||||
|             li { |             li { | ||||||
|                 padding: .2em; |                 padding: .2em; | ||||||
|                 display: inline-block; |  | ||||||
| 
 | 
 | ||||||
|                 a { |                 a { | ||||||
|                     background: var(--primary); |                     background: var(--primary); | ||||||
|  | |||||||
| @ -2,8 +2,7 @@ | |||||||
|     <ion-label class="ion-text-wrap"> |     <ion-label class="ion-text-wrap"> | ||||||
|         <h3> |         <h3> | ||||||
|             <span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span> |             <span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span> | ||||||
|             <core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id"> |             <core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id" /> | ||||||
|             </core-format-text> |  | ||||||
|         </h3> |         </h3> | ||||||
|     </ion-label> |     </ion-label> | ||||||
| </ion-item> | </ion-item> | ||||||
| @ -20,18 +19,16 @@ | |||||||
|                 <ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding"> |                 <ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding"> | ||||||
|                     <ion-col class="addon-block-timeline-activity-main ion-no-padding"> |                     <ion-col class="addon-block-timeline-activity-main ion-no-padding"> | ||||||
|                         <ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding"> |                         <ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding"> | ||||||
|                             <ion-col class="addon-block-timeline-activity-time ion-no-padding"> |                             <ion-col class="addon-block-timeline-activity-time ion-no-padding ion-text-nowrap"> | ||||||
|                                 <small>{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</small> |                                 <small>{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</small> | ||||||
|                                 <core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance" |                                 <core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance" | ||||||
|                                     [modname]="event.modulename" [purpose]="event.purpose"> |                                     [modname]="event.modulename" [purpose]="event.purpose" /> | ||||||
|                                 </core-mod-icon> |  | ||||||
|                             </ion-col> |                             </ion-col> | ||||||
|                             <ion-col class="addon-block-timeline-activity-name ion-no-padding"> |                             <ion-col class="addon-block-timeline-activity-name ion-no-padding"> | ||||||
|                                 <p class="item-heading"> |                                 <p class="item-heading"> | ||||||
|                                     <span> |                                     <span> | ||||||
|                                         <core-format-text [text]="event.activityname || event.name" contextLevel="module" |                                         <core-format-text [text]="event.activityname || event.name" contextLevel="module" | ||||||
|                                             [contextInstanceId]="event.id" [courseId]="event.course?.id"> |                                             [contextInstanceId]="event.id" [courseId]="event.course?.id" /> | ||||||
|                                         </core-format-text> |  | ||||||
|                                     </span> |                                     </span> | ||||||
|                                     <ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }} |                                     <ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }} | ||||||
|                                     </ion-badge> |                                     </ion-badge> | ||||||
| @ -39,15 +36,13 @@ | |||||||
|                                 <p *ngIf="showInlineCourse && event.course"> |                                 <p *ngIf="showInlineCourse && event.course"> | ||||||
|                                     <span> |                                     <span> | ||||||
|                                         <core-format-text [text]="event.course.fullnamedisplay" contextLevel="course" |                                         <core-format-text [text]="event.course.fullnamedisplay" contextLevel="course" | ||||||
|                                             [contextInstanceId]="event.course.id"> |                                             [contextInstanceId]="event.course.id" /> | ||||||
|                                         </core-format-text> |  | ||||||
|                                     </span> |                                     </span> | ||||||
|                                 </p> |                                 </p> | ||||||
|                                 <p *ngIf="event.activitystr"> |                                 <p *ngIf="event.activitystr"> | ||||||
|                                     <span> |                                     <span> | ||||||
|                                         <core-format-text *ngIf="event.activitystr" [text]="event.activitystr" contextLevel="module" |                                         <core-format-text *ngIf="event.activitystr" [text]="event.activitystr" contextLevel="module" | ||||||
|                                             [contextInstanceId]="event.id"> |                                             [contextInstanceId]="event.id" /> | ||||||
|                                         </core-format-text> |  | ||||||
|                                     </span> |                                     </span> | ||||||
|                                 </p> |                                 </p> | ||||||
|                             </ion-col> |                             </ion-col> | ||||||
| @ -72,5 +67,5 @@ | |||||||
|     <ion-button expand="block" (click)="loadMore.emit()" fill="outline" *ngIf="!loadingMore"> |     <ion-button expand="block" (click)="loadMore.emit()" fill="outline" *ngIf="!loadingMore"> | ||||||
|         {{ 'core.loadmore' | translate }} |         {{ 'core.loadmore' | translate }} | ||||||
|     </ion-button> |     </ion-button> | ||||||
|     <ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate"></ion-spinner> |     <ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate" /> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -9,12 +9,13 @@ | |||||||
|             <!-- Filter courses. --> |             <!-- Filter courses. --> | ||||||
|             <core-search-box (onSubmit)="searchChanged($event)" (onClear)="searchChanged('')" |             <core-search-box (onSubmit)="searchChanged($event)" (onClear)="searchChanged('')" | ||||||
|                 [placeholder]="'addon.block_timeline.searchevents' | translate" autocorrect="off" spellcheck="false" [lengthCheck]="2" |                 [placeholder]="'addon.block_timeline.searchevents' | translate" autocorrect="off" spellcheck="false" [lengthCheck]="2" | ||||||
|                 searchArea="AddonBlockTimeline"></core-search-box> |                 searchArea="AddonBlockTimeline" /> | ||||||
|         </ion-col> |         </ion-col> | ||||||
|     </ion-row> |     </ion-row> | ||||||
|     <ion-row class="ion-justify-content-between ion-align-items-center addon-block-timeline-filter"> |     <ion-row class="ion-justify-content-between ion-align-items-center addon-block-timeline-filter"> | ||||||
|         <ion-col size="auto"> |         <ion-col size="auto"> | ||||||
|             <core-combobox [formControl]="filter" (onChange)="filterChanged($event)"> |             <core-combobox [formControl]="filter" (onChange)="filterChanged($event)" | ||||||
|  |                 [label]="'addon.block_timeline.ariadayfilter' | translate"> | ||||||
|                 <ion-select-option *ngFor="let option of statusFilterOptions; last as last" |                 <ion-select-option *ngFor="let option of statusFilterOptions; last as last" | ||||||
|                     [attr.class]="'ion-text-wrap' + last ? ' core-select-option-border-bottom' : ''" [value]="option.value"> |                     [attr.class]="'ion-text-wrap' + last ? ' core-select-option-border-bottom' : ''" [value]="option.value"> | ||||||
|                     {{ option.name | translate }} |                     {{ option.name | translate }} | ||||||
| @ -31,11 +32,11 @@ | |||||||
|             <!-- Filter courses. --> |             <!-- Filter courses. --> | ||||||
|             <core-search-box (onSubmit)="searchChanged($event)" (onClear)="searchChanged('')" |             <core-search-box (onSubmit)="searchChanged($event)" (onClear)="searchChanged('')" | ||||||
|                 [placeholder]="'addon.block_timeline.searchevents' | translate" autocorrect="off" spellcheck="false" [lengthCheck]="2" |                 [placeholder]="'addon.block_timeline.searchevents' | translate" autocorrect="off" spellcheck="false" [lengthCheck]="2" | ||||||
|                 searchArea="AddonBlockTimeline"></core-search-box> |                 searchArea="AddonBlockTimeline" /> | ||||||
|         </ion-col> |         </ion-col> | ||||||
|         <ion-col size="auto"> |         <ion-col size="auto"> | ||||||
|             <core-combobox [label]="'core.sortby' | translate" [formControl]="sort" (onChange)="sortChanged($event)" |             <core-combobox [label]="'core.sortby' | translate" [formControl]="sort" (onChange)="sortChanged($event)" | ||||||
|                 icon="fas-arrow-down-short-wide"> |                 icon="fas-arrow-down-short-wide" class="no-border"> | ||||||
|                 <ion-select-option *ngFor="let option of sortOptions" class="ion-text-wrap" [value]="option.value"> |                 <ion-select-option *ngFor="let option of sortOptions" class="ion-text-wrap" [value]="option.value"> | ||||||
|                     {{ option.name | translate }} |                     {{ option.name | translate }} | ||||||
|                 </ion-select-option> |                 </ion-select-option> | ||||||
| @ -46,9 +47,9 @@ | |||||||
|         <ng-container *ngFor="let section of sections"> |         <ng-container *ngFor="let section of sections"> | ||||||
|             <addon-block-timeline-events *ngIf="section.data$ | async as data" [events]="data.events" |             <addon-block-timeline-events *ngIf="section.data$ | async as data" [events]="data.events" | ||||||
|                 [showInlineCourse]="(sort$ | async) === AddonBlockTimelineSort.ByDates" [canLoadMore]="data.canLoadMore" |                 [showInlineCourse]="(sort$ | async) === AddonBlockTimelineSort.ByDates" [canLoadMore]="data.canLoadMore" | ||||||
|                 [loadingMore]="data.loadingMore" (loadMore)="section.loadMore()" [course]="section.course"> </addon-block-timeline-events> |                 [loadingMore]="data.loadingMore" (loadMore)="section.loadMore()" [course]="section.course" /> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|         <core-empty-box *ngIf="sections && sections.length === 0" image="assets/img/icons/courses.svg" |         <core-empty-box *ngIf="sections && sections.length === 0" image="assets/img/icons/courses.svg" | ||||||
|             [message]="'addon.block_timeline.noevents' | translate"></core-empty-box> |             [message]="'addon.block_timeline.noevents' | translate" /> | ||||||
|     </ng-container> |     </ng-container> | ||||||
| </core-loading> | </core-loading> | ||||||
|  | |||||||
| @ -39,10 +39,10 @@ import { CoreLogger } from '@singletons/logger'; | |||||||
| }) | }) | ||||||
| export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent { | export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent { | ||||||
| 
 | 
 | ||||||
|     sort = new FormControl(); |     sort = new FormControl(AddonBlockTimelineSort.ByDates); | ||||||
|     sort$!: Observable<AddonBlockTimelineSort>; |     sort$!: Observable<AddonBlockTimelineSort>; | ||||||
|     sortOptions!: AddonBlockTimelineOption<AddonBlockTimelineSort>[]; |     sortOptions!: AddonBlockTimelineOption<AddonBlockTimelineSort>[]; | ||||||
|     filter = new FormControl(); |     filter = new FormControl(AddonBlockTimelineFilter.Next30Days); | ||||||
|     filter$!: Observable<AddonBlockTimelineFilter>; |     filter$!: Observable<AddonBlockTimelineFilter>; | ||||||
|     statusFilterOptions!: AddonBlockTimelineOption<AddonBlockTimelineFilter>[]; |     statusFilterOptions!: AddonBlockTimelineOption<AddonBlockTimelineFilter>[]; | ||||||
|     dateFilterOptions!: AddonBlockTimelineOption<AddonBlockTimelineFilter>[]; |     dateFilterOptions!: AddonBlockTimelineOption<AddonBlockTimelineFilter>[]; | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| { | { | ||||||
|  |     "ariadayfilter": "Filter timeline by date", | ||||||
|     "duedate": "Due date", |     "duedate": "Due date", | ||||||
|     "next30days": "Next 30 days", |     "next30days": "Next 30 days", | ||||||
|     "next3months": "Next 3 months", |     "next3months": "Next 3 months", | ||||||
|  | |||||||
| @ -57,7 +57,7 @@ Feature: Timeline block. | |||||||
|     But I should not find "Assignment 01" within "Timeline" "ion-card" in the app |     But I should not find "Assignment 01" within "Timeline" "ion-card" in the app | ||||||
|     And I should not find "Course 3" within "Timeline" "ion-card" in the app |     And I should not find "Course 3" within "Timeline" "ion-card" in the app | ||||||
| 
 | 
 | ||||||
|     When I press "Next 30 days" in the app |     When I press "Filter timeline by date" in the app | ||||||
|     And I press "Overdue" in the app |     And I press "Overdue" in the app | ||||||
|     Then I should find "Assignment 01" within "Timeline" "ion-card" in the app |     Then I should find "Assignment 01" within "Timeline" "ion-card" in the app | ||||||
|     And I should find "Course 2" within "Timeline" "ion-card" in the app |     And I should find "Course 2" within "Timeline" "ion-card" in the app | ||||||
| @ -66,7 +66,7 @@ Feature: Timeline block. | |||||||
|     And I should not find "Course 1" within "Timeline" "ion-card" in the app |     And I should not find "Course 1" within "Timeline" "ion-card" in the app | ||||||
|     And I should not find "Course 3" within "Timeline" "ion-card" in the app |     And I should not find "Course 3" within "Timeline" "ion-card" in the app | ||||||
| 
 | 
 | ||||||
|     When I press "Overdue" in the app |     When I press "Filter timeline by date" in the app | ||||||
|     And I press "All" in the app |     And I press "All" in the app | ||||||
|     Then I should find "Assignment 19" within "Timeline" "ion-card" in the app |     Then I should find "Assignment 19" within "Timeline" "ion-card" in the app | ||||||
|     And I should find "Course 3" within "Timeline" "ion-card" in the app |     And I should find "Course 3" within "Timeline" "ion-card" in the app | ||||||
| @ -76,7 +76,7 @@ Feature: Timeline block. | |||||||
|     Then I should find "Assignment 21" within "Timeline" "ion-card" in the app |     Then I should find "Assignment 21" within "Timeline" "ion-card" in the app | ||||||
|     And I should find "Assignment 25" within "Timeline" "ion-card" in the app |     And I should find "Assignment 25" within "Timeline" "ion-card" in the app | ||||||
| 
 | 
 | ||||||
|     When I press "All" in the app |     When I press "Filter timeline by date" in the app | ||||||
|     And I press "Next 7 days" in the app |     And I press "Next 7 days" in the app | ||||||
|     And I press "Sort by" in the app |     And I press "Sort by" in the app | ||||||
|     And I press "Sort by courses" in the app |     And I press "Sort by courses" in the app | ||||||
|  | |||||||
| @ -1,37 +1,36 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ title | translate }}</h1> |             <h1>{{ title | translate }}</h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <core-user-menu-button></core-user-menu-button> |             <core-user-menu-button /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content class="limited-width"> | <ion-content class="limited-width"> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
|         <ion-item *ngIf="showMyEntriesToggle"> |         <ion-item *ngIf="showMyEntriesToggle"> | ||||||
|             <ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label> |             <ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)"> | ||||||
|             <ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)" slot="end"></ion-toggle> |                 {{ 'addon.blog.showonlyyourentries' | translate }} | ||||||
|  |             </ion-toggle> | ||||||
|         </ion-item> |         </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"> |         <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"> |                 <ion-item class="ion-text-wrap"> | ||||||
|                     <core-user-avatar [user]="entry.user" slot="start" [courseId]="entry.courseid"></core-user-avatar> |                     <core-user-avatar [user]="entry.user" slot="start" [courseId]="entry.courseid" /> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <div class="flex-row ion-justify-content-between ion-align-items-center"> |                         <div class="flex-row ion-justify-content-between ion-align-items-center"> | ||||||
|                             <h2> |                             <h2> | ||||||
|                                 <core-format-text [text]="entry.subject" [contextLevel]="contextLevel" |                                 <core-format-text [text]="entry.subject" [contextLevel]="contextLevel" | ||||||
|                                     [contextInstanceId]="contextInstanceId"> |                                     [contextInstanceId]="contextInstanceId" /> | ||||||
|                                 </core-format-text> |  | ||||||
|                             </h2> |                             </h2> | ||||||
|                             <ion-note class="ion-text-end"> |                             <ion-note class="ion-text-end"> | ||||||
|                                 {{ 'addon.blog.' + entry.publishTranslated! | translate}} |                                 {{ 'addon.blog.' + entry.publishTranslated! | translate}} | ||||||
| @ -49,35 +48,32 @@ | |||||||
|                     <ion-item class="ion-text-wrap"> |                     <ion-item class="ion-text-wrap"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <core-format-text [text]="entry.summary" [component]="this.component" [componentId]="entry.id" |                             <core-format-text [text]="entry.summary" [component]="this.component" [componentId]="entry.id" | ||||||
|                                 [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"> |                                 [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry.tags && entry.tags!.length > 0"> |                     <ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry.tags && entry.tags!.length > 0"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <div slot="start">{{ 'core.tag.tags' | translate }}:</div> |                             <div slot="start">{{ 'core.tag.tags' | translate }}:</div> | ||||||
|                             <core-tag-list [tags]="entry.tags"></core-tag-list> |                             <core-tag-list [tags]="entry.tags" /> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <core-comments *ngIf="commentsEnabled" [component]="this.component" [itemId]="entry.id" area="format_blog" |                     <core-comments *ngIf="commentsEnabled" [component]="this.component" [itemId]="entry.id" area="format_blog" | ||||||
|                         [instanceId]="entry.userid" contextLevel="user" [showItem]="true"> |                         [instanceId]="entry.userid" contextLevel="user" [showItem]="true" /> | ||||||
|                     </core-comments> |  | ||||||
|                     <core-file *ngFor="let file of entry.attachmentfiles" [file]="file" [component]="this.component" |                     <core-file *ngFor="let file of entry.attachmentfiles" [file]="file" [component]="this.component" | ||||||
|                         [componentId]="entry.id"> |                         [componentId]="entry.id" /> | ||||||
|                     </core-file> |  | ||||||
|                     <ion-item *ngIf="entry.uniquehash" [href]="entry.uniquehash" core-link [detail]="true"> |                     <ion-item *ngIf="entry.uniquehash" [href]="entry.uniquehash" core-link [detail]="true"> | ||||||
|                         <ion-label>{{ 'addon.blog.linktooriginalentry' | translate }}</ion-label> |                         <ion-label>{{ 'addon.blog.linktooriginalentry' | translate }}</ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                 </ion-card-content> |                 </ion-card-content> | ||||||
|                 <div class="ion-text-center ion-margin-bottom" *ngIf="entry.lastmodified > entry.created"> |                 <div class="ion-text-center ion-margin-bottom" *ngIf="entry.lastmodified > entry.created"> | ||||||
|                     <ion-note> |                     <ion-note> | ||||||
|                         <ion-icon name="fas-clock" [attr.aria-label]="'core.lastmodified' | translate"></ion-icon> {{entry.lastmodified |                         <ion-icon name="fas-clock" [attr.aria-label]="'core.lastmodified' | translate" /> {{entry.lastmodified | ||||||
|                         | |                         | | ||||||
|                         coreTimeAgo}} |                         coreTimeAgo}} | ||||||
|                     </ion-note> |                     </ion-note> | ||||||
|                 </div> |                 </div> | ||||||
|             </ion-card> |             </ion-card> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|         <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError"></core-infinite-loading> |         <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError" /> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -2,8 +2,7 @@ | |||||||
| <core-navbar-buttons slot="end" prepend> | <core-navbar-buttons slot="end" prepend> | ||||||
|     <core-context-menu> |     <core-context-menu> | ||||||
|         <core-context-menu-item *ngIf="canNavigate && !selectedMonthIsCurrent() && displayNavButtons" [priority]="900" |         <core-context-menu-item *ngIf="canNavigate && !selectedMonthIsCurrent() && displayNavButtons" [priority]="900" | ||||||
|             [content]="'addon.calendar.currentmonth' | translate" iconAction="fas-calendar-day" (action)="goToCurrentMonth()"> |             [content]="'addon.calendar.currentmonth' | translate" iconAction="fas-calendar-day" (action)="goToCurrentMonth()" /> | ||||||
|         </core-context-menu-item> |  | ||||||
|     </core-context-menu> |     </core-context-menu> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
| @ -14,19 +13,18 @@ | |||||||
|             <ion-row class="ion-align-items-center"> |             <ion-row class="ion-align-items-center"> | ||||||
|                 <ion-col class="ion-text-start" *ngIf="canNavigate"> |                 <ion-col class="ion-text-start" *ngIf="canNavigate"> | ||||||
|                     <ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'core.previous' | translate"> |                     <ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'core.previous' | translate"> | ||||||
|                         <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> |                         <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true" /> | ||||||
|                     </ion-button> |                     </ion-button> | ||||||
|                 </ion-col> |                 </ion-col> | ||||||
|                 <ion-col class="ion-text-center addon-calendar-period"> |                 <ion-col class="ion-text-center addon-calendar-period"> | ||||||
|                     <h2 id="addon-calendar-monthname"> |                     <h2 id="addon-calendar-monthname"> | ||||||
|                         {{ periodName }} |                         {{ periodName }} | ||||||
|                         <ion-spinner *ngIf="!selectedMonthLoaded()" class="addon-calendar-loading-month"> |                         <ion-spinner *ngIf="!selectedMonthLoaded()" class="addon-calendar-loading-month" /> | ||||||
|                         </ion-spinner> |  | ||||||
|                     </h2> |                     </h2> | ||||||
|                 </ion-col> |                 </ion-col> | ||||||
|                 <ion-col class="ion-text-end" *ngIf="canNavigate"> |                 <ion-col class="ion-text-end" *ngIf="canNavigate"> | ||||||
|                     <ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate"> |                     <ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate"> | ||||||
|                         <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> |                         <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true" /> | ||||||
|                     </ion-button> |                     </ion-button> | ||||||
|                 </ion-col> |                 </ion-col> | ||||||
|             </ion-row> |             </ion-row> | ||||||
| @ -50,8 +48,7 @@ | |||||||
|                         <!-- Weeks. --> |                         <!-- Weeks. --> | ||||||
|                         <ion-row *ngFor="let week of month.weeks" class="addon-calendar-week" role="row"> |                         <ion-row *ngFor="let week of month.weeks" class="addon-calendar-week" role="row"> | ||||||
|                             <!-- Empty slots (first week). --> |                             <!-- Empty slots (first week). --> | ||||||
|                             <ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell"> |                             <ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell" /> | ||||||
|                             </ion-col> |  | ||||||
|                             <ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{ |                             <ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{ | ||||||
|                                     "hasevents": day.hasevents, |                                     "hasevents": day.hasevents, | ||||||
|                                     "today": month.isCurrentMonth && day.istoday, |                                     "today": month.isCurrentMonth && day.istoday, | ||||||
| @ -71,14 +68,14 @@ | |||||||
|                                 <!-- In tablet, display list of events. --> |                                 <!-- In tablet, display list of events. --> | ||||||
|                                 <div class="ion-hide-md-down addon-calendar-day-events" *ngIf="day.filteredEvents"> |                                 <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"> |                                     <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)" |                                             [class.addon-calendar-event-past]="event.ispast" (ariaButtonClick)="eventClicked(event, $event)" | ||||||
|                                             [tabindex]="activeView ? 0 : -1"> |                                             [tabindex]="activeView ? 0 : -1"> | ||||||
|                                             <span class="calendar_event_type calendar_event_{{event.formattedType}}"></span> |                                             <span class="calendar_event_type calendar_event_{{event.formattedType}}"></span> | ||||||
|                                             <ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock" |                                             <ion-icon *ngIf="event.offline && !event.deleted" name="fas-clock" | ||||||
|                                                 [attr.aria-label]="'core.notsent' | translate"></ion-icon> |                                                 [attr.aria-label]="'core.notsent' | translate" /> | ||||||
|                                             <ion-icon *ngIf="event.deleted" name="fas-trash" |                                             <ion-icon *ngIf="event.deleted" name="fas-trash" | ||||||
|                                                 [attr.aria-label]="'core.deletedoffline' | translate"></ion-icon> |                                                 [attr.aria-label]="'core.deletedoffline' | translate" /> | ||||||
|                                             <span class="addon-calendar-event-time"> |                                             <span class="addon-calendar-event-time"> | ||||||
|                                                 {{ event.timestart * 1000 | coreFormatDate: timeFormat }} |                                                 {{ event.timestart * 1000 | coreFormatDate: timeFormat }} | ||||||
|                                             </span> |                                             </span> | ||||||
| @ -98,8 +95,7 @@ | |||||||
|                                 </div> |                                 </div> | ||||||
|                             </ion-col> |                             </ion-col> | ||||||
|                             <!-- Empty slots (last week). --> |                             <!-- Empty slots (last week). --> | ||||||
|                             <ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell"> |                             <ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell" /> | ||||||
|                             </ion-col> |  | ||||||
|                         </ion-row> |                         </ion-row> | ||||||
|                     </div> |                     </div> | ||||||
|                 </ion-grid> |                 </ion-grid> | ||||||
|  | |||||||
| @ -142,7 +142,7 @@ | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ion-slide { |     swiper-slide { | ||||||
|         display: block; |         display: block; | ||||||
|         font-size: inherit; |         font-size: inherit; | ||||||
|         justify-content: start; |         justify-content: start; | ||||||
|  | |||||||
| @ -64,7 +64,7 @@ import { Translate } from '@singletons'; | |||||||
| }) | }) | ||||||
| export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy { | export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy { | ||||||
| 
 | 
 | ||||||
|     @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent<PreloadedMonth>; |     @ViewChild(CoreSwipeSlidesComponent) swipeSlidesComponent?: CoreSwipeSlidesComponent<PreloadedMonth>; | ||||||
| 
 | 
 | ||||||
|     @Input() initialYear?: number; // Initial year to load.
 |     @Input() initialYear?: number; // Initial year to load.
 | ||||||
|     @Input() initialMonth?: number; // Initial month to load.
 |     @Input() initialMonth?: number; // Initial month to load.
 | ||||||
| @ -142,7 +142,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Component loaded. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|         this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate); |         this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate); | ||||||
| @ -164,7 +164,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngDoCheck(): void { |     ngDoCheck(): void { | ||||||
|         const items = this.manager?.getSource().getItems(); |         const items = this.manager?.getSource().getItems(); | ||||||
| @ -185,7 +185,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro | |||||||
|             this.hiddenDiffer = this.hidden; |             this.hiddenDiffer = this.hidden; | ||||||
| 
 | 
 | ||||||
|             if (!this.hidden) { |             if (!this.hidden) { | ||||||
|                 this.slides?.slides?.getSwiper().then(swipper => swipper.update()); |                 this.swipeSlidesComponent?.updateSlidesComponent(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -248,14 +248,14 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro | |||||||
|      * Load next month. |      * Load next month. | ||||||
|      */ |      */ | ||||||
|     loadNext(): void { |     loadNext(): void { | ||||||
|         this.slides?.slideNext(); |         this.swipeSlidesComponent?.slideNext(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Load previous month. |      * Load previous month. | ||||||
|      */ |      */ | ||||||
|     loadPrevious(): void { |     loadPrevious(): void { | ||||||
|         this.slides?.slidePrev(); |         this.swipeSlidesComponent?.slidePrev(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -343,8 +343,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro | |||||||
|      */ |      */ | ||||||
|     async viewMonth(month: number, year: number): Promise<void> { |     async viewMonth(month: number, year: number): Promise<void> { | ||||||
|         const manager = this.manager; |         const manager = this.manager; | ||||||
|         const slides = this.slides; |         if (!manager || !this.swipeSlidesComponent) { | ||||||
|         if (!manager || !slides) { |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -360,7 +359,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro | |||||||
|             // Make sure the day is loaded.
 |             // Make sure the day is loaded.
 | ||||||
|             await manager.getSource().loadItem(item); |             await manager.getSource().loadItem(item); | ||||||
| 
 | 
 | ||||||
|             slides.slideToItem(item); |             this.swipeSlidesComponent.slideToItem(item); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); |             CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); | ||||||
|         } finally { |         } finally { | ||||||
| @ -369,7 +368,7 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Component destroyed. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnDestroy(): void { |     ngOnDestroy(): void { | ||||||
|         this.undeleteEventObserver?.off(); |         this.undeleteEventObserver?.off(); | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||||
|                 <ion-icon name="fas-xmark" slot="icon-only" aria-hidden=true></ion-icon> |                 <ion-icon name="fas-xmark" slot="icon-only" aria-hidden=true /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| @ -10,18 +10,18 @@ | |||||||
| <ion-content [fullscreen]="true"> | <ion-content [fullscreen]="true"> | ||||||
|     <ion-list> |     <ion-list> | ||||||
|         <ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]"> |         <ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]"> | ||||||
|             <ion-icon [name]="typeIcons[type]" slot="start" aria-hidden="true"></ion-icon> |             <ion-icon [name]="typeIcons[type]" slot="start" aria-hidden="true" /> | ||||||
|             <ion-label>{{ 'addon.calendar.' + type + 'events' | translate}}</ion-label> |             <ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()"> | ||||||
|             <ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()" slot="end"></ion-toggle> |                 {{ 'addon.calendar.' + type + 'events' | translate}} | ||||||
|  |             </ion-toggle> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|         <core-spacer *ngIf="filter.course || filter.category || filter.group"></core-spacer> |         <core-spacer *ngIf="filter.course || filter.category || filter.group" /> | ||||||
|         <ng-container *ngIf="filter.course || filter.category || filter.group"> |         <ng-container *ngIf="filter.course || filter.category || filter.group"> | ||||||
|             <ion-radio-group [(ngModel)]="courseId" (ionChange)="onChange()"> |             <ion-radio-group [(ngModel)]="courseId" (ionChange)="onChange()"> | ||||||
|                 <ion-item class="ion-text-wrap" *ngFor="let course of sortedCourses"> |                 <ion-item class="ion-text-wrap" *ngFor="let course of sortedCourses"> | ||||||
|                     <ion-label> |                     <ion-radio [value]="course.id"> | ||||||
|                         <core-format-text [text]="course.shortname"></core-format-text> |                         <core-format-text [text]="course.shortname" /> | ||||||
|                     </ion-label> |                     </ion-radio> | ||||||
|                     <ion-radio slot="end" [value]="course.id"></ion-radio> |  | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </ion-radio-group> |             </ion-radio-group> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <core-loading [hideUntil]="loaded"> | <core-loading [hideUntil]="loaded"> | ||||||
|     <core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="fas-calendar" [message]="'addon.calendar.noevents' | translate"> |     <core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="fas-calendar" | ||||||
|     </core-empty-box> |         [message]="'addon.calendar.noevents' | translate" /> | ||||||
| 
 | 
 | ||||||
|     <ion-list *ngIf="filteredEvents && filteredEvents.length" class="list-item-limited-width"> |     <ion-list *ngIf="filteredEvents && filteredEvents.length" class="list-item-limited-width"> | ||||||
|         <ng-container *ngFor="let event of filteredEvents"> |         <ng-container *ngFor="let event of filteredEvents"> | ||||||
| @ -8,9 +8,8 @@ | |||||||
|                 <ion-item class="ion-text-wrap addon-calendar-event" [attr.aria-label]="event.name" (click)="eventClicked(event)" button |                 <ion-item class="ion-text-wrap addon-calendar-event" [attr.aria-label]="event.name" (click)="eventClicked(event)" button | ||||||
|                     [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" [detail]="false"> |                     [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" [detail]="false"> | ||||||
|                     <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [modname]="event.modulename" |                     <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [modname]="event.modulename" | ||||||
|                         [componentId]="event.instance" [showAlt]="false" [purpose]="event.purpose"></core-mod-icon> |                         [componentId]="event.instance" [showAlt]="false" [purpose]="event.purpose" /> | ||||||
|                     <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" aria-hidden="true"> |                     <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" aria-hidden="true" /> | ||||||
|                     </ion-icon> |  | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <!-- Add the icon title so accessibility tools read it. --> |                         <!-- Add the icon title so accessibility tools read it. --> | ||||||
|                         <span class="sr-only"> |                         <span class="sr-only"> | ||||||
| @ -19,18 +18,18 @@ | |||||||
|                         </span> |                         </span> | ||||||
|                         <p class="item-heading"> |                         <p class="item-heading"> | ||||||
|                             <core-format-text [text]="event.name" [contextLevel]="event.contextLevel" |                             <core-format-text [text]="event.name" [contextLevel]="event.contextLevel" | ||||||
|                                 [contextInstanceId]="event.contextInstanceId"></core-format-text> |                                 [contextInstanceId]="event.contextInstanceId" /> | ||||||
|                         </p> |                         </p> | ||||||
|                         <p> |                         <p> | ||||||
|                             <core-format-text [text]="event.formattedtime" [filter]="false"></core-format-text> |                             <core-format-text [text]="event.formattedtime" [filter]="false" /> | ||||||
|                         </p> |                         </p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                     <ion-note *ngIf="event.offline && !event.deleted" slot="end"> |                     <ion-note *ngIf="event.offline && !event.deleted" slot="end"> | ||||||
|                         <ion-icon name="fas-clock" aria-hidden="true"></ion-icon> |                         <ion-icon name="fas-clock" aria-hidden="true" /> | ||||||
|                         <span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span> |                         <span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span> | ||||||
|                     </ion-note> |                     </ion-note> | ||||||
|                     <ion-note *ngIf="event.deleted" slot="end"> |                     <ion-note *ngIf="event.deleted" slot="end"> | ||||||
|                         <ion-icon name="fas-trash" aria-hidden="true"></ion-icon> |                         <ion-icon name="fas-trash" aria-hidden="true" /> | ||||||
|                         <span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span> |                         <span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span> | ||||||
|                     </ion-note> |                     </ion-note> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|  | |||||||
| @ -1,30 +1,28 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.calendar.calendarevents' | translate }}</h1> |             <h1>{{ 'addon.calendar.calendarevents' | translate }}</h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button fill="clear" (click)="openFilter()" [attr.aria-label]="'core.filter' | translate"> |             <ion-button fill="clear" (click)="openFilter()" [attr.aria-label]="'core.filter' | translate"> | ||||||
|                 <ion-icon slot="icon-only" name="fas-filter" aria-hidden="true"></ion-icon> |                 <ion-icon slot="icon-only" name="fas-filter" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <core-context-menu> |             <core-context-menu> | ||||||
|                 <core-context-menu-item *ngIf="!selectedDayIsCurrent()" [priority]="900" [content]="'addon.calendar.today' | translate" |                 <core-context-menu-item *ngIf="!selectedDayIsCurrent()" [priority]="900" [content]="'addon.calendar.today' | translate" | ||||||
|                     iconAction="fas-calendar-day" (action)="goToCurrentDay()"> |                     iconAction="fas-calendar-day" (action)="goToCurrentDay()" /> | ||||||
|                 </core-context-menu-item> |  | ||||||
|                 <core-context-menu-item [hidden]="!loaded || !selectedDayHasOffline() || !isOnline" [priority]="400" |                 <core-context-menu-item [hidden]="!loaded || !selectedDayHasOffline() || !isOnline" [priority]="400" | ||||||
|                     [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event)" [iconAction]="syncIcon" |                     [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event)" [iconAction]="syncIcon" | ||||||
|                     [closeOnClick]="false"> |                     [closeOnClick]="false" /> | ||||||
|                 </core-context-menu-item> |  | ||||||
|             </core-context-menu> |             </core-context-menu> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
| 
 | 
 | ||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
| @ -34,7 +32,7 @@ | |||||||
|                 <ion-row class="ion-align-items-center"> |                 <ion-row class="ion-align-items-center"> | ||||||
|                     <ion-col class="ion-text-start"> |                     <ion-col class="ion-text-start"> | ||||||
|                         <ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'addon.calendar.dayprev' | translate"> |                         <ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'addon.calendar.dayprev' | translate"> | ||||||
|                             <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon> |                             <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true" /> | ||||||
|                         </ion-button> |                         </ion-button> | ||||||
|                     </ion-col> |                     </ion-col> | ||||||
|                     <ion-col class="ion-text-center addon-calendar-period"> |                     <ion-col class="ion-text-center addon-calendar-period"> | ||||||
| @ -42,7 +40,7 @@ | |||||||
|                     </ion-col> |                     </ion-col> | ||||||
|                     <ion-col class="ion-text-end"> |                     <ion-col class="ion-text-end"> | ||||||
|                         <ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'addon.calendar.daynext' | translate"> |                         <ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'addon.calendar.daynext' | translate"> | ||||||
|                             <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon> |                             <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true" /> | ||||||
|                         </ion-button> |                         </ion-button> | ||||||
|                     </ion-col> |                     </ion-col> | ||||||
|                 </ion-row> |                 </ion-row> | ||||||
| @ -54,14 +52,13 @@ | |||||||
|                         <!-- There is data to be synchronized --> |                         <!-- There is data to be synchronized --> | ||||||
|                         <ion-card class="core-warning-card list-item-limited-width" *ngIf="day.hasOffline"> |                         <ion-card class="core-warning-card list-item-limited-width" *ngIf="day.hasOffline"> | ||||||
|                             <ion-item> |                             <ion-item> | ||||||
|                                 <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true"></ion-icon> |                                 <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" /> | ||||||
|                                 <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}</ion-label> |                                 <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}</ion-label> | ||||||
|                             </ion-item> |                             </ion-item> | ||||||
|                         </ion-card> |                         </ion-card> | ||||||
| 
 | 
 | ||||||
|                         <core-empty-box *ngIf="!day.filteredEvents || !day.filteredEvents.length" icon="fas-calendar" |                         <core-empty-box *ngIf="!day.filteredEvents || !day.filteredEvents.length" icon="fas-calendar" | ||||||
|                             [message]="'addon.calendar.noevents' | translate"> |                             [message]="'addon.calendar.noevents' | translate" /> | ||||||
|                         </core-empty-box> |  | ||||||
| 
 | 
 | ||||||
|                         <ion-list *ngIf="day.filteredEvents && day.filteredEvents.length" class="list-item-limited-width"> |                         <ion-list *ngIf="day.filteredEvents && day.filteredEvents.length" class="list-item-limited-width"> | ||||||
|                             <ng-container *ngFor="let event of day.filteredEvents"> |                             <ng-container *ngFor="let event of day.filteredEvents"> | ||||||
| @ -70,11 +67,9 @@ | |||||||
|                                         (click)="gotoEvent(event.id, day)" [class.item-dimmed]="event.ispast" |                                         (click)="gotoEvent(event.id, day)" [class.item-dimmed]="event.ispast" | ||||||
|                                         [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button [detail]="false"> |                                         [ngClass]="['addon-calendar-eventtype-'+event.eventtype]" button [detail]="false"> | ||||||
|                                         <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [showAlt]="false" |                                         <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" slot="start" [showAlt]="false" | ||||||
|                                             [modname]="event.modulename" [componentId]="event.instance" [purpose]="event.purpose"> |                                             [modname]="event.modulename" [componentId]="event.instance" [purpose]="event.purpose" /> | ||||||
|                                         </core-mod-icon> |  | ||||||
|                                         <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" |                                         <ion-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" slot="start" | ||||||
|                                             aria-hidden="true"> |                                             aria-hidden="true" /> | ||||||
|                                         </ion-icon> |  | ||||||
|                                         <ion-label> |                                         <ion-label> | ||||||
|                                             <!-- Add the icon title so accessibility tools read it. --> |                                             <!-- Add the icon title so accessibility tools read it. --> | ||||||
|                                             <span class="sr-only"> |                                             <span class="sr-only"> | ||||||
| @ -84,18 +79,18 @@ | |||||||
|                                             </span> |                                             </span> | ||||||
|                                             <p class="item-heading"> |                                             <p class="item-heading"> | ||||||
|                                                 <core-format-text [text]="event.name" [contextLevel]="event.contextLevel" |                                                 <core-format-text [text]="event.name" [contextLevel]="event.contextLevel" | ||||||
|                                                     [contextInstanceId]="event.contextInstanceId"></core-format-text> |                                                     [contextInstanceId]="event.contextInstanceId" /> | ||||||
|                                             </p> |                                             </p> | ||||||
|                                             <p> |                                             <p> | ||||||
|                                                 <core-format-text [text]="event.formattedtime" [filter]="false"></core-format-text> |                                                 <core-format-text [text]="event.formattedtime" [filter]="false" /> | ||||||
|                                             </p> |                                             </p> | ||||||
|                                         </ion-label> |                                         </ion-label> | ||||||
|                                         <ion-note *ngIf="event.offline && !event.deleted" slot="end"> |                                         <ion-note *ngIf="event.offline && !event.deleted" slot="end"> | ||||||
|                                             <ion-icon name="fas-clock" aria-hidden="true"></ion-icon> |                                             <ion-icon name="fas-clock" aria-hidden="true" /> | ||||||
|                                             <span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span> |                                             <span class="ion-text-wrap">{{ 'core.notsent' | translate }}</span> | ||||||
|                                         </ion-note> |                                         </ion-note> | ||||||
|                                         <ion-note *ngIf="event.deleted" slot="end"> |                                         <ion-note *ngIf="event.deleted" slot="end"> | ||||||
|                                             <ion-icon name="fas-trash" aria-hidden="true"></ion-icon> |                                             <ion-icon name="fas-trash" aria-hidden="true" /> | ||||||
|                                             <span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span> |                                             <span class="ion-text-wrap">{{ 'core.deletedoffline' | translate }}</span> | ||||||
|                                         </ion-note> |                                         </ion-note> | ||||||
|                                     </ion-item> |                                     </ion-item> | ||||||
| @ -111,7 +106,7 @@ | |||||||
|     <!-- Create a calendar event. --> |     <!-- Create a calendar event. --> | ||||||
|     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canCreate && loaded"> |     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canCreate && loaded"> | ||||||
|         <ion-fab-button (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate"> |         <ion-fab-button (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate"> | ||||||
|             <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> |             <ion-icon name="fas-plus" aria-hidden="true" /> | ||||||
|             <span class="sr-only">{{ 'addon.calendar.newevent' | translate }}</span> |             <span class="sr-only">{{ 'addon.calendar.newevent' | translate }}</span> | ||||||
|         </ion-fab-button> |         </ion-fab-button> | ||||||
|     </ion-fab> |     </ion-fab> | ||||||
|  | |||||||
| @ -60,20 +60,13 @@ import { CoreTime } from '@singletons/time'; | |||||||
| }) | }) | ||||||
| export class AddonCalendarDayPage implements OnInit, OnDestroy { | export class AddonCalendarDayPage implements OnInit, OnDestroy { | ||||||
| 
 | 
 | ||||||
|     @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent<PreloadedDay>; |     @ViewChild(CoreSwipeSlidesComponent) swipeSlidesComponent?: CoreSwipeSlidesComponent<PreloadedDay>; | ||||||
| 
 | 
 | ||||||
|     protected currentSiteId: string; |     protected currentSiteId: string; | ||||||
| 
 | 
 | ||||||
|     // Observers.
 |     // Observers.
 | ||||||
|     protected newEventObserver: CoreEventObserver; |     protected eventObservers: CoreEventObserver[] = []; | ||||||
|     protected discardedObserver: CoreEventObserver; |  | ||||||
|     protected editEventObserver: CoreEventObserver; |  | ||||||
|     protected deleteEventObserver: CoreEventObserver; |  | ||||||
|     protected undeleteEventObserver: CoreEventObserver; |  | ||||||
|     protected syncObserver: CoreEventObserver; |  | ||||||
|     protected manualSyncObserver: CoreEventObserver; |  | ||||||
|     protected onlineObserver: Subscription; |     protected onlineObserver: Subscription; | ||||||
|     protected filterChangedObserver: CoreEventObserver; |  | ||||||
|     protected managerUnsubscribe?: () => void; |     protected managerUnsubscribe?: () => void; | ||||||
|     protected logView: () => void; |     protected logView: () => void; | ||||||
| 
 | 
 | ||||||
| @ -97,7 +90,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|         this.currentSiteId = CoreSites.getCurrentSiteId(); |         this.currentSiteId = CoreSites.getCurrentSiteId(); | ||||||
| 
 | 
 | ||||||
|         // Listen for events added. When an event is added, reload the data.
 |         // Listen for events added. When an event is added, reload the data.
 | ||||||
|         this.newEventObserver = CoreEvents.on( |         this.eventObservers.push(CoreEvents.on( | ||||||
|             AddonCalendarProvider.NEW_EVENT_EVENT, |             AddonCalendarProvider.NEW_EVENT_EVENT, | ||||||
|             (data) => { |             (data) => { | ||||||
|                 if (data && data.eventId) { |                 if (data && data.eventId) { | ||||||
| @ -106,16 +99,16 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             this.currentSiteId, |             this.currentSiteId, | ||||||
|         ); |         )); | ||||||
| 
 | 
 | ||||||
|         // Listen for new event discarded event. When it does, reload the data.
 |         // Listen for new event discarded event. When it does, reload the data.
 | ||||||
|         this.discardedObserver = CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { |         this.eventObservers.push(CoreEvents.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => { | ||||||
|             this.manager?.getSource().markAllItemsUnloaded(); |             this.manager?.getSource().markAllItemsUnloaded(); | ||||||
|             this.refreshData(true, true); |             this.refreshData(true, true); | ||||||
|         }, this.currentSiteId); |         }, this.currentSiteId)); | ||||||
| 
 | 
 | ||||||
|         // Listen for events edited. When an event is edited, reload the data.
 |         // Listen for events edited. When an event is edited, reload the data.
 | ||||||
|         this.editEventObserver = CoreEvents.on( |         this.eventObservers.push(CoreEvents.on( | ||||||
|             AddonCalendarProvider.EDIT_EVENT_EVENT, |             AddonCalendarProvider.EDIT_EVENT_EVENT, | ||||||
|             (data) => { |             (data) => { | ||||||
|                 if (data && data.eventId) { |                 if (data && data.eventId) { | ||||||
| @ -124,25 +117,25 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             this.currentSiteId, |             this.currentSiteId, | ||||||
|         ); |         )); | ||||||
| 
 | 
 | ||||||
|         // Refresh data if calendar events are synchronized automatically.
 |         // Refresh data if calendar events are synchronized automatically.
 | ||||||
|         this.syncObserver = CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => { |         this.eventObservers.push(CoreEvents.on(AddonCalendarSyncProvider.AUTO_SYNCED, () => { | ||||||
|             this.manager?.getSource().markAllItemsUnloaded(); |             this.manager?.getSource().markAllItemsUnloaded(); | ||||||
|             this.refreshData(false, true); |             this.refreshData(false, true); | ||||||
|         }, this.currentSiteId); |         }, this.currentSiteId)); | ||||||
| 
 | 
 | ||||||
|         // Refresh data if calendar events are synchronized manually but not by this page.
 |         // Refresh data if calendar events are synchronized manually but not by this page.
 | ||||||
|         this.manualSyncObserver = CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { |         this.eventObservers.push(CoreEvents.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => { | ||||||
|             const selectedDay = this.manager?.getSelectedItem(); |             const selectedDay = this.manager?.getSelectedItem(); | ||||||
|             if (data && (data.source != 'day' || !selectedDay || !data.moment || !selectedDay.moment.isSame(data.moment, 'day'))) { |             if (data && (data.source != 'day' || !selectedDay || !data.moment || !selectedDay.moment.isSame(data.moment, 'day'))) { | ||||||
|                 this.manager?.getSource().markAllItemsUnloaded(); |                 this.manager?.getSource().markAllItemsUnloaded(); | ||||||
|                 this.refreshData(false, true); |                 this.refreshData(false, true); | ||||||
|             } |             } | ||||||
|         }, this.currentSiteId); |         }, this.currentSiteId)); | ||||||
| 
 | 
 | ||||||
|         // Update the events when an event is deleted.
 |         // Update the events when an event is deleted.
 | ||||||
|         this.deleteEventObserver = CoreEvents.on( |         this.eventObservers.push(CoreEvents.on( | ||||||
|             AddonCalendarProvider.DELETED_EVENT_EVENT, |             AddonCalendarProvider.DELETED_EVENT_EVENT, | ||||||
|             (data) => { |             (data) => { | ||||||
|                 if (data && !data.sent) { |                 if (data && !data.sent) { | ||||||
| @ -154,10 +147,10 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             this.currentSiteId, |             this.currentSiteId, | ||||||
|         ); |         )); | ||||||
| 
 | 
 | ||||||
|         // Listen for events "undeleted" (offline).
 |         // Listen for events "undeleted" (offline).
 | ||||||
|         this.undeleteEventObserver = CoreEvents.on( |         this.eventObservers.push(CoreEvents.on( | ||||||
|             AddonCalendarProvider.UNDELETED_EVENT_EVENT, |             AddonCalendarProvider.UNDELETED_EVENT_EVENT, | ||||||
|             (data) => { |             (data) => { | ||||||
|                 if (!data || !data.eventId) { |                 if (!data || !data.eventId) { | ||||||
| @ -168,9 +161,9 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|                 this.manager?.getSource().markAsDeleted(data.eventId, false); |                 this.manager?.getSource().markAsDeleted(data.eventId, false); | ||||||
|             }, |             }, | ||||||
|             this.currentSiteId, |             this.currentSiteId, | ||||||
|         ); |         )); | ||||||
| 
 | 
 | ||||||
|         this.filterChangedObserver = CoreEvents.on( |         this.eventObservers.push(CoreEvents.on( | ||||||
|             AddonCalendarProvider.FILTER_CHANGED_EVENT, |             AddonCalendarProvider.FILTER_CHANGED_EVENT, | ||||||
|             async (data) => { |             async (data) => { | ||||||
|                 this.filter = data; |                 this.filter = data; | ||||||
| @ -180,7 +173,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|                 this.manager?.getSource().filterAllDayEvents(this.filter); |                 this.manager?.getSource().filterAllDayEvents(this.filter); | ||||||
|             }, |             }, | ||||||
|         ); |         )); | ||||||
| 
 | 
 | ||||||
|         // Refresh online status when changes.
 |         // Refresh online status when changes.
 | ||||||
|         this.onlineObserver = CoreNetwork.onChange().subscribe(() => { |         this.onlineObserver = CoreNetwork.onChange().subscribe(() => { | ||||||
| @ -214,7 +207,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * View loaded. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|         const types: string[] = []; |         const types: string[] = []; | ||||||
| @ -434,8 +427,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|      */ |      */ | ||||||
|     async goToCurrentDay(): Promise<void> { |     async goToCurrentDay(): Promise<void> { | ||||||
|         const manager = this.manager; |         const manager = this.manager; | ||||||
|         const slides = this.slides; |         if (!manager || !this.swipeSlidesComponent) { | ||||||
|         if (!manager || !slides) { |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -448,7 +440,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|             // Make sure the day is loaded.
 |             // Make sure the day is loaded.
 | ||||||
|             await manager.getSource().loadItem(currentDay); |             await manager.getSource().loadItem(currentDay); | ||||||
| 
 | 
 | ||||||
|             slides.slideToItem(currentDay); |             this.swipeSlidesComponent.slideToItem(currentDay); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); |             CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); | ||||||
|         } finally { |         } finally { | ||||||
| @ -460,29 +452,22 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { | |||||||
|      * Load next day. |      * Load next day. | ||||||
|      */ |      */ | ||||||
|     async loadNext(): Promise<void> { |     async loadNext(): Promise<void> { | ||||||
|         this.slides?.slideNext(); |         this.swipeSlidesComponent?.slideNext(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Load previous day. |      * Load previous day. | ||||||
|      */ |      */ | ||||||
|     async loadPrevious(): Promise<void> { |     async loadPrevious(): Promise<void> { | ||||||
|         this.slides?.slidePrev(); |         this.swipeSlidesComponent?.slidePrev(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Page destroyed. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnDestroy(): void { |     ngOnDestroy(): void { | ||||||
|         this.newEventObserver?.off(); |         this.eventObservers.forEach((observer) => observer.off()); | ||||||
|         this.discardedObserver?.off(); |  | ||||||
|         this.editEventObserver?.off(); |  | ||||||
|         this.deleteEventObserver?.off(); |  | ||||||
|         this.undeleteEventObserver?.off(); |  | ||||||
|         this.syncObserver?.off(); |  | ||||||
|         this.manualSyncObserver?.off(); |  | ||||||
|         this.onlineObserver?.unsubscribe(); |         this.onlineObserver?.unsubscribe(); | ||||||
|         this.filterChangedObserver?.off(); |  | ||||||
|         this.manager?.getSource().forgetRelatedSources(); |         this.manager?.getSource().forgetRelatedSources(); | ||||||
|         this.manager?.destroy(); |         this.manager?.destroy(); | ||||||
|         this.managerUnsubscribe?.(); |         this.managerUnsubscribe?.(); | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ title | translate }}</h1> |             <h1>{{ title | translate }}</h1> | ||||||
| @ -10,19 +10,18 @@ | |||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
| 
 | 
 | ||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
|         <form [formGroup]="form" *ngIf="!error" #editEventForm> |         <form [formGroup]="form" *ngIf="!error" #editEventForm> | ||||||
|             <!-- Event name. --> |             <!-- Event name. --> | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
|                 <ion-label position="stacked"> |                 <ion-input labelPlacement="stacked" type="text" name="name" [placeholder]="'addon.calendar.eventname' | translate" | ||||||
|                     <p class="item-heading" [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</p> |                     formControlName="name"> | ||||||
|                 </ion-label> |                     <div slot="label" [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</div> | ||||||
|                 <ion-input type="text" name="name" [placeholder]="'addon.calendar.eventname' | translate" formControlName="name"> |  | ||||||
|                 </ion-input> |                 </ion-input> | ||||||
|                 <core-input-errors [control]="form.controls.name" [errorMessages]="errors"></core-input-errors> |                 <core-input-errors [control]="form.controls.name" /> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
|             <!-- Date. --> |             <!-- Date. --> | ||||||
| @ -30,20 +29,26 @@ | |||||||
|                 <ion-label position="stacked"> |                 <ion-label position="stacked"> | ||||||
|                     <p class="item-heading" [core-mark-required]="true">{{ 'core.date' | translate }}</p> |                     <p class="item-heading" [core-mark-required]="true">{{ 'core.date' | translate }}</p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|                 <ion-datetime formControlName="timestart" [placeholder]="'core.date' | translate" [displayFormat]="dateFormat" |                 <ion-datetime-button datetime="timestart" /> | ||||||
|                     [max]="maxDate" [min]="minDate" [displayTimezone]="displayTimezone"> |                 <ion-modal [keepContentsMounted]="true"> | ||||||
|  |                     <ng-template> | ||||||
|  |                         <ion-datetime id="timestart" formControlName="timestart" presentation="date-time" [max]="maxDate" [min]="minDate"> | ||||||
|  |                             <span slot="title">{{'core.date' | translate}}</span> | ||||||
|                         </ion-datetime> |                         </ion-datetime> | ||||||
|                 <core-input-errors [control]="form.controls.timestart" [errorMessages]="errors"></core-input-errors> |                     </ng-template> | ||||||
|  |                 </ion-modal> | ||||||
|  |                 <core-input-errors [control]="form.controls.timestart" /> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
|             <!-- Type. --> |             <!-- Type. --> | ||||||
|             <ion-item class="ion-text-wrap addon-calendar-eventtype-container"> |             <ion-item class="ion-text-wrap addon-calendar-eventtype-container"> | ||||||
|                 <ion-label> |                 <ion-label *ngIf="eventTypes.length === 1"> | ||||||
|                     <p class="item-heading" [core-mark-required]="true">{{ 'addon.calendar.eventkind' | translate }}</p> |                     <p class="item-heading" [core-mark-required]="true">{{ 'addon.calendar.eventkind' | translate }}</p> | ||||||
|                 </ion-label> |                 </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" |                 <ion-select *ngIf="eventTypes.length > 1" formControlName="eventtype" interface="action-sheet" | ||||||
|                     [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'addon.calendar.eventkind' | translate}"> |                     [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'addon.calendar.eventkind' | translate}"> | ||||||
|  |                     <div [core-mark-required]="true" slot="label">{{ 'addon.calendar.eventkind' | translate }}</div> | ||||||
|                     <ion-select-option *ngFor="let type of eventTypes" [value]="type.value"> |                     <ion-select-option *ngFor="let type of eventTypes" [value]="type.value"> | ||||||
|                         {{ type.name | translate }} |                         {{ type.name | translate }} | ||||||
|                     </ion-select-option> |                     </ion-select-option> | ||||||
| @ -52,11 +57,9 @@ | |||||||
| 
 | 
 | ||||||
|             <!-- Category. --> |             <!-- Category. --> | ||||||
|             <ion-item class="ion-text-wrap" *ngIf="typeControl.value === 'category'"> |             <ion-item class="ion-text-wrap" *ngIf="typeControl.value === 'category'"> | ||||||
|                 <ion-label> |  | ||||||
|                     <p class="item-heading" [core-mark-required]="true">{{ 'core.category' | translate }}</p> |  | ||||||
|                 </ion-label> |  | ||||||
|                 <ion-select formControlName="categoryid" interface="action-sheet" [placeholder]="'core.noselection' | translate" |                 <ion-select formControlName="categoryid" interface="action-sheet" [placeholder]="'core.noselection' | translate" | ||||||
|                     [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.category' | translate}"> |                     [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.category' | translate}"> | ||||||
|  |                     <p [core-mark-required]="true" slot="label">{{ 'core.category' | translate }}</p> | ||||||
|                     <ion-select-option *ngFor="let category of categories" [value]="category.id"> |                     <ion-select-option *ngFor="let category of categories" [value]="category.id"> | ||||||
|                         {{ category.name }} |                         {{ category.name }} | ||||||
|                     </ion-select-option> |                     </ion-select-option> | ||||||
| @ -65,11 +68,9 @@ | |||||||
| 
 | 
 | ||||||
|             <!-- Course. --> |             <!-- Course. --> | ||||||
|             <ion-item class="ion-text-wrap" *ngIf="typeControl.value === 'course'"> |             <ion-item class="ion-text-wrap" *ngIf="typeControl.value === 'course'"> | ||||||
|                 <ion-label> |  | ||||||
|                     <p class="item-heading" [core-mark-required]="true">{{ 'core.course' | translate }}</p> |  | ||||||
|                 </ion-label> |  | ||||||
|                 <ion-select formControlName="courseid" interface="action-sheet" [placeholder]="'core.noselection' | translate" |                 <ion-select formControlName="courseid" interface="action-sheet" [placeholder]="'core.noselection' | translate" | ||||||
|                     [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.course' | translate}"> |                     [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.course' | translate}"> | ||||||
|  |                     <p [core-mark-required]="true" slot="label">{{ 'core.course' | translate }}</p> | ||||||
|                     <ion-select-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-select-option> |                     <ion-select-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-select-option> | ||||||
|                 </ion-select> |                 </ion-select> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| @ -78,12 +79,10 @@ | |||||||
|             <ng-container *ngIf="typeControl.value === 'group'"> |             <ng-container *ngIf="typeControl.value === 'group'"> | ||||||
|                 <!-- Select the course. --> |                 <!-- Select the course. --> | ||||||
|                 <ion-item class="ion-text-wrap"> |                 <ion-item class="ion-text-wrap"> | ||||||
|                     <ion-label> |  | ||||||
|                         <p class="item-heading" [core-mark-required]="true">{{ 'core.course' | translate }}</p> |  | ||||||
|                     </ion-label> |  | ||||||
|                     <ion-select formControlName="groupcourseid" interface="action-sheet" [placeholder]="'core.noselection' | translate" |                     <ion-select formControlName="groupcourseid" interface="action-sheet" [placeholder]="'core.noselection' | translate" | ||||||
|                         [cancelText]="'core.cancel' | translate" (ionChange)="groupCourseSelected()" |                         [cancelText]="'core.cancel' | translate" (ionChange)="groupCourseSelected()" | ||||||
|                         [interfaceOptions]="{header: 'core.course' | translate}"> |                         [interfaceOptions]="{header: 'core.course' | translate}"> | ||||||
|  |                         <p [core-mark-required]="true" slot="label">{{ 'core.course' | translate }}</p> | ||||||
|                         <ion-select-option *ngFor="let course of courses" [value]="course.id"> |                         <ion-select-option *ngFor="let course of courses" [value]="course.id"> | ||||||
|                             {{ course.fullname }} |                             {{ course.fullname }} | ||||||
|                         </ion-select-option> |                         </ion-select-option> | ||||||
| @ -97,18 +96,16 @@ | |||||||
|                 </ion-item> |                 </ion-item> | ||||||
|                 <!-- Select the group. --> |                 <!-- Select the group. --> | ||||||
|                 <ion-item class="ion-text-wrap core-edit-set-group" *ngIf="!loadingGroups && groups.length > 0"> |                 <ion-item class="ion-text-wrap core-edit-set-group" *ngIf="!loadingGroups && groups.length > 0"> | ||||||
|                     <ion-label> |  | ||||||
|                         <p class="item-heading" [core-mark-required]="true">{{ 'core.group' | translate }}</p> |  | ||||||
|                     </ion-label> |  | ||||||
|                     <ion-select formControlName="groupid" interface="action-sheet" [placeholder]="'core.noselection' | translate" |                     <ion-select formControlName="groupid" interface="action-sheet" [placeholder]="'core.noselection' | translate" | ||||||
|                         [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.group' | translate}"> |                         [cancelText]="'core.cancel' | translate" [interfaceOptions]="{header: 'core.group' | translate}"> | ||||||
|  |                         <p [core-mark-required]="true" slot="label">{{ 'core.group' | translate }}</p> | ||||||
|                         <ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option> |                         <ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option> | ||||||
|                     </ion-select> |                     </ion-select> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|                 <!-- Loading groups. --> |                 <!-- Loading groups. --> | ||||||
|                 <ion-item class="ion-text-wrap" *ngIf="loadingGroups"> |                 <ion-item class="ion-text-wrap" *ngIf="loadingGroups"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <ion-spinner *ngIf="loadingGroups" [attr.aria-label]="'core.loading' | translate"></ion-spinner> |                         <ion-spinner *ngIf="loadingGroups" [attr.aria-label]="'core.loading' | translate" /> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </ng-container> |             </ng-container> | ||||||
| @ -121,7 +118,7 @@ | |||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                     <ion-button fill="clear" (click)="addReminder()" slot="end" |                     <ion-button fill="clear" (click)="addReminder()" slot="end" | ||||||
|                         [attr.aria-label]="'addon.calendar.setnewreminder' | translate"> |                         [attr.aria-label]="'addon.calendar.setnewreminder' | translate"> | ||||||
|                         <ion-icon name="fas-plus" slot="icon-only" aria-hidden="true"></ion-icon> |                         <ion-icon name="fas-plus" slot="icon-only" aria-hidden="true" /> | ||||||
|                     </ion-button> |                     </ion-button> | ||||||
|                 </ion-item-divider> |                 </ion-item-divider> | ||||||
|                 <ion-item *ngFor="let reminder of reminders" class="ion-text-wrap"> |                 <ion-item *ngFor="let reminder of reminders" class="ion-text-wrap"> | ||||||
| @ -129,7 +126,7 @@ | |||||||
|                         <p>{{ reminder.label }}</p> |                         <p>{{ reminder.label }}</p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                     <ion-button fill="clear" (click)="removeReminder(reminder)" [attr.aria-label]="'core.delete' | translate" slot="end"> |                     <ion-button fill="clear" (click)="removeReminder(reminder)" [attr.aria-label]="'core.delete' | translate" slot="end"> | ||||||
|                         <ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true"></ion-icon> |                         <ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true" /> | ||||||
|                     </ion-button> |                     </ion-button> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </ng-container> |             </ng-container> | ||||||
| @ -143,34 +140,35 @@ | |||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item-divider> |                     </ion-item-divider> | ||||||
|                     <ion-item> |                     <ion-item> | ||||||
|                         <ion-label> |                         <ion-radio [value]="0"> | ||||||
|                             <p>{{ 'addon.calendar.durationnone' | translate }}</p> |                             <p>{{ 'addon.calendar.durationnone' | translate }}</p> | ||||||
|                         </ion-label> |                         </ion-radio> | ||||||
|                         <ion-radio slot="end" [value]="0"></ion-radio> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <ion-item> |                     <ion-item> | ||||||
|                         <ion-label> |                         <ion-radio [value]="1"> | ||||||
|                             <p>{{ 'addon.calendar.durationuntil' | translate }}</p> |                             <p>{{ 'addon.calendar.durationuntil' | translate }}</p> | ||||||
|                         </ion-label> |                         </ion-radio> | ||||||
|                         <ion-radio slot="end" [value]="1"></ion-radio> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <ion-item *ngIf="form.controls.duration.value === 1"> |                     <ion-item *ngIf="form.controls.duration.value === 1"> | ||||||
|                         <ion-label position="stacked"></ion-label> |                         <ion-label position="stacked" /> | ||||||
|                         <ion-datetime formControlName="timedurationuntil" [max]="maxDate" [min]="minDate" |                         <ion-datetime-button datetime="timedurationuntil" /> | ||||||
|                             [placeholder]="'addon.calendar.durationuntil' | translate" [displayFormat]="dateFormat" |                         <ion-modal [keepContentsMounted]="true"> | ||||||
|                             [displayTimezone]="displayTimezone"> |                             <ng-template> | ||||||
|  |                                 <ion-datetime id="timedurationuntil" formControlName="timedurationuntil" [max]="maxDate" [min]="minDate" | ||||||
|  |                                     presentation="date-time"> | ||||||
|  |                                     <span slot="title">{{'addon.calendar.durationuntil' | translate}}</span> | ||||||
|                                 </ion-datetime> |                                 </ion-datetime> | ||||||
|  |                             </ng-template> | ||||||
|  |                         </ion-modal> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <ion-item> |                     <ion-item> | ||||||
|                         <ion-label> |                         <ion-radio [value]="2"> | ||||||
|                             <p>{{ 'addon.calendar.durationminutes' | translate }}</p> |                             <p id="durationinminutes">{{ 'addon.calendar.durationminutes' | translate }}</p> | ||||||
|                         </ion-label> |                         </ion-radio> | ||||||
|                         <ion-radio slot="end" [value]="2"></ion-radio> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <ion-item *ngIf="form.controls.duration.value === 2"> |                     <ion-item *ngIf="form.controls.duration.value === 2"> | ||||||
|                         <ion-label class="sr-only">{{ 'addon.calendar.durationminutes' | translate }}</ion-label> |                         <ion-input type="number" name="timedurationminutes" labelPlacement="start" aria-labelledby="durationinminutes" | ||||||
|                         <ion-input type="number" name="timedurationminutes" slot="end" |                             [placeholder]="'addon.calendar.durationminutes' | translate" formControlName="timedurationminutes" /> | ||||||
|                             [placeholder]="'addon.calendar.durationminutes' | translate" formControlName="timedurationminutes"></ion-input> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                 </ion-radio-group> |                 </ion-radio-group> | ||||||
|             </div> |             </div> | ||||||
| @ -178,17 +176,13 @@ | |||||||
|             <!-- Repeat (for new events). --> |             <!-- Repeat (for new events). --> | ||||||
|             <ng-container *ngIf="!eventId || eventId < 0"> |             <ng-container *ngIf="!eventId || eventId < 0"> | ||||||
|                 <ion-item class="ion-text-wrap divider"> |                 <ion-item class="ion-text-wrap divider"> | ||||||
|                     <ion-label> |                     <ion-checkbox labelPlacement="start" formControlName="repeat"> | ||||||
|                         <p class="item-heading">{{ 'addon.calendar.repeatevent' | translate }}</p> |                         <p class="item-heading">{{ 'addon.calendar.repeatevent' | translate }}</p> | ||||||
|                     </ion-label> |                     </ion-checkbox> | ||||||
|                     <ion-checkbox slot="end" formControlName="repeat"></ion-checkbox> |  | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|                 <ion-item class="ion-text-wrap"> |                 <ion-item class="ion-text-wrap"> | ||||||
|                     <ion-label position="stacked"> |                     <ion-input labelPlacement="stacked" [label]="'addon.calendar.repeatweeksl' | translate" type="number" name="repeats" | ||||||
|                         <p class="item-heading">{{ 'addon.calendar.repeatweeksl' | translate }}</p> |                         formControlName="repeats" [disabled]="!form.controls.repeat.value" /> | ||||||
|                     </ion-label> |  | ||||||
|                     <ion-input type="number" name="repeats" formControlName="repeats" [disabled]="!form.controls.repeat.value"> |  | ||||||
|                     </ion-input> |  | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </ng-container> |             </ng-container> | ||||||
| 
 | 
 | ||||||
| @ -201,16 +195,14 @@ | |||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item-divider> |                     </ion-item-divider> | ||||||
|                     <ion-item> |                     <ion-item> | ||||||
|                         <ion-label> |                         <ion-radio value="1"> | ||||||
|                             <p>{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}</p> |                             <p>{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}</p> | ||||||
|                         </ion-label> |                         </ion-radio> | ||||||
|                         <ion-radio slot="end" value="1"></ion-radio> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <ion-item> |                     <ion-item> | ||||||
|                         <ion-label> |                         <ion-radio value="0"> | ||||||
|                             <p>{{ 'addon.calendar.repeateditthis' | translate }}</p> |                             <p>{{ 'addon.calendar.repeateditthis' | translate }}</p> | ||||||
|                         </ion-label> |                         </ion-radio> | ||||||
|                         <ion-radio slot="end" value="0"></ion-radio> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                 </ion-radio-group> |                 </ion-radio-group> | ||||||
|             </div> |             </div> | ||||||
| @ -222,16 +214,13 @@ | |||||||
|                 </ion-label> |                 </ion-label> | ||||||
|                 <core-rich-text-editor [control]="descriptionControl" [attr.aria-label]="'core.description' | translate" |                 <core-rich-text-editor [control]="descriptionControl" [attr.aria-label]="'core.description' | translate" | ||||||
|                     [placeholder]="'core.description' | translate" name="description" [component]="component" [componentId]="eventId" |                     [placeholder]="'core.description' | translate" name="description" [component]="component" [componentId]="eventId" | ||||||
|                     [autoSave]="false"></core-rich-text-editor> |                     [autoSave]="false" /> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
|             <!-- Location. --> |             <!-- Location. --> | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
|                 <ion-label position="stacked"> |                 <ion-input type="text" name="location" [placeholder]="'core.location' | translate" [label]="'core.location' | translate" | ||||||
|                     <p class="item-heading">{{ 'core.location' | translate }}</p> |                     labelPlacement="stacked" formControlName="location" /> | ||||||
|                 </ion-label> |  | ||||||
|                 <ion-input type="text" name="location" [placeholder]="'core.location' | translate" formControlName="location"> |  | ||||||
|                 </ion-input> |  | ||||||
|             </ion-item> |             </ion-item> | ||||||
|         </form> |         </form> | ||||||
|         <div collapsible-footer appearOnBottom *ngIf="loaded && !error" slot="fixed"> |         <div collapsible-footer appearOnBottom *ngIf="loaded && !error" slot="fixed"> | ||||||
|  | |||||||
| @ -45,7 +45,6 @@ import { CoreForms } from '@singletons/form'; | |||||||
| import { CoreReminders, CoreRemindersService, CoreRemindersUnits } from '@features/reminders/services/reminders'; | import { CoreReminders, CoreRemindersService, CoreRemindersUnits } from '@features/reminders/services/reminders'; | ||||||
| import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu'; | import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu'; | ||||||
| import moment from 'moment-timezone'; | import moment from 'moment-timezone'; | ||||||
| import { CoreAppProvider } from '@services/app'; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that displays a form to create/edit an event. |  * Page that displays a form to create/edit an event. | ||||||
| @ -61,7 +60,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { | |||||||
|     @ViewChild('editEventForm') formElement!: ElementRef; |     @ViewChild('editEventForm') formElement!: ElementRef; | ||||||
| 
 | 
 | ||||||
|     title = 'addon.calendar.newevent'; |     title = 'addon.calendar.newevent'; | ||||||
|     dateFormat: string; |  | ||||||
|     component = AddonCalendarProvider.COMPONENT; |     component = AddonCalendarProvider.COMPONENT; | ||||||
|     loaded = false; |     loaded = false; | ||||||
|     hasOffline = false; |     hasOffline = false; | ||||||
| @ -71,20 +69,18 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { | |||||||
|     groups: CoreGroup[] = []; |     groups: CoreGroup[] = []; | ||||||
|     loadingGroups = false; |     loadingGroups = false; | ||||||
|     courseGroupSet = false; |     courseGroupSet = false; | ||||||
|     errors: Record<string, string>; |  | ||||||
|     error = false; |     error = false; | ||||||
|     eventRepeatId?: number; |     eventRepeatId?: number; | ||||||
|     otherEventsCount = 0; |     otherEventsCount = 0; | ||||||
|     eventId?: number; |     eventId?: number; | ||||||
|     maxDate: string; |     maxDate: string; | ||||||
|     minDate: string; |     minDate: string; | ||||||
|     displayTimezone?: string; |  | ||||||
| 
 | 
 | ||||||
|     // Form variables.
 |     // Form variables.
 | ||||||
|     form: FormGroup; |     form: FormGroup; | ||||||
|     typeControl: FormControl; |     typeControl: FormControl<AddonCalendarEventType | null>; | ||||||
|     groupControl: FormControl; |     groupControl: FormControl<number | null>; | ||||||
|     descriptionControl: FormControl; |     descriptionControl: FormControl<string>; | ||||||
| 
 | 
 | ||||||
|     // Reminders.
 |     // Reminders.
 | ||||||
|     remindersEnabled = false; |     remindersEnabled = false; | ||||||
| @ -103,21 +99,13 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { | |||||||
|     ) { |     ) { | ||||||
|         this.currentSite = CoreSites.getRequiredCurrentSite(); |         this.currentSite = CoreSites.getRequiredCurrentSite(); | ||||||
|         this.remindersEnabled = CoreReminders.isEnabled(); |         this.remindersEnabled = CoreReminders.isEnabled(); | ||||||
|         this.errors = { |  | ||||||
|             required: Translate.instant('core.required'), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
 |  | ||||||
|         this.dateFormat = CoreTimeUtils.convertPHPToMoment(Translate.instant('core.strftimedatetimeshort')) |  | ||||||
|             .replace(/[[\]]/g, ''); |  | ||||||
|         this.displayTimezone = CoreAppProvider.getForcedTimezone(); |  | ||||||
| 
 | 
 | ||||||
|         this.form = new FormGroup({}); |         this.form = new FormGroup({}); | ||||||
| 
 | 
 | ||||||
|         // Initialize form variables.
 |         // Initialize form variables.
 | ||||||
|         this.typeControl = this.fb.control('', Validators.required); |         this.typeControl = this.fb.control(null, Validators.required); | ||||||
|         this.groupControl = this.fb.control(''); |         this.groupControl = this.fb.control(null); | ||||||
|         this.descriptionControl = this.fb.control(''); |         this.descriptionControl = this.fb.control('', { nonNullable: true }); | ||||||
|         this.form.addControl('name', this.fb.control('', Validators.required)); |         this.form.addControl('name', this.fb.control('', Validators.required)); | ||||||
|         this.form.addControl('eventtype', this.typeControl); |         this.form.addControl('eventtype', this.typeControl); | ||||||
|         this.form.addControl('categoryid', this.fb.control('')); |         this.form.addControl('categoryid', this.fb.control('')); | ||||||
| @ -334,11 +322,11 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { | |||||||
| 
 | 
 | ||||||
|         this.form.controls.name.setValue(event.name); |         this.form.controls.name.setValue(event.name); | ||||||
|         this.form.controls.timestart.setValue(CoreTimeUtils.toDatetimeFormat(event.timestart * 1000)); |         this.form.controls.timestart.setValue(CoreTimeUtils.toDatetimeFormat(event.timestart * 1000)); | ||||||
|         this.form.controls.eventtype.setValue(event.eventtype); |         this.typeControl.setValue(event.eventtype as AddonCalendarEventType); | ||||||
|         this.form.controls.categoryid.setValue(event.categoryid || ''); |         this.form.controls.categoryid.setValue(event.categoryid || ''); | ||||||
|         this.form.controls.courseid.setValue(courseId || ''); |         this.form.controls.courseid.setValue(courseId || ''); | ||||||
|         this.form.controls.groupcourseid.setValue(courseId || ''); |         this.form.controls.groupcourseid.setValue(courseId || ''); | ||||||
|         this.form.controls.groupid.setValue(event.groupid || ''); |         this.groupControl.setValue(event.groupid || null); | ||||||
|         this.form.controls.description.setValue(event.description); |         this.form.controls.description.setValue(event.description); | ||||||
|         this.form.controls.location.setValue(event.location); |         this.form.controls.location.setValue(event.location); | ||||||
| 
 | 
 | ||||||
| @ -422,7 +410,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { | |||||||
|         try { |         try { | ||||||
|             await this.loadGroups(courseId); |             await this.loadGroups(courseId); | ||||||
| 
 | 
 | ||||||
|             this.groupControl.setValue(''); |             this.groupControl.setValue(null); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error getting data.'); |             CoreDomUtils.showErrorModalDefault(error, 'Error getting data.'); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -1,42 +1,38 @@ | |||||||
| <ion-header collapsible> | <ion-header collapsible> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1 *ngIf="event"> |             <h1 *ngIf="event"> | ||||||
|                 <core-format-text [text]="event.name" [contextLevel]="event.contextLevel" [contextInstanceId]="event.contextInstanceId"> |                 <core-format-text [text]="event.name" [contextLevel]="event.contextLevel" [contextInstanceId]="event.contextInstanceId" /> | ||||||
|                 </core-format-text> |  | ||||||
|             </h1> |             </h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <core-context-menu> |             <core-context-menu> | ||||||
|                 <core-context-menu-item [hidden]="!eventLoaded || (!hasOffline && event && !event.deleted) || !isOnline" [priority]="400" |                 <core-context-menu-item [hidden]="!eventLoaded || (!hasOffline && event && !event.deleted) || !isOnline" [priority]="400" | ||||||
|                     [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)" |                     [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)" | ||||||
|                     [iconAction]="syncIcon" [closeOnClick]="false"> |                     [iconAction]="syncIcon" [closeOnClick]="false" /> | ||||||
|                 </core-context-menu-item> |  | ||||||
|                 <core-context-menu-item [hidden]="!event || !event.canedit || event.deleted || (!canEdit && event.id > 0)" [priority]="300" |                 <core-context-menu-item [hidden]="!event || !event.canedit || event.deleted || (!canEdit && event.id > 0)" [priority]="300" | ||||||
|                     [content]="'core.edit' | translate" (action)="openEdit()" iconAction="fas-pen"> |                     [content]="'core.edit' | translate" (action)="openEdit()" iconAction="fas-pen" /> | ||||||
|                 </core-context-menu-item> |  | ||||||
|                 <core-context-menu-item [hidden]="!event || !event.candelete || event.deleted" [priority]="200" |                 <core-context-menu-item [hidden]="!event || !event.candelete || event.deleted" [priority]="200" | ||||||
|                     [content]="'core.delete' | translate" (action)="deleteEvent()" iconAction="fas-trash"></core-context-menu-item> |                     [content]="'core.delete' | translate" (action)="deleteEvent()" iconAction="fas-trash" /> | ||||||
|                 <core-context-menu-item [hidden]="!event || !event.deleted" [priority]="200" [content]="'core.restore' | translate" |                 <core-context-menu-item [hidden]="!event || !event.deleted" [priority]="200" [content]="'core.restore' | translate" | ||||||
|                     (action)="undoDelete()" iconAction="fas-rotate-left"></core-context-menu-item> |                     (action)="undoDelete()" iconAction="fas-rotate-left" /> | ||||||
|             </core-context-menu> |             </core-context-menu> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content [core-swipe-navigation]="events"> | <ion-content [core-swipe-navigation]="events"> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!eventLoaded" (ionRefresh)="doRefresh($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!eventLoaded" (ionRefresh)="doRefresh($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="eventLoaded"> |     <core-loading [hideUntil]="eventLoaded"> | ||||||
|         <ion-list *ngIf="event"> |         <ion-list *ngIf="event"> | ||||||
|             <ion-item class="ion-text-wrap addon-calendar-event" collapsible [ngClass]="['addon-calendar-eventtype-'+event.eventtype]"> |             <ion-item class="ion-text-wrap addon-calendar-event" collapsible [ngClass]="['addon-calendar-eventtype-'+event.eventtype]"> | ||||||
|                 <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false" [modname]="event.modulename" |                 <core-mod-icon *ngIf="event.moduleIcon" [modicon]="event.moduleIcon" [showAlt]="false" [modname]="event.modulename" | ||||||
|                     [componentId]="event.instance" slot="start" [purpose]="event.purpose"></core-mod-icon> |                     [componentId]="event.instance" slot="start" [purpose]="event.purpose" /> | ||||||
|                 <ion-icon *ngIf=" event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" aria-hidden="true" slot="start"> |                 <ion-icon *ngIf=" event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" aria-hidden="true" slot="start" /> | ||||||
|                 </ion-icon> |  | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <!-- Add the icon title so accessibility tools read it. --> |                     <!-- Add the icon title so accessibility tools read it. --> | ||||||
|                     <span class="sr-only"> |                     <span class="sr-only"> | ||||||
| @ -45,25 +41,24 @@ | |||||||
|                     </span> |                     </span> | ||||||
|                     <h1> |                     <h1> | ||||||
|                         <core-format-text [text]="event.name" [contextLevel]="event.contextLevel" |                         <core-format-text [text]="event.name" [contextLevel]="event.contextLevel" | ||||||
|                             [contextInstanceId]="event.contextInstanceId"> |                             [contextInstanceId]="event.contextInstanceId" /> | ||||||
|                         </core-format-text> |  | ||||||
|                     </h1> |                     </h1> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|             <!-- There is data to be synchronized --> |             <!-- There is data to be synchronized --> | ||||||
|             <ion-card class="core-warning-card" *ngIf="hasOffline || event.deleted"> |             <ion-card class="core-warning-card" *ngIf="hasOffline || event.deleted"> | ||||||
|                 <ion-item> |                 <ion-item> | ||||||
|                     <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true"></ion-icon> |                     <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" /> | ||||||
|                     <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }}</ion-label> |                     <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }}</ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </ion-card> |             </ion-card> | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p class="item-heading">{{ 'addon.calendar.when' | translate }}</p> |                     <p class="item-heading">{{ 'addon.calendar.when' | translate }}</p> | ||||||
|                     <core-format-text [text]="event.formattedtime" [filter]="false"></core-format-text> |                     <core-format-text [text]="event.formattedtime" [filter]="false" /> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|                 <ion-note slot="end" *ngIf="event.deleted"> |                 <ion-note slot="end" *ngIf="event.deleted"> | ||||||
|                     <ion-icon name="fas-trash" aria-hidden="true"></ion-icon> {{ 'core.deletedoffline' | translate }} |                     <ion-icon name="fas-trash" aria-hidden="true" /> {{ 'core.deletedoffline' | translate }} | ||||||
|                 </ion-note> |                 </ion-note> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|             <ion-item> |             <ion-item> | ||||||
| @ -76,8 +71,7 @@ | |||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p class="item-heading">{{ 'core.course' | translate}}</p> |                     <p class="item-heading">{{ 'core.course' | translate}}</p> | ||||||
|                     <p> |                     <p> | ||||||
|                         <core-format-text [text]="courseName" contextLevel="course" [contextInstanceId]="courseId"> |                         <core-format-text [text]="courseName" contextLevel="course" [contextInstanceId]="courseId" /> | ||||||
|                         </core-format-text> |  | ||||||
|                     </p> |                     </p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| @ -85,8 +79,7 @@ | |||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p class="item-heading">{{ 'core.group' | translate}}</p> |                     <p class="item-heading">{{ 'core.group' | translate}}</p> | ||||||
|                     <p> |                     <p> | ||||||
|                         <core-format-text [text]="groupName" contextLevel="course" [contextInstanceId]="event.courseid"> |                         <core-format-text [text]="groupName" contextLevel="course" [contextInstanceId]="event.courseid" /> | ||||||
|                         </core-format-text> |  | ||||||
|                     </p> |                     </p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| @ -94,8 +87,7 @@ | |||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p class="item-heading">{{ 'core.category' | translate}}</p> |                     <p class="item-heading">{{ 'core.category' | translate}}</p> | ||||||
|                     <p> |                     <p> | ||||||
|                         <core-format-text [text]="categoryPath" contextLevel="coursecat" [contextInstanceId]="event.categoryid"> |                         <core-format-text [text]="categoryPath" contextLevel="coursecat" [contextInstanceId]="event.categoryid" /> | ||||||
|                         </core-format-text> |  | ||||||
|                     </p> |                     </p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| @ -104,7 +96,7 @@ | |||||||
|                     <p class="item-heading">{{ 'core.description' | translate}}</p> |                     <p class="item-heading">{{ 'core.description' | translate}}</p> | ||||||
|                     <p> |                     <p> | ||||||
|                         <core-format-text [text]="event.description" [contextLevel]="event.contextLevel" |                         <core-format-text [text]="event.description" [contextLevel]="event.contextLevel" | ||||||
|                             [contextInstanceId]="event.contextInstanceId"></core-format-text> |                             [contextInstanceId]="event.contextInstanceId" /> | ||||||
|                     </p> |                     </p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| @ -114,7 +106,7 @@ | |||||||
|                     <p> |                     <p> | ||||||
|                         <a [href]="event.encodedLocation" core-link auto-login="no"> |                         <a [href]="event.encodedLocation" core-link auto-login="no"> | ||||||
|                             <core-format-text [text]="event.location" [contextLevel]="event.contextLevel" |                             <core-format-text [text]="event.location" [contextLevel]="event.contextLevel" | ||||||
|                                 [contextInstanceId]="event.contextInstanceId"></core-format-text> |                                 [contextInstanceId]="event.contextInstanceId" /> | ||||||
|                         </a> |                         </a> | ||||||
|                     </p> |                     </p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
| @ -142,7 +134,7 @@ | |||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                     <ion-button fill="clear" (click)="deleteReminder(reminder.id, $event)" [attr.aria-label]="'core.delete' | translate" |                     <ion-button fill="clear" (click)="deleteReminder(reminder.id, $event)" [attr.aria-label]="'core.delete' | translate" | ||||||
|                         slot="end"> |                         slot="end"> | ||||||
|                         <ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true"></ion-icon> |                         <ion-icon name="fas-trash" color="danger" slot="icon-only" aria-hidden="true" /> | ||||||
|                     </ion-button> |                     </ion-button> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </ng-container> |             </ng-container> | ||||||
|  | |||||||
| @ -639,8 +639,10 @@ class AddonCalendarEventsSwipeItemsManager extends CoreSwipeNavigationItemsManag | |||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { |     protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { | ||||||
|         return route.params.id; |         const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; | ||||||
|  | 
 | ||||||
|  |         return snapshot.params.id; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,55 +1,52 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}</h1> |             <h1>{{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}</h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button fill="clear" (click)="openFilter()" [attr.aria-label]="'core.filter' | translate"> |             <ion-button fill="clear" (click)="openFilter()" [attr.aria-label]="'core.filter' | translate"> | ||||||
|                 <ion-icon slot="icon-only" name="fas-filter" aria-hidden="true"></ion-icon> |                 <ion-icon slot="icon-only" name="fas-filter" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <core-context-menu> |             <core-context-menu> | ||||||
|                 <core-context-menu-item *ngIf="showCalendar" [priority]="800" [content]="'addon.calendar.upcomingevents' | translate" |                 <core-context-menu-item *ngIf="showCalendar" [priority]="800" [content]="'addon.calendar.upcomingevents' | translate" | ||||||
|                     iconAction="fas-table-list" (action)="toggleDisplay()"></core-context-menu-item> |                     iconAction="fas-table-list" (action)="toggleDisplay()" /> | ||||||
|                 <core-context-menu-item *ngIf="!showCalendar" [priority]="800" [content]="'addon.calendar.monthlyview' | translate" |                 <core-context-menu-item *ngIf="!showCalendar" [priority]="800" [content]="'addon.calendar.monthlyview' | translate" | ||||||
|                     iconAction="fas-calendar-days" (action)="toggleDisplay()"></core-context-menu-item> |                     iconAction="fas-calendar-days" (action)="toggleDisplay()" /> | ||||||
|                 <core-context-menu-item [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" |                 <core-context-menu-item [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" | ||||||
|                     iconAction="fas-gears"> |                     iconAction="fas-gears" /> | ||||||
|                 </core-context-menu-item> |  | ||||||
|                 <core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400" |                 <core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400" | ||||||
|                     [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)" |                     [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(undefined, $event, true)" | ||||||
|                     [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> |                     [iconAction]="syncIcon" [closeOnClick]="false" /> | ||||||
|             </core-context-menu> |             </core-context-menu> | ||||||
|             <core-user-menu-button></core-user-menu-button> |             <core-user-menu-button /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
| 
 | 
 | ||||||
|     <!-- There is data to be synchronized --> |     <!-- There is data to be synchronized --> | ||||||
|     <ion-card class="core-warning-card" *ngIf="hasOffline"> |     <ion-card class="core-warning-card" *ngIf="hasOffline"> | ||||||
|         <ion-item> |         <ion-item> | ||||||
|             <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true"></ion-icon> |             <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" /> | ||||||
|             <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }}</ion-label> |             <ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }}</ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|     </ion-card> |     </ion-card> | ||||||
| 
 | 
 | ||||||
|     <addon-calendar-calendar [hidden]="!showCalendar" [initialYear]="year" [initialMonth]="month" [filter]="filter" |     <addon-calendar-calendar [hidden]="!showCalendar" [initialYear]="year" [initialMonth]="month" [filter]="filter" | ||||||
|         [displayNavButtons]="showCalendar" (onEventClicked)="gotoEvent($event)" (onDayClicked)="gotoDay($event)"> |         [displayNavButtons]="showCalendar" (onEventClicked)="gotoEvent($event)" (onDayClicked)="gotoDay($event)" /> | ||||||
|     </addon-calendar-calendar> |  | ||||||
| 
 | 
 | ||||||
|     <addon-calendar-upcoming-events *ngIf="loadUpcoming" [hidden]="showCalendar" [filter]="filter" (onEventClicked)="gotoEvent($event)"> |     <addon-calendar-upcoming-events *ngIf="loadUpcoming" [hidden]="showCalendar" [filter]="filter" (onEventClicked)="gotoEvent($event)" /> | ||||||
|     </addon-calendar-upcoming-events> |  | ||||||
| 
 | 
 | ||||||
|     <!-- Create a calendar event. --> |     <!-- Create a calendar event. --> | ||||||
|     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canCreate"> |     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canCreate"> | ||||||
|         <ion-fab-button (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate"> |         <ion-fab-button (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate"> | ||||||
|             <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> |             <ion-icon name="fas-plus" aria-hidden="true" /> | ||||||
|             <span class="sr-only">{{ 'addon.calendar.newevent' | translate }}</span> |             <span class="sr-only">{{ 'addon.calendar.newevent' | translate }}</span> | ||||||
|         </ion-fab-button> |         </ion-fab-button> | ||||||
|     </ion-fab> |     </ion-fab> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'core.settings.settings' | translate }}</h1> |             <h1>{{ 'core.settings.settings' | translate }}</h1> | ||||||
| @ -11,8 +11,9 @@ | |||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-list> |     <ion-list> | ||||||
|         <ion-item *ngIf="defaultTimeLabel"> |         <ion-item *ngIf="defaultTimeLabel"> | ||||||
|             <ion-label>{{ 'addon.calendar.defaultnotificationtime' | translate }}</ion-label> | 
 | ||||||
|             <ion-select [(ngModel)]="defaultTimeLabel" (click)="changeDefaultTime($event)"> |             <ion-select [(ngModel)]="defaultTimeLabel" (click)="changeDefaultTime($event)" | ||||||
|  |                 [label]="'addon.calendar.defaultnotificationtime' | translate"> | ||||||
|                 <ion-select-option [value]="defaultTimeLabel">{{ defaultTimeLabel }}</ion-select-option> |                 <ion-select-option [value]="defaultTimeLabel">{{ defaultTimeLabel }}</ion-select-option> | ||||||
|             </ion-select> |             </ion-select> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|  | |||||||
| @ -20,12 +20,14 @@ Feature: Test creation of calendar events in app | |||||||
|       | teacher1 | C1 | editingteacher | |       | teacher1 | C1 | editingteacher | | ||||||
|       | student1 | C1 | student | |       | student1 | C1 | student | | ||||||
| 
 | 
 | ||||||
|  |   # This test is flaky due to timestamp. | ||||||
|   Scenario: Create user event as student from monthly view |   Scenario: Create user event as student from monthly view | ||||||
|     Given I entered the app as "student1" |     Given I entered the app as "student1" | ||||||
|     When I press "More" in the app |     When I press "More" in the app | ||||||
|     And I press "Calendar" in the app |     And I press "Calendar" in the app | ||||||
|     And I press "New event" in the app |     And I press "New event" in the app | ||||||
|     Then the field "Date" matches value "## now ##%d/%m/%y, %H:%M##" in the app |     # Flaky step, sometimes it fails due to minute change when checking. | ||||||
|  |     Then the field "Date" matches value "## now ##%Y-%m-%dT%H:%M##" in the app | ||||||
|     And I should not be able to press "Save" in the app |     And I should not be able to press "Save" in the app | ||||||
| 
 | 
 | ||||||
|     # Check that student can only create User events. |     # Check that student can only create User events. | ||||||
| @ -35,7 +37,7 @@ Feature: Test creation of calendar events in app | |||||||
| 
 | 
 | ||||||
|     # Create the event. |     # Create the event. | ||||||
|     When I set the field "Event title" to "User Event 01" in the app |     When I set the field "Event title" to "User Event 01" in the app | ||||||
|     And I set the field "Date" to "2025-04-11T09:00+08:00" in the app |     And I set the field "Date" to "2025-04-11T09:00" in the app | ||||||
|     And I press "Without duration" in the app |     And I press "Without duration" in the app | ||||||
|     And I set the field "Description" to "This is User Event 01 description." in the app |     And I set the field "Description" to "This is User Event 01 description." in the app | ||||||
|     And I set the field "Location" to "Barcelona" in the app |     And I set the field "Location" to "Barcelona" in the app | ||||||
|  | |||||||
| @ -1,12 +1,11 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1> |             <h1> | ||||||
|                 <core-format-text [text]="title" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"> |                 <core-format-text [text]="title" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" /> | ||||||
|                 </core-format-text> |  | ||||||
|             </h1> |             </h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| @ -14,7 +13,7 @@ | |||||||
| <ion-content> | <ion-content> | ||||||
|     <core-split-view> |     <core-split-view> | ||||||
|         <ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshCompetencies($event.target)"> |         <ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshCompetencies($event.target)"> | ||||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|         </ion-refresher> |         </ion-refresher> | ||||||
|         <core-loading [hideUntil]="competencies.loaded"> |         <core-loading [hideUntil]="competencies.loaded"> | ||||||
|             <ion-list> |             <ion-list> | ||||||
| @ -24,8 +23,7 @@ | |||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading"> |                         <p class="item-heading"> | ||||||
|                             <core-format-text [text]="competency.competency.shortname" [contextLevel]="contextLevel" |                             <core-format-text [text]="competency.competency.shortname" [contextLevel]="contextLevel" | ||||||
|                                 [contextInstanceId]="contextInstanceId"> |                                 [contextInstanceId]="contextInstanceId" /> <em>{{competency.competency.idnumber}}</em> | ||||||
|                             </core-format-text> <em>{{competency.competency.idnumber}}</em> |  | ||||||
|                         </p> |                         </p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                     <ion-badge slot="end" *ngIf="competency.usercompetency" |                     <ion-badge slot="end" *ngIf="competency.usercompetency" | ||||||
|  | |||||||
| @ -1,25 +1,24 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1 *ngIf="competency"> |             <h1 *ngIf="competency"> | ||||||
|                 <core-format-text [text]="competency.competency.competency.shortname" [contextLevel]="contextLevel" |                 <core-format-text [text]="competency.competency.competency.shortname" [contextLevel]="contextLevel" | ||||||
|                     [contextInstanceId]="contextInstanceId"> |                     [contextInstanceId]="contextInstanceId" /> <small>{{ competency.competency.competency.idnumber }}</small> | ||||||
|                 </core-format-text> <small>{{ competency.competency.competency.idnumber }}</small> |  | ||||||
|             </h1> |             </h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content [core-swipe-navigation]="competencies" class="limited-width"> | <ion-content [core-swipe-navigation]="competencies" class="limited-width"> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="competencyLoaded"> |     <core-loading [hideUntil]="competencyLoaded"> | ||||||
|         <ion-card *ngIf="user"> |         <ion-card *ngIf="user"> | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
|                 <core-user-avatar [user]="user" slot="start"></core-user-avatar> |                 <core-user-avatar [user]="user" slot="start" /> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p class="item-heading">{{ user.fullname }}</p> |                     <p class="item-heading">{{ user.fullname }}</p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
| @ -30,8 +29,7 @@ | |||||||
|             <ion-item class="ion-text-wrap" *ngIf="competency.competency.competency.description"> |             <ion-item class="ion-text-wrap" *ngIf="competency.competency.competency.description"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <core-format-text [text]="competency.competency.competency.description" [contextLevel]="contextLevel" |                     <core-format-text [text]="competency.competency.competency.description" [contextLevel]="contextLevel" | ||||||
|                         [contextInstanceId]="contextInstanceId"> |                         [contextInstanceId]="contextInstanceId" /> | ||||||
|                     </core-format-text> |  | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|             <ion-item class="ion-text-wrap only-links"> |             <ion-item class="ion-text-wrap only-links"> | ||||||
| @ -40,26 +38,22 @@ | |||||||
|                     <p> |                     <p> | ||||||
|                         <a *ngIf="competency.competency.comppath.showlinks" [href]="competencyFrameworkUrl" core-link> |                         <a *ngIf="competency.competency.comppath.showlinks" [href]="competencyFrameworkUrl" core-link> | ||||||
|                             <core-format-text [text]="competency.competency.comppath.framework.name" [contextLevel]="contextLevel" |                             <core-format-text [text]="competency.competency.comppath.framework.name" [contextLevel]="contextLevel" | ||||||
|                                 [contextInstanceId]="contextInstanceId"> |                                 [contextInstanceId]="contextInstanceId" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </a> |                         </a> | ||||||
|                         <ng-container *ngIf="!competency.competency.comppath.showlinks"> |                         <ng-container *ngIf="!competency.competency.comppath.showlinks"> | ||||||
|                             <core-format-text [text]="competency.competency.comppath.framework.name" [contextLevel]="contextLevel" |                             <core-format-text [text]="competency.competency.comppath.framework.name" [contextLevel]="contextLevel" | ||||||
|                                 [contextInstanceId]="contextInstanceId"> |                                 [contextInstanceId]="contextInstanceId" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </ng-container> |                         </ng-container> | ||||||
|                          /  |                          /  | ||||||
|                         <ng-container *ngFor="let ancestor of competency.competency.comppath.ancestors"> |                         <ng-container *ngFor="let ancestor of competency.competency.comppath.ancestors"> | ||||||
|                             <button *ngIf="competency.competency.comppath.showlinks" (click)="openCompetencySummary(ancestor.id)" |                             <button *ngIf="competency.competency.comppath.showlinks" (click)="openCompetencySummary(ancestor.id)" | ||||||
|                                 class="as-link"> |                                 class="as-link"> | ||||||
|                                 <core-format-text [text]="ancestor.name" [contextLevel]="contextLevel" |                                 <core-format-text [text]="ancestor.name" [contextLevel]="contextLevel" | ||||||
|                                     [contextInstanceId]="contextInstanceId"> |                                     [contextInstanceId]="contextInstanceId" /> | ||||||
|                                 </core-format-text> |  | ||||||
|                             </button> |                             </button> | ||||||
|                             <ng-container *ngIf="!competency.competency.comppath.showlinks"> |                             <ng-container *ngIf="!competency.competency.comppath.showlinks"> | ||||||
|                                 <core-format-text [text]="ancestor.name" [contextLevel]="contextLevel" |                                 <core-format-text [text]="ancestor.name" [contextLevel]="contextLevel" | ||||||
|                                     [contextInstanceId]="contextInstanceId"> |                                     [contextInstanceId]="contextInstanceId" /> | ||||||
|                                 </core-format-text> |  | ||||||
|                             </ng-container> |                             </ng-container> | ||||||
|                             <ng-container *ngIf="!ancestor.last"> / </ng-container> |                             <ng-container *ngIf="!ancestor.last"> / </ng-container> | ||||||
|                         </ng-container> |                         </ng-container> | ||||||
| @ -76,8 +70,7 @@ | |||||||
|                         <p *ngFor="let relatedcomp of competency.competency.relatedcompetencies"> |                         <p *ngFor="let relatedcomp of competency.competency.relatedcompetencies"> | ||||||
|                             <button (click)="openCompetencySummary(relatedcomp.id)" class="as-link"> |                             <button (click)="openCompetencySummary(relatedcomp.id)" class="as-link"> | ||||||
|                                 <core-format-text [text]="relatedcomp.shortname" [contextLevel]="contextLevel" |                                 <core-format-text [text]="relatedcomp.shortname" [contextLevel]="contextLevel" | ||||||
|                                     [contextInstanceId]="contextInstanceId"> |                                     [contextInstanceId]="contextInstanceId" /> - {{ relatedcomp.idnumber }} | ||||||
|                                 </core-format-text> - {{ relatedcomp.idnumber }} |  | ||||||
|                             </button> |                             </button> | ||||||
|                         </p> |                         </p> | ||||||
|                     </ng-container> |                     </ng-container> | ||||||
| @ -86,17 +79,15 @@ | |||||||
|             <ion-item class="ion-text-wrap" *ngIf="coursemodules"> |             <ion-item class="ion-text-wrap" *ngIf="coursemodules"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p class="item-heading">{{ 'addon.competency.activities' | translate }}</p> |                     <p class="item-heading">{{ 'addon.competency.activities' | translate }}</p> | ||||||
|                     <p *ngIf="coursemodules.length == 0"> |                     <p *ngIf="coursemodules.length === 0"> | ||||||
|                         {{ 'addon.competency.noactivities' | translate }} |                         {{ 'addon.competency.noactivities' | translate }} | ||||||
|                     </p> |                     </p> | ||||||
|                     <ion-item class="ion-text-wrap" *ngFor="let activity of coursemodules" [href]="activity.url" |                     <ion-item class="ion-text-wrap" *ngFor="let activity of coursemodules" [href]="activity.url" | ||||||
|                         [attr.aria-label]="activity.name" core-link capture="true"> |                         [attr.aria-label]="activity.name" core-link capture="true"> | ||||||
|                         <core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl"> |                         <core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl" /> | ||||||
|                         </core-mod-icon> |  | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id" |                             <core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id" | ||||||
|                                 [courseId]="courseId"> |                                 [courseId]="courseId" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
| @ -130,13 +121,13 @@ | |||||||
| 
 | 
 | ||||||
|         <div *ngIf="competency"> |         <div *ngIf="competency"> | ||||||
|             <h2 class="ion-margin-horizontal">{{ 'addon.competency.evidence' | translate }}</h2> |             <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 }} |                 {{ 'addon.competency.noevidence' | translate }} | ||||||
|             </p> |             </p> | ||||||
|             <ion-card *ngFor="let evidence of competency.evidence"> |             <ion-card *ngFor="let evidence of competency.evidence"> | ||||||
|                 <ion-item class="ion-text-wrap" *ngIf="evidence.actionuser" core-user-link [userId]="evidence.actionuser.id" |                 <ion-item class="ion-text-wrap" *ngIf="evidence.actionuser" core-user-link [userId]="evidence.actionuser.id" | ||||||
|                     [courseId]="courseId"> |                     [courseId]="courseId"> | ||||||
|                     <core-user-avatar [user]="evidence.actionuser" slot="start" [linkProfile]="false"></core-user-avatar> |                     <core-user-avatar [user]="evidence.actionuser" slot="start" [linkProfile]="false" /> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading">{{ evidence.actionuser.fullname }}</p> |                         <p class="item-heading">{{ evidence.actionuser.fullname }}</p> | ||||||
|                         <p>{{ evidence.timemodified * 1000 | coreFormatDate }}</p> |                         <p>{{ evidence.timemodified * 1000 | coreFormatDate }}</p> | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.mod | |||||||
| import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; | import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; | ||||||
| import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; | import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; | ||||||
| import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; | import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; | ||||||
| import { ActivatedRouteSnapshot } from '@angular/router'; | import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; | ||||||
| import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; | import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; | ||||||
| import { CoreTime } from '@singletons/time'; | import { CoreTime } from '@singletons/time'; | ||||||
| import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; | import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; | ||||||
| @ -350,8 +350,10 @@ class AddonCompetencyCompetenciesSwipeManager | |||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { |     protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { | ||||||
|         return route.params.competencyId; |         const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; | ||||||
|  | 
 | ||||||
|  |         return snapshot.params.competencyId; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,28 +1,26 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1 *ngIf="competency"> |             <h1 *ngIf="competency"> | ||||||
|                 <core-format-text [text]="competency.competency.shortname" [contextLevel]="contextLevel" |                 <core-format-text [text]="competency.competency.shortname" [contextLevel]="contextLevel" | ||||||
|                     [contextInstanceId]="contextInstanceId"> |                     [contextInstanceId]="contextInstanceId" /> <small>{{ competency.competency.idnumber }}</small> | ||||||
|                 </core-format-text> <small>{{ competency.competency.idnumber }}</small> |  | ||||||
|             </h1> |             </h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content class="limited-width"> | <ion-content class="limited-width"> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="competencyLoaded"> |     <core-loading [hideUntil]="competencyLoaded"> | ||||||
|         <ion-card *ngIf="competency"> |         <ion-card *ngIf="competency"> | ||||||
|             <ion-item class="ion-text-wrap" *ngIf="competency.competency.description"> |             <ion-item class="ion-text-wrap" *ngIf="competency.competency.description"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <core-format-text [text]="competency.competency.description" [contextLevel]="contextLevel" |                     <core-format-text [text]="competency.competency.description" [contextLevel]="contextLevel" | ||||||
|                         [contextInstanceId]="contextInstanceId"> |                         [contextInstanceId]="contextInstanceId" /> | ||||||
|                     </core-format-text> |  | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
| @ -30,14 +28,12 @@ | |||||||
|                     <p class="item-heading">{{ 'addon.competency.path' | translate }}</p> |                     <p class="item-heading">{{ 'addon.competency.path' | translate }}</p> | ||||||
|                     <p> |                     <p> | ||||||
|                         <core-format-text [text]="competency.comppath.framework.name" [contextLevel]="contextLevel" |                         <core-format-text [text]="competency.comppath.framework.name" [contextLevel]="contextLevel" | ||||||
|                             [contextInstanceId]="contextInstanceId"> |                             [contextInstanceId]="contextInstanceId" /> | ||||||
|                         </core-format-text> |  | ||||||
|                         <ng-container *ngFor="let ancestor of competency.comppath.ancestors"> |                         <ng-container *ngFor="let ancestor of competency.comppath.ancestors"> | ||||||
|                              /  |                              /  | ||||||
|                             <button class="as-link" (click)="openCompetencySummary(ancestor.id)"> |                             <button class="as-link" (click)="openCompetencySummary(ancestor.id)"> | ||||||
|                                 <core-format-text [text]="ancestor.name" [contextLevel]="contextLevel" |                                 <core-format-text [text]="ancestor.name" [contextLevel]="contextLevel" | ||||||
|                                     [contextInstanceId]="contextInstanceId"> |                                     [contextInstanceId]="contextInstanceId" /> | ||||||
|                                 </core-format-text> |  | ||||||
|                             </button> |                             </button> | ||||||
|                         </ng-container> |                         </ng-container> | ||||||
|                     </p> |                     </p> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.competency.coursecompetencies' | translate }}</h1> |             <h1>{{ 'addon.competency.coursecompetencies' | translate }}</h1> | ||||||
| @ -10,7 +10,7 @@ | |||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content class="limited-width"> | <ion-content class="limited-width"> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshCourseCompetencies($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshCourseCompetencies($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="competencies.loaded"> |     <core-loading [hideUntil]="competencies.loaded"> | ||||||
|         <ion-card *ngIf="!user && courseCompetencies && courseCompetencies.statistics.competencycount > 0"> |         <ion-card *ngIf="!user && courseCompetencies && courseCompetencies.statistics.competencycount > 0"> | ||||||
| @ -29,8 +29,7 @@ | |||||||
|                         {x: courseCompetencies.statistics.proficientcompetencycount, y: courseCompetencies.statistics.competencycount} } }} |                         {x: courseCompetencies.statistics.proficientcompetencycount, y: courseCompetencies.statistics.competencycount} } }} | ||||||
|                     </span> |                     </span> | ||||||
|                     <core-progress-bar [progress]="courseCompetencies.statistics.proficientcompetencypercentage" |                     <core-progress-bar [progress]="courseCompetencies.statistics.proficientcompetencypercentage" | ||||||
|                         ariaDescribedBy="addon-competency-course-{{courseId}}-progress"> |                         ariaDescribedBy="addon-competency-course-{{courseId}}-progress" /> | ||||||
|                     </core-progress-bar> |  | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|             <ion-item class="ion-text-wrap" |             <ion-item class="ion-text-wrap" | ||||||
| @ -39,8 +38,8 @@ | |||||||
|                     <p class="item-heading">{{ 'addon.competency.competenciesmostoftennotproficientincourse' | translate }}</p> |                     <p class="item-heading">{{ 'addon.competency.competenciesmostoftennotproficientincourse' | translate }}</p> | ||||||
|                     <p *ngFor="let comp of courseCompetencies.statistics.leastproficient"> |                     <p *ngFor="let comp of courseCompetencies.statistics.leastproficient"> | ||||||
|                         <button class="as-link" (click)="openCompetencySummary(comp.id)"> |                         <button class="as-link" (click)="openCompetencySummary(comp.id)"> | ||||||
|                             <core-format-text [text]="comp.shortname" contextLevel="course" [contextInstanceId]="courseId"> |                             <core-format-text [text]="comp.shortname" contextLevel="course" [contextInstanceId]="courseId" /> - {{ | ||||||
|                             </core-format-text> - {{ comp.idnumber }} |                             comp.idnumber }} | ||||||
|                         </button> |                         </button> | ||||||
|                     </p> |                     </p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
| @ -52,15 +51,14 @@ | |||||||
|         </h2> |         </h2> | ||||||
|         <ion-card *ngIf="user"> |         <ion-card *ngIf="user"> | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
|                 <core-user-avatar [user]="user" slot="start"></core-user-avatar> |                 <core-user-avatar [user]="user" slot="start" /> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p class="item-heading">{{ user.fullname }}</p> |                     <p class="item-heading">{{ user.fullname }}</p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|         </ion-card> |         </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 }}"> |             message="{{ 'addon.competency.nocompetenciesincourse' | translate }}" /> | ||||||
|         </core-empty-box> |  | ||||||
| 
 | 
 | ||||||
|         <div *ngIf="competencies.loaded"> |         <div *ngIf="competencies.loaded"> | ||||||
|             <ion-card *ngFor="let competency of competencies.items"> |             <ion-card *ngFor="let competency of competencies.items"> | ||||||
| @ -68,8 +66,8 @@ | |||||||
|                     [attr.aria-label]="competency.competency.shortname" [detail]="true" button> |                     [attr.aria-label]="competency.competency.shortname" [detail]="true" button> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading"> |                         <p class="item-heading"> | ||||||
|                             <core-format-text [text]="competency.competency.shortname" contextLevel="course" [contextInstanceId]="courseId"> |                             <core-format-text [text]="competency.competency.shortname" contextLevel="course" | ||||||
|                             </core-format-text> <em>{{competency.competency.idnumber}}</em> |                                 [contextInstanceId]="courseId" /> <em>{{competency.competency.idnumber}}</em> | ||||||
|                         </p> |                         </p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                     <ion-badge slot="end" *ngIf="competency.usercompetencycourse && competency.usercompetencycourse.gradename" |                     <ion-badge slot="end" *ngIf="competency.usercompetencycourse && competency.usercompetencycourse.gradename" | ||||||
| @ -81,8 +79,7 @@ | |||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p *ngIf="competency.competency.description"> |                         <p *ngIf="competency.competency.description"> | ||||||
|                             <core-format-text [text]="competency.competency.description" contextLevel="course" |                             <core-format-text [text]="competency.competency.description" contextLevel="course" | ||||||
|                                 [contextInstanceId]="courseId"> |                                 [contextInstanceId]="courseId" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </p> |                         </p> | ||||||
|                         <div> |                         <div> | ||||||
|                             <p class="item-heading">{{ 'addon.competency.path' | translate }}</p> |                             <p class="item-heading">{{ 'addon.competency.path' | translate }}</p> | ||||||
| @ -90,24 +87,20 @@ | |||||||
|                                 <a *ngIf="competency.comppath.showlinks" [href]="getCompetencyFrameworkUrl(competency)" core-link |                                 <a *ngIf="competency.comppath.showlinks" [href]="getCompetencyFrameworkUrl(competency)" core-link | ||||||
|                                     [title]="competency.comppath.framework.name"> |                                     [title]="competency.comppath.framework.name"> | ||||||
|                                     <core-format-text [text]="competency.comppath.framework.name" contextLevel="course" |                                     <core-format-text [text]="competency.comppath.framework.name" contextLevel="course" | ||||||
|                                         [contextInstanceId]="courseId"> |                                         [contextInstanceId]="courseId" /> | ||||||
|                                     </core-format-text> |  | ||||||
|                                 </a> |                                 </a> | ||||||
|                                 <ng-container *ngIf="!competency.comppath.showlinks"> |                                 <ng-container *ngIf="!competency.comppath.showlinks"> | ||||||
|                                     <core-format-text [text]="competency.comppath.framework.name" contextLevel="course" |                                     <core-format-text [text]="competency.comppath.framework.name" contextLevel="course" | ||||||
|                                         [contextInstanceId]="courseId"> |                                         [contextInstanceId]="courseId" /> | ||||||
|                                     </core-format-text> |  | ||||||
|                                 </ng-container> |                                 </ng-container> | ||||||
|                                  /  |                                  /  | ||||||
|                                 <ng-container *ngFor="let ancestor of competency.comppath.ancestors"> |                                 <ng-container *ngFor="let ancestor of competency.comppath.ancestors"> | ||||||
|                                     <button class="as-link" *ngIf="competency.comppath.showlinks" |                                     <button class="as-link" *ngIf="competency.comppath.showlinks" | ||||||
|                                         (click)="openCompetencySummary(ancestor.id)"> |                                         (click)="openCompetencySummary(ancestor.id)"> | ||||||
|                                         <core-format-text [text]="ancestor.name" contextLevel="course" [contextInstanceId]="courseId"> |                                         <core-format-text [text]="ancestor.name" contextLevel="course" [contextInstanceId]="courseId" /> | ||||||
|                                         </core-format-text> |  | ||||||
|                                     </button> |                                     </button> | ||||||
|                                     <ng-container *ngIf="!competency.comppath.showlinks"> |                                     <ng-container *ngIf="!competency.comppath.showlinks"> | ||||||
|                                         <core-format-text [text]="ancestor.name" contextLevel="course" [contextInstanceId]="courseId"> |                                         <core-format-text [text]="ancestor.name" contextLevel="course" [contextInstanceId]="courseId" /> | ||||||
|                                         </core-format-text> |  | ||||||
|                                     </ng-container> |                                     </ng-container> | ||||||
|                                     <ng-container *ngIf="!ancestor.last"> / </ng-container> |                                     <ng-container *ngIf="!ancestor.last"> / </ng-container> | ||||||
|                                 </ng-container> |                                 </ng-container> | ||||||
| @ -121,30 +114,27 @@ | |||||||
|                         </div> |                         </div> | ||||||
|                         <div> |                         <div> | ||||||
|                             <p class="item-heading">{{ 'addon.competency.activities' | translate }}</p> |                             <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 }} |                                 {{ 'addon.competency.noactivities' | translate }} | ||||||
|                             </p> |                             </p> | ||||||
|                             <ion-item class="ion-text-wrap core-course-module-handler" [attr.aria-label]="activity.name" core-link |                             <ion-item class="ion-text-wrap core-course-module-handler" [attr.aria-label]="activity.name" core-link | ||||||
|                                 *ngFor="let activity of competency.coursemodules" [href]="activity.url" capture="true"> |                                 *ngFor="let activity of competency.coursemodules" [href]="activity.url" capture="true"> | ||||||
|                                 <core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl"> |                                 <core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl" /> | ||||||
|                                 </core-mod-icon> |  | ||||||
|                                 <ion-label> |                                 <ion-label> | ||||||
|                                     <core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id" |                                     <core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id" | ||||||
|                                         [courseId]="courseId"> |                                         [courseId]="courseId" /> | ||||||
|                                     </core-format-text> |  | ||||||
|                                 </ion-label> |                                 </ion-label> | ||||||
|                             </ion-item> |                             </ion-item> | ||||||
|                         </div> |                         </div> | ||||||
|                         <div *ngIf="competency.plans"> |                         <div *ngIf="competency.plans"> | ||||||
|                             <p class="item-heading">{{ 'addon.competency.userplans' | translate }}</p> |                             <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 }} |                                 {{ 'addon.competency.nouserplanswithcompetency' | translate }} | ||||||
|                             </p> |                             </p> | ||||||
|                             <ion-item class="ion-text-wrap" *ngFor="let plan of competency.plans" [href]="plan.url" |                             <ion-item class="ion-text-wrap" *ngFor="let plan of competency.plans" [href]="plan.url" | ||||||
|                                 [attr.aria-label]="plan.name" core-link capture="true"> |                                 [attr.aria-label]="plan.name" core-link capture="true"> | ||||||
|                                 <ion-label> |                                 <ion-label> | ||||||
|                                     <core-format-text [text]="plan.name" contextLevel="user" [contextInstanceId]="plan.userid"> |                                     <core-format-text [text]="plan.name" contextLevel="user" [contextInstanceId]="plan.userid" /> | ||||||
|                                     </core-format-text> |  | ||||||
|                                 </ion-label> |                                 </ion-label> | ||||||
|                             </ion-item> |                             </ion-item> | ||||||
|                         </div> |                         </div> | ||||||
|  | |||||||
| @ -1,25 +1,24 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1 *ngIf="plan"> |             <h1 *ngIf="plan"> | ||||||
|                 <core-format-text [text]="plan.plan.name" contextLevel="user" [contextInstanceId]="plan.plan.userid"> |                 <core-format-text [text]="plan.plan.name" contextLevel="user" [contextInstanceId]="plan.plan.userid" /> | ||||||
|                 </core-format-text> |  | ||||||
|             </h1> |             </h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content [core-swipe-navigation]="plans" class="limited-width"> | <ion-content [core-swipe-navigation]="plans" class="limited-width"> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshLearningPlan($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshLearningPlan($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="competencies.loaded"> |     <core-loading [hideUntil]="competencies.loaded"> | ||||||
|         <ion-card *ngIf="user"> |         <ion-card *ngIf="user"> | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <core-user-avatar [user]="user" slot="start"></core-user-avatar> |                     <core-user-avatar [user]="user" slot="start" /> | ||||||
|                     <p class="item-heading">{{ user.fullname }}</p> |                     <p class="item-heading">{{ user.fullname }}</p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| @ -29,8 +28,7 @@ | |||||||
|                 <ion-item class="ion-text-wrap" *ngIf="plan.plan.description"> |                 <ion-item class="ion-text-wrap" *ngIf="plan.plan.description"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p> |                         <p> | ||||||
|                             <core-format-text [text]="plan.plan.description" contextLevel="user" [contextInstanceId]="plan.plan.userid"> |                             <core-format-text [text]="plan.plan.description" contextLevel="user" [contextInstanceId]="plan.plan.userid" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </p> |                         </p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| @ -50,8 +48,7 @@ | |||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading">{{ 'addon.competency.template' | translate }}</p> |                         <p class="item-heading">{{ 'addon.competency.template' | translate }}</p> | ||||||
|                         <p> |                         <p> | ||||||
|                             <core-format-text [text]="plan.plan.template.shortname" contextLevel="system" [contextInstanceId]="0"> |                             <core-format-text [text]="plan.plan.template.shortname" contextLevel="system" [contextInstanceId]="0" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </p> |                         </p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| @ -64,8 +61,7 @@ | |||||||
|                         </p> |                         </p> | ||||||
|                         <core-progress-bar [progress]="plan.proficientcompetencypercentage" |                         <core-progress-bar [progress]="plan.proficientcompetencypercentage" | ||||||
|                             [text]="plan.proficientcompetencypercentageformatted" |                             [text]="plan.proficientcompetencypercentageformatted" | ||||||
|                             ariaDescribedBy="addon-competency-plan-{{plan.plan.id}}-progress"> |                             ariaDescribedBy="addon-competency-plan-{{plan.plan.id}}-progress" /> | ||||||
|                         </core-progress-bar> |  | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </ion-list> |             </ion-list> | ||||||
| @ -75,7 +71,7 @@ | |||||||
|                 <ion-card-title>{{ 'addon.competency.learningplancompetencies' | translate }}</ion-card-title> |                 <ion-card-title>{{ 'addon.competency.learningplancompetencies' | translate }}</ion-card-title> | ||||||
|             </ion-card-header> |             </ion-card-header> | ||||||
|             <ion-list> |             <ion-list> | ||||||
|                 <ion-item class="ion-text-wrap" *ngIf="plan.competencycount == 0"> |                 <ion-item class="ion-text-wrap" *ngIf="plan.competencycount === 0"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p>{{ 'addon.competency.nocompetencies' | translate }}</p> |                         <p>{{ 'addon.competency.nocompetencies' | translate }}</p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
| @ -85,8 +81,7 @@ | |||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading"> |                         <p class="item-heading"> | ||||||
|                             <core-format-text [text]="competency.competency.shortname" contextLevel="user" |                             <core-format-text [text]="competency.competency.shortname" contextLevel="user" | ||||||
|                                 [contextInstanceId]="plan.plan.userid"> |                                 [contextInstanceId]="plan.plan.userid" /> <em>{{competency.competency.idnumber}}</em> | ||||||
|                             </core-format-text> <em>{{competency.competency.idnumber}}</em> |  | ||||||
|                         </p> |                         </p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                     <ion-badge *ngIf="competency.usercompetencyplan" slot="end" |                     <ion-badge *ngIf="competency.usercompetencyplan" slot="end" | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.competency.userplans' | translate }}</h1> |             <h1>{{ 'addon.competency.userplans' | translate }}</h1> | ||||||
| @ -11,19 +11,16 @@ | |||||||
| <ion-content> | <ion-content> | ||||||
|     <core-split-view> |     <core-split-view> | ||||||
|         <ion-refresher slot="fixed" [disabled]="!plans.loaded" (ionRefresh)="refreshLearningPlans($event.target)"> |         <ion-refresher slot="fixed" [disabled]="!plans.loaded" (ionRefresh)="refreshLearningPlans($event.target)"> | ||||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|         </ion-refresher> |         </ion-refresher> | ||||||
|         <core-loading [hideUntil]="plans.loaded"> |         <core-loading [hideUntil]="plans.loaded"> | ||||||
|             <core-empty-box *ngIf="plans.empty" icon="fas-route" [message]="'addon.competency.noplanswerecreated' | translate"> |             <core-empty-box *ngIf="plans.empty" icon="fas-route" [message]="'addon.competency.noplanswerecreated' | translate" /> | ||||||
| 
 |  | ||||||
|             </core-empty-box> |  | ||||||
|             <ion-list *ngIf="!plans.empty" class="ion-no-margin"> |             <ion-list *ngIf="!plans.empty" class="ion-no-margin"> | ||||||
|                 <ion-item class="ion-text-wrap" *ngFor="let plan of plans.items" [attr.aria-label]="plan.name" (click)="plans.select(plan)" |                 <ion-item class="ion-text-wrap" *ngFor="let plan of plans.items" [attr.aria-label]="plan.name" (click)="plans.select(plan)" | ||||||
|                     [attr.aria-current]="plans.getItemAriaCurrent(plan)" button [detail]="true"> |                     [attr.aria-current]="plans.getItemAriaCurrent(plan)" button [detail]="true"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading"> |                         <p class="item-heading"> | ||||||
|                             <core-format-text [text]="plan.name" contextLevel="user" [contextInstanceId]="plan.userid"> |                             <core-format-text [text]="plan.name" contextLevel="user" [contextInstanceId]="plan.userid" /> | ||||||
|                             </core-format-text> |  | ||||||
|                         </p> |                         </p> | ||||||
|                         <p *ngIf="plan.duedate > 0"> |                         <p *ngIf="plan.duedate > 0"> | ||||||
|                             {{ 'addon.competency.duedate' | translate }}:  |                             {{ 'addon.competency.duedate' | translate }}:  | ||||||
|  | |||||||
| @ -389,7 +389,7 @@ Feature: Test competency navigation | |||||||
|     # Participant competencies |     # Participant competencies | ||||||
|     When I press "Participants" in the app |     When I press "Participants" in the app | ||||||
|     And I press "Student first" in the app |     And I press "Student first" in the app | ||||||
|     And I press "Competencies" in the app |     And I press "Competencies" within "Student first" "page-core-user-participants" in the app | ||||||
|     Then I should find "Student first" in the app |     Then I should find "Student first" in the app | ||||||
|     And I should find "Salads are important" in the app |     And I should find "Salads are important" in the app | ||||||
|     And I should find "Good" within "salads" "ion-item" in the app |     And I should find "Good" within "salads" "ion-item" in the app | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.coursecompletion.coursecompletion' | translate }}</h1> |             <h1>{{ 'addon.coursecompletion.coursecompletion' | translate }}</h1> | ||||||
| @ -10,11 +10,11 @@ | |||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!completionLoaded" (ionRefresh)="refreshCompletion($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!completionLoaded" (ionRefresh)="refreshCompletion($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="completionLoaded"> |     <core-loading [hideUntil]="completionLoaded"> | ||||||
|         <ion-item class="ion-text-wrap" *ngIf="user"> |         <ion-item class="ion-text-wrap" *ngIf="user"> | ||||||
|             <core-user-avatar [user]="user" [courseId]="courseId" slot="start" [linkProfile]="false"></core-user-avatar> |             <core-user-avatar [user]="user" [courseId]="courseId" slot="start" [linkProfile]="false" /> | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <p class="item-heading">{{user.fullname}}</p> |                 <p class="item-heading">{{user.fullname}}</p> | ||||||
|             </ion-label> |             </ion-label> | ||||||
| @ -44,10 +44,10 @@ | |||||||
|             <ion-item class="ion-hide-md-up ion-text-wrap" *ngFor="let criteria of completion.completions"> |             <ion-item class="ion-hide-md-up ion-text-wrap" *ngFor="let criteria of completion.completions"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p class="item-heading"> |                     <p class="item-heading"> | ||||||
|                         <core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false"></core-format-text> |                         <core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false" /> | ||||||
|                     </p> |                     </p> | ||||||
|                     <p> |                     <p> | ||||||
|                         <core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false"></core-format-text> |                         <core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false" /> | ||||||
|                     </p> |                     </p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|                 <strong slot="end" *ngIf="criteria.complete">{{ 'core.yes' | translate }}</strong> |                 <strong slot="end" *ngIf="criteria.complete">{{ 'core.yes' | translate }}</strong> | ||||||
| @ -65,23 +65,23 @@ | |||||||
|                     </ion-row> |                     </ion-row> | ||||||
|                     <ion-row *ngFor="let criteria of completion.completions"> |                     <ion-row *ngFor="let criteria of completion.completions"> | ||||||
|                         <ion-col> |                         <ion-col> | ||||||
|                             <core-format-text clean="true" [text]="criteria.details.type" [filter]="false"></core-format-text> |                             <core-format-text clean="true" [text]="criteria.details.type" [filter]="false" /> | ||||||
|                         </ion-col> |                         </ion-col> | ||||||
|                         <ion-col> |                         <ion-col> | ||||||
|                             <core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false"></core-format-text> |                             <core-format-text clean="true" [text]="criteria.details.criteria" [filter]="false" /> | ||||||
|                         </ion-col> |                         </ion-col> | ||||||
|                         <ion-col> |                         <ion-col> | ||||||
|                             <core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false"></core-format-text> |                             <core-format-text clean="true" [text]="criteria.details.requirement" [filter]="false" /> | ||||||
|                         </ion-col> |                         </ion-col> | ||||||
|                         <ion-col> |                         <ion-col> | ||||||
|                             <core-format-text [text]="criteria.details.status" [filter]="false"></core-format-text> |                             <core-format-text [text]="criteria.details.status" [filter]="false" /> | ||||||
|                         </ion-col> |                         </ion-col> | ||||||
|                         <ion-col *ngIf="criteria.complete">{{ 'core.yes' | translate }}</ion-col> |                         <ion-col *ngIf="criteria.complete">{{ 'core.yes' | translate }}</ion-col> | ||||||
|                         <ion-col *ngIf="!criteria.complete">{{ 'core.no' | translate }}</ion-col> |                         <ion-col *ngIf="!criteria.complete">{{ 'core.no' | translate }}</ion-col> | ||||||
|                         <ion-col *ngIf="criteria.timecompleted"> |                         <ion-col *ngIf="criteria.timecompleted"> | ||||||
|                             {{ criteria.timecompleted * 1000 | coreFormatDate :'strftimedatetimeshort' }} |                             {{ criteria.timecompleted * 1000 | coreFormatDate :'strftimedatetimeshort' }} | ||||||
|                         </ion-col> |                         </ion-col> | ||||||
|                         <ion-col *ngIf="!criteria.timecompleted"></ion-col> |                         <ion-col *ngIf="!criteria.timecompleted" /> | ||||||
|                     </ion-row> |                     </ion-row> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| @ -103,7 +103,7 @@ | |||||||
| 
 | 
 | ||||||
|         <ion-card class="core-warning-card" *ngIf="!tracked"> |         <ion-card class="core-warning-card" *ngIf="!tracked"> | ||||||
|             <ion-item> |             <ion-item> | ||||||
|                 <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" /> | ||||||
|                 <ion-label>{{ 'addon.coursecompletion.nottracked' | translate }}</ion-label> |                 <ion-label>{{ 'addon.coursecompletion.nottracked' | translate }}</ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|         </ion-card> |         </ion-card> | ||||||
|  | |||||||
| @ -21,9 +21,10 @@ import { CoreSite  } from '@classes/sites/site'; | |||||||
| import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; | import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; | ||||||
| import { makeSingleton } from '@singletons'; | import { makeSingleton } from '@singletons'; | ||||||
| import { CoreError } from '@classes/errors/error'; | 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 { map } from 'rxjs/operators'; | ||||||
| import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site'; | import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site'; | ||||||
|  | import { firstValueFrom } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| const ROOT_CACHE_KEY = 'mmaCourseCompletion:'; | const ROOT_CACHE_KEY = 'mmaCourseCompletion:'; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable, ViewContainerRef, ComponentFactoryResolver } from '@angular/core'; | import { Injectable, ViewContainerRef } from '@angular/core'; | ||||||
| 
 | 
 | ||||||
| import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter'; | import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter'; | ||||||
| import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; | import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; | ||||||
| @ -32,10 +32,6 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle | |||||||
| 
 | 
 | ||||||
|     protected template = document.createElement('template'); // A template element to convert HTML to element.
 |     protected template = document.createElement('template'); // A template element to convert HTML to element.
 | ||||||
| 
 | 
 | ||||||
|     constructor(protected factoryResolver: ComponentFactoryResolver) { |  | ||||||
|         super(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
| @ -95,8 +91,7 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle | |||||||
|             const url = placeholder.getAttribute('data-player-src') || ''; |             const url = placeholder.getAttribute('data-player-src') || ''; | ||||||
| 
 | 
 | ||||||
|             // Create the component to display the player.
 |             // Create the component to display the player.
 | ||||||
|             const factory = this.factoryResolver.resolveComponentFactory(CoreH5PPlayerComponent); |             const componentRef = viewContainerRef.createComponent<CoreH5PPlayerComponent>(CoreH5PPlayerComponent); | ||||||
|             const componentRef = viewContainerRef.createComponent<CoreH5PPlayerComponent>(factory); |  | ||||||
| 
 | 
 | ||||||
|             componentRef.instance.src = url; |             componentRef.instance.src = url; | ||||||
|             componentRef.instance.component = component; |             componentRef.instance.component = component; | ||||||
|  | |||||||
| @ -411,7 +411,7 @@ type MathJaxWindow = Window & { | |||||||
|             _configured: boolean; // eslint-disable-line @typescript-eslint/naming-convention
 |             _configured: boolean; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||||
|             // Add the configuration to the head and set the lang.
 |             // Add the configuration to the head and set the lang.
 | ||||||
|             configure: (params: Record<string, unknown>) => void; |             configure: (params: Record<string, unknown>) => void; | ||||||
|             _setLocale: () => void; // eslint-disable-line @typescript-eslint/naming-convention
 |             _setLocale: () => void; | ||||||
|             typeset: (container: HTMLElement) => void; |             typeset: (container: HTMLElement) => void; | ||||||
|         }; |         }; | ||||||
|     }; |     }; | ||||||
|  | |||||||
| @ -147,7 +147,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|         if (el.hasOwnProperty(name)) { |         if (el.hasOwnProperty(name)) { | ||||||
|             el[name] = value; |             el[name] = value; | ||||||
|         } |         } | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if browser/device is supported by Ogv.JS. |      * Check if browser/device is supported by Ogv.JS. | ||||||
| @ -156,7 +156,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static isSupported(): boolean { |     static isSupported(): boolean { | ||||||
|         return OGVCompat.supported('OGVPlayer'); |         return OGVCompat.supported('OGVPlayer'); | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the tech can support the given type. |      * Check if the tech can support the given type. | ||||||
| @ -166,7 +166,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static canPlayType(type: string): string { |     static canPlayType(type: string): string { | ||||||
|         return (type.indexOf('/ogg') !== -1 || type.indexOf('/webm')) ? 'maybe' : ''; |         return (type.indexOf('/ogg') !== -1 || type.indexOf('/webm')) ? 'maybe' : ''; | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the tech can support the given source. |      * Check if the tech can support the given source. | ||||||
| @ -176,7 +176,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static canPlaySource(srcObj: TechSourceObject): string { |     static canPlaySource(srcObj: TechSourceObject): string { | ||||||
|         return VideoJSOgvJS.canPlayType(srcObj.type); |         return VideoJSOgvJS.canPlayType(srcObj.type); | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the volume can be changed in this browser/device. |      * 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
 |         // eslint-disable-next-line no-prototype-builtins
 | ||||||
|         return player.hasOwnProperty('volume'); |         return player.hasOwnProperty('volume'); | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the volume can be muted in this browser/device. |      * Check if the volume can be muted in this browser/device. | ||||||
| @ -203,7 +203,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static canMuteVolume(): boolean { |     static canMuteVolume(): boolean { | ||||||
|         return true; |         return true; | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the playback rate can be changed in this browser/device. |      * Check if the playback rate can be changed in this browser/device. | ||||||
| @ -212,7 +212,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static canControlPlaybackRate(): boolean { |     static canControlPlaybackRate(): boolean { | ||||||
|         return true; |         return true; | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check to see if native 'TextTracks' are supported by this browser/device. |      * Check to see if native 'TextTracks' are supported by this browser/device. | ||||||
| @ -221,7 +221,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static supportsNativeTextTracks(): boolean { |     static supportsNativeTextTracks(): boolean { | ||||||
|         return false; |         return false; | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the fullscreen resize is supported by this browser/device. |      * Check if the fullscreen resize is supported by this browser/device. | ||||||
| @ -230,7 +230,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static supportsFullscreenResize(): boolean { |     static supportsFullscreenResize(): boolean { | ||||||
|         return true; |         return true; | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the progress events is supported by this browser/device. |      * Check if the progress events is supported by this browser/device. | ||||||
| @ -239,7 +239,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static supportsProgressEvents(): boolean { |     static supportsProgressEvents(): boolean { | ||||||
|         return true; |         return true; | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Check if the time update events is supported by this browser/device. |      * Check if the time update events is supported by this browser/device. | ||||||
| @ -248,7 +248,7 @@ export class VideoJSOgvJS extends Tech { | |||||||
|      */ |      */ | ||||||
|     static supportsTimeupdateEvents(): boolean { |     static supportsTimeupdateEvents(): boolean { | ||||||
|         return true; |         return true; | ||||||
|     }; |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Create the 'OgvJS' Tech's DOM element. |      * Create the 'OgvJS' Tech's DOM element. | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.messageoutput_airnotifier.processorsettingsdesc' | translate }}</h1> |             <h1>{{ 'addon.messageoutput_airnotifier.processorsettingsdesc' | translate }}</h1> | ||||||
| @ -10,7 +10,7 @@ | |||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshDevices($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshDevices($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
|         <ng-container *ngFor="let platform of platformDevices"> |         <ng-container *ngFor="let platform of platformDevices"> | ||||||
| @ -23,7 +23,7 @@ | |||||||
|                 <ion-list> |                 <ion-list> | ||||||
|                     <ion-item class="ion-text-wrap" *ngFor="let device of platform.devices" [class.item-current]="device.current"> |                     <ion-item class="ion-text-wrap" *ngFor="let device of platform.devices" [class.item-current]="device.current"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <p class="item-heading"> |                             <p class="item-heading" id="device-{{device.id}}"> | ||||||
|                                 <strong>{{ device.name }} {{ device.model }}</strong> ({{platform.platform}} {{ device.version }}) |                                 <strong>{{ device.name }} {{ device.model }}</strong> ({{platform.platform}} {{ device.version }}) | ||||||
|                             </p> |                             </p> | ||||||
|                             <p *ngIf="device.current"><strong>{{ 'core.currentdevice' | translate }}</strong></p> |                             <p *ngIf="device.current"><strong>{{ 'core.currentdevice' | translate }}</strong></p> | ||||||
| @ -33,8 +33,8 @@ | |||||||
|                             </p> |                             </p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                         <core-button-with-spinner [loading]="device.updating" slot="end"> |                         <core-button-with-spinner [loading]="device.updating" slot="end"> | ||||||
|                             <ion-toggle [(ngModel)]="device.enable" (ngModelChange)="enableDevice(device, device.enable)"> |                             <ion-toggle [(ngModel)]="device.enable" (ngModelChange)="enableDevice(device, device.enable)" | ||||||
|                             </ion-toggle> |                                 [attr.aria-labelledby]="'device-'+ device.id " /> | ||||||
|                         </core-button-with-spinner> |                         </core-button-with-spinner> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                 </ion-list> |                 </ion-list> | ||||||
|  | |||||||
| @ -5,14 +5,14 @@ | |||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||||
|                 <ion-icon name="fas-xmark" slot="icon-only" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-xmark" slot="icon-only" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
| 
 | 
 | ||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
| @ -23,12 +23,11 @@ | |||||||
|                         onError="this.src='assets/img/group-avatar.svg'"> |                         onError="this.src='assets/img/group-avatar.svg'"> | ||||||
|                 </div> |                 </div> | ||||||
|                 <h2> |                 <h2> | ||||||
|                     <core-format-text [text]="conversation.name" contextLevel="system" [contextInstanceId]="0"></core-format-text> |                     <core-format-text [text]="conversation.name" contextLevel="system" [contextInstanceId]="0" /> | ||||||
|                 </h2> |                 </h2> | ||||||
|                 <p> |                 <p> | ||||||
|                     <core-format-text *ngIf="conversation.subname" [text]="conversation.subname" contextLevel="system" |                     <core-format-text *ngIf="conversation.subname" [text]="conversation.subname" contextLevel="system" | ||||||
|                         [contextInstanceId]="0"> |                         [contextInstanceId]="0" /> | ||||||
|                     </core-format-text> |  | ||||||
|                 </p> |                 </p> | ||||||
|                 <p>{{ 'addon.messages.numparticipants' | translate:{$a: conversation.membercount} }}</p> |                 <p>{{ 'addon.messages.numparticipants' | translate:{$a: conversation.membercount} }}</p> | ||||||
|             </ion-label> |             </ion-label> | ||||||
| @ -36,19 +35,16 @@ | |||||||
| 
 | 
 | ||||||
|         <ion-item class="ion-text-wrap addon-messages-conversation-item" *ngFor="let member of members" (click)="closeModal(member.id)" |         <ion-item class="ion-text-wrap addon-messages-conversation-item" *ngFor="let member of members" (click)="closeModal(member.id)" | ||||||
|             [detail]="true" button> |             [detail]="true" button> | ||||||
|             <core-user-avatar [user]="member" [linkProfile]="false" [checkOnline]="member.showonlinestatus" slot="start"> |             <core-user-avatar [user]="member" [linkProfile]="false" [checkOnline]="member.showonlinestatus" slot="start" /> | ||||||
|             </core-user-avatar> |  | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <p class="item-heading"> |                 <p class="item-heading"> | ||||||
|                     {{ member.fullname }} |                     {{ member.fullname }} | ||||||
|                     <ion-icon name="fas-user-slash" *ngIf="member.isblocked" |                     <ion-icon name="fas-user-slash" *ngIf="member.isblocked" | ||||||
|                         [attr.aria-label]="'addon.messages.contactblocked' | translate"> |                         [attr.aria-label]="'addon.messages.contactblocked' | translate" /> | ||||||
|                     </ion-icon> |  | ||||||
|                 </p> |                 </p> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| 
 | 
 | ||||||
|         <core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreMembers($event)" [error]="loadMoreError"> |         <core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreMembers($event)" [error]="loadMoreError" /> | ||||||
|         </core-infinite-loading> |  | ||||||
|     </core-loading> |     </core-loading> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -12,31 +12,17 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router'; | ||||||
| import { ActivatedRouteSnapshot, CanActivate, UrlTree } from '@angular/router'; |  | ||||||
| import { Router } from '@singletons'; | import { Router } from '@singletons'; | ||||||
| import { AddonMessagesMainMenuHandlerService } from '../services/handlers/mainmenu'; | import { AddonMessagesMainMenuHandlerService } from '../services/handlers/mainmenu'; | ||||||
| import { AddonMessages } from '../services/messages'; | import { AddonMessages } from '../services/messages'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Guard to redirect to the right page based on the current Moodle site version. |  * Guard to redirect to the right page based on the current Moodle site version. | ||||||
|  */ |  | ||||||
| @Injectable({ providedIn: 'root' }) |  | ||||||
| export class AddonMessagesIndexGuard implements CanActivate { |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * @inheritdoc |  | ||||||
|      */ |  | ||||||
|     canActivate(route: ActivatedRouteSnapshot): UrlTree { |  | ||||||
|         return this.guard(route); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Check if there is a pending redirect and trigger it. |  | ||||||
|  * |  * | ||||||
|      * @returns The redirection route. |  * @returns Route. | ||||||
|  */ |  */ | ||||||
|     private guard(route: ActivatedRouteSnapshot): UrlTree { | export const messagesIndexGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { | ||||||
|     const enabled = AddonMessages.isGroupMessagingEnabled(); |     const enabled = AddonMessages.isGroupMessagingEnabled(); | ||||||
|     const path = `/main/${AddonMessagesMainMenuHandlerService.PAGE_NAME}/` + ( enabled ? 'group-conversations' : 'index'); |     const path = `/main/${AddonMessagesMainMenuHandlerService.PAGE_NAME}/` + ( enabled ? 'group-conversations' : 'index'); | ||||||
| 
 | 
 | ||||||
| @ -45,6 +31,4 @@ export class AddonMessagesIndexGuard implements CanActivate { | |||||||
|     newRoute.queryParams = route.queryParams; |     newRoute.queryParams = route.queryParams; | ||||||
| 
 | 
 | ||||||
|     return newRoute; |     return newRoute; | ||||||
|     } | }; | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -28,7 +28,7 @@ import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/comp | |||||||
| import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; | import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; | ||||||
| import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | ||||||
| import { CoreScreen } from '@services/screen'; | import { CoreScreen } from '@services/screen'; | ||||||
| import { AddonMessagesIndexGuard } from './guards'; | import { messagesIndexGuard } from './guards'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Build module routes. |  * Build module routes. | ||||||
| @ -120,7 +120,7 @@ function buildRoutes(injector: Injector): Routes { | |||||||
|             loadChildren: () => import('./messages-settings-lazy.module').then(m => m.AddonMessagesSettingsLazyModule), |             loadChildren: () => import('./messages-settings-lazy.module').then(m => m.AddonMessagesSettingsLazyModule), | ||||||
|         }, |         }, | ||||||
|         ...buildTabMainRoutes(injector, { |         ...buildTabMainRoutes(injector, { | ||||||
|             canActivate: [AddonMessagesIndexGuard], |             canActivate: [messagesIndexGuard], | ||||||
|         }), |         }), | ||||||
|     ]; |     ]; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,32 +1,32 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.messages.contacts' | translate }}</h1> |             <h1>{{ 'addon.messages.contacts' | translate }}</h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> |             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||||
|             <core-context-menu></core-context-menu> |             <core-context-menu /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <core-split-view> |     <core-split-view> | ||||||
|         <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)"> |         <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)"> | ||||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|         </ion-refresher> |         </ion-refresher> | ||||||
| 
 | 
 | ||||||
|         <core-search-box (onSubmit)="search($event)" (onClear)="clearSearch()" [placeholder]="'addon.messages.contactname' | translate" |         <core-search-box (onSubmit)="search($event)" (onClear)="clearSearch()" [placeholder]="'addon.messages.contactname' | translate" | ||||||
|             autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded" searchArea="AddonMessagesContacts"></core-search-box> |             autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded" searchArea="AddonMessagesContacts" /> | ||||||
| 
 | 
 | ||||||
|         <core-loading [hideUntil]="loaded" [message]="loadingMessage"> |         <core-loading [hideUntil]="loaded" [message]="loadingMessage"> | ||||||
|             <core-empty-box *ngIf="!hasContacts && searchString === ''" icon="fas-address-book" |             <core-empty-box *ngIf="!hasContacts && searchString === ''" icon="fas-address-book" | ||||||
|                 [message]="'addon.messages.contactlistempty' | translate"></core-empty-box> |                 [message]="'addon.messages.contactlistempty' | translate" /> | ||||||
| 
 | 
 | ||||||
|             <core-empty-box *ngIf="!hasContacts && searchString !== ''" icon="fas-address-book" |             <core-empty-box *ngIf="!hasContacts && searchString !== ''" icon="fas-address-book" | ||||||
|                 [message]="'addon.messages.nousersfound' | translate"></core-empty-box> |                 [message]="'addon.messages.nousersfound' | translate" /> | ||||||
| 
 | 
 | ||||||
|             <ion-list *ngFor="let contactType of contactTypes" class="ion-no-margin"> |             <ion-list *ngFor="let contactType of contactTypes" class="ion-no-margin"> | ||||||
|                 <ng-container *ngIf="contacts[contactType] && (contacts[contactType].length > 0 || contactType === searchType)"> |                 <ng-container *ngIf="contacts[contactType] && (contacts[contactType].length > 0 || contactType === searchType)"> | ||||||
| @ -43,8 +43,8 @@ | |||||||
|                         <ion-item class="ion-text-wrap addon-messages-conversation-item" |                         <ion-item class="ion-text-wrap addon-messages-conversation-item" | ||||||
|                             *ngIf="contact.profileimageurl || contact.profileimageurlsmall" [attr.aria-label]="contact.fullname" |                             *ngIf="contact.profileimageurl || contact.profileimageurlsmall" [attr.aria-label]="contact.fullname" | ||||||
|                             (click)="gotoDiscussion(contact.id)" [detail]="true" button |                             (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> |                             <core-user-avatar [user]="contact" slot="start" [checkOnline]="contact.showonlinestatus" /> | ||||||
|                             <ion-label> |                             <ion-label> | ||||||
|                                 <p class="item-heading">{{ contact.fullname }}</p> |                                 <p class="item-heading">{{ contact.fullname }}</p> | ||||||
|                             </ion-label> |                             </ion-label> | ||||||
|  | |||||||
| @ -1,17 +1,17 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.messages.contacts' | translate }}</h1> |             <h1>{{ 'addon.messages.contacts' | translate }}</h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button fill="clear" (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate"> |             <ion-button fill="clear" (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate"> | ||||||
|                 <ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> |             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||||
|             <core-context-menu></core-context-menu> |             <core-context-menu /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| @ -23,35 +23,30 @@ | |||||||
|             <core-tab [title]="'addon.messages.contacts' | translate" (ionSelect)="selectTab('confirmed')"> |             <core-tab [title]="'addon.messages.contacts' | translate" (ionSelect)="selectTab('confirmed')"> | ||||||
|                 <ng-template> |                 <ng-template> | ||||||
|                     <ion-refresher slot="fixed" [disabled]="!confirmedLoaded" (ionRefresh)="refreshData($event.target)"> |                     <ion-refresher slot="fixed" [disabled]="!confirmedLoaded" (ionRefresh)="refreshData($event.target)"> | ||||||
|                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|                     </ion-refresher> |                     </ion-refresher> | ||||||
|                     <core-loading [hideUntil]="confirmedLoaded"> |                     <core-loading [hideUntil]="confirmedLoaded"> | ||||||
|                         <ion-list class="ion-no-margin" *ngIf="confirmedContacts.length"> |                         <ion-list class="ion-no-margin" *ngIf="confirmedContacts.length"> | ||||||
|                             <ion-item class="ion-text-wrap addon-messages-conversation-item" (click)="selectUser(contact.id)" button |                             <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" |                                 *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" |                                 <core-user-avatar slot="start" [user]="contact" [checkOnline]="contact.showonlinestatus" | ||||||
|                                     [linkProfile]="false"> |                                     [linkProfile]="false" /> | ||||||
|                                 </core-user-avatar> |  | ||||||
|                                 <ion-label> |                                 <ion-label> | ||||||
|                                     <p class="item-heading"> |                                     <p class="item-heading"> | ||||||
|                                         <core-format-text [text]="contact.fullname" contextLevel="system" [contextInstanceId]="0"> |                                         <core-format-text [text]="contact.fullname" contextLevel="system" [contextInstanceId]="0" /> | ||||||
|                                         </core-format-text> |  | ||||||
|                                         <ion-icon *ngIf="contact.isblocked" name="fas-user-slash" slot="end" |                                         <ion-icon *ngIf="contact.isblocked" name="fas-user-slash" slot="end" | ||||||
|                                             [attr.aria-label]="'addon.messages.contactblocked' | translate"> |                                             [attr.aria-label]="'addon.messages.contactblocked' | translate" /> | ||||||
|                                         </ion-icon> |  | ||||||
|                                     </p> |                                     </p> | ||||||
|                                 </ion-label> |                                 </ion-label> | ||||||
|                             </ion-item> |                             </ion-item> | ||||||
|                         </ion-list> |                         </ion-list> | ||||||
| 
 | 
 | ||||||
|                         <core-empty-box *ngIf="!confirmedContacts.length" icon="far-address-book" |                         <core-empty-box *ngIf="!confirmedContacts.length" icon="far-address-book" | ||||||
|                             [message]="'addon.messages.nocontactsgetstarted' | translate"> |                             [message]="'addon.messages.nocontactsgetstarted' | translate" /> | ||||||
|                         </core-empty-box> |  | ||||||
| 
 | 
 | ||||||
|                         <core-infinite-loading [enabled]="confirmedCanLoadMore" (action)="loadMore($event)" [error]="confirmedLoadMoreError" |                         <core-infinite-loading [enabled]="confirmedCanLoadMore" (action)="loadMore($event)" [error]="confirmedLoadMoreError" | ||||||
|                             position="bottom"> |                             position="bottom" /> | ||||||
|                         </core-infinite-loading> |  | ||||||
|                     </core-loading> |                     </core-loading> | ||||||
|                 </ng-template> |                 </ng-template> | ||||||
|             </core-tab> |             </core-tab> | ||||||
| @ -61,17 +56,16 @@ | |||||||
|                 badgeA11yText="addon.messages.pendingcontactrequests"> |                 badgeA11yText="addon.messages.pendingcontactrequests"> | ||||||
|                 <ng-template> |                 <ng-template> | ||||||
|                     <ion-refresher slot="fixed" [disabled]="!requestsLoaded" (ionRefresh)="refreshData($event.target)"> |                     <ion-refresher slot="fixed" [disabled]="!requestsLoaded" (ionRefresh)="refreshData($event.target)"> | ||||||
|                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |                         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|                     </ion-refresher> |                     </ion-refresher> | ||||||
|                     <core-loading [hideUntil]="requestsLoaded"> |                     <core-loading [hideUntil]="requestsLoaded"> | ||||||
|                         <ion-list class="ion-no-margin" *ngIf="requests.length"> |                         <ion-list class="ion-no-margin" *ngIf="requests.length"> | ||||||
|                             <ion-item class="ion-text-wrap addon-messages-conversation-item" *ngFor="let request of requests" |                             <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-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> |                                 <core-user-avatar slot="start" [user]="request" [linkProfile]="false" /> | ||||||
|                                 <ion-label> |                                 <ion-label> | ||||||
|                                     <core-format-text [text]="request.fullname" contextLevel="system" [contextInstanceId]="0"> |                                     <core-format-text [text]="request.fullname" contextLevel="system" [contextInstanceId]="0" /> | ||||||
|                                     </core-format-text> |  | ||||||
|                                     <p *ngIf="!request.iscontact"> |                                     <p *ngIf="!request.iscontact"> | ||||||
|                                         {{ 'addon.messages.wouldliketocontactyou' | translate }} |                                         {{ 'addon.messages.wouldliketocontactyou' | translate }} | ||||||
|                                     </p> |                                     </p> | ||||||
| @ -79,11 +73,9 @@ | |||||||
|                             </ion-item> |                             </ion-item> | ||||||
|                         </ion-list> |                         </ion-list> | ||||||
|                         <core-empty-box *ngIf="!requests.length" icon="far-address-book" |                         <core-empty-box *ngIf="!requests.length" icon="far-address-book" | ||||||
|                             [message]="'addon.messages.nocontactrequests' | translate"> |                             [message]="'addon.messages.nocontactrequests' | translate" /> | ||||||
|                         </core-empty-box> |  | ||||||
|                         <core-infinite-loading [enabled]="requestsCanLoadMore" (action)="loadMore($event)" [error]="requestsLoadMoreError" |                         <core-infinite-loading [enabled]="requestsCanLoadMore" (action)="loadMore($event)" [error]="requestsLoadMoreError" | ||||||
|                             position="bottom"> |                             position="bottom" /> | ||||||
|                         </core-infinite-loading> |  | ||||||
|                     </core-loading> |                     </core-loading> | ||||||
|                 </ng-template> |                 </ng-template> | ||||||
|             </core-tab> |             </core-tab> | ||||||
|  | |||||||
| @ -1,67 +1,57 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1> |             <h1> | ||||||
|                 <img *ngIf="loaded && !otherMember && conversationImage" class="core-bar-button-image" [src]="conversationImage" alt="" |                 <img *ngIf="loaded && !otherMember && conversationImage" class="core-bar-button-image" [src]="conversationImage" alt="" | ||||||
|                     onError="this.src='assets/img/group-avatar.svg'" core-external-content role="presentation" [siteId]="siteId"> |                     onError="this.src='assets/img/group-avatar.svg'" core-external-content role="presentation" [siteId]="siteId"> | ||||||
|                 <core-user-avatar *ngIf="loaded && otherMember" class="core-bar-button-image" [user]="otherMember" [linkProfile]="false" |                 <core-user-avatar *ngIf="loaded && otherMember" class="core-bar-button-image" [user]="otherMember" [linkProfile]="false" | ||||||
|                     [checkOnline]="otherMember.showonlinestatus"> |                     [checkOnline]="otherMember.showonlinestatus" /> | ||||||
|                 </core-user-avatar> |                 <core-format-text [text]="title" contextLevel="system" [contextInstanceId]="0" /> | ||||||
|                 <core-format-text [text]="title" contextLevel="system" [contextInstanceId]="0"></core-format-text> |  | ||||||
|                 <ion-icon *ngIf="conversation && conversation.isfavourite" name="fas-star" |                 <ion-icon *ngIf="conversation && conversation.isfavourite" name="fas-star" | ||||||
|                     [attr.aria-label]="'core.favourites' | translate"> |                     [attr.aria-label]="'core.favourites' | translate" /> | ||||||
|                 </ion-icon> |  | ||||||
|                 <ion-icon *ngIf="conversation && conversation.ismuted" name="fas-bell-slash" |                 <ion-icon *ngIf="conversation && conversation.ismuted" name="fas-bell-slash" | ||||||
|                     [attr.aria-label]="'addon.messages.mutedconversation' | translate"> |                     [attr.aria-label]="'addon.messages.mutedconversation' | translate" /> | ||||||
|                 </ion-icon> |  | ||||||
|             </h1> |             </h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"></ion-buttons> |         <ion-buttons slot="end" /> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
|     <core-navbar-buttons slot="end"> |     <core-navbar-buttons slot="end"> | ||||||
|         <core-context-menu [attr.aria-label]="'addon.messages.conversationactions' | translate"> |         <core-context-menu [attr.aria-label]="'addon.messages.conversationactions' | translate"> | ||||||
|             <core-context-menu-item [hidden]="isSelf || !showInfo || isGroup" [priority]="1000" |             <core-context-menu-item [hidden]="isSelf || !showInfo || isGroup" [priority]="1000" | ||||||
|                 [content]="'addon.messages.info' | translate" (action)="viewInfo()" iconAction="fas-circle-info"></core-context-menu-item> |                 [content]="'addon.messages.info' | translate" (action)="viewInfo()" iconAction="fas-circle-info" /> | ||||||
|             <core-context-menu-item [hidden]="isSelf || !showInfo || !isGroup" [priority]="1000" |             <core-context-menu-item [hidden]="isSelf || !showInfo || !isGroup" [priority]="1000" | ||||||
|                 [content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()" iconAction="fas-circle-info"> |                 [content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()" iconAction="fas-circle-info" /> | ||||||
|             </core-context-menu-item> |  | ||||||
|             <core-context-menu-item [hidden]="!groupMessagingEnabled || !conversation" [priority]="800" (action)="changeFavourite($event)" |             <core-context-menu-item [hidden]="!groupMessagingEnabled || !conversation" [priority]="800" (action)="changeFavourite($event)" | ||||||
|                 [closeOnClick]="false" [content]="(conversation && conversation.isfavourite ? 'addon.messages.removefromfavourites' : |                 [closeOnClick]="false" [content]="(conversation && conversation.isfavourite ? 'addon.messages.removefromfavourites' : | ||||||
|                 'addon.messages.addtofavourites') | translate" [iconAction]="favouriteIcon" [iconSlash]="favouriteIconSlash"> |                 'addon.messages.addtofavourites') | translate" [iconAction]="favouriteIcon" [iconSlash]="favouriteIconSlash" /> | ||||||
|             </core-context-menu-item> |  | ||||||
|             <core-context-menu-item [hidden]="isSelf || !otherMember || otherMember.isblocked" [priority]="700" |             <core-context-menu-item [hidden]="isSelf || !otherMember || otherMember.isblocked" [priority]="700" | ||||||
|                 [content]="'addon.messages.blockuser' | translate" (action)="blockUser()" [iconAction]="blockIcon"> |                 [content]="'addon.messages.blockuser' | translate" (action)="blockUser()" [iconAction]="blockIcon" /> | ||||||
|             </core-context-menu-item> |  | ||||||
|             <core-context-menu-item [hidden]="isSelf || !otherMember || !otherMember.isblocked" [priority]="700" |             <core-context-menu-item [hidden]="isSelf || !otherMember || !otherMember.isblocked" [priority]="700" | ||||||
|                 [content]="'addon.messages.unblockuser' | translate" (action)="unblockUser()" [iconAction]="blockIcon"> |                 [content]="'addon.messages.unblockuser' | translate" (action)="unblockUser()" [iconAction]="blockIcon" /> | ||||||
|             </core-context-menu-item> |  | ||||||
|             <core-context-menu-item [hidden]="isSelf || !muteEnabled || !conversation" [priority]="600" (action)="changeMute($event)" |             <core-context-menu-item [hidden]="isSelf || !muteEnabled || !conversation" [priority]="600" (action)="changeMute($event)" | ||||||
|                 [closeOnClick]="false" [content]="(conversation && conversation.ismuted ? 'addon.messages.unmuteconversation' : |                 [closeOnClick]="false" [content]="(conversation && conversation.ismuted ? 'addon.messages.unmuteconversation' : | ||||||
|                 'addon.messages.muteconversation') | translate" [iconAction]="muteIcon"></core-context-menu-item> |                 'addon.messages.muteconversation') | translate" [iconAction]="muteIcon" /> | ||||||
|             <core-context-menu-item [hidden]="!canDelete || !messages || !messages.length" [priority]="400" |             <core-context-menu-item [hidden]="!canDelete || !messages || !messages.length" [priority]="400" | ||||||
|                 [content]="'addon.messages.showdeletemessages' | translate" iconAction="toggle" [(toggle)]="showDelete"> |                 [content]="'addon.messages.showdeletemessages' | translate" iconAction="toggle" [(toggle)]="showDelete" /> | ||||||
|             </core-context-menu-item> |  | ||||||
|             <core-context-menu-item [hidden]="!groupMessagingEnabled || !conversationId || isGroup || !messages || !messages.length" |             <core-context-menu-item [hidden]="!groupMessagingEnabled || !conversationId || isGroup || !messages || !messages.length" | ||||||
|                 [priority]="200" [content]="'addon.messages.deleteconversation' | translate" (action)="deleteConversation($event)" |                 [priority]="200" [content]="'addon.messages.deleteconversation' | translate" (action)="deleteConversation($event)" | ||||||
|                 [closeOnClick]="false" [iconAction]="deleteIcon"></core-context-menu-item> |                 [closeOnClick]="false" [iconAction]="deleteIcon" /> | ||||||
|             <core-context-menu-item |             <core-context-menu-item | ||||||
|                 [hidden]="isSelf || !otherMember || otherMember.iscontact || requestContactSent || requestContactReceived" [priority]="100" |                 [hidden]="isSelf || !otherMember || otherMember.iscontact || requestContactSent || requestContactReceived" [priority]="100" | ||||||
|                 [content]="'addon.messages.addtoyourcontacts' | translate" (action)="createContactRequest()" [iconAction]="addRemoveIcon"> |                 [content]="'addon.messages.addtoyourcontacts' | translate" (action)="createContactRequest()" [iconAction]="addRemoveIcon" /> | ||||||
|             </core-context-menu-item> |  | ||||||
|             <core-context-menu-item [hidden]="isSelf || !otherMember || !otherMember.iscontact" [priority]="100" |             <core-context-menu-item [hidden]="isSelf || !otherMember || !otherMember.iscontact" [priority]="100" | ||||||
|                 [content]="'addon.messages.removefromyourcontacts' | translate" (action)="removeContact()" [iconAction]="addRemoveIcon" |                 [content]="'addon.messages.removefromyourcontacts' | translate" (action)="removeContact()" [iconAction]="addRemoveIcon" | ||||||
|                 [iconSlash]="true"></core-context-menu-item> |                 [iconSlash]="true" /> | ||||||
|         </core-context-menu> |         </core-context-menu> | ||||||
|     </core-navbar-buttons> |     </core-navbar-buttons> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content (ionScroll)="scrollFunction()"> | <ion-content (ionScroll)="scrollFunction()"> | ||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
|         <!-- Load previous messages. --> |         <!-- Load previous messages. --> | ||||||
|         <core-infinite-loading [enabled]="canLoadMore" (action)="loadPrevious($event)" position="top" [error]="loadMoreError"> |         <core-infinite-loading [enabled]="canLoadMore" (action)="loadPrevious($event)" position="top" [error]="loadMoreError" /> | ||||||
|         </core-infinite-loading> |  | ||||||
| 
 | 
 | ||||||
|         <ng-container *ngIf="isSelf && !canLoadMore"> |         <ng-container *ngIf="isSelf && !canLoadMore"> | ||||||
|             <p class="ion-text-center">{{ 'addon.messages.selfconversation' | translate }}</p> |             <p class="ion-text-center">{{ 'addon.messages.selfconversation' | translate }}</p> | ||||||
| @ -76,27 +66,25 @@ | |||||||
|                     {{ message.timecreated | coreFormatDate: "strftimedayshort" }} |                     {{ message.timecreated | coreFormatDate: "strftimedayshort" }} | ||||||
|                 </h3> |                 </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-label>{{ 'addon.messages.newmessages' | translate }}</ion-label> | ||||||
|                     <ion-icon name="fas-arrow-down" aria-hidden="true"></ion-icon> |                     <ion-icon name="fas-arrow-down" aria-hidden="true" /> | ||||||
|                 </ion-chip> |                 </ion-chip> | ||||||
| 
 | 
 | ||||||
|                 <core-message [message]="message" [user]="members[message.useridfrom]" (afterRender)="last && scrollToBottom()" |                 <core-message [message]="message" [user]="members[message.useridfrom]" (afterRender)="last && scrollToBottom()" | ||||||
|                     [text]="message.text" (onDeleteMessage)="deleteMessage(message, index)" [showDelete]="showDelete" |                     [text]="message.text" (onDeleteMessage)="deleteMessage(message, index)" [showDelete]="showDelete" | ||||||
|                     [time]="message.timecreated"> |                     [time]="message.timecreated" /> | ||||||
|                 </core-message> |  | ||||||
|             </ng-container> |             </ng-container> | ||||||
|         </ion-list> |         </ion-list> | ||||||
| 
 | 
 | ||||||
|         <core-empty-box *ngIf="!messages || messages.length <= 0" icon="far-comments" |         <core-empty-box *ngIf="!messages || messages.length <= 0" icon="far-comments" | ||||||
|             [message]="'addon.messages.nomessagesfound' | translate"> |             [message]="'addon.messages.nomessagesfound' | translate" /> | ||||||
|         </core-empty-box> |  | ||||||
|     </core-loading> |     </core-loading> | ||||||
|     <!-- Scroll bottom. --> |     <!-- Scroll bottom. --> | ||||||
|     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && newMessages > 0"> |     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="loaded && newMessages > 0"> | ||||||
|         <ion-fab-button size="small" (click)="scrollToFirstUnreadMessage()" color="light" |         <ion-fab-button size="small" (click)="scrollToFirstUnreadMessage()" color="light" | ||||||
|             [attr.aria-label]="'addon.messages.newmessages' | translate"> |             [attr.aria-label]="'addon.messages.newmessages' | translate"> | ||||||
|             <ion-icon name="fas-arrow-down" aria-hidden="true"></ion-icon> |             <ion-icon name="fas-arrow-down" aria-hidden="true" /> | ||||||
|             <span class="sr-only">{{ 'addon.messages.newmessages' | translate }}</span> |             <span class="sr-only">{{ 'addon.messages.newmessages' | translate }}</span> | ||||||
|         </ion-fab-button> |         </ion-fab-button> | ||||||
|         <ion-badge class="core-discussion-messages-badge">{{ newMessages }}</ion-badge> |         <ion-badge class="core-discussion-messages-badge">{{ newMessages }}</ion-badge> | ||||||
| @ -138,6 +126,6 @@ | |||||||
|             </p> |             </p> | ||||||
|         </div> |         </div> | ||||||
|         <core-send-message-form *ngIf="footerType === 'message'" (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" |         <core-send-message-form *ngIf="footerType === 'message'" (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" | ||||||
|             [placeholder]="'addon.messages.newmessage' | translate"></core-send-message-form> |             [placeholder]="'addon.messages.newmessage' | translate" /> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-footer> | </ion-footer> | ||||||
|  | |||||||
| @ -1,27 +1,27 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.messages.messages' | translate }}</h1> |             <h1>{{ 'addon.messages.messages' | translate }}</h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> |             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||||
|             <core-context-menu></core-context-menu> |             <core-context-menu /> | ||||||
|             <core-user-menu-button></core-user-menu-button> |             <core-user-menu-button /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <core-split-view> |     <core-split-view> | ||||||
|         <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)"> |         <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event.target)"> | ||||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|         </ion-refresher> |         </ion-refresher> | ||||||
| 
 | 
 | ||||||
|         <core-search-box (onSubmit)="searchMessage($event)" (onClear)="clearSearch()" [placeholder]=" 'addon.messages.message' | translate" |         <core-search-box (onSubmit)="searchMessage($event)" (onClear)="clearSearch()" [placeholder]=" 'addon.messages.message' | translate" | ||||||
|             autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded" searchArea="AddonMessagesDiscussions" |             autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded" searchArea="AddonMessagesDiscussions" | ||||||
|             [autoFocus]="false"></core-search-box> |             [autoFocus]="false" /> | ||||||
| 
 | 
 | ||||||
|         <core-loading [hideUntil]="loaded" [message]="loadingMessage"> |         <core-loading [hideUntil]="loaded" [message]="loadingMessage"> | ||||||
| 
 | 
 | ||||||
| @ -29,7 +29,7 @@ | |||||||
| 
 | 
 | ||||||
|                 <ion-item class="ion-text-wrap addon-message-discussion" (click)="gotoContacts()" |                 <ion-item class="ion-text-wrap addon-message-discussion" (click)="gotoContacts()" | ||||||
|                     [attr.aria-label]="'addon.messages.contacts' | translate" [detail]="true" button> |                     [attr.aria-label]="'addon.messages.contacts' | translate" [detail]="true" button> | ||||||
|                     <ion-icon name="fas-address-book" slot="start" aria-hidden="true"></ion-icon> |                     <ion-icon name="fas-address-book" slot="start" aria-hidden="true" /> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <p class="item-heading">{{ 'addon.messages.contacts' | translate }}</p> |                         <p class="item-heading">{{ 'addon.messages.contacts' | translate }}</p> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
| @ -46,13 +46,13 @@ | |||||||
|                     </ion-item-divider> |                     </ion-item-divider> | ||||||
|                     <ion-item class="ion-text-wrap addon-message-discussion" *ngFor="let result of search.results" button |                     <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-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> |                         <core-user-avatar [user]="result" slot="start" [checkOnline]="result.showonlinestatus" /> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <p class="item-heading">{{ result.fullname }}</p> |                             <p class="item-heading">{{ result.fullname }}</p> | ||||||
|                             <p> |                             <p> | ||||||
|                                 <core-format-text clean="true" singleLine="true" [text]="result.lastmessage" contextLevel="system" |                                 <core-format-text clean="true" singleLine="true" [text]="result.lastmessage" contextLevel="system" | ||||||
|                                     [contextInstanceId]="0"></core-format-text> |                                     [contextInstanceId]="0" /> | ||||||
|                             </p> |                             </p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| @ -60,8 +60,8 @@ | |||||||
|                 <ng-container *ngIf="!search.showResults"> |                 <ng-container *ngIf="!search.showResults"> | ||||||
|                     <ion-item class="ion-text-wrap addon-message-discussion" *ngFor="let discussion of discussions" button |                     <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-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> |                         <core-user-avatar [user]="discussion" slot="start" checkOnline="false" /> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <div class="flex-row ion-justify-content-between"> |                             <div class="flex-row ion-justify-content-between"> | ||||||
|                                 <p class="item-heading">{{ discussion.fullname }}</p> |                                 <p class="item-heading">{{ discussion.fullname }}</p> | ||||||
| @ -69,8 +69,7 @@ | |||||||
|                                     <span *ngIf="discussion.message!.timecreated > 0" class="addon-message-last-message-date"> |                                     <span *ngIf="discussion.message!.timecreated > 0" class="addon-message-last-message-date"> | ||||||
|                                         {{discussion.message!.timecreated / 1000 | coreDateDayOrTime}} |                                         {{discussion.message!.timecreated / 1000 | coreDateDayOrTime}} | ||||||
|                                     </span> |                                     </span> | ||||||
|                                     <ion-icon *ngIf="discussion.unread" name="fas-circle" color="primary" aria-hidden="true"> |                                     <ion-icon *ngIf="discussion.unread" name="fas-circle" color="primary" aria-hidden="true" /> | ||||||
|                                     </ion-icon> |  | ||||||
|                                     <span *ngIf="discussion.unread" class="sr-only"> |                                     <span *ngIf="discussion.unread" class="sr-only"> | ||||||
|                                         {{ 'addon.messages.unreadmessages' | translate }} |                                         {{ 'addon.messages.unreadmessages' | translate }} | ||||||
|                                     </span> |                                     </span> | ||||||
| @ -78,8 +77,7 @@ | |||||||
|                             </div> |                             </div> | ||||||
|                             <p> |                             <p> | ||||||
|                                 <core-format-text clean="true" singleLine="true" [text]="discussion.message!.message" contextLevel="system" |                                 <core-format-text clean="true" singleLine="true" [text]="discussion.message!.message" contextLevel="system" | ||||||
|                                     [contextInstanceId]="0"> |                                     [contextInstanceId]="0" /> | ||||||
|                                 </core-format-text> |  | ||||||
|                             </p> |                             </p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| @ -87,10 +85,10 @@ | |||||||
|             </ion-list> |             </ion-list> | ||||||
| 
 | 
 | ||||||
|             <core-empty-box *ngIf="(!discussions || discussions.length <= 0) && !search.showResults" icon="far-comments" |             <core-empty-box *ngIf="(!discussions || discussions.length <= 0) && !search.showResults" icon="far-comments" | ||||||
|                 [message]="'addon.messages.nomessagesfound' | translate"></core-empty-box> |                 [message]="'addon.messages.nomessagesfound' | translate" /> | ||||||
| 
 | 
 | ||||||
|             <core-empty-box *ngIf="(!search.results || search.results.length <= 0) && search.showResults" icon="fas-magnifying-glass" |             <core-empty-box *ngIf="(!search.results || search.results.length <= 0) && search.showResults" icon="fas-magnifying-glass" | ||||||
|                 [message]="'core.noresults' | translate"></core-empty-box> |                 [message]="'core.noresults' | translate" /> | ||||||
|         </core-loading> |         </core-loading> | ||||||
|     </core-split-view> |     </core-split-view> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -1,34 +1,34 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.messages.messages' | translate }}</h1> |             <h1>{{ 'addon.messages.messages' | translate }}</h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button fill="clear" (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate"> |             <ion-button fill="clear" (click)="gotoSearch()" [attr.aria-label]="'addon.messages.searchcombined' | translate"> | ||||||
|                 <ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <ion-button (click)="gotoSettings()" [attr.aria-label]="'addon.messages.messagepreferences' | translate"> |             <ion-button (click)="gotoSettings()" [attr.aria-label]="'addon.messages.messagepreferences' | translate"> | ||||||
|                 <ion-icon name="fas-gear" slot="icon-only" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-gear" slot="icon-only" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> |             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||||
|             <core-context-menu></core-context-menu> |             <core-context-menu /> | ||||||
|             <core-user-menu-button></core-user-menu-button> |             <core-user-menu-button /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <core-split-view> |     <core-split-view> | ||||||
|         <ion-refresher slot="fixed" [disabled]="!loaded || !currentListEl" (ionRefresh)="refreshData($event.target)"> |         <ion-refresher slot="fixed" [disabled]="!loaded || !currentListEl" (ionRefresh)="refreshData($event.target)"> | ||||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|         </ion-refresher> |         </ion-refresher> | ||||||
| 
 | 
 | ||||||
|         <core-loading [hideUntil]="loaded" [message]="loadingMessage"> |         <core-loading [hideUntil]="loaded" [message]="loadingMessage"> | ||||||
|             <ion-list> |             <ion-list> | ||||||
|                 <ion-item class="ion-text-wrap addon-message-discussion" (click)="gotoContacts()" [detail]="true" button> |                 <ion-item class="ion-text-wrap addon-message-discussion" (click)="gotoContacts()" [detail]="true" button> | ||||||
|                     <ion-icon name="fas-address-book" slot="start" aria-hidden="true"></ion-icon> |                     <ion-icon name="fas-address-book" slot="start" aria-hidden="true" /> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <h2>{{ 'addon.messages.contacts' | translate }}</h2> |                         <h2>{{ 'addon.messages.contacts' | translate }}</h2> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
| @ -43,8 +43,7 @@ | |||||||
|                     [attr.aria-expanded]="favourites.expanded" aria-controls="addon-messages-groupconversations-favourite" role="heading" |                     [attr.aria-expanded]="favourites.expanded" aria-controls="addon-messages-groupconversations-favourite" role="heading" | ||||||
|                     [detail]="false"> |                     [detail]="false"> | ||||||
|                     <ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon" |                     <ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon" | ||||||
|                         [class.expandable-status-icon-expanded]="favourites.expanded"> |                         [class.expandable-status-icon-expanded]="favourites.expanded" /> | ||||||
|                     </ion-icon> |  | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <h2>{{ 'core.favourites' | translate }} ({{ favourites.count }})</h2> |                         <h2>{{ 'core.favourites' | translate }} ({{ favourites.count }})</h2> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
| @ -55,12 +54,11 @@ | |||||||
|                 </ion-item> |                 </ion-item> | ||||||
|                 <div [hidden]="!favourites.conversations || !favourites.expanded || favourites.loading" #favlist |                 <div [hidden]="!favourites.conversations || !favourites.expanded || favourites.loading" #favlist | ||||||
|                     id="addon-messages-groupconversations-favourite"> |                     id="addon-messages-groupconversations-favourite"> | ||||||
|                     <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: favourites.conversations}"> |                     <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: favourites.conversations}" /> | ||||||
|                     </ng-container> |  | ||||||
|                     <!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. --> |                     <!-- 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)" |                     <core-infinite-loading [enabled]="favourites.canLoadMore" (action)="loadMoreConversations(favourites, $event)" | ||||||
|                         [error]="favourites.loadMoreError"></core-infinite-loading> |                         [error]="favourites.loadMoreError" /> | ||||||
|                     <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> |                         <ion-label> | ||||||
|                             <p>{{ 'addon.messages.nofavourites' | translate }}</p> |                             <p>{{ 'addon.messages.nofavourites' | translate }}</p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
| @ -68,7 +66,7 @@ | |||||||
|                 </div> |                 </div> | ||||||
|                 <ion-item class="ion-text-center" *ngIf="favourites.loading"> |                 <ion-item class="ion-text-center" *ngIf="favourites.loading"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> |                         <ion-spinner [attr.aria-label]="'core.loading' | translate" /> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -77,8 +75,7 @@ | |||||||
|                     [attr.aria-label]="(group.expanded ? 'core.collapse' : 'core.expand') | translate" [attr.aria-expanded]="group.expanded" |                     [attr.aria-label]="(group.expanded ? 'core.collapse' : 'core.expand') | translate" [attr.aria-expanded]="group.expanded" | ||||||
|                     aria-controls="addon-messages-groupconversations-group" role="heading" [detail]="false"> |                     aria-controls="addon-messages-groupconversations-group" role="heading" [detail]="false"> | ||||||
|                     <ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon" |                     <ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon" | ||||||
|                         [class.expandable-status-icon-expanded]="group.expanded"> |                         [class.expandable-status-icon-expanded]="group.expanded" /> | ||||||
|                     </ion-icon> |  | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <h2>{{ 'addon.messages.groupconversations' | translate }} ({{ group.count }})</h2> |                         <h2>{{ 'addon.messages.groupconversations' | translate }} ({{ group.count }})</h2> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
| @ -89,12 +86,11 @@ | |||||||
|                 </ion-item> |                 </ion-item> | ||||||
|                 <div [hidden]="!group.conversations || !group.expanded || group.loading" #grouplist |                 <div [hidden]="!group.conversations || !group.expanded || group.loading" #grouplist | ||||||
|                     id="addon-messages-groupconversations-group"> |                     id="addon-messages-groupconversations-group"> | ||||||
|                     <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: group.conversations}"> |                     <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: group.conversations}" /> | ||||||
|                     </ng-container> |  | ||||||
|                     <!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. --> |                     <!-- 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)" |                     <core-infinite-loading [enabled]="group.canLoadMore" (action)="loadMoreConversations(group, $event)" | ||||||
|                         [error]="group.loadMoreError"></core-infinite-loading> |                         [error]="group.loadMoreError" /> | ||||||
|                     <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> |                         <ion-label> | ||||||
|                             <p>{{ 'addon.messages.nogroupconversations' | translate }}</p> |                             <p>{{ 'addon.messages.nogroupconversations' | translate }}</p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
| @ -102,7 +98,7 @@ | |||||||
|                 </div> |                 </div> | ||||||
|                 <ion-item class="ion-text-center" *ngIf="group.loading"> |                 <ion-item class="ion-text-center" *ngIf="group.loading"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> |                         <ion-spinner [attr.aria-label]="'core.loading' | translate" /> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -111,8 +107,7 @@ | |||||||
|                     [attr.aria-expanded]="individual.expanded" aria-controls="addon-messages-groupconversations-individual" role="heading" |                     [attr.aria-expanded]="individual.expanded" aria-controls="addon-messages-groupconversations-individual" role="heading" | ||||||
|                     [detail]="false"> |                     [detail]="false"> | ||||||
|                     <ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon" |                     <ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon" | ||||||
|                         [class.expandable-status-icon-expanded]="individual.expanded"> |                         [class.expandable-status-icon-expanded]="individual.expanded" /> | ||||||
|                     </ion-icon> |  | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <h2>{{ 'addon.messages.individualconversations' | translate }} ({{ individual.count }})</h2> |                         <h2>{{ 'addon.messages.individualconversations' | translate }} ({{ individual.count }})</h2> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
| @ -123,12 +118,11 @@ | |||||||
|                 </ion-item> |                 </ion-item> | ||||||
|                 <div [hidden]="!individual.conversations || !individual.expanded || individual.loading" #indlist |                 <div [hidden]="!individual.conversations || !individual.expanded || individual.loading" #indlist | ||||||
|                     id="addon-messages-groupconversations-individual"> |                     id="addon-messages-groupconversations-individual"> | ||||||
|                     <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: individual.conversations}"> |                     <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: individual.conversations}" /> | ||||||
|                     </ng-container> |  | ||||||
|                     <!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. --> |                     <!-- 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)" |                     <core-infinite-loading [enabled]="individual.canLoadMore" (action)="loadMoreConversations(individual, $event)" | ||||||
|                         [error]="individual.loadMoreError"></core-infinite-loading> |                         [error]="individual.loadMoreError" /> | ||||||
|                     <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> |                         <ion-label> | ||||||
|                             <p>{{ 'addon.messages.noindividualconversations' | translate }}</p> |                             <p>{{ 'addon.messages.noindividualconversations' | translate }}</p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
| @ -136,7 +130,7 @@ | |||||||
|                 </div> |                 </div> | ||||||
|                 <ion-item class="ion-text-center" *ngIf="individual.loading"> |                 <ion-item class="ion-text-center" *ngIf="individual.loading"> | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> |                         <ion-spinner [attr.aria-label]="'core.loading' | translate" /> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -148,29 +142,29 @@ | |||||||
| <!-- Template to render a list of conversations. --> | <!-- Template to render a list of conversations. --> | ||||||
| <ng-template #conversationsTemplate let-conversations="conversations"> | <ng-template #conversationsTemplate let-conversations="conversations"> | ||||||
|     <ion-item class="ion-text-wrap addon-message-discussion" *ngFor="let conversation of conversations" button [detail]="false" |     <ion-item class="ion-text-wrap addon-message-discussion" *ngFor="let conversation of conversations" button [detail]="false" | ||||||
|  |         [attr.aria-current]="((conversation.id && conversation.id === selectedConversationId) || | ||||||
|  |             (conversation.userid && conversation.userid === selectedUserId)) ? 'page': 'false'" | ||||||
|         (click)="gotoConversation(conversation.id, conversation.userid)" |         (click)="gotoConversation(conversation.id, conversation.userid)" | ||||||
|         [attr.aria-current]="((conversation.id && |  | ||||||
|             conversation.id == selectedConversationId) || (conversation.userid && conversation.userid == selectedUserId)) ? 'page': 'false'" |  | ||||||
|         id="addon-message-conversation-{{ conversation.id ? conversation.id : 'user-' + conversation.userid }}" |         id="addon-message-conversation-{{ conversation.id ? conversation.id : 'user-' + conversation.userid }}" | ||||||
|         [attr.aria-label]="conversation.name"> |         [attr.aria-label]="conversation.name"> | ||||||
|         <!-- Group conversation image. --> |         <!-- 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 |             <img [src]="conversation.imageurl" [alt]="conversation.name" core-external-content | ||||||
|                 onError="this.src='assets/img/group-avatar.svg'"> |                 onError="this.src='assets/img/group-avatar.svg'"> | ||||||
|         </ion-avatar> |         </ion-avatar> | ||||||
| 
 | 
 | ||||||
|         <!-- Avatar for individual conversations. --> |         <!-- 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> |             [checkOnline]="conversation.showonlinestatus" slot="start" /> | ||||||
| 
 | 
 | ||||||
|         <ion-label> |         <ion-label> | ||||||
|             <div class="flex-row ion-justify-content-between"> |             <div class="flex-row ion-justify-content-between"> | ||||||
|                 <p class="item-heading"> |                 <p class="item-heading"> | ||||||
|                     <core-format-text [text]="conversation.name" contextLevel="system" [contextInstanceId]="0"></core-format-text> |                     <core-format-text [text]="conversation.name" contextLevel="system" [contextInstanceId]="0" /> | ||||||
|                     <ion-icon name="fas-user-slash" *ngIf="conversation.isblocked" [title]="'addon.messages.contactblocked' | translate"> |                     <ion-icon name="fas-user-slash" *ngIf="conversation.isblocked" | ||||||
|                     </ion-icon> |                         [attr.aria-label]="'addon.messages.contactblocked' | translate" /> | ||||||
|                     <ion-icon *ngIf="conversation.ismuted" name="fas-volume-xmark" [title]="'addon.messages.mutedconversation' | translate"> |                     <ion-icon *ngIf="conversation.ismuted" name="fas-volume-xmark" | ||||||
|                     </ion-icon> |                         [title]="'addon.messages.mutedconversation' | translate" /> | ||||||
|                 </p> |                 </p> | ||||||
|                 <ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount"> |                 <ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount"> | ||||||
|                     <span *ngIf="conversation.lastmessagedate > 0" class="addon-message-last-message-date"> |                     <span *ngIf="conversation.lastmessagedate > 0" class="addon-message-last-message-date"> | ||||||
| @ -183,16 +177,16 @@ | |||||||
|                 </ion-note> |                 </ion-note> | ||||||
|             </div> |             </div> | ||||||
|             <p *ngIf="conversation.subname"> |             <p *ngIf="conversation.subname"> | ||||||
|                 <core-format-text [text]="conversation.subname" contextLevel="system" [contextInstanceId]="0"></core-format-text> |                 <core-format-text [text]="conversation.subname" contextLevel="system" [contextInstanceId]="0" /> | ||||||
|             </p> |             </p> | ||||||
|             <p *ngIf="conversation.lastmessage !== undefined" class="addon-message-last-message"> |             <p *ngIf="conversation.lastmessage !== undefined" class="addon-message-last-message"> | ||||||
|                 <span *ngIf="conversation.sentfromcurrentuser" class="addon-message-last-message-user"> |                 <span *ngIf="conversation.sentfromcurrentuser" class="addon-message-last-message-user"> | ||||||
|                     {{ 'addon.messages.you' | translate }} |                     {{ 'addon.messages.you' | translate }} | ||||||
|                 </span> |                 </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> |                     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" |                 <core-format-text clean="true" singleLine="true" [text]="conversation.lastmessage" class="addon-message-last-message-text" | ||||||
|                     contextLevel="system" [contextInstanceId]="0"></core-format-text> |                     contextLevel="system" [contextInstanceId]="0" /> | ||||||
|             </p> |             </p> | ||||||
|         </ion-label> |         </ion-label> | ||||||
|     </ion-item> |     </ion-item> | ||||||
|  | |||||||
| @ -1,35 +1,34 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.messages.searchcombined' | translate }}</h1> |             <h1>{{ 'addon.messages.searchcombined' | translate }}</h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> |             <!-- Add an empty context menu so split view pages can add items, otherwise the menu disappears in some cases. --> | ||||||
|             <core-context-menu></core-context-menu> |             <core-context-menu /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <core-split-view> |     <core-split-view> | ||||||
|         <core-search-box (onSubmit)="search($event)" (onClear)="clearSearch()" [disabled]="disableSearch" autocorrect="off" |         <core-search-box (onSubmit)="search($event)" (onClear)="clearSearch()" [disabled]="disableSearch" autocorrect="off" | ||||||
|             [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1" searchArea="AddonMessagesSearch"></core-search-box> |             [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1" searchArea="AddonMessagesSearch" /> | ||||||
| 
 | 
 | ||||||
|         <core-loading [hideUntil]="!displaySearching" [message]="'core.searching' | translate"> |         <core-loading [hideUntil]="!displaySearching" [message]="'core.searching' | translate"> | ||||||
|             <ion-list *ngIf="displayResults"> |             <ion-list *ngIf="displayResults"> | ||||||
|                 <ng-container *ngTemplateOutlet="resultsTemplate; context: {item: contacts}"></ng-container> |                 <ng-container *ngTemplateOutlet="resultsTemplate; context: {item: contacts}" /> | ||||||
|                 <ng-container *ngTemplateOutlet="resultsTemplate; context: {item: nonContacts}"></ng-container> |                 <ng-container *ngTemplateOutlet="resultsTemplate; context: {item: nonContacts}" /> | ||||||
|                 <ng-container *ngTemplateOutlet="resultsTemplate; context: {item: messages}"></ng-container> |                 <ng-container *ngTemplateOutlet="resultsTemplate; context: {item: messages}" /> | ||||||
|                 <!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. --> |                 <!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. --> | ||||||
|                 <core-infinite-loading [enabled]="messages.canLoadMore" (action)="search(query, 'messages', $event)" |                 <core-infinite-loading [enabled]="messages.canLoadMore" (action)="search(query, 'messages', $event)" | ||||||
|                     [error]="messages.loadMoreError"></core-infinite-loading> |                     [error]="messages.loadMoreError" /> | ||||||
|             </ion-list> |             </ion-list> | ||||||
| 
 | 
 | ||||||
|             <core-empty-box *ngIf="displayResults && !contacts.results.length && !nonContacts.results.length && !messages.results.length" |             <core-empty-box *ngIf="displayResults && !contacts.results.length && !nonContacts.results.length && !messages.results.length" | ||||||
|                 icon="fas-magnifying-glass" [message]="'core.noresults' | translate"> |                 icon="fas-magnifying-glass" [message]="'core.noresults' | translate" /> | ||||||
|             </core-empty-box> |  | ||||||
|         </core-loading> |         </core-loading> | ||||||
|     </core-split-view> |     </core-split-view> | ||||||
| </ion-content> | </ion-content> | ||||||
| @ -45,14 +44,12 @@ | |||||||
| 
 | 
 | ||||||
|         <!-- List of results --> |         <!-- List of results --> | ||||||
|         <ion-item class="addon-message-discussion ion-text-wrap" *ngFor="let result of item.results" [attr.aria-label]="result.fullname" |         <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> |             <core-user-avatar slot="start" [user]="result" [checkOnline]="true" [linkProfile]="false" /> | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <p class="item-heading"> |                 <p class="item-heading"> | ||||||
|                     <core-format-text [text]="result.fullname" [highlight]="result.highlightName" [filter]="false"> |                     <core-format-text [text]="result.fullname" [highlight]="result.highlightName" [filter]="false" /> | ||||||
|                     </core-format-text> |                     <ion-icon name="fas-ban" *ngIf="result.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate" /> | ||||||
|                     <ion-icon name="fas-ban" *ngIf="result.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"> |  | ||||||
|                     </ion-icon> |  | ||||||
|                 </p> |                 </p> | ||||||
|                 <ion-note *ngIf="result.lastmessagedate > 0"> |                 <ion-note *ngIf="result.lastmessagedate > 0"> | ||||||
|                     {{result.lastmessagedate | coreDateDayOrTime}} |                     {{result.lastmessagedate | coreDateDayOrTime}} | ||||||
| @ -62,7 +59,7 @@ | |||||||
|                         {{ 'addon.messages.you' | translate }} |                         {{ 'addon.messages.you' | translate }} | ||||||
|                     </span> |                     </span> | ||||||
|                     <core-format-text clean="true" singleLine="true" [text]="result.lastmessage" [highlight]="result.highlightMessage" |                     <core-format-text clean="true" singleLine="true" [text]="result.lastmessage" [highlight]="result.highlightMessage" | ||||||
|                         contextLevel="system" [contextInstanceId]="0" class="addon-message-last-message-text"></core-format-text> |                         contextLevel="system" [contextInstanceId]="0" class="addon-message-last-message-text" /> | ||||||
|                 </p> |                 </p> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| @ -75,7 +72,7 @@ | |||||||
|                 </ion-button> |                 </ion-button> | ||||||
|             </div> |             </div> | ||||||
|             <div *ngIf="item.loadingMore" class="ion-padding ion-text-center"> |             <div *ngIf="item.loadingMore" class="ion-padding ion-text-center"> | ||||||
|                 <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> |                 <ion-spinner [attr.aria-label]="'core.loading' | translate" /> | ||||||
|             </div> |             </div> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|     </ng-container> |     </ng-container> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1>{{ 'addon.messages.messages' | translate }}</h1> |             <h1>{{ 'addon.messages.messages' | translate }}</h1> | ||||||
| @ -10,7 +10,7 @@ | |||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!preferencesLoaded" (ionRefresh)="refreshPreferences($event.target)"> |     <ion-refresher slot="fixed" [disabled]="!preferencesLoaded" (ionRefresh)="refreshPreferences($event.target)"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" /> | ||||||
|     </ion-refresher> |     </ion-refresher> | ||||||
|     <core-loading [hideUntil]="preferencesLoaded"> |     <core-loading [hideUntil]="preferencesLoaded"> | ||||||
|         <!-- General settings. --> |         <!-- General settings. --> | ||||||
| @ -22,21 +22,18 @@ | |||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item-divider> |                 </ion-item-divider> | ||||||
|                 <ion-item class="ion-text-wrap"> |                 <ion-item class="ion-text-wrap"> | ||||||
|                     <ion-label> |                     <ion-toggle [(ngModel)]="sendOnEnter" (ngModelChange)="sendOnEnterChanged()"> | ||||||
|                         <p>{{ 'addon.messages.useentertosend' | translate }}</p> |                         {{ 'addon.messages.useentertosend' | translate }} | ||||||
|                     </ion-label> |                     </ion-toggle> | ||||||
|                     <ion-toggle [(ngModel)]="sendOnEnter" (ngModelChange)="sendOnEnterChanged()" slot="end"></ion-toggle> |  | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|             </ion-list> |             </ion-list> | ||||||
|         </ion-card> |         </ion-card> | ||||||
| 
 | 
 | ||||||
|         <!-- Contactable privacy. --> |         <!-- Contactable privacy. --> | ||||||
|         <ion-card> |         <ion-card> | ||||||
|             <ion-item *ngIf="!advancedContactable"> |             <ion-item *ngIf="!advancedContactable" class="ion-text-wrap"> | ||||||
|                 <ion-label class="ion-text-wrap"> |                 <ion-toggle [(ngModel)]="contactablePrivacy" (ngModelChange)="saveContactablePrivacy(contactablePrivacy)"> | ||||||
|                     <p>{{ 'addon.messages.blocknoncontacts' | translate }}</p> |                     {{ 'addon.messages.blocknoncontacts' | translate }} | ||||||
|                 </ion-label> |  | ||||||
|                 <ion-toggle [(ngModel)]="contactablePrivacy" (ngModelChange)="saveContactablePrivacy(contactablePrivacy)" slot="end"> |  | ||||||
|                 </ion-toggle> |                 </ion-toggle> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -48,22 +45,19 @@ | |||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item-divider> |                     </ion-item-divider> | ||||||
|                     <ion-item class="ion-text-wrap"> |                     <ion-item class="ion-text-wrap"> | ||||||
|                         <ion-label> |                         <ion-radio labelPlacement="end" justify="start" [value]="onlyContactsValue"> | ||||||
|                             <p>{{ 'addon.messages.contactableprivacy_onlycontacts' | translate }}</p> |                             {{ 'addon.messages.contactableprivacy_onlycontacts' | translate }} | ||||||
|                         </ion-label> |                         </ion-radio> | ||||||
|                         <ion-radio slot="start" [value]="onlyContactsValue"></ion-radio> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <ion-item class="ion-text-wrap"> |                     <ion-item class="ion-text-wrap"> | ||||||
|                         <ion-label> |                         <ion-radio labelPlacement="end" justify="start" [value]="courseMemberValue"> | ||||||
|                             <p>{{ 'addon.messages.contactableprivacy_coursemember' | translate }}</p> |                             {{ 'addon.messages.contactableprivacy_coursemember' | translate }} | ||||||
|                         </ion-label> |                         </ion-radio> | ||||||
|                         <ion-radio slot="start" [value]="courseMemberValue"></ion-radio> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                     <ion-item *ngIf="allowSiteMessaging" class="ion-text-wrap"> |                     <ion-item *ngIf="allowSiteMessaging" class="ion-text-wrap"> | ||||||
|                         <ion-label> |                         <ion-radio labelPlacement="end" justify="start" [value]="siteValue"> | ||||||
|                             <p>{{ 'addon.messages.contactableprivacy_site' | translate }}</p> |                             {{ 'addon.messages.contactableprivacy_site' | translate }} | ||||||
|                         </ion-label> |                         </ion-radio> | ||||||
|                         <ion-radio slot="start" [value]="siteValue"></ion-radio> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
|                 </ion-radio-group> |                 </ion-radio-group> | ||||||
|             </ion-list> |             </ion-list> | ||||||
| @ -72,10 +66,10 @@ | |||||||
|         <!-- Notifications. --> |         <!-- Notifications. --> | ||||||
|         <ng-container *ngIf="preferences"> |         <ng-container *ngIf="preferences"> | ||||||
|             <ng-container *ngIf="!groupMessagingEnabled"> |             <ng-container *ngIf="!groupMessagingEnabled"> | ||||||
|                 <ng-container *ngTemplateOutlet="legacySettings; context: {preferences: preferences}"></ng-container> |                 <ng-container *ngTemplateOutlet="legacySettings; context: {preferences: preferences}" /> | ||||||
|             </ng-container> |             </ng-container> | ||||||
|             <ng-container *ngIf="groupMessagingEnabled"> |             <ng-container *ngIf="groupMessagingEnabled"> | ||||||
|                 <ng-container *ngTemplateOutlet="settings; context: {preferences: preferences}"></ng-container> |                 <ng-container *ngTemplateOutlet="settings; context: {preferences: preferences}" /> | ||||||
|             </ng-container> |             </ng-container> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|     </core-loading> |     </core-loading> | ||||||
| @ -109,8 +103,7 @@ | |||||||
|                             <!-- If notifications enabled, show toggle. --> |                             <!-- If notifications enabled, show toggle. --> | ||||||
|                             <core-button-with-spinner *ngIf="!processor.locked" [loading]="notification['updating'+state]" slot="end"> |                             <core-button-with-spinner *ngIf="!processor.locked" [loading]="notification['updating'+state]" slot="end"> | ||||||
|                                 <ion-toggle [(ngModel)]="processor[state].checked" |                                 <ion-toggle [(ngModel)]="processor[state].checked" | ||||||
|                                     (ngModelChange)="changePreferenceLegacy(notification, processor, state)"> |                                     (ngModelChange)="changePreferenceLegacy(notification, processor, state)" /> | ||||||
|                                 </ion-toggle> |  | ||||||
|                             </core-button-with-spinner> |                             </core-button-with-spinner> | ||||||
|                             <span *ngIf="processor.locked && processor[state].checked" class="text-gray" slot="end"> |                             <span *ngIf="processor.locked && processor[state].checked" class="text-gray" slot="end"> | ||||||
|                                 {{'core.settings.forced' | translate }} |                                 {{'core.settings.forced' | translate }} | ||||||
| @ -145,8 +138,7 @@ | |||||||
|                     <ng-container *ngIf="!preferences.disableall"> |                     <ng-container *ngIf="!preferences.disableall"> | ||||||
|                         <!-- If notifications enabled, show toggle. --> |                         <!-- If notifications enabled, show toggle. --> | ||||||
|                         <core-button-with-spinner *ngIf="!processor.locked" [loading]="notification.updating" slot="end"> |                         <core-button-with-spinner *ngIf="!processor.locked" [loading]="notification.updating" slot="end"> | ||||||
|                             <ion-toggle [(ngModel)]="processor.enabled" (ngModelChange)="changePreference(notification, processor)"> |                             <ion-toggle [(ngModel)]="processor.enabled" (ngModelChange)="changePreference(notification, processor)" /> | ||||||
|                             </ion-toggle> |  | ||||||
|                         </core-button-with-spinner> |                         </core-button-with-spinner> | ||||||
|                         <span class="text-gray" *ngIf="processor.locked" slot="end"> |                         <span class="text-gray" *ngIf="processor.locked" slot="end"> | ||||||
|                             {{ processor.lockedmessage }} |                             {{ processor.lockedmessage }} | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB | 
| @ -16,7 +16,7 @@ import { conditionalRoutes } from '@/app/app-routing.module'; | |||||||
| import { CoreSharedModule } from '@/core/shared.module'; | import { CoreSharedModule } from '@/core/shared.module'; | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
| import { RouterModule, Routes } from '@angular/router'; | import { RouterModule, Routes } from '@angular/router'; | ||||||
| import { CanLeaveGuard } from '@guards/can-leave'; | import { canLeaveGuard } from '@guards/can-leave'; | ||||||
| import { CoreScreen } from '@services/screen'; | import { CoreScreen } from '@services/screen'; | ||||||
| import { AddonModAssignComponentsModule } from './components/components.module'; | import { AddonModAssignComponentsModule } from './components/components.module'; | ||||||
| import { AddonModAssignEditPage } from './pages/edit/edit'; | import { AddonModAssignEditPage } from './pages/edit/edit'; | ||||||
| @ -32,7 +32,7 @@ const commonRoutes: Routes = [ | |||||||
|     { |     { | ||||||
|         path: ':courseId/:cmId/edit', |         path: ':courseId/:cmId/edit', | ||||||
|         component: AddonModAssignEditPage, |         component: AddonModAssignEditPage, | ||||||
|         canDeactivate: [CanLeaveGuard], |         canDeactivate: [canLeaveGuard], | ||||||
|     }, |     }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| @ -45,7 +45,7 @@ const mobileRoutes: Routes = [ | |||||||
|     { |     { | ||||||
|         path: ':courseId/:cmId/submission/:submitId', |         path: ':courseId/:cmId/submission/:submitId', | ||||||
|         component: AddonModAssignSubmissionReviewPage, |         component: AddonModAssignSubmissionReviewPage, | ||||||
|         canDeactivate: [CanLeaveGuard], |         canDeactivate: [canLeaveGuard], | ||||||
|     }, |     }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| @ -58,7 +58,7 @@ const tabletRoutes: Routes = [ | |||||||
|             { |             { | ||||||
|                 path: ':submitId', |                 path: ':submitId', | ||||||
|                 component: AddonModAssignSubmissionReviewPage, |                 component: AddonModAssignSubmissionReviewPage, | ||||||
|                 canDeactivate: [CanLeaveGuard], |                 canDeactivate: [canLeaveGuard], | ||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -5,15 +5,14 @@ | |||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
|             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> |             <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||||
|                 <ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true"></ion-icon> |                 <ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <form name="addon-mod_assign-edit-feedback-form" *ngIf="userId && plugin" #editFeedbackForm> |     <form name="addon-mod_assign-edit-feedback-form" *ngIf="userId && plugin" #editFeedbackForm> | ||||||
|         <addon-mod-assign-feedback-plugin [assign]="assign" [submission]="submission" [userId]="userId" [plugin]="plugin" [edit]="true"> |         <addon-mod-assign-feedback-plugin [assign]="assign" [submission]="submission" [userId]="userId" [plugin]="plugin" [edit]="true" /> | ||||||
|         </addon-mod-assign-feedback-plugin> |  | ||||||
|         <ion-button expand="block" (click)="done($event)">{{ 'core.done' | translate }}</ion-button> |         <ion-button expand="block" (click)="done($event)">{{ 'core.done' | translate }}</ion-button> | ||||||
|     </form> |     </form> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -9,12 +9,10 @@ | |||||||
|                 </ion-badge> |                 </ion-badge> | ||||||
|                 <p *ngIf="text"> |                 <p *ngIf="text"> | ||||||
|                     <core-format-text [component]="component" [componentId]="assign.cmid" collapsible-item [text]="text" |                     <core-format-text [component]="component" [componentId]="assign.cmid" collapsible-item [text]="text" | ||||||
|                         contextLevel="module" [contextInstanceId]="assign.cmid" [courseId]="assign.course"> |                         contextLevel="module" [contextInstanceId]="assign.cmid" [courseId]="assign.course" /> | ||||||
|                     </core-format-text> |  | ||||||
|                 </p> |                 </p> | ||||||
|                 <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" |                 <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" | ||||||
|                     [alwaysDownload]="true"> |                     [alwaysDownload]="true" /> | ||||||
|                 </core-file> |  | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|     </core-loading> |     </core-loading> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <!-- Buttons to add to the header. --> | <!-- Buttons to add to the header. --> | ||||||
| <core-navbar-buttons slot="end"> | <core-navbar-buttons slot="end"> | ||||||
|     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> |     <ion-button fill="clear" (click)="openModuleSummary()" aria-haspopup="true" [attr.aria-label]="'core.info' | translate"> | ||||||
|         <ion-icon name="fas-circle-info" slot="icon-only" aria-hidden="true"></ion-icon> |         <ion-icon name="fas-circle-info" slot="icon-only" aria-hidden="true" /> | ||||||
|     </ion-button> |     </ion-button> | ||||||
| </core-navbar-buttons> | </core-navbar-buttons> | ||||||
| 
 | 
 | ||||||
| @ -12,8 +12,7 @@ | |||||||
|     <core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId" |     <core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId" | ||||||
|         [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()"> |         [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()"> | ||||||
|         <div description *ngIf="assign && assign.introattachments?.length && !assign.submissionattachments"> |         <div description *ngIf="assign && assign.introattachments?.length && !assign.submissionattachments"> | ||||||
|             <core-file *ngFor="let file of assign.introattachments" [file]="file" [component]="component" [componentId]="componentId"> |             <core-file *ngFor="let file of assign.introattachments" [file]="file" [component]="component" [componentId]="componentId" /> | ||||||
|             </core-file> |  | ||||||
|         </div> |         </div> | ||||||
|     </core-course-module-info> |     </core-course-module-info> | ||||||
| 
 | 
 | ||||||
| @ -21,8 +20,7 @@ | |||||||
|     <ng-container *ngIf="assign && canViewAllSubmissions"> |     <ng-container *ngIf="assign && canViewAllSubmissions"> | ||||||
|         <ion-list class="core-list-align-detail-right"> |         <ion-list class="core-list-align-detail-right"> | ||||||
| 
 | 
 | ||||||
|             <core-group-selector [groupInfo]="groupInfo" [(selected)]="group" (selectedChange)="setGroup(group)" [courseId]="courseId"> |             <core-group-selector [groupInfo]="groupInfo" [(selected)]="group" (selectedChange)="setGroup(group)" [courseId]="courseId" /> | ||||||
|             </core-group-selector> |  | ||||||
| 
 | 
 | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
| @ -115,19 +113,17 @@ | |||||||
|         <!-- Ungrouped users. --> |         <!-- Ungrouped users. --> | ||||||
|         <ion-card *ngIf="assign.teamsubmission && summary && summary.warnofungroupedusers" class="core-info-card"> |         <ion-card *ngIf="assign.teamsubmission && summary && summary.warnofungroupedusers" class="core-info-card"> | ||||||
|             <ion-item> |             <ion-item> | ||||||
|                 <ion-icon name="fas-circle-question" slot="start" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-circle-question" slot="start" aria-hidden="true" /> | ||||||
|                 <ion-label>{{ 'addon.mod_assign.'+summary.warnofungroupedusers | translate }}</ion-label> |                 <ion-label>{{ 'addon.mod_assign.'+summary.warnofungroupedusers | translate }}</ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
|         </ion-card> |         </ion-card> | ||||||
| 
 | 
 | ||||||
|         <div collapsible-footer *ngIf="!showLoading" slot="fixed"> |         <div collapsible-footer *ngIf="!showLoading" slot="fixed"> | ||||||
|             <core-course-module-navigation [courseId]="courseId" [currentModuleId]="module.id"> |             <core-course-module-navigation [courseId]="courseId" [currentModuleId]="module.id" /> | ||||||
|             </core-course-module-navigation> |  | ||||||
|         </div> |         </div> | ||||||
|     </ng-container> |     </ng-container> | ||||||
| 
 | 
 | ||||||
|     <!-- If it's a student, display his submission. --> |     <!-- If it's a student, display his submission. --> | ||||||
|     <addon-mod-assign-submission *ngIf="!showLoading && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" |     <addon-mod-assign-submission *ngIf="!showLoading && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" | ||||||
|         [moduleId]="module.id"> |         [moduleId]="module.id" /> | ||||||
|     </addon-mod-assign-submission> |  | ||||||
| </core-loading> | </core-loading> | ||||||
|  | |||||||
| @ -9,12 +9,10 @@ | |||||||
|                 </ion-badge> |                 </ion-badge> | ||||||
|                 <p *ngIf="text"> |                 <p *ngIf="text"> | ||||||
|                     <core-format-text [component]="component" [componentId]="assign.cmid" collapsible-item [text]="text" |                     <core-format-text [component]="component" [componentId]="assign.cmid" collapsible-item [text]="text" | ||||||
|                         contextLevel="module" [contextInstanceId]="assign.cmid" [courseId]="assign.course"> |                         contextLevel="module" [contextInstanceId]="assign.cmid" [courseId]="assign.course" /> | ||||||
|                     </core-format-text> |  | ||||||
|                 </p> |                 </p> | ||||||
|                 <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" |                 <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" | ||||||
|                     [alwaysDownload]="true"> |                     [alwaysDownload]="true" /> | ||||||
|                 </core-file> |  | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
|     </core-loading> |     </core-loading> | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ | |||||||
|         <!-- Time limit is over. --> |         <!-- Time limit is over. --> | ||||||
|         <ion-card *ngIf="timeLimitFinished && (canEdit || canSubmit)" class="core-danger-card"> |         <ion-card *ngIf="timeLimitFinished && (canEdit || canSubmit)" class="core-danger-card"> | ||||||
|             <ion-item class="ion-text-wrap"> |             <ion-item class="ion-text-wrap"> | ||||||
|                 <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" /> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <p>{{ 'addon.mod_assign.caneditsubmission' | translate }}</p> |                     <p>{{ 'addon.mod_assign.caneditsubmission' | translate }}</p> | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
| @ -13,10 +13,10 @@ | |||||||
|         <!-- User and status of the submission. --> |         <!-- User and status of the submission. --> | ||||||
|         <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId" |         <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId" | ||||||
|             [attr.aria-label]="user!.fullname"> |             [attr.aria-label]="user!.fullname"> | ||||||
|             <core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar> |             <core-user-avatar [user]="user" slot="start" [linkProfile]="false" /> | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <p class="item-heading">{{ user!.fullname }}</p> |                 <p class="item-heading">{{ user!.fullname }}</p> | ||||||
|                 <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> |                 <ng-container *ngTemplateOutlet="submissionStatus" /> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -24,7 +24,7 @@ | |||||||
|         <ion-item class="ion-text-wrap" *ngIf="blindMarking && !user"> |         <ion-item class="ion-text-wrap" *ngIf="blindMarking && !user"> | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <p class="item-heading">{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</p> |                 <p class="item-heading">{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</p> | ||||||
|                 <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> |                 <ng-container *ngTemplateOutlet="submissionStatus" /> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -32,7 +32,7 @@ | |||||||
|         <ion-item class="ion-text-wrap" *ngIf="(blindMarking && user) || (!blindMarking && !user)"> |         <ion-item class="ion-text-wrap" *ngIf="(blindMarking && user) || (!blindMarking && !user)"> | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <p class="item-heading">{{ 'addon.mod_assign.submissionstatus' | translate }}</p> |                 <p class="item-heading">{{ 'addon.mod_assign.submissionstatus' | translate }}</p> | ||||||
|                 <ng-container *ngTemplateOutlet="submissionStatus"></ng-container> |                 <ng-container *ngTemplateOutlet="submissionStatus" /> | ||||||
|             </ion-label> |             </ion-label> | ||||||
|         </ion-item> |         </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -45,11 +45,11 @@ | |||||||
|                     <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading"> |                     <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <p class="item-heading">{{ 'addon.mod_assign.attemptnumber' | translate }}</p> |                             <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 : |                                 {{ 'addon.mod_assign.outof' | translate : | ||||||
|                                 {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} |                                 {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||||
|                             </p> |                             </p> | ||||||
|                             <p *ngIf="assign!.maxattempts != unlimitedAttempts"> |                             <p *ngIf="assign!.maxattempts !== unlimitedAttempts"> | ||||||
|                                 {{ 'addon.mod_assign.outof' | translate : |                                 {{ 'addon.mod_assign.outof' | translate : | ||||||
|                                 {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} |                                 {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} | ||||||
|                             </p> |                             </p> | ||||||
| @ -103,8 +103,7 @@ | |||||||
|                             <p class="item-heading">{{ 'addon.mod_assign.timeremaining' | translate }}</p> |                             <p class="item-heading">{{ 'addon.mod_assign.timeremaining' | translate }}</p> | ||||||
|                             <p *ngIf="!timeLimitEndTime" [innerHTML]="timeRemaining"></p> |                             <p *ngIf="!timeLimitEndTime" [innerHTML]="timeRemaining"></p> | ||||||
|                             <core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" mode="basic" timeUpText="00:00:00" |                             <core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" mode="basic" timeUpText="00:00:00" | ||||||
|                                 [timeLeftClassThreshold]="-1" [underTimeClassThresholds]="[300, 900]" (finished)="timeUp()"> |                                 [timeLeftClassThreshold]="-1" [underTimeClassThresholds]="[300, 900]" (finished)="timeUp()" /> | ||||||
|                             </core-timer> |  | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -128,7 +127,7 @@ | |||||||
| 
 | 
 | ||||||
|                     <!-- Last modified. --> |                     <!-- Last modified. --> | ||||||
|                     <ion-item class="ion-text-wrap" |                     <ion-item class="ion-text-wrap" | ||||||
|                         *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified"> |                         *ngIf="userSubmission && userSubmission!.status !== statusNew && userSubmission!.timemodified"> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <p class="item-heading">{{ 'addon.mod_assign.timemodified' | translate }}</p> |                             <p class="item-heading">{{ 'addon.mod_assign.timemodified' | translate }}</p> | ||||||
|                             <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p> |                             <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p> | ||||||
| @ -136,8 +135,7 @@ | |||||||
|                     </ion-item> |                     </ion-item> | ||||||
| 
 | 
 | ||||||
|                     <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign" |                     <addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign" | ||||||
|                         [submission]="userSubmission" [plugin]="plugin"> |                         [submission]="userSubmission" [plugin]="plugin" /> | ||||||
|                     </addon-mod-assign-submission-plugin> |  | ||||||
| 
 | 
 | ||||||
|                     <!-- Team members that need to submit it too. --> |                     <!-- Team members that need to submit it too. --> | ||||||
|                     <ion-item-divider class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0"> |                     <ion-item-divider class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0"> | ||||||
| @ -149,7 +147,7 @@ | |||||||
|                         <ng-container *ngFor="let user of membersToSubmit"> |                         <ng-container *ngFor="let user of membersToSubmit"> | ||||||
|                             <ion-item class="ion-text-wrap" core-user-link [userId]="user.id" [courseId]="courseId" |                             <ion-item class="ion-text-wrap" core-user-link [userId]="user.id" [courseId]="courseId" | ||||||
|                                 [attr.aria-label]="user.fullname"> |                                 [attr.aria-label]="user.fullname"> | ||||||
|                                 <core-user-avatar [user]="user" slot="start" [linkProfile]="false"></core-user-avatar> |                                 <core-user-avatar [user]="user" slot="start" [linkProfile]="false" /> | ||||||
|                                 <ion-label> |                                 <ion-label> | ||||||
|                                     <p class="item-heading">{{ user.fullname }}</p> |                                     <p class="item-heading">{{ user.fullname }}</p> | ||||||
|                                 </ion-label> |                                 </ion-label> | ||||||
| @ -175,7 +173,7 @@ | |||||||
|                                     </ion-button> |                                     </ion-button> | ||||||
|                                     <!-- If no submission or is new, show add submission. --> |                                     <!-- If no submission or is new, show add submission. --> | ||||||
|                                     <ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline && |                                     <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"> |                                         <ng-container *ngIf="!assign?.timelimit || userSubmission?.timestarted"> | ||||||
|                                             {{ 'addon.mod_assign.addsubmission' | translate }} |                                             {{ 'addon.mod_assign.addsubmission' | translate }} | ||||||
|                                         </ng-container> |                                         </ng-container> | ||||||
| @ -184,7 +182,7 @@ | |||||||
|                                         </ng-container> |                                         </ng-container> | ||||||
|                                     </ion-button> |                                     </ion-button> | ||||||
|                                     <!-- If reopened, show addfromprevious and addnewattempt. --> |                                     <!-- 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" |                                         <ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" | ||||||
|                                             (click)="copyPrevious()"> |                                             (click)="copyPrevious()"> | ||||||
|                                             {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }} |                                             {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }} | ||||||
| @ -195,8 +193,8 @@ | |||||||
|                                     </ng-container> |                                     </ng-container> | ||||||
|                                     <!-- Else show editsubmission. --> |                                     <!-- Else show editsubmission. --> | ||||||
|                                     <ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline && userSubmission && |                                     <ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline && userSubmission && | ||||||
|                                         userSubmission!.status && userSubmission!.status != statusNew && |                                         userSubmission!.status && userSubmission!.status !== statusNew && | ||||||
|                                         userSubmission!.status != statusReopened" (click)="goToEdit()"> |                                         userSubmission!.status !== statusReopened" (click)="goToEdit()"> | ||||||
|                                         {{ 'addon.mod_assign.editsubmission' | translate }} |                                         {{ 'addon.mod_assign.editsubmission' | translate }} | ||||||
|                                     </ion-button> |                                     </ion-button> | ||||||
|                                 </ng-container> |                                 </ng-container> | ||||||
| @ -213,7 +211,7 @@ | |||||||
|                                             <ion-button expand="block" *ngIf="submissionUrl" [href]="submissionUrl" core-link |                                             <ion-button expand="block" *ngIf="submissionUrl" [href]="submissionUrl" core-link | ||||||
|                                                 [showBrowserWarning]="false"> |                                                 [showBrowserWarning]="false"> | ||||||
|                                                 {{ 'core.openinbrowser' | translate }} |                                                 {{ 'core.openinbrowser' | translate }} | ||||||
|                                                 <ion-icon name="fas-up-right-from-square" slot="end" aria-hidden="true"></ion-icon> |                                                 <ion-icon name="fas-up-right-from-square" slot="end" aria-hidden="true" /> | ||||||
|                                             </ion-button> |                                             </ion-button> | ||||||
| 
 | 
 | ||||||
|                                         </ng-container> |                                         </ng-container> | ||||||
| @ -228,10 +226,8 @@ | |||||||
|                             <!-- Submit for grading form. --> |                             <!-- Submit for grading form. --> | ||||||
|                             <ng-container *ngIf="canSubmit"> |                             <ng-container *ngIf="canSubmit"> | ||||||
|                                 <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> |                                 <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> | ||||||
|                                     <ion-label> |                                     <ion-checkbox name="submissionstatement" [(ngModel)]="acceptStatement"> | ||||||
|                                         <core-format-text [text]="submissionStatement" [filter]="false"></core-format-text> |                                         <core-format-text [text]="submissionStatement" [filter]="false" /> | ||||||
|                                     </ion-label> |  | ||||||
|                                     <ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="acceptStatement"> |  | ||||||
|                                     </ion-checkbox> |                                     </ion-checkbox> | ||||||
|                                 </ion-item> |                                 </ion-item> | ||||||
|                                 <!-- Submit button. --> |                                 <!-- Submit button. --> | ||||||
| @ -253,8 +249,7 @@ | |||||||
|                                 </ion-item> |                                 </ion-item> | ||||||
|                             </ng-container> |                             </ng-container> | ||||||
|                         </div> |                         </div> | ||||||
|                         <core-course-module-navigation [courseId]="courseId" [currentModuleId]="moduleId"> |                         <core-course-module-navigation [courseId]="courseId" [currentModuleId]="moduleId" /> | ||||||
|                         </core-course-module-navigation> |  | ||||||
|                     </div> |                     </div> | ||||||
|                 </ng-template> |                 </ng-template> | ||||||
|             </core-tab> |             </core-tab> | ||||||
| @ -268,12 +263,12 @@ | |||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <p class="item-heading">{{ 'addon.mod_assign.currentgrade' | translate }}</p> |                             <p class="item-heading">{{ 'addon.mod_assign.currentgrade' | translate }}</p> | ||||||
|                             <p> |                             <p> | ||||||
|                                 <core-format-text [text]="feedback!.gradefordisplay" [filter]="false"></core-format-text> |                                 <core-format-text [text]="feedback!.gradefordisplay" [filter]="false" /> | ||||||
|                             </p> |                             </p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                         <ion-button slot="end" *ngIf="feedback!.advancedgrade" (click)="showAdvancedGrade()" |                         <ion-button slot="end" *ngIf="feedback!.advancedgrade" (click)="showAdvancedGrade()" | ||||||
|                             [attr.aria-label]="'core.showadvanced' |translate"> |                             [attr.aria-label]="'core.showadvanced' |translate"> | ||||||
|                             <ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true"></ion-icon> |                             <ion-icon name="fas-magnifying-glass" slot="icon-only" aria-hidden="true" /> | ||||||
|                         </ion-button> |                         </ion-button> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| 
 | 
 | ||||||
| @ -281,23 +276,18 @@ | |||||||
|                         <!-- Numeric grade. |                         <!-- Numeric grade. | ||||||
|                         Use a text input because otherwise we cannot readthe value if it has an invalid character. --> |                         Use a text input because otherwise we cannot readthe value if it has an invalid character. --> | ||||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.method === 'simple' && !grade.scale"> |                         <ion-item class="ion-text-wrap" *ngIf="grade.method === 'simple' && !grade.scale"> | ||||||
|                             <ion-label position="stacked"> |  | ||||||
|                                 <p class="item-heading">{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</p> |  | ||||||
|                             </ion-label> |  | ||||||
|                             <ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade" |                             <ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade" | ||||||
|                                 [lang]="grade.lang"> |                                 [lang]="grade.lang" [label]="'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade}" | ||||||
|                             </ion-input> |                                 labelPlacement="stacked" | ||||||
|                             <p *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p> |                                 [helperText]="grade.disabled ? ('addon.mod_assign.gradelocked' | translate) : null" /> | ||||||
|                         </ion-item> |                         </ion-item> | ||||||
| 
 | 
 | ||||||
|                         <!-- Grade using a scale. --> |                         <!-- Grade using a scale. --> | ||||||
|                         <ion-item class="ion-text-wrap" *ngIf="grade.method === 'simple' && grade.scale"> |                         <ion-item class="ion-text-wrap" *ngIf="grade.method === 'simple' && grade.scale"> | ||||||
|                             <ion-label> |  | ||||||
|                                 <p class="item-heading">{{ 'addon.mod_assign.grade' | translate }}</p> |  | ||||||
|                             </ion-label> |  | ||||||
|                             <ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled" |                             <ion-select [(ngModel)]="grade.grade" interface="action-sheet" [disabled]="grade.disabled" | ||||||
|                                 [cancelText]="'core.cancel' | translate" |                                 [cancelText]="'core.cancel' | translate" | ||||||
|                                 [interfaceOptions]="{header: 'addon.mod_assign.grade' | translate}"> |                                 [interfaceOptions]="{header: 'addon.mod_assign.grade' | translate}"> | ||||||
|  |                                 <p class="item-heading" slot="label">{{ 'addon.mod_assign.grade' | translate }}</p> | ||||||
|                                 <ion-select-option *ngFor="let grade of grade.scale" [value]="grade.value"> |                                 <ion-select-option *ngFor="let grade of grade.scale" [value]="grade.value"> | ||||||
|                                     {{grade.label}} |                                     {{grade.label}} | ||||||
|                                 </ion-select-option> |                                 </ion-select-option> | ||||||
| @ -306,12 +296,10 @@ | |||||||
| 
 | 
 | ||||||
|                         <!-- Outcomes. --> |                         <!-- Outcomes. --> | ||||||
|                         <ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes"> |                         <ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes"> | ||||||
|                             <ion-label> |  | ||||||
|                                 <p class="item-heading">{{ outcome.name }}</p> |  | ||||||
|                             </ion-label> |  | ||||||
|                             <ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" |                             <ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" | ||||||
|                                 interface="action-sheet" [disabled]="gradeInfo!.disabled" [cancelText]="'core.cancel' | translate" |                                 interface="action-sheet" [disabled]="gradeInfo!.disabled" [cancelText]="'core.cancel' | translate" | ||||||
|                                 [interfaceOptions]="{header: outcome.name }"> |                                 [interfaceOptions]="{header: outcome.name }"> | ||||||
|  |                                 <p class="item-heading" slot="label">{{ outcome.name }}</p> | ||||||
|                                 <ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value"> |                                 <ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value"> | ||||||
|                                     {{grade.label}} |                                     {{grade.label}} | ||||||
|                                 </ion-select-option> |                                 </ion-select-option> | ||||||
| @ -345,8 +333,7 @@ | |||||||
| 
 | 
 | ||||||
|                     <ng-container *ngIf="feedback"> |                     <ng-container *ngIf="feedback"> | ||||||
|                         <addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback.plugins" [assign]="assign" |                         <addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback.plugins" [assign]="assign" | ||||||
|                             [submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades"> |                             [submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades" /> | ||||||
|                         </addon-mod-assign-feedback-plugin> |  | ||||||
|                     </ng-container> |                     </ng-container> | ||||||
| 
 | 
 | ||||||
|                     <!-- Workflow status. --> |                     <!-- Workflow status. --> | ||||||
| @ -359,23 +346,22 @@ | |||||||
| 
 | 
 | ||||||
|                     <!--- Apply grade to all team members. --> |                     <!--- Apply grade to all team members. --> | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades"> |                     <ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades"> | ||||||
|                         <ion-label> |                         <ion-toggle [(ngModel)]="grade.applyToAll"> | ||||||
|                             <p class="item-heading">{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</p> |                             <p class="item-heading">{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</p> | ||||||
|                             <p>{{ 'addon.mod_assign.applytoteam' | translate }}</p> |                             <p>{{ 'addon.mod_assign.applytoteam' | translate }}</p> | ||||||
|                         </ion-label> |                         </ion-toggle> | ||||||
|                         <ion-toggle [(ngModel)]="grade.applyToAll" slot="end"></ion-toggle> |  | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| 
 | 
 | ||||||
|                     <!-- Attempt status. --> |                     <!-- Attempt status. --> | ||||||
|                     <ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone"> |                     <ng-container *ngIf="isGrading && assign!.attemptreopenmethod !== attemptReopenMethodNone"> | ||||||
|                         <ion-item class="ion-text-wrap"> |                         <ion-item class="ion-text-wrap"> | ||||||
|                             <ion-label> |                             <ion-label> | ||||||
|                                 <p class="item-heading">{{ 'addon.mod_assign.attemptsettings' | translate }}</p> |                                 <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 : |                                     {{ 'addon.mod_assign.outof' | translate : | ||||||
|                                     {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} |                                     {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} | ||||||
|                                 </p> |                                 </p> | ||||||
|                                 <p *ngIf="assign!.maxattempts != unlimitedAttempts"> |                                 <p *ngIf="assign!.maxattempts !== unlimitedAttempts"> | ||||||
|                                     {{ 'addon.mod_assign.outof' | translate : |                                     {{ 'addon.mod_assign.outof' | translate : | ||||||
|                                     {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} |                                     {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }} | ||||||
|                                 </p> |                                 </p> | ||||||
| @ -386,18 +372,19 @@ | |||||||
|                             </ion-label> |                             </ion-label> | ||||||
|                         </ion-item> |                         </ion-item> | ||||||
|                         <ion-item *ngIf="canSaveGrades && allowAddAttempt"> |                         <ion-item *ngIf="canSaveGrades && allowAddAttempt"> | ||||||
|                             <ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label> |                             <ion-toggle [(ngModel)]="grade.addAttempt"> | ||||||
|                             <ion-toggle [(ngModel)]="grade.addAttempt" slot="end"></ion-toggle> |                                 <p>{{ 'addon.mod_assign.addattempt' | translate }}</p> | ||||||
|  |                             </ion-toggle> | ||||||
|                         </ion-item> |                         </ion-item> | ||||||
|                     </ng-container> |                     </ng-container> | ||||||
| 
 | 
 | ||||||
|                     <!-- Data about the grader (teacher who graded). --> |                     <!-- Data about the grader (teacher who graded). --> | ||||||
|                     <ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader!.id" [courseId]="courseId" |                     <ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader.id" [courseId]="courseId" | ||||||
|                         [attr.aria-label]="grader!.fullname" [detail]="true"> |                         [attr.aria-label]="grader.fullname" [detail]="true"> | ||||||
|                         <core-user-avatar [user]="grader" slot="start" [linkProfile]="false"></core-user-avatar> |                         <core-user-avatar [user]="grader" slot="start" [linkProfile]="false" /> | ||||||
|                         <ion-label> |                         <ion-label> | ||||||
|                             <p class="item-heading">{{ 'addon.mod_assign.gradedby' | translate }}</p> |                             <p class="item-heading">{{ 'addon.mod_assign.gradedby' | translate }}</p> | ||||||
|                             <p class="item-heading">{{ grader!.fullname }}</p> |                             <p class="item-heading">{{ grader.fullname }}</p> | ||||||
|                             <p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p> |                             <p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p> | ||||||
|                         </ion-label> |                         </ion-label> | ||||||
|                     </ion-item> |                     </ion-item> | ||||||
| @ -413,12 +400,12 @@ | |||||||
|                     <!-- Warning message if cannot save grades. --> |                     <!-- Warning message if cannot save grades. --> | ||||||
|                     <ion-card *ngIf="isGrading && !canSaveGrades" class="core-warning-card"> |                     <ion-card *ngIf="isGrading && !canSaveGrades" class="core-warning-card"> | ||||||
|                         <ion-item> |                         <ion-item> | ||||||
|                             <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true"></ion-icon> |                             <ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" /> | ||||||
|                             <ion-label> |                             <ion-label> | ||||||
|                                 <p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p> |                                 <p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p> | ||||||
|                                 <ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link [showBrowserWarning]="false"> |                                 <ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link [showBrowserWarning]="false"> | ||||||
|                                     {{ 'core.openinbrowser' | translate }} |                                     {{ 'core.openinbrowser' | translate }} | ||||||
|                                     <ion-icon name="fas-up-right-from-square" slot="end" aria-hidden="true"></ion-icon> |                                     <ion-icon name="fas-up-right-from-square" slot="end" aria-hidden="true" /> | ||||||
|                                 </ion-button> |                                 </ion-button> | ||||||
|                             </ion-label> |                             </ion-label> | ||||||
|                         </ion-item> |                         </ion-item> | ||||||
| @ -434,8 +421,7 @@ | |||||||
|     <ng-container *ngIf="assign && assign!.teamsubmission && lastAttempt"> |     <ng-container *ngIf="assign && assign!.teamsubmission && lastAttempt"> | ||||||
|         <p *ngIf="lastAttempt.submissiongroup && lastAttempt.submissiongroupname" class="core-groupname"> |         <p *ngIf="lastAttempt.submissiongroup && lastAttempt.submissiongroupname" class="core-groupname"> | ||||||
|             <core-format-text [text]="lastAttempt.submissiongroupname" contextLevel="course" [contextInstanceId]="courseId" |             <core-format-text [text]="lastAttempt.submissiongroupname" contextLevel="course" [contextInstanceId]="courseId" | ||||||
|                 [wsNotFiltered]="true"> |                 [wsNotFiltered]="true" /> | ||||||
|             </core-format-text> |  | ||||||
|         </p> |         </p> | ||||||
|         <ng-container *ngIf="assign!.preventsubmissionnotingroup && |         <ng-container *ngIf="assign!.preventsubmissionnotingroup && | ||||||
|             !lastAttempt!.submissiongroup && |             !lastAttempt!.submissiongroup && | ||||||
|  | |||||||
| @ -1124,9 +1124,15 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can | |||||||
|             return []; |             return []; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         // Receved submission statement should not be undefined. It would mean that the WS is not returning the value.
 | ||||||
|         const submissionStatementMissing = !!this.assign.requiresubmissionstatement && |         const submissionStatementMissing = !!this.assign.requiresubmissionstatement && | ||||||
|             this.assign.submissionstatement === undefined; |             this.assign.submissionstatement === undefined; | ||||||
| 
 | 
 | ||||||
|  |         // If received submission statement is empty, then it's not required.
 | ||||||
|  |         if(!this.assign.submissionstatement && this.assign.submissionstatement !== undefined) { | ||||||
|  |             this.assign.requiresubmissionstatement = 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && (lastAttempt.cansubmit || |         this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && (lastAttempt.cansubmit || | ||||||
|             (this.hasOffline && AddonModAssign.canSubmitOffline(this.assign, submissionStatus))); |             (this.hasOffline && AddonModAssign.canSubmitOffline(this.assign, submissionStatus))); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,18 +4,17 @@ | |||||||
|         <h2>{{ plugin.name }}</h2> |         <h2>{{ plugin.name }}</h2> | ||||||
|         <p> |         <p> | ||||||
|             <core-format-text [component]="component" [componentId]="assign.cmid" collapsible-item [text]="text" contextLevel="module" |             <core-format-text [component]="component" [componentId]="assign.cmid" collapsible-item [text]="text" contextLevel="module" | ||||||
|                 [contextInstanceId]="assign.cmid" [courseId]="assign.course"> |                 [contextInstanceId]="assign.cmid" [courseId]="assign.course" /> | ||||||
|             </core-format-text> |  | ||||||
|         </p> |         </p> | ||||||
|     </ion-label> |     </ion-label> | ||||||
|     <div slot="end"> |     <div slot="end"> | ||||||
|         <div class="ion-text-end"> |         <div class="ion-text-end"> | ||||||
|             <ion-button fill="clear" *ngIf="canEdit" (click)="editComment()" [attr.aria-label]="'core.edit' | translate"> |             <ion-button fill="clear" *ngIf="canEdit" (click)="editComment()" [attr.aria-label]="'core.edit' | translate"> | ||||||
|                 <ion-icon name="fas-pen" slot="icon-only" aria-hidden="true"></ion-icon> |                 <ion-icon name="fas-pen" slot="icon-only" aria-hidden="true" /> | ||||||
|             </ion-button> |             </ion-button> | ||||||
|         </div> |         </div> | ||||||
|         <ion-note *ngIf="!isSent" color="dark"> |         <ion-note *ngIf="!isSent" color="dark"> | ||||||
|             <ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'core.notsent' | translate }} |             <ion-icon name="fas-clock" aria-hidden="true" /> {{ 'core.notsent' | translate }} | ||||||
|         </ion-note> |         </ion-note> | ||||||
|     </div> |     </div> | ||||||
| </ion-item> | </ion-item> | ||||||
| @ -25,6 +24,5 @@ | |||||||
|     <ion-label class="sr-only">{{ plugin.name }}</ion-label> |     <ion-label class="sr-only">{{ plugin.name }}</ion-label> | ||||||
|     <core-rich-text-editor [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor" [component]="component" |     <core-rich-text-editor [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor" [component]="component" | ||||||
|         [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" |         [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" | ||||||
|         elementId="assignfeedbackcomments_editor" [draftExtraParams]="{userid: userId, action: 'grade'}"> |         elementId="assignfeedbackcomments_editor" [draftExtraParams]="{userid: userId, action: 'grade'}" /> | ||||||
|     </core-rich-text-editor> |  | ||||||
| </ion-item> | </ion-item> | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ import { AddonModAssignFeedbackPluginBaseComponent } from '@addons/mod/assign/cl | |||||||
| }) | }) | ||||||
| export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedbackPluginBaseComponent implements OnInit { | export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedbackPluginBaseComponent implements OnInit { | ||||||
| 
 | 
 | ||||||
|     control?: FormControl; |     control?: FormControl<string>; | ||||||
|     component = AddonModAssignProvider.COMPONENT; |     component = AddonModAssignProvider.COMPONENT; | ||||||
|     text = ''; |     text = ''; | ||||||
|     isSent = false; |     isSent = false; | ||||||
| @ -76,7 +76,7 @@ export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedb | |||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|             } else if (this.edit) { |             } else if (this.edit) { | ||||||
|                 this.control = this.fb.control(this.text); |                 this.control = this.fb.control(this.text, { nonNullable: true }); | ||||||
|             } |             } | ||||||
|         } finally { |         } finally { | ||||||
|             this.loaded = true; |             this.loaded = true; | ||||||
|  | |||||||
| @ -3,8 +3,8 @@ | |||||||
|     <ion-label> |     <ion-label> | ||||||
|         <h2>{{plugin.name}}</h2> |         <h2>{{plugin.name}}</h2> | ||||||
|         <ng-container> |         <ng-container> | ||||||
|             <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"> |             <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" | ||||||
|             </core-file> |                 [alwaysDownload]="true" /> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|     </ion-label> |     </ion-label> | ||||||
| </ion-item> | </ion-item> | ||||||
|  | |||||||
| @ -3,8 +3,8 @@ | |||||||
|     <ion-label> |     <ion-label> | ||||||
|         <h2>{{plugin.name}}</h2> |         <h2>{{plugin.name}}</h2> | ||||||
|         <ng-container> |         <ng-container> | ||||||
|             <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"> |             <core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" | ||||||
|             </core-file> |                 [alwaysDownload]="true" /> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|     </ion-label> |     </ion-label> | ||||||
| </ion-item> | </ion-item> | ||||||
|  | |||||||
| @ -1,12 +1,11 @@ | |||||||
| <ion-header> | <ion-header> | ||||||
|     <ion-toolbar> |     <ion-toolbar> | ||||||
|         <ion-buttons slot="start"> |         <ion-buttons slot="start"> | ||||||
|             <ion-back-button [text]="'core.back' | translate"></ion-back-button> |             <ion-back-button [text]="'core.back' | translate" /> | ||||||
|         </ion-buttons> |         </ion-buttons> | ||||||
|         <ion-title> |         <ion-title> | ||||||
|             <h1> |             <h1> | ||||||
|                 <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId"> |                 <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId" /> | ||||||
|                 </core-format-text> |  | ||||||
|             </h1> |             </h1> | ||||||
|         </ion-title> |         </ion-title> | ||||||
|         <ion-buttons slot="end"> |         <ion-buttons slot="end"> | ||||||
| @ -22,38 +21,32 @@ | |||||||
|             <!-- @todo plagiarism_print_disclosure --> |             <!-- @todo plagiarism_print_disclosure --> | ||||||
|             <core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" (finished)="timeUp()" timeUpText="00:00:00" |             <core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" (finished)="timeUp()" timeUpText="00:00:00" | ||||||
|                 [timerText]="'addon.mod_assign.assigntimeleft' | translate" [align]="'center'" [timeLeftClassThreshold]="-1" |                 [timerText]="'addon.mod_assign.assigntimeleft' | translate" [align]="'center'" [timeLeftClassThreshold]="-1" | ||||||
|                 [underTimeClassThresholds]="[300, 900]"> |                 [underTimeClassThresholds]="[300, 900]" /> | ||||||
|             </core-timer> |  | ||||||
| 
 | 
 | ||||||
|             <!-- Assign activity instructions and attachments if needed. --> |             <!-- Assign activity instructions and attachments if needed. --> | ||||||
|             <ion-item class="ion-text-wrap" *ngIf="activityInstructions"> |             <ion-item class="ion-text-wrap" *ngIf="activityInstructions"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <core-format-text [text]="activityInstructions" [component]="component" [componentId]="moduleId" contextLevel="module" |                     <core-format-text [text]="activityInstructions" [component]="component" [componentId]="moduleId" contextLevel="module" | ||||||
|                         [contextInstanceId]="moduleId" [courseId]="courseId"> |                         [contextInstanceId]="moduleId" [courseId]="courseId" /> | ||||||
|                     </core-format-text> |  | ||||||
|                 </ion-label> |                 </ion-label> | ||||||
|             </ion-item> |             </ion-item> | ||||||
| 
 | 
 | ||||||
|             <ng-container *ngIf="assign?.submissionattachments"> |             <ng-container *ngIf="assign?.submissionattachments"> | ||||||
|                 <core-file *ngFor="let file of introAttachments" [file]="file" [component]="component" [componentId]="moduleId"> |                 <core-file *ngFor="let file of introAttachments" [file]="file" [component]="component" [componentId]="moduleId" /> | ||||||
|                 </core-file> |  | ||||||
|             </ng-container> |             </ng-container> | ||||||
| 
 | 
 | ||||||
|             <form name="addon-mod_assign-edit-form" #editSubmissionForm> |             <form name="addon-mod_assign-edit-form" #editSubmissionForm> | ||||||
|                 <!-- Submission statement. --> |                 <!-- Submission statement. --> | ||||||
|                 <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> |                 <ion-item class="ion-text-wrap" *ngIf="submissionStatement"> | ||||||
|                     <ion-label> |                     <ion-checkbox name="submissionstatement" [(ngModel)]="submissionStatementAccepted"> | ||||||
|                         <core-format-text [text]="submissionStatement" [filter]="false"> |                         <core-format-text [text]="submissionStatement" [filter]="false" /> | ||||||
|                         </core-format-text> |                     </ion-checkbox> | ||||||
|                     </ion-label> |  | ||||||
|                     <ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="submissionStatementAccepted"></ion-checkbox> |  | ||||||
|                     <!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. --> |                     <!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. --> | ||||||
|                     <input type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement"> |                     <input type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement"> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| 
 | 
 | ||||||
|                 <addon-mod-assign-submission-plugin *ngFor="let plugin of userSubmission.plugins" [assign]="assign" |                 <addon-mod-assign-submission-plugin *ngFor="let plugin of userSubmission.plugins" [assign]="assign" | ||||||
|                     [submission]="userSubmission" [plugin]="plugin" [edit]="true" [allowOffline]="allowOffline"> |                     [submission]="userSubmission" [plugin]="plugin" [edit]="true" [allowOffline]="allowOffline" /> | ||||||
|                 </addon-mod-assign-submission-plugin> |  | ||||||
|             </form> |             </form> | ||||||
|         </ion-list> |         </ion-list> | ||||||
|     </core-loading> |     </core-loading> | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user