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,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
51
angular.json
51
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": {
|
||||||
|
@ -18,7 +17,7 @@
|
||||||
"path": "./webpack.config.js"
|
"path": "./webpack.config.js"
|
||||||
},
|
},
|
||||||
"allowedCommonJsDependencies":[
|
"allowedCommonJsDependencies":[
|
||||||
"chart.js"
|
"chart.js"
|
||||||
],
|
],
|
||||||
"outputPath": "www",
|
"outputPath": "www",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
|
@ -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,10 +101,11 @@
|
||||||
"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",
|
||||||
|
@ -122,14 +117,14 @@
|
||||||
"builder": "@angular-eslint/builder:lint",
|
"builder": "@angular-eslint/builder:lint",
|
||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": [
|
"lintFilePatterns": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/core/**/*.html",
|
"src/core/**/*.html",
|
||||||
"src/addons/**/*.html"
|
"src/addons/**/*.html"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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" />
|
||||||
|
|
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();
|
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
205
package.json
205
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).
|
|
||||||
*/
|
|
|
@ -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,25 +10,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-button {
|
||||||
|
--border-width: 0;
|
||||||
|
|
||||||
.select-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
core-combobox ::ng-deep ion-select {
|
ion-searchbar {
|
||||||
margin: 0;
|
|
||||||
--a11y-min-target-size: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
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">
|
||||||
</ion-datetime>
|
<ng-template>
|
||||||
<core-input-errors [control]="form.controls.timestart" [errorMessages]="errors"></core-input-errors>
|
<ion-datetime id="timestart" formControlName="timestart" presentation="date-time" [max]="maxDate" [min]="minDate">
|
||||||
|
<span slot="title">{{'core.date' | translate}}</span>
|
||||||
|
</ion-datetime>
|
||||||
|
</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>
|
<ion-datetime id="timedurationuntil" formControlName="timedurationuntil" [max]="maxDate" [min]="minDate"
|
||||||
|
presentation="date-time">
|
||||||
|
<span slot="title">{{'addon.calendar.durationuntil' | translate}}</span>
|
||||||
|
</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,39 +12,23 @@
|
||||||
// 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.
|
||||||
|
*
|
||||||
|
* @returns Route.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
export const messagesIndexGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
|
||||||
export class AddonMessagesIndexGuard implements CanActivate {
|
const enabled = AddonMessages.isGroupMessagingEnabled();
|
||||||
|
const path = `/main/${AddonMessagesMainMenuHandlerService.PAGE_NAME}/` + ( enabled ? 'group-conversations' : 'index');
|
||||||
|
|
||||||
/**
|
const newRoute = Router.parseUrl(path);
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
canActivate(route: ActivatedRouteSnapshot): UrlTree {
|
|
||||||
return this.guard(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
newRoute.queryParams = route.queryParams;
|
||||||
* Check if there is a pending redirect and trigger it.
|
|
||||||
*
|
|
||||||
* @returns The redirection route.
|
|
||||||
*/
|
|
||||||
private guard(route: ActivatedRouteSnapshot): UrlTree {
|
|
||||||
const enabled = AddonMessages.isGroupMessagingEnabled();
|
|
||||||
const path = `/main/${AddonMessagesMainMenuHandlerService.PAGE_NAME}/` + ( enabled ? 'group-conversations' : 'index');
|
|
||||||
|
|
||||||
const newRoute = Router.parseUrl(path);
|
return newRoute;
|
||||||
|
};
|
||||||
newRoute.queryParams = route.queryParams;
|
|
||||||
|
|
||||||
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…
Reference in New Issue