Merge pull request #2326 from moodlehq/integration

Integration
main
Juan Leyva 2020-03-30 12:57:11 +02:00 committed by GitHub
commit 5dfccb1291
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
366 changed files with 12243 additions and 5786 deletions

View File

@ -1,4 +1,4 @@
sudo: required
os: linux
dist: bionic
group: edge
@ -23,4 +23,3 @@ script:
after_success:
- scripts/ci.sh

View File

@ -3,17 +3,17 @@
<plist version="1.0">
<dict>
<key>AD_UNIT_ID_FOR_BANNER_TEST</key>
<string>ca-app-pub-3940256099942544/2934735716</string>
<string></string>
<key>AD_UNIT_ID_FOR_INTERSTITIAL_TEST</key>
<string>ca-app-pub-3940256099942544/4411468910</string>
<string></string>
<key>CLIENT_ID</key>
<string>694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr.apps.googleusercontent.com</string>
<string></string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.694767596569-c2cjrca92k99f6nkp3363lsb7ljhdgdr</string>
<string></string>
<key>API_KEY</key>
<string>AIzaSyA-77ZjkxII6GV97CC9rdUl83rzdEXu_rM</string>
<string></string>
<key>GCM_SENDER_ID</key>
<string>694767596569</string>
<string></string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
@ -21,20 +21,20 @@
<key>PROJECT_ID</key>
<string>moodlemobile-push</string>
<key>STORAGE_BUCKET</key>
<string>moodlemobile-push.appspot.com</string>
<string></string>
<key>IS_ADS_ENABLED</key>
<true></true>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<false></false>
<key>IS_GCM_ENABLED</key>
<true></true>
<false></false>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<false></false>
<key>GOOGLE_APP_ID</key>
<string>1:694767596569:ios:a4cdad4d168c9d1a</string>
<string></string>
<key>DATABASE_URL</key>
<string>https://moodlemobile-push.firebaseio.com</string>
<string></string>
</dict>
</plist>

View File

@ -0,0 +1,15 @@
Package updates known problems
=================
@ionic/app-scripts 3.2.3 shows error Cannot find type definition file for '@types'. on ngc build.
com-darryncampbell-cordova-plugin-intent 2.0.0 onwards needs Android X Support. Unsupported on PGB.
typescript is needed to be less than 2.7 for @angular/compiler-cli
cordova-plugin-ionic-keyboard has problems on greater versions than 2.1.3
jszip has problems with "lie" dependency on greater versions than 3.1
promise.prototype.finally has problems on greater versions than 3.1

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="com.moodle.moodlemobile" version="3.8.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">
<widget id="com.moodle.moodlemobile" version="3.8.1" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Moodle</name>
<description>Moodle official app</description>
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
@ -23,7 +23,7 @@
<preference name="webviewbounce" value="false" />
<preference name="AppendUserAgent" value="MoodleMobile" />
<preference name="android-minSdkVersion" value="19" />
<preference name="android-targetSdkVersion" value="28" />
<preference name="android-targetSdkVersion" value="29" />
<preference name="UIWebViewBounce" value="false" />
<preference name="DisallowOverscroll" value="true" />
<preference name="BackupWebStorage" value="none" />
@ -122,6 +122,9 @@
<edit-config file="*-Info.plist" mode="merge" target="NSLocationWhenInUseUsageDescription">
<string>We need your location so you can attach it as part of your submissions.</string>
</edit-config>
<edit-config file="*-Info.plist" mode="merge" target="NSLocationAlwaysUsageDescription">
<string>We need your location so you can attach it as part of your submissions.</string>
</edit-config>
<icon height="20" src="resources/ios/icon/icon-20.png" width="20" />
<icon height="40" src="resources/ios/icon/icon-20@2x.png" width="40" />
<icon height="60" src="resources/ios/icon/icon-20@3x.png" width="60" />
@ -181,9 +184,6 @@
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application">
<application android:usesCleartextTraffic="true" />
</edit-config>
<edit-config file="AndroidManifest.xml" mode="overwrite" target="/manifest/uses-feature[@android:name='android.hardware.location.gps']">
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
</edit-config>
<config-file parent="/manifest/application" target="AndroidManifest.xml">
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
</config-file>

View File

@ -5,5 +5,11 @@ module.exports = {
'node_modules/ionicons/dist/scss',
'node_modules/ionic-angular/fonts',
'node_modules/font-awesome/scss'
],
includeFiles: [
/\.(s(c|a)ss)$/i
],
excludeFiles: [
/\.(wp).(scss)$/i
]
};

View File

@ -0,0 +1,19 @@
// Check https://github.com/mishoo/UglifyJS2/tree/harmony#minify-options-structure
module.exports = {
/**
* mangle: uglify 2's mangle option
*/
mangle: {
keep_classnames: true,
keep_fnames: true
},
/**
* compress: uglify 2's compress option
*/
compress: {
toplevel: true,
pure_getters: true
},
keep_classnames: true,
keep_fnames: true
}

View File

@ -6,7 +6,7 @@
<Identity Name="3312ADB7.MoodleDesktop"
ProcessorArchitecture="x64"
Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6"
Version="3.8.0.0" />
Version="3.8.1.0" />
<Properties>
<DisplayName>Moodle Desktop</DisplayName>
<PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName>

View File

@ -1,30 +1,30 @@
{
"project_info": {
"project_number": "694767596569",
"firebase_url": "https://moodlemobile-push.firebaseio.com",
"project_id": "moodlemobile-push",
"storage_bucket": "moodlemobile-push.appspot.com"
"project_number": "",
"firebase_url": "",
"project_id": "",
"storage_bucket": ""
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:694767596569:android:a4cdad4d168c9d1a",
"mobilesdk_app_id": "1:111111111111:android:1111111111111111",
"android_client_info": {
"package_name": "com.moodle.moodlemobile"
}
},
"oauth_client": [
{
"client_id": "694767596569-icveqqa2n56oh44l6ev1dr2oh67nh8il.apps.googleusercontent.com",
"client_id": "",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCb2zogu0P_aZ2LNgdwzshWExITPKTXJyk"
"current_key": ""
},
{
"current_key": "AIzaSyDRT1HwT0gSsTty0whOVtoNKAh8SPrJXLE"
"current_key": ""
}
],
"services": {

View File

@ -5,12 +5,12 @@ var gulp = require('gulp'),
path = require('path'),
slash = require('gulp-slash'),
clipEmptyFiles = require('gulp-clip-empty-files'),
gutil = require('gulp-util'),
File = require('vinyl'),
flatten = require('gulp-flatten'),
npmPath = require('path'),
concat = require('gulp-concat'),
bufferFrom = require('buffer-from')
File = gutil.File,
htmlmin = require('gulp-htmlmin'),
bufferFrom = require('buffer-from'),
exec = require('child_process').exec,
license = '' +
'// (C) Copyright 2015 Moodle Pty Ltd.\n' +
@ -294,6 +294,12 @@ gulp.task('copy-component-templates', function(done) {
gulp.src(templatesSrc, { allowEmpty: true })
.pipe(flatten())
// Check options here: https://github.com/kangax/html-minifier
.pipe(htmlmin({
collapseWhitespace: true,
removeComments: true,
caseSensitive: true
}))
.pipe(gulp.dest(templatesDest))
.on('end', done);
});
@ -311,6 +317,11 @@ function getReplace(capture, baseDir, paths, parsedFiles) {
var parse = path.parse(path.resolve(baseDir, capture + '.scss'));
var file = parse.dir + '/' + parse.name;
if (file.slice(-3) === '.wp') {
console.log('Windows Phone not supported "' + capture);
// File was already parsed, leave the import commented.
return '// @import "' + capture + '";';
}
if (!fs.existsSync(file + '.scss')) {
// File not found, might be a partial file.

4356
licenses.json 100644

File diff suppressed because it is too large Load Diff

2650
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "moodlemobile",
"version": "3.8.0",
"version": "3.8.1",
"description": "The official app for Moodle.",
"author": {
"name": "Moodle Pty Ltd.",
@ -9,6 +9,7 @@
"config": {
"ionic_webpack": "./config/webpack.config.js",
"ionic_copy": "./config/copy.config.js",
"ionic_uglifyjs": "./config/uglifyjs.config.js",
"ionic_sass": "./config/sass.config.js"
},
"repository": {
@ -40,63 +41,56 @@
"windows.store": "npx electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop"
},
"dependencies": {
"@angular/animations": "5.2.10",
"@angular/common": "5.2.10",
"@angular/compiler": "5.2.10",
"@angular/compiler-cli": "5.2.10",
"@angular/core": "5.2.10",
"@angular/forms": "5.2.10",
"@angular/http": "5.2.10",
"@angular/platform-browser": "5.2.10",
"@angular/platform-browser-dynamic": "5.2.10",
"@ionic-native/badge": "4.17.0",
"@ionic-native/camera": "4.17.0",
"@ionic-native/clipboard": "4.17.0",
"@ionic-native/core": "4.11.0",
"@ionic-native/device": "4.17.0",
"@ionic-native/file": "4.17.0",
"@ionic-native/file-opener": "4.17.0",
"@ionic-native/file-transfer": "4.17.0",
"@ionic-native/geolocation": "4.17.0",
"@ionic-native/globalization": "4.17.0",
"@ionic-native/in-app-browser": "4.17.0",
"@ionic-native/keyboard": "4.17.0",
"@ionic-native/local-notifications": "4.17.0",
"@ionic-native/media-capture": "4.17.0",
"@ionic-native/network": "4.17.0",
"@ionic-native/push": "4.17.0",
"@ionic-native/screen-orientation": "4.17.0",
"@ionic-native/splash-screen": "4.17.0",
"@ionic-native/sqlite": "4.17.0",
"@ionic-native/status-bar": "4.17.0",
"@ionic-native/web-intent": "4.17.0",
"@ionic-native/zip": "4.17.0",
"@angular/animations": "5.2.11",
"@angular/common": "5.2.11",
"@angular/compiler": "5.2.11",
"@angular/compiler-cli": "5.2.11",
"@angular/core": "5.2.11",
"@angular/forms": "5.2.11",
"@angular/platform-browser": "5.2.11",
"@angular/platform-browser-dynamic": "5.2.11",
"@ionic-native/badge": "4.20.0",
"@ionic-native/camera": "4.20.0",
"@ionic-native/clipboard": "4.20.0",
"@ionic-native/core": "4.20.0",
"@ionic-native/device": "4.20.0",
"@ionic-native/file": "4.20.0",
"@ionic-native/file-opener": "4.20.0",
"@ionic-native/file-transfer": "4.20.0",
"@ionic-native/geolocation": "4.20.0",
"@ionic-native/globalization": "4.20.0",
"@ionic-native/in-app-browser": "4.20.0",
"@ionic-native/keyboard": "4.20.0",
"@ionic-native/local-notifications": "4.20.0",
"@ionic-native/media-capture": "4.20.0",
"@ionic-native/network": "4.20.0",
"@ionic-native/push": "4.20.0",
"@ionic-native/screen-orientation": "4.20.0",
"@ionic-native/splash-screen": "4.20.0",
"@ionic-native/sqlite": "4.20.0",
"@ionic-native/status-bar": "4.20.0",
"@ionic-native/web-intent": "4.20.0",
"@ionic-native/zip": "4.20.0",
"@ngx-translate/core": "8.0.0",
"@ngx-translate/http-loader": "2.0.1",
"@types/cordova": "0.0.34",
"@types/cordova-plugin-file-transfer": "0.0.3",
"@types/cordova-plugin-globalization": "0.0.3",
"@types/cordova-plugin-network-information": "0.0.3",
"@types/node": "8.10.19",
"@types/promise.prototype.finally": "2.0.2",
"ajv": "6.10.2",
"chart.js": "2.7.2",
"ajv": "6.11.0",
"chart.js": "2.9.3",
"com-darryncampbell-cordova-plugin-intent": "1.3.0",
"cordova": "9.0.0",
"cordova-android": "8.0.0",
"cordova-android": "8.1.0",
"cordova-android-support-gradle-release": "3.0.1",
"cordova-clipboard": "1.3.0",
"cordova-ios": "5.0.1",
"cordova-ios": "5.1.1",
"cordova-plugin-badge": "0.8.8",
"cordova-plugin-camera": "4.1.0",
"cordova-plugin-customurlscheme": "4.4.0",
"cordova-plugin-customurlscheme": "5.0.0",
"cordova-plugin-device": "2.0.3",
"cordova-plugin-file": "6.0.2",
"cordova-plugin-file-opener2": "2.2.1",
"cordova-plugin-file-opener2": "3.0.0",
"cordova-plugin-file-transfer": "1.7.1",
"cordova-plugin-geolocation": "4.0.2",
"cordova-plugin-globalization": "1.11.0",
"cordova-plugin-inappbrowser": "3.1.0",
"cordova-plugin-inappbrowser": "3.2.0",
"cordova-plugin-ionic-keyboard": "2.1.3",
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
"cordova-plugin-media-capture": "3.0.3",
@ -106,41 +100,48 @@
"cordova-plugin-statusbar": "2.4.3",
"cordova-plugin-whitelist": "1.3.4",
"cordova-plugin-zip": "3.1.0",
"cordova-sqlite-storage": "3.4.0",
"cordova-support-google-services": "1.2.1",
"cordova-sqlite-storage": "4.0.0",
"cordova-support-google-services": "1.3.2",
"es6-promise-plugin": "4.2.2",
"font-awesome": "4.7.0",
"ionic-angular": "3.9.3",
"ionic-angular": "3.9.9",
"ionicons": "3.0.0",
"jszip": "3.1.5",
"mathjax": "2.7.7",
"moment": "2.22.2",
"moment": "2.24.0",
"nl.kingsquare.cordova.background-audio": "1.0.1",
"phonegap-plugin-multidex": "1.0.0",
"phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v3",
"promise.prototype.finally": "3.1.0",
"rxjs": "5.5.11",
"rxjs": "5.5.12",
"sw-toolbox": "3.6.0",
"ts-md5": "1.2.4",
"web-animations-js": "2.3.1",
"zone.js": "0.8.26"
"ts-md5": "1.2.7",
"web-animations-js": "2.3.2",
"zone.js": "0.8.29"
},
"devDependencies": {
"@ionic/app-scripts": "3.2.2",
"@ionic/app-scripts": "3.2.3",
"@types/cordova": "0.0.34",
"@types/cordova-plugin-file-transfer": "0.0.3",
"@types/cordova-plugin-globalization": "0.0.3",
"@types/cordova-plugin-network-information": "0.0.3",
"@types/node": "8.10.59",
"@types/promise.prototype.finally": "2.0.4",
"electron-builder-lib": "20.23.1",
"electron-rebuild": "1.8.1",
"electron-rebuild": "1.10.0",
"gulp": "4.0.2",
"gulp-clip-empty-files": "0.1.2",
"gulp-concat": "2.6.1",
"gulp-flatten": "0.4.0",
"gulp-rename": "1.3.0",
"gulp-htmlmin": "5.0.1",
"gulp-rename": "2.0.0",
"gulp-slash": "1.1.3",
"gulp-util": "3.0.8",
"lodash.template": "4.5.0",
"node-loader": "0.6.0",
"through": "2.3.8",
"typescript": "2.6.2",
"webpack-merge": "4.1.2"
"vinyl": "2.2.0",
"webpack-merge": "4.2.2"
},
"browser": {
"electron": false
@ -224,7 +225,7 @@
"category": "public.app-category.education",
"icon": "resources/desktop/icon.icns",
"target": "mas",
"bundleVersion": "3.8.0",
"bundleVersion": "3.8.1",
"extendInfo": {
"ElectronTeamID": "2NU57U5PAW"
}

View File

@ -1,47 +0,0 @@
#!/bin/bash
source "scripts/functions.sh"
print_title "NPM packages list"
# List first level of installed libraries so we can check the installed versions.
npm list --depth=0
if [ "$TRAVIS_BRANCH" == 'master' ] && [ ! -z $GIT_TOKEN ] && [ "$TRAVIS_REPO_SLUG" == 'moodlehq/moodleapp' ]; then
print_title "Update langpacks"
cd scripts
./update_lang.sh
cd ..
print_title "Update generated lang files"
git remote set-url origin https://$GIT_TOKEN@github.com/$TRAVIS_REPO_SLUG.git
git fetch -q origin
git add -A src/assets/lang
git add */en.json
git add src/config.json
git commit -m 'Update lang files [ci skip]'
git push origin HEAD:$TRAVIS_BRANCH
fi
print_title "AOT Compilation"
sed -ie $'s~throw new Error("No ResourceLoader.*~url = "templates/" + url;\\\nvar resolve;\\\nvar reject;\\\nvar promise = new Promise(function (res, rej) {\\\nresolve = res;\\\nreject = rej;\\\n});\\\nvar xhr = new XMLHttpRequest();\\\nxhr.open("GET", url, true);\\\nxhr.responseType = "text";\\\nxhr.onload = function () {\\\nvar response = xhr.response || xhr.responseText;\\\nvar status = xhr.status === 1223 ? 204 : xhr.status;\\\nif (status === 0) {\\\nstatus = response ? 200 : 0;\\\n}\\\nif (200 <= status \&\& status <= 300) {\\\nresolve(response);\\\n}\\\nelse {\\\nreject("Failed to load " + url);\\\n}\\\n};\\\nxhr.onerror = function () { reject("Failed to load " + url); };\\\nxhr.send();\\\nreturn promise;\\\n~g' node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js
sed -ie "s/context\.isProd || hasArg('--minifyJs')/hasArg('--minifyJs')/g" node_modules/@ionic/app-scripts/dist/util/config.js
sed -ie "s/context\.isProd || hasArg('--optimizeJs')/hasArg('--optimizeJs')/g" node_modules/@ionic/app-scripts/dist/util/config.js
npm run ionic:build -- --prod
if [ $TRAVIS_BRANCH == 'integration' ] || [ $TRAVIS_BRANCH == 'master' ] || [ $TRAVIS_BRANCH == 'desktop' ] ; then
if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] ; then
if [ "$TRAVIS_REPO_SLUG" == 'moodlehq/moodleapp' ]; then
print_title "Mirror repository"
git remote add mirror https://$GIT_TOKEN@github.com/$GIT_ORG_PRIVATE/moodleapp.git
git fetch -q mirror
git push -f mirror HEAD:$TRAVIS_BRANCH
git push mirror --tags
else
print_title "Run scripts"
git clone --depth 1 https://$GIT_TOKEN@github.com/$GIT_ORG_PRIVATE/apps-scripts.git ../scripts
cp ../scripts/build.sh scripts/
./scripts/build.sh
fi
fi
fi

View File

@ -1,8 +1,81 @@
#!/bin/bash
source "scripts/functions.sh"
if [ $TRAVIS_EVENT_TYPE == 'cron' ] ; then
if [ "$TRAVIS_EVENT_TYPE" == 'cron' ] ; then
# Tests scripts.
echo 'CRON NOT IMPLEMENTED YET'
print_error 'CRON NOT IMPLEMENTED YET'
else
./scripts/aot.sh
if [ -z $GIT_ORG_PRIVATE ] || [ -z $GIT_TOKEN ]; then
print_error "Env vars not correctly defined"
exit 1
fi
# List first level of installed libraries so we can check the installed versions.
print_title "NPM packages list"
npm list --depth=0
if [ "$TRAVIS_REPO_SLUG" == 'moodlehq/moodleapp' ]; then
if [ "$TRAVIS_BRANCH" == 'master' ]; then
print_title "Update langpacks"
cd scripts
./update_lang.sh
cd ..
print_title "Update generated lang files"
git remote set-url origin https://$GIT_TOKEN@github.com/$TRAVIS_REPO_SLUG.git
git fetch -q origin
git add -A src/assets/lang
git add */en.json
git add src/config.json
git commit -m 'Update lang files [ci skip]'
print_title "Update Licenses"
npm install -g license-checker
jq --version
license-checker --json --production --relativeLicensePath > licenses.json
jq 'del(.[].path)' licenses.json > licenses_old.json
mv licenses_old.json licenses.json
licenses=`jq -r 'keys[]' licenses.json`
echo "{" > licensesurl.json
first=1
for license in $licenses; do
obj=`jq --arg lic $license '.[$lic]' licenses.json`
licensePath=`echo $obj | jq -r '.licenseFile'`
file=""
if [[ ! -z "$licensePath" ]] || [[ "$licensePath" != "null" ]]; then
file=$(basename $licensePath)
if [ $first -eq 1 ] ; then
first=0
echo "\"$license\" : { \"licenseFile\" : \"$file\"}" >> licensesurl.json
else
echo ",\"$license\" : { \"licenseFile\" : \"$file\"}" >> licensesurl.json
fi
fi
done
echo "}" >> licensesurl.json
jq -s '.[0] * .[1]' licenses.json licensesurl.json > licenses_old.json
mv licenses_old.json licenses.json
rm licensesurl.json
git add licenses.json
git commit -m 'Update licenses [ci skip]'
git push origin HEAD:$TRAVIS_BRANCH
fi
if [ "$TRAVIS_BRANCH" == 'integration' ] || [ "$TRAVIS_BRANCH" == 'master' ] || [ "$TRAVIS_BRANCH" == 'desktop' ] ; then
print_title "Mirror repository"
git remote add mirror https://$GIT_TOKEN@github.com/$GIT_ORG_PRIVATE/moodleapp.git
git fetch -q --unshallow mirror
git push -f mirror HEAD:$TRAVIS_BRANCH
git push mirror --tags
fi
elif [ "$TRAVIS_REPO_SLUG" == "$GIT_ORG_PRIVATE/moodleapp" ]; then
print_title "Run scripts"
git clone --depth 1 https://$GIT_TOKEN@github.com/$GIT_ORG_PRIVATE/apps-scripts.git ../scripts
cp ../scripts/build.sh scripts/
./scripts/build.sh
fi
fi

View File

@ -3,8 +3,8 @@ source "functions.sh"
#Saves or updates a key on langindex_old.json
function save_key {
key=$1
found=$2
local key=$1
local found=$2
print_ok "$key=$found"
echo "{\"$key\": \"$found\"}" > langindex_old.json
@ -14,19 +14,17 @@ function save_key {
#Removes a key on langindex_old.json
function remove_key {
key=$1
found=$2
local key=$1
print_ok "$key=$found"
echo "{\"$key\": \"$found\"}" > langindex_old.json
jq -s '.[0] - .[1]' langindex.json langindex_old.json > langindex_new.json
cat langindex.json | jq 'del(."'$key'")' > langindex_new.json
mv langindex_new.json langindex.json
print_ok "Deleted unused key $key"
}
#Check if and i exists in php file
function exists_in_file {
file=$1
id=$2
local file=$1
local id=$2
file=`echo $file | sed s/^mod_workshop_assessment/workshopform/1`
file=`echo $file | sed s/^mod_assign_/assign/1`
@ -45,7 +43,7 @@ function exists_in_file {
#Checks if a key exists on the original local_moodlemobileapp.php
function exists_in_mobile {
file='local_moodlemobileapp'
local file='local_moodlemobileapp'
exists_in_file $file $key
}
@ -114,12 +112,12 @@ function find_single_matches {
#Tries to gues the file where the id will be found.
function guess_file {
key=$1
value=$2
local key=$1
local value=$2
type=`echo $key | cut -d'.' -f1`
component=`echo $key | cut -d'.' -f2`
plainid=`echo $key | cut -d'.' -f3-`
local type=`echo $key | cut -d'.' -f1`
local component=`echo $key | cut -d'.' -f2`
local plainid=`echo $key | cut -d'.' -f3-`
if [ -z "$plainid" ]; then
plainid=$component
@ -161,23 +159,52 @@ function guess_file {
fi
}
function current_translation_exists {
local key=$1
local current=$2
local file=$3
plainid=`echo $key | cut -d'.' -f3-`
if [ -z "$plainid" ]; then
plainid=`echo $key | cut -d'.' -f2`
fi
local currentFile=`echo $current | cut -d'/' -f1`
local currentStr=`echo $current | cut -d'/' -f2-`
if [ $currentFile == $current ]; then
currentStr=$plainid
fi
exists_in_file $currentFile $currentStr
if [ $found == 0 ]; then
# Translation not found.
exec="jq -r .\"$key\" $file"
value=`$exec`
print_error "Translation of '$currentStr' not found in '$currentFile'"
guess_file $key "$value"
fi
}
#Finds if there's a better file where to get the id from.
function find_better_file {
key=$1
value=$2
current=$3
local key=$1
local value=$2
local current=$3
type=`echo $key | cut -d'.' -f1`
component=`echo $key | cut -d'.' -f2`
plainid=`echo $key | cut -d'.' -f3-`
local type=`echo $key | cut -d'.' -f1`
local component=`echo $key | cut -d'.' -f2`
local plainid=`echo $key | cut -d'.' -f3-`
if [ -z "$plainid" ]; then
plainid=$component
component='moodle'
fi
currentFile=`echo $current | cut -d'/' -f1`
currentStr=`echo $current | cut -d'/' -f2-`
local currentFile=`echo $current | cut -d'/' -f1`
local currentStr=`echo $current | cut -d'/' -f2-`
if [ $currentFile == $current ]; then
currentStr=$plainid
fi
@ -217,7 +244,7 @@ function find_better_file {
fi
}
#Parses the file.
# Parses the file.
function parse_file {
findbetter=$2
keys=`jq -r 'keys[]' $1`
@ -226,12 +253,43 @@ function parse_file {
exec="jq -r .\"$key\" langindex.json"
found=`$exec`
if [ -z "$found" ] || [ "$found" == 'null' ]; then
exec="jq -r .\"$key\" $1"
value=`$exec`
if [ -z "$found" ] || [ "$found" == 'null' ]; then
guess_file $key "$value"
elif [ ! -z "$findbetter" ]; then
else
if [ ! -z "$findbetter" ]; then
exec="jq -r .\"$key\" $1"
value=`$exec`
find_better_file "$key" "$value" "$found"
elif [ "$found" != 'local_moodlemobileapp' ]; then
current_translation_exists "$key" "$found" "$1"
fi
fi
done
# Do some cleanup
langkeys=`jq -r 'keys[]' langindex.json`
findkeys="${keys[@]}"
for key in $langkeys; do
# Check if already used.
array_contains "$key" "$findkeys"
if [ -z "$found" ] || [ "$found" == 'null' ]; then
remove_key $key
fi
done
}
# Checks if an array contains an string.
function array_contains {
local hayjack=$2
local needle=$1
found=''
for i in $hayjack; do
if [ "$i" == "$needle" ] ; then
found=$i
return
fi
done
}

View File

@ -208,7 +208,7 @@ function build_lang($lang, $keys) {
// Apply translations.
if (!$string) {
if (TOTRANSLATE) {
echo "\n\t\To translate $value->string on $value->file";
echo "\n\t\tTo translate $value->string on $value->file";
}
continue;
}
@ -217,9 +217,12 @@ function build_lang($lang, $keys) {
// Not yet translated. Do not override.
if ($langFile && is_array($langFile) && isset($langFile[$key])) {
$translations[$key] = $langFile[$key];
if ($value->file == 'local_moodlemobileapp') {
$local++;
}
if (TOTRANSLATE) {
}
if (TOTRANSLATE && !isset($string[$value->string])) {
echo "\n\t\tTo translate $value->string on $value->file";
}
continue;
@ -265,7 +268,7 @@ function build_lang($lang, $keys) {
}
function progressbar($percentage) {
$done = $percentage/10;
$done = floor($percentage/10);
return "\t".str_repeat('=', $done) . str_repeat('-', 10-$done);
}

View File

@ -29,7 +29,6 @@
"addon.block_activitymodules.pluginname": "block_activity_modules",
"addon.block_badges.pluginname": "block_badges",
"addon.block_blogmenu.pluginname": "block_blog_menu",
"addon.block_blogrecent.nocourses": "block_blog_recent",
"addon.block_blogrecent.pluginname": "block_blog_recent",
"addon.block_blogtags.pluginname": "block_blog_tags",
"addon.block_calendarmonth.pluginname": "block_calendar_month",
@ -427,6 +426,8 @@
"addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext",
"addon.mod_book.errorchapter": "book",
"addon.mod_book.modulenameplural": "book",
"addon.mod_book.navnexttitle": "book",
"addon.mod_book.navprevtitle": "book",
"addon.mod_book.tagarea_book_chapters": "book",
"addon.mod_book.toc": "book",
"addon.mod_chat.beep": "chat",
@ -1323,12 +1324,12 @@
"core.all": "moodle",
"core.allgroups": "moodle",
"core.allparticipants": "moodle",
"core.android": "local_moodlemobileapp",
"core.answer": "moodle",
"core.answered": "quiz",
"core.areyousure": "moodle",
"core.back": "moodle",
"core.block.blocks": "moodle",
"core.browser": "local_moodlemobileapp",
"core.cancel": "moodle",
"core.cannotconnect": "local_moodlemobileapp",
"core.cannotdownloadfiles": "local_moodlemobileapp",
@ -1355,7 +1356,6 @@
"core.comments.savecomment": "moodle",
"core.comments.warningcommentsnotsent": "local_moodlemobileapp",
"core.commentscount": "moodle",
"core.commentsnotworking": "local_moodlemobileapp",
"core.completion-alt-auto-fail": "completion",
"core.completion-alt-auto-n": "completion",
"core.completion-alt-auto-n-override": "completion",
@ -1467,6 +1467,7 @@
"core.deleteduser": "bulkusers",
"core.deleting": "local_moodlemobileapp",
"core.description": "moodle",
"core.desktop": "local_moodlemobileapp",
"core.dfdaymonthyear": "local_moodlemobileapp",
"core.dfdayweekmonth": "local_moodlemobileapp",
"core.dffulldate": "local_moodlemobileapp",
@ -1477,11 +1478,27 @@
"core.digitalminor_desc": "moodle",
"core.discard": "local_moodlemobileapp",
"core.dismiss": "local_moodlemobileapp",
"core.displayoptions": "atto_media",
"core.done": "survey",
"core.download": "moodle",
"core.downloaded": "local_moodlemobileapp",
"core.downloading": "local_moodlemobileapp",
"core.edit": "moodle",
"core.editor.autosavesucceeded": "editor_atto",
"core.editor.bold": "atto_bold/pluginname",
"core.editor.clear": "atto_clear/pluginname",
"core.editor.h3": "atto_title",
"core.editor.h4": "atto_title",
"core.editor.h5": "atto_title",
"core.editor.hidetoolbar": "local_moodlemobileapp",
"core.editor.italic": "atto_italic/pluginname",
"core.editor.orderedlist": "atto_orderedlist/pluginname",
"core.editor.p": "atto_title",
"core.editor.strike": "atto_strike/pluginname",
"core.editor.textrecovered": "editor_atto",
"core.editor.toggle": "local_moodlemobileapp",
"core.editor.underline": "atto_underline/pluginname",
"core.editor.unorderedlist": "atto_unorderedlist/pluginname",
"core.emptysplit": "local_moodlemobileapp",
"core.error": "moodle",
"core.errorchangecompletion": "local_moodlemobileapp",
@ -1619,6 +1636,7 @@
"core.h5p.offlineDialogRetryButtonLabel": "h5p",
"core.h5p.offlineDialogRetryMessage": "h5p",
"core.h5p.offlineSuccessfulSubmit": "h5p",
"core.h5p.offlinedisabled": "local_moodlemobileapp",
"core.h5p.originator": "h5p",
"core.h5p.pd": "h5p",
"core.h5p.pddl": "h5p",
@ -1653,7 +1671,6 @@
"core.imageviewer": "local_moodlemobileapp",
"core.info": "moodle",
"core.invalidformdata": "error",
"core.ios": "local_moodlemobileapp",
"core.labelsep": "langconfig",
"core.lastaccess": "moodle",
"core.lastdownloaded": "local_moodlemobileapp",
@ -1674,7 +1691,6 @@
"core.login.changepasswordinstructions": "local_moodlemobileapp",
"core.login.changepasswordlogoutinstructions": "local_moodlemobileapp",
"core.login.changepasswordreconnectinstructions": "local_moodlemobileapp",
"core.login.checksiteversion": "local_moodlemobileapp",
"core.login.confirmdeletesite": "local_moodlemobileapp",
"core.login.connect": "local_moodlemobileapp",
"core.login.connecttomoodle": "local_moodlemobileapp",
@ -1689,7 +1705,6 @@
"core.login.emailconfirmsentnoemail": "local_moodlemobileapp",
"core.login.emailconfirmsentsuccess": "moodle",
"core.login.emailnotmatch": "local_moodlemobileapp",
"core.login.enterthewordsabove": "auth",
"core.login.erroraccesscontrolalloworigin": "local_moodlemobileapp",
"core.login.errordeletesite": "local_moodlemobileapp",
"core.login.errorupdatesite": "local_moodlemobileapp",
@ -1697,7 +1712,6 @@
"core.login.firsttime": "moodle",
"core.login.forcepasswordchangenotice": "moodle",
"core.login.forgotten": "moodle",
"core.login.getanothercaptcha": "auth",
"core.login.help": "moodle",
"core.login.helpmelogin": "local_moodlemobileapp",
"core.login.instructions": "auth",
@ -1710,9 +1724,6 @@
"core.login.invalidurl": "scorm",
"core.login.invalidvaluemax": "local_moodlemobileapp",
"core.login.invalidvaluemin": "local_moodlemobileapp",
"core.login.legacymoodleversion": "local_moodlemobileapp",
"core.login.legacymoodleversiondesktop": "local_moodlemobileapp",
"core.login.legacymoodleversiondesktopdownloadold": "local_moodlemobileapp",
"core.login.localmobileunexpectedresponse": "local_moodlemobileapp",
"core.login.loggedoutssodescription": "local_moodlemobileapp",
"core.login.login": "moodle",
@ -1767,7 +1778,6 @@
"core.login.visitchangepassword": "local_moodlemobileapp",
"core.login.webservicesnotenabled": "local_moodlemobileapp",
"core.lostconnection": "local_moodlemobileapp",
"core.mainmenu.appsettings": "local_moodlemobileapp",
"core.mainmenu.changesite": "local_moodlemobileapp",
"core.mainmenu.help": "moodle",
"core.mainmenu.logout": "moodle",
@ -1898,7 +1908,8 @@
"core.sending": "chat",
"core.serverconnection": "error",
"core.settings.about": "local_moodlemobileapp",
"core.settings.appready": "local_moodlemobileapp",
"core.settings.appsettings": "local_moodlemobileapp",
"core.settings.appversion": "local_moodlemobileapp",
"core.settings.cannotsyncoffline": "local_moodlemobileapp",
"core.settings.cannotsyncwithoutwifi": "local_moodlemobileapp",
"core.settings.colorscheme": "local_moodlemobileapp",
@ -1906,6 +1917,7 @@
"core.settings.colorscheme-dark": "local_moodlemobileapp",
"core.settings.colorscheme-light": "local_moodlemobileapp",
"core.settings.compilationinfo": "local_moodlemobileapp",
"core.settings.copyinfo": "local_moodlemobileapp",
"core.settings.cordovadevicemodel": "local_moodlemobileapp",
"core.settings.cordovadeviceosversion": "local_moodlemobileapp",
"core.settings.cordovadeviceplatform": "local_moodlemobileapp",
@ -1918,7 +1930,6 @@
"core.settings.deletesitefilestitle": "local_moodlemobileapp",
"core.settings.deviceinfo": "local_moodlemobileapp",
"core.settings.deviceos": "local_moodlemobileapp",
"core.settings.devicewebworkers": "local_moodlemobileapp",
"core.settings.disableall": "message",
"core.settings.disabled": "lesson",
"core.settings.displayformat": "local_moodlemobileapp",
@ -1935,6 +1946,7 @@
"core.settings.filesystemroot": "local_moodlemobileapp",
"core.settings.fontsize": "local_moodlemobileapp",
"core.settings.fontsizecharacter": "block_accessibility/char",
"core.settings.forcedsetting": "local_moodlemobileapp",
"core.settings.general": "moodle",
"core.settings.language": "moodle",
"core.settings.license": "moodle",
@ -1946,19 +1958,24 @@
"core.settings.navigatorlanguage": "local_moodlemobileapp",
"core.settings.navigatoruseragent": "local_moodlemobileapp",
"core.settings.networkstatus": "local_moodlemobileapp",
"core.settings.opensourcelicenses": "local_moodlemobileapp",
"core.settings.preferences": "moodle",
"core.settings.privacypolicy": "local_moodlemobileapp",
"core.settings.publisher": "local_moodlemobileapp",
"core.settings.pushid": "local_moodlemobileapp",
"core.settings.reportinbackground": "local_moodlemobileapp",
"core.settings.screen": "local_moodlemobileapp",
"core.settings.settings": "moodle",
"core.settings.showdownloadoptions": "local_moodlemobileapp",
"core.settings.siteinfo": "local_moodlemobileapp",
"core.settings.sites": "moodle",
"core.settings.spaceusage": "local_moodlemobileapp",
"core.settings.spaceusagehelp": "local_moodlemobileapp",
"core.settings.synchronization": "local_moodlemobileapp",
"core.settings.synchronizenow": "local_moodlemobileapp",
"core.settings.synchronizenowhelp": "local_moodlemobileapp",
"core.settings.syncsettings": "local_moodlemobileapp",
"core.settings.total": "moodle",
"core.settings.versioncode": "local_moodlemobileapp",
"core.settings.versionname": "local_moodlemobileapp",
"core.settings.wificonnection": "local_moodlemobileapp",
"core.sharedfiles.chooseaccountstorefile": "local_moodlemobileapp",
"core.sharedfiles.chooseactionrepeatedfile": "local_moodlemobileapp",
@ -2003,18 +2020,18 @@
"core.submit": "moodle",
"core.success": "moodle",
"core.tablet": "local_moodlemobileapp",
"core.tag.defautltagcoll": "moodle",
"core.tag.defautltagcoll": "tag",
"core.tag.errorareanotsupported": "local_moodlemobileapp",
"core.tag.inalltagcoll": "moodle",
"core.tag.itemstaggedwith": "moodle",
"core.tag.notagsfound": "moodle",
"core.tag.searchtags": "moodle",
"core.tag.showingfirsttags": "moodle",
"core.tag.inalltagcoll": "tag",
"core.tag.itemstaggedwith": "tag",
"core.tag.notagsfound": "tag",
"core.tag.searchtags": "tag",
"core.tag.showingfirsttags": "tag",
"core.tag.tag": "moodle",
"core.tag.tagarea_course": "moodle",
"core.tag.tagarea_course_modules": "moodle",
"core.tag.tagarea_post": "moodle",
"core.tag.tagarea_user": "moodle",
"core.tag.tagarea_course": "tag",
"core.tag.tagarea_course_modules": "tag",
"core.tag.tagarea_post": "tag",
"core.tag.tagarea_user": "tag",
"core.tag.tags": "moodle",
"core.tag.warningareasnotsupported": "local_moodlemobileapp",
"core.teachers": "moodle",
@ -2076,7 +2093,6 @@
"core.whoops": "local_moodlemobileapp",
"core.whyisthishappening": "local_moodlemobileapp",
"core.whyisthisrequired": "moodle",
"core.windowsphone": "local_moodlemobileapp",
"core.wsfunctionnotavailable": "local_moodlemobileapp",
"core.year": "moodle",
"core.years": "moodle",

View File

@ -15,12 +15,3 @@ ion-app.app-root.ios addon-block-activitymodules {
height: 24px;
}
}
ion-app.app-root.wp addon-block-activitymodules {
.core-module-icon {
margin-top: $item-wp-padding-top;
margin-bottom: $item-wp-padding-bottom;
width: 24px;
height: 24px;
}
}

View File

@ -2,7 +2,7 @@
<h2>{{ 'addon.block_activitymodules.pluginname' | translate }}</h2>
</ion-item-divider>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<a ion-item text-wrap *ngFor="let entry of entries" class="item-media" detail-none [navPush]="'CoreCourseListModTypePage'" [navParams]="{title: entry.name, courseId: instanceId, modName: entry.modName}">
<a ion-item text-wrap *ngFor="let entry of entries" class="item-media" detail-none navPush="CoreCourseListModTypePage" [navParams]="{title: entry.name, courseId: instanceId, modName: entry.modName}">
<img item-start [src]="entry.icon" alt="" role="presentation" class="core-module-icon">
{{ entry.name }}
</a>

View File

@ -218,7 +218,8 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
this.showFilters.favourite = this.getShowFilterValue(
this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined' &&
(!config || config.displaygroupingstarred.value == '1'),
(!config || (config.displaygroupingstarred && config.displaygroupingstarred.value == '1') ||
(config.displaygroupingfavourites && config.displaygroupingfavourites.value == '1')),
this.courses.favourite.length === 0);
this.showFilters.custom = this.getShowFilterValue(this.showSelectorFilter && config &&

View File

@ -25,7 +25,6 @@ import { CoreCronDelegate } from '@providers/cron';
import { CoreInitDelegate } from '@providers/init';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { AddonCalendarComponentsModule } from './components/components.module';
@ -56,7 +55,7 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [
export class AddonCalendarModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler,
initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider,
localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider,
localNotificationsProvider: CoreLocalNotificationsProvider,
cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler,
contentLinksDelegate: CoreContentLinksDelegate, viewLinkHandler: AddonCalendarViewLinkHandler) {
@ -88,18 +87,5 @@ export class AddonCalendarModule {
});
}
});
// Allow migrating the table from the old app to the new schema.
// In the old app some calculated properties were stored when it shouldn't. Filter only the fields we want.
updateManager.registerSiteTableMigration({
name: 'calendar_events',
newName: AddonCalendarProvider.EVENTS_TABLE,
filterFields: ['id', 'name', 'description', 'format', 'eventtype', 'courseid', 'timestart', 'timeduration',
'categoryid', 'groupid', 'userid', 'instance', 'modulename', 'timemodified', 'repeatid', 'visible', 'uuid',
'sequence', 'subscriptionid']
});
// Migrate the component name.
updateManager.registerLocalNotifComponentMigration('mmaCalendarComponent', AddonCalendarProvider.COMPONENT);
}
}

View File

@ -89,6 +89,7 @@ ion-app.app-root addon-calendar-calendar {
@include border-end(1px, solid, $calendar-border-color);
overflow: hidden;
min-height: 60px;
cursor: pointer;
&:first-child {
@include padding(null, null, null, 10px);
@ -131,6 +132,7 @@ ion-app.app-root addon-calendar-calendar {
border-radius: 50%;
}
&.dayblank {
cursor: auto;
background-color: $gray-lighter;
@include darkmode() {
background-color: $black;

View File

@ -0,0 +1,5 @@
ion-app.app-root addon-calendar-upcoming-events {
.addon-calendar-event {
cursor: pointer;
}
}

View File

@ -48,7 +48,7 @@
<ion-list *ngIf="filteredEvents && filteredEvents.length" no-margin>
<ng-container *ngFor="let event of filteredEvents">
<ion-item text-wrap [title]="event.name" (click)="gotoEvent(event.id)" [class.item-dimmed]="event.ispast" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+event.eventtype]">
<a ion-item text-wrap [title]="event.name" (click)="gotoEvent(event.id)" [class.item-dimmed]="event.ispast" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+event.eventtype]">
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" item-start class="core-module-icon">
<core-icon *ngIf="event.eventIcon && !event.moduleIcon" [name]="event.eventIcon" item-start></core-icon>
<h2><core-format-text [text]="event.name" [contextLevel]="event.contextLevel" [contextInstanceId]="event.contextInstanceId"></core-format-text></h2>
@ -61,7 +61,7 @@
<ion-icon name="trash"></ion-icon>
<span text-wrap>{{ 'core.deletedoffline' | translate }}</span>
</ion-note>
</ion-item>
</a>
</ng-container>
</ion-list>

View File

@ -9,7 +9,7 @@
</ion-refresher>
<core-loading [hideUntil]="loaded">
<form ion-list [formGroup]="eventForm" *ngIf="!error">
<form ion-list [formGroup]="eventForm" *ngIf="!error" #editEventForm>
<!-- Event name. -->
<ion-item text-wrap>
<ion-label stacked><h2 [core-mark-required]="true">{{ 'addon.calendar.eventname' | translate }}</h2></ion-label>
@ -86,7 +86,7 @@
<!-- Description. -->
<ion-item text-wrap>
<ion-label stacked><h2>{{ 'core.description' | translate }}</h2></ion-label>
<core-rich-text-editor item-content [control]="descriptionControl" [placeholder]="'core.description' | translate" name="description" [component]="component" [componentId]="eventId"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="descriptionControl" [placeholder]="'core.description' | translate" name="description" [component]="component" [componentId]="eventId" [autoSave]="false"></core-rich-text-editor>
</ion-item>
<!-- Location. -->

View File

@ -17,6 +17,7 @@ import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
import { AddonCalendarEditEventPage } from './edit-event';
@NgModule({
@ -26,6 +27,7 @@ import { AddonCalendarEditEventPage } from './edit-event';
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonCalendarEditEventPage),
TranslateModule.forChild()
],

View File

@ -14,13 +14,6 @@ ion-app.app-root page-addon-calendar-edit-event {
@include padding-horizontal($datetime-md-padding-start - $text-input-md-margin-start, null);
}
}
&.item-wp {
@include padding-horizontal($item-wp-padding-start * 2, null);
ion-input {
@include padding-horizontal($datetime-wp-padding-start - $text-input-wp-margin-start, null);
}
}
}
.addon-calendar-eventtype-container.item-select-disabled {

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, Optional, ViewChild } from '@angular/core';
import { Component, OnInit, OnDestroy, Optional, ViewChild, ElementRef } from '@angular/core';
import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
@ -25,7 +25,7 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts';
import { CoreEditorRichTextEditorComponent } from '@core/editor/components/rich-text-editor/rich-text-editor.ts';
import { AddonCalendarProvider, AddonCalendarGetAccessInfoResult, AddonCalendarEvent } from '../../providers/calendar';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper';
@ -43,7 +43,8 @@ import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
})
export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
@ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent;
@ViewChild(CoreEditorRichTextEditorComponent) descriptionEditor: CoreEditorRichTextEditorComponent;
@ViewChild('editEventForm') formElement: ElementRef;
title: string;
dateFormat: string;
@ -496,6 +497,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
this.calendarProvider.submitEvent(this.eventId, data).then((result) => {
event = result.event;
this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.currentSite.getId());
if (result.sent) {
// Event created or edited, invalidate right days & months.
const numberOfRepetitions = formData.repeat ? formData.repeats :
@ -557,6 +560,9 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
discard(): void {
this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
this.calendarOffline.deleteEvent(this.eventId).then(() => {
this.domUtils.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
this.returnToList();
}).catch(() => {
// Shouldn't happen.
@ -572,16 +578,18 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.calendarHelper.hasEventDataChanged(this.eventForm.value, this.originalData)) {
// Show confirmation if some data has been modified.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} else {
return Promise.resolve();
}
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
this.domUtils.triggerFormCancelledEvent(this.formElement, this.currentSite.getId());
}
/**
* Unblock sync.
*/
protected unblockSync(): void {
if (this.eventId) {
this.syncProvider.unblockOperation(AddonCalendarProvider.COMPONENT, this.eventId);

View File

@ -25,7 +25,7 @@
<ng-container *ngIf="!competency.competency.comppath.showlinks">{{ competency.competency.comppath.framework.name }}</ng-container>
&nbsp;/&nbsp;
<span *ngFor="let ancestor of competency.competency.comppath.ancestors">
<a *ngIf="competency.competency.comppath.showlinks" (click)="openCompetencySummary(ancestor.id)">{{ ancestor.name }}</a>
<a *ngIf="competency.competency.comppath.showlinks" (click)="openCompetencySummary(ancestor.id)" class="core-clickable">{{ ancestor.name }}</a>
<ng-container *ngIf="!competency.competency.comppath.showlinks">{{ ancestor.name }}</ng-container>
<ng-container *ngIf="!ancestor.last">&nbsp;/&nbsp;</ng-container>
</span>
@ -35,7 +35,7 @@
<div *ngIf="!competency.competency.hasrelatedcompetencies">{{ 'addon.competency.nocrossreferencedcompetencies' | translate }}</div>
<div *ngIf="competency.competency.hasrelatedcompetencies">
<p *ngFor="let relatedcomp of competency.competency.relatedcompetencies">
<a (click)="openCompetencySummary(relatedcomp.id)">
<a (click)="openCompetencySummary(relatedcomp.id)" class="core-clickable">
{{ relatedcomp.shortname }} - {{ relatedcomp.idnumber }}
</a>
</p>

View File

@ -23,7 +23,7 @@
<!-- List of files. -->
<ion-list *ngIf="files && files.length > 0">
<ng-container *ngFor="let file of files">
<a *ngIf="file.isdir" ion-item class="item-media" [navPush]="'AddonFilesListPage'" [navParams]="{path: file.link, title: file.filename}">
<a *ngIf="file.isdir" ion-item class="item-media" navPush="AddonFilesListPage" [navParams]="{path: file.link, title: file.filename}">
<img [src]="file.imgPath" alt="" role="presentation" item-start>
<p>{{file.filename}}</p>
</a>

View File

@ -19,6 +19,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
import { AddonMessagesDiscussionsComponent } from '../components/discussions/discussions';
import { AddonMessagesConfirmedContactsComponent } from '../components/confirmed-contacts/confirmed-contacts';
import { AddonMessagesContactRequestsComponent } from '../components/contact-requests/contact-requests';
@ -37,7 +38,8 @@ import { AddonMessagesContactsComponent } from '../components/contacts/contacts'
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule
CorePipesModule,
CoreSearchComponentsModule,
],
providers: [
],

View File

@ -3,7 +3,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)" [placeholder]=" 'addon.messages.contactname' | translate" autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded"></core-search-box>
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)" [placeholder]=" 'addon.messages.contactname' | translate" autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded" searchArea="AddonMessagesContacts"></core-search-box>
<core-loading [hideUntil]="loaded" [message]="loadingMessage">
<core-empty-box *ngIf="!hasContacts && searchString == ''" icon="person" [message]="'addon.messages.contactlistempty' | translate"></core-empty-box>

View File

@ -3,7 +3,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-search-box *ngIf="search.enabled" (onSubmit)="searchMessage($event)" (onClear)="clearSearch($event)" [placeholder]=" 'addon.messages.message' | translate" autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded"></core-search-box>
<core-search-box *ngIf="search.enabled" (onSubmit)="searchMessage($event)" (onClear)="clearSearch($event)" [placeholder]=" 'addon.messages.message' | translate" autocorrect="off" spellcheck="false" lengthCheck="2" [disabled]="!loaded" searchArea="AddonMessagesDiscussions"></core-search-box>
<core-loading [hideUntil]="loaded" [message]="loadingMessage">

View File

@ -38,7 +38,6 @@ import { CoreSettingsDelegate } from '@core/settings/providers/delegate';
import { AddonMessagesSettingsHandler } from './providers/settings-handler';
import { CorePushNotificationsDelegate } from '@core/pushnotifications/providers/delegate';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
export const ADDON_MESSAGES_PROVIDERS: any[] = [
@ -75,7 +74,7 @@ export class AddonMessagesModule {
userDelegate: CoreUserDelegate, cronDelegate: CoreCronDelegate, syncHandler: AddonMessagesSyncCronHandler,
network: Network, zone: NgZone, messagesSync: AddonMessagesSyncProvider, appProvider: CoreAppProvider,
localNotifications: CoreLocalNotificationsProvider, messagesProvider: AddonMessagesProvider,
sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider, updateManager: CoreUpdateManagerProvider,
sitesProvider: CoreSitesProvider, linkHelper: CoreContentLinksHelperProvider,
settingsHandler: AddonMessagesSettingsHandler, settingsDelegate: CoreSettingsDelegate,
pushNotificationsDelegate: CorePushNotificationsDelegate, utils: CoreUtilsProvider,
addContactHandler: AddonMessagesAddContactUserHandler, blockContactHandler: AddonMessagesBlockContactUserHandler,
@ -136,21 +135,5 @@ export class AddonMessagesModule {
// Listen for clicks in simulated push notifications.
localNotifications.registerClick(AddonMessagesProvider.PUSH_SIMULATION_COMPONENT, notificationClicked);
}
// Allow migrating the table from the old app to the new schema.
updateManager.registerSiteTableMigration({
name: 'mma_messages_offline_messages',
newName: AddonMessagesOfflineProvider.MESSAGES_TABLE,
fields: [
{
name: 'textformat',
delete: true
}
]
});
// Migrate the component name.
updateManager.registerLocalNotifComponentMigration('mmaMessagesPushSimulation',
AddonMessagesProvider.PUSH_SIMULATION_COMPONENT);
}
}

View File

@ -366,9 +366,12 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
return;
}
// Don't use domUtils.getScrollHeight because it gives an outdated value after receiving a new message.
const scrollHeight = this.content && this.content.getScrollElement() ? this.content.getScrollElement().scrollHeight : 0;
// Check if we are at the bottom to scroll it after render.
// Use a 5px error margin because in iOS there is 1px difference for some reason.
this.scrollBottom = Math.abs(this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) -
this.scrollBottom = Math.abs(scrollHeight - this.domUtils.getScrollTop(this.content) -
this.domUtils.getContentHeight(this.content)) < 5;
if (this.messagesBeingSent > 0) {

View File

@ -9,7 +9,7 @@
</ion-header>
<core-split-view>
<ion-content>
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)" [disabled]="disableSearch" autocorrect="off" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1"></core-search-box>
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)" [disabled]="disableSearch" autocorrect="off" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1" searchArea="AddonMessagesSearch"></core-search-box>
<core-loading [hideUntil]="!displaySearching" [message]="'core.searching' | translate">
<ion-list *ngIf="displayResults">
<ng-container *ngTemplateOutlet="resultsTemplate; context: {item: contacts}"></ng-container>

View File

@ -19,6 +19,7 @@ import { AddonMessagesSearchPage } from './search';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
import { AddonMessagesComponentsModule } from '../../components/components.module';
@NgModule({
@ -29,9 +30,10 @@ import { AddonMessagesComponentsModule } from '../../components/components.modul
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreSearchComponentsModule,
AddonMessagesComponentsModule,
IonicPageModule.forChild(AddonMessagesSearchPage),
TranslateModule.forChild()
TranslateModule.forChild(),
],
})
export class AddonMessagesSearchPageModule {}

View File

@ -1,6 +1,6 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.messages.messagepreferences' | translate }}</ion-title>
<ion-title>{{ 'addon.messages.messages' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>

View File

@ -44,9 +44,9 @@ export class AddonMessagesSettingsHandler implements CoreSettingsHandler {
getDisplayData(): CoreSettingsHandlerData {
return {
icon: 'chatbubbles',
title: 'addon.messages.messagepreferences',
title: 'addon.messages.messages',
page: 'AddonMessagesSettingsPage',
class: 'addon-messages-settings-handler'
class: 'addon-messages-settings-handler',
};
}

View File

@ -34,7 +34,6 @@ import { AddonModAssignListLinkHandler } from './providers/list-link-handler';
import { AddonModAssignPushClickHandler } from './providers/push-click-handler';
import { AddonModAssignSubmissionModule } from './submission/submission.module';
import { AddonModAssignFeedbackModule } from './feedback/feedback.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
export const ADDON_MOD_ASSIGN_PROVIDERS: any[] = [
@ -73,7 +72,7 @@ export const ADDON_MOD_ASSIGN_PROVIDERS: any[] = [
export class AddonModAssignModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModAssignModuleHandler,
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModAssignPrefetchHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModAssignSyncCronHandler, updateManager: CoreUpdateManagerProvider,
cronDelegate: CoreCronDelegate, syncHandler: AddonModAssignSyncCronHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModAssignIndexLinkHandler,
listLinkHandler: AddonModAssignListLinkHandler, pushNotificationsDelegate: CorePushNotificationsDelegate,
pushClickHandler: AddonModAssignPushClickHandler) {
@ -84,57 +83,5 @@ export class AddonModAssignModule {
contentLinksDelegate.registerHandler(linkHandler);
contentLinksDelegate.registerHandler(listLinkHandler);
pushNotificationsDelegate.registerClickHandler(pushClickHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTablesMigration([
{
name: 'mma_mod_assign_submissions',
newName: AddonModAssignOfflineProvider.SUBMISSIONS_TABLE,
fields: [
{
name: 'assignmentid',
newName: 'assignid'
},
{
name: 'submitted',
type: 'boolean'
},
{
name: 'submissionstatement',
type: 'boolean'
},
{
name: 'plugindata',
type: 'object'
}
]
},
{
name: 'mma_mod_assign_submissions_grading',
newName: AddonModAssignOfflineProvider.SUBMISSIONS_GRADES_TABLE,
fields: [
{
name: 'assignmentid',
newName: 'assignid'
},
{
name: 'addattempt',
type: 'boolean'
},
{
name: 'applytoall',
type: 'boolean'
},
{
name: 'outcomes',
type: 'object'
},
{
name: 'plugindata',
type: 'object'
}
]
}
]);
}
}

View File

@ -15,7 +15,7 @@
<core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Description and intro attachments. -->
<ion-card *ngIf="description" (click)="expandDescription($event)">
<ion-card *ngIf="description" (click)="expandDescription($event)" class="core-clickable">
<ion-item text-wrap>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId" (click)="expandDescription($event)"></core-format-text>
</ion-item>

View File

@ -21,6 +21,7 @@ import { AddonModAssignFeedbackCommentsComponent } from './component/comments';
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -31,7 +32,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
CoreDirectivesModule,
CoreEditorComponentsModule,
],
providers: [
AddonModAssignFeedbackCommentsHandler

View File

@ -19,5 +19,6 @@
<!-- Edit -->
<ion-item text-wrap *ngIf="edit && loaded">
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor" [draftExtraParams]="{userid: userId, action: 'grade'}">
</core-rich-text-editor>
</ion-item>

View File

@ -9,7 +9,7 @@
</ion-navbar>
</ion-header>
<ion-content>
<form name="addon-mod_assign-edit-feedback-form" *ngIf="userId && plugin">
<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>
<button ion-button block (click)="done($event)">{{ 'core.done' | translate }}</button>
</form>

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input } from '@angular/core';
import { Component, Input, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
import {
@ -36,10 +38,17 @@ export class AddonModAssignEditFeedbackModalPage {
@Input() plugin: AddonModAssignPlugin; // The plugin object.
@Input() userId: number; // The user ID of the submission.
@ViewChild('editFeedbackForm') formElement: ElementRef;
protected forceLeave = false; // To allow leaving the page without checking for changes.
constructor(params: NavParams, protected viewCtrl: ViewController, protected domUtils: CoreDomUtilsProvider,
protected translate: TranslateService, protected feedbackDelegate: AddonModAssignFeedbackDelegate) {
constructor(params: NavParams,
protected viewCtrl: ViewController,
protected domUtils: CoreDomUtilsProvider,
protected translate: TranslateService,
protected feedbackDelegate: AddonModAssignFeedbackDelegate,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.assign = params.get('assign');
this.submission = params.get('submission');
@ -52,16 +61,17 @@ export class AddonModAssignEditFeedbackModalPage {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return true;
return;
}
return this.hasDataChanged().then((changed) => {
const changed = await this.hasDataChanged();
if (changed) {
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
});
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
}
/**
@ -82,6 +92,8 @@ export class AddonModAssignEditFeedbackModalPage {
e.preventDefault();
e.stopPropagation();
this.domUtils.triggerFormSubmittedEvent(this.formElement, false, this.sitesProvider.getCurrentSiteId());
// Close the modal, sending the input data.
this.forceLeave = true;
this.closeModal(this.getInputData());

View File

@ -13,7 +13,7 @@
<core-loading [hideUntil]="loaded">
<ion-list>
<!-- @todo: plagiarism_print_disclosure -->
<form name="addon-mod_assign-edit-form" *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length">
<form name="addon-mod_assign-edit-form" *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length" #editSubmissionForm>
<!-- Submission statement. -->
<ion-item text-wrap *ngIf="submissionStatement">
<ion-label><core-format-text [text]="submissionStatement" [filter]="false"></core-format-text></ion-label>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
@ -34,6 +34,9 @@ import { AddonModAssignHelperProvider } from '../../providers/helper';
templateUrl: 'edit.html',
})
export class AddonModAssignEditPage implements OnInit, OnDestroy {
@ViewChild('editSubmissionForm') formElement: ElementRef;
title: string; // Title to display.
assign: AddonModAssignAssign; // Assignment.
courseId: number; // Course ID the assignment belongs to.
@ -82,20 +85,21 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return true;
return;
}
// Check if data has changed.
return this.hasDataChanged().then((changed) => {
const changed = await this.hasDataChanged();
if (changed) {
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
}).then(() => {
// Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, this.getInputData());
});
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
}
/**
@ -265,50 +269,57 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
*
* @return Promise resolved when done.
*/
protected saveSubmission(): Promise<any> {
protected async saveSubmission(): Promise<void> {
const inputData = this.getInputData();
if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) {
return Promise.reject(this.translate.instant('addon.mod_assign.acceptsubmissionstatement'));
throw this.translate.instant('addon.mod_assign.acceptsubmissionstatement');
}
let modal = this.domUtils.showModalLoading();
let size;
// Get size to ask for confirmation.
return this.assignHelper.getSubmissionSizeForEdit(this.assign, this.userSubmission, inputData).catch(() => {
try {
size = await this.assignHelper.getSubmissionSizeForEdit(this.assign, this.userSubmission, inputData);
} catch (error) {
// Error calculating size, return -1.
return -1;
}).then((size) => {
size = -1;
}
modal.dismiss();
try {
// Confirm action.
return this.fileUploaderHelper.confirmUploadFile(size, true, this.allowOffline);
}).then(() => {
await this.fileUploaderHelper.confirmUploadFile(size, true, this.allowOffline);
modal = this.domUtils.showModalLoading('core.sending', true);
return this.prepareSubmissionData(inputData).then((pluginData) => {
const pluginData = await this.prepareSubmissionData(inputData);
if (!Object.keys(pluginData).length) {
// Nothing to save.
return;
}
let promise;
let sent: boolean;
if (this.saveOffline) {
// Save submission in offline.
promise = this.assignOfflineProvider.saveSubmission(this.assign.id, this.courseId, pluginData,
sent = false;
await this.assignOfflineProvider.saveSubmission(this.assign.id, this.courseId, pluginData,
this.userSubmission.timemodified, !this.assign.submissiondrafts, this.userId);
} else {
// Try to send it to server.
promise = this.assignProvider.saveSubmission(this.assign.id, this.courseId, pluginData, this.allowOffline,
sent = await this.assignProvider.saveSubmission(this.assign.id, this.courseId, pluginData, this.allowOffline,
this.userSubmission.timemodified, !!this.assign.submissiondrafts, this.userId);
}
return promise.then(() => {
// Clear temporary data from plugins.
return this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData);
}).then(() => {
// Submission saved, trigger event.
await this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData);
// Submission saved, trigger events.
this.domUtils.triggerFormSubmittedEvent(this.formElement, sent, this.sitesProvider.getCurrentSiteId());
const params = {
assignmentId: this.assign.id,
submissionId: this.userSubmission.id,
@ -323,11 +334,9 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy {
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, params,
this.sitesProvider.getCurrentSiteId());
}
});
});
}).finally(() => {
} finally {
modal.dismiss();
});
}
}
/**

View File

@ -354,15 +354,7 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
}).then(() => {
// Participiants already fetched, we don't need to ignore cache now.
return this.assignHelper.getParticipants(assign, group.id, false, siteId).then((participants) => {
const promises = [];
participants.forEach((participant) => {
if (participant.profileimageurl) {
promises.push(this.filepoolProvider.addToQueueByUrl(siteId, participant.profileimageurl));
}
});
return Promise.all(promises);
return this.userProvider.prefetchUserAvatars(participants, 'profileimageurl', siteId);
}).catch(() => {
// Fail silently (Moodle < 3.2).
});
@ -443,8 +435,12 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
// Prefetch grade items.
if (userId) {
promises.push(this.courseProvider.getModuleBasicGradeInfo(moduleId, siteId).then((gradeInfo) => {
if (gradeInfo) {
promises.push(this.gradesHelper.getGradeModuleItems(courseId, moduleId, userId, undefined, siteId, true));
}
}));
}
// Prefetch feedback.
if (submission.feedback) {

View File

@ -15,6 +15,6 @@
<p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}</p>
</ion-item>
<ion-item text-wrap>
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" [componentId]="assign.cmid"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}"></core-rich-text-editor>
</ion-item>
</div>

View File

@ -14,6 +14,7 @@
import { Component, OnInit, ElementRef } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonModAssignProvider } from '../../../providers/assign';
@ -35,16 +36,23 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS
text: string;
loaded: boolean;
wordLimitEnabled: boolean;
currentUserId: number;
protected wordCountTimeout: any;
protected element: HTMLElement;
constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider,
protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
element: ElementRef) {
constructor(
protected fb: FormBuilder,
protected domUtils: CoreDomUtilsProvider,
protected textUtils: CoreTextUtilsProvider,
protected assignProvider: AddonModAssignProvider,
protected assignOfflineProvider: AddonModAssignOfflineProvider,
element: ElementRef,
sitesProvider: CoreSitesProvider) {
super();
this.element = element.nativeElement;
this.currentUserId = sitesProvider.getCurrentSiteUserId();
}
/**

View File

@ -21,6 +21,7 @@ import { AddonModAssignSubmissionOnlineTextComponent } from './component/onlinet
import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -31,7 +32,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
CoreDirectivesModule,
CoreEditorComponentsModule,
],
providers: [
AddonModAssignSubmissionOnlineTextHandler

View File

@ -19,13 +19,13 @@
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description>
<div padding class="safe-padding-horizontal">
<core-navigation-bar [previous]="previousChapter > 0 && previousChapter" [next]="nextChapter > 0 && nextChapter" (action)="changeChapter($event)"></core-navigation-bar>
<core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter && previousChapter.id" [previousTitle]="previousNavBarTitle" [next]="nextChapter && nextChapter.id" [nextTitle]="nextNavBarTitle" (action)="changeChapter($event)"></core-navigation-bar>
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
<div margin-top *ngIf="tagsEnabled && contentsMap && contentsMap[currentChapter] && contentsMap[currentChapter].tags && contentsMap[currentChapter].tags.length > 0">
<b>{{ 'core.tag.tags' | translate }}:</b>
<core-tag-list [tags]="contentsMap[currentChapter].tags"></core-tag-list>
</div>
<core-navigation-bar [previous]="previousChapter > 0 && previousChapter" [next]="nextChapter > 0 && nextChapter" (action)="changeChapter($event)"></core-navigation-bar>
<core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter && previousChapter.id" [previousTitle]="previousNavBarTitle" [next]="nextChapter && nextChapter.id" [nextTitle]="nextNavBarTitle" (action)="changeChapter($event)"></core-navigation-bar>
</div>
</core-loading>

View File

@ -17,7 +17,9 @@ import { Content, ModalController } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component';
import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book';
import {
AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter, AddonModBookBook, AddonModBookNavStyle
} from '../../providers/book';
import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler';
import { CoreTagProvider } from '@core/tag/providers/tag';
@ -33,13 +35,18 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
component = AddonModBookProvider.COMPONENT;
chapterContent: string;
previousChapter: string;
nextChapter: string;
previousChapter: AddonModBookTocChapter;
nextChapter: AddonModBookTocChapter;
tagsEnabled: boolean;
displayNavBar = true;
previousNavBarTitle: string;
nextNavBarTitle: string;
protected chapters: AddonModBookTocChapter[];
protected currentChapter: string;
protected contentsMap: AddonModBookContentsMap;
protected book: AddonModBookBook;
protected displayTitlesInNavBar = false;
constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider,
private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler,
@ -69,7 +76,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
moduleId: this.module.id,
chapters: this.chapters,
selected: this.currentChapter,
courseId: this.courseId
courseId: this.courseId,
book: this.book,
}, { cssClass: 'core-modal-lateral',
showBackdrop: true,
enableBackdropDismiss: true,
@ -97,7 +105,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
if (chapterId && chapterId != this.currentChapter) {
this.loaded = false;
this.refreshIcon = 'spinner';
this.loadChapter(chapterId);
this.loadChapter(chapterId, true);
}
}
@ -119,19 +127,24 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
protected fetchContent(refresh?: boolean): Promise<any> {
const promises = [];
let downloadFailed = false;
let downloadFailError;
// Try to get the book data.
promises.push(this.bookProvider.getBook(this.courseId, this.module.id).then((book) => {
this.book = book;
this.dataRetrieved.emit(book);
this.description = book.intro;
this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY;
this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT;
}).catch(() => {
// Ignore errors since this WS isn't available in some Moodle versions.
}));
// Download content. This function also loads module contents if needed.
promises.push(this.prefetchDelegate.download(this.module, this.courseId).catch(() => {
promises.push(this.prefetchDelegate.download(this.module, this.courseId).catch((error) => {
// Mark download as failed but go on since the main files could have been downloaded.
downloadFailed = true;
downloadFailError = error;
if (!this.module.contents.length) {
// Try to load module contents for offline usage.
@ -160,10 +173,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
}
// Show chapter.
return this.loadChapter(this.currentChapter).then(() => {
return this.loadChapter(this.currentChapter, refresh).then(() => {
if (downloadFailed && this.appProvider.isOnline()) {
// We could load the main file but the download failed. Show error message.
this.domUtils.showErrorModal('core.errordownloadingsomefiles', true);
this.showErrorDownloadingSomeFiles(downloadFailError);
}
}).catch(() => {
// Ignore errors, they're handled inside the loadChapter function.
@ -177,9 +190,10 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
* Load a book chapter.
*
* @param chapterId Chapter to load.
* @param logChapterId Whether chapter ID should be passed to the log view function.
* @return Promise resolved when done.
*/
protected loadChapter(chapterId: string): Promise<void> {
protected loadChapter(chapterId: string, logChapterId: boolean): Promise<void> {
this.currentChapter = chapterId;
this.domUtils.scrollToTop(this.content);
@ -188,10 +202,15 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
this.previousChapter = this.bookProvider.getPreviousChapter(this.chapters, chapterId);
this.nextChapter = this.bookProvider.getNextChapter(this.chapters, chapterId);
this.previousNavBarTitle = this.previousChapter && this.displayTitlesInNavBar ?
this.translate.instant('addon.mod_book.navprevtitle', {$a: this.previousChapter.title}) : '';
this.nextNavBarTitle = this.nextChapter && this.displayTitlesInNavBar ?
this.translate.instant('addon.mod_book.navnexttitle', {$a: this.nextChapter.title}) : '';
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
this.bookProvider.logView(this.module.instance, chapterId, this.module.name).then(() => {
this.bookProvider.logView(this.module.instance, logChapterId ? chapterId : undefined, this.module.name).then(() => {
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
if (this.nextChapter == '0') {
if (!this.nextChapter) {
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata);
}
}).catch(() => {

View File

@ -1,6 +1,8 @@
{
"errorchapter": "Error reading chapter of book.",
"modulenameplural": "Books",
"navnexttitle": "Next: {{$a}}",
"navprevtitle": "Previous: {{$a}}",
"tagarea_book_chapters": "Book chapters",
"toc": "Table of contents"
}

View File

@ -12,7 +12,11 @@
<nav>
<ion-list>
<a ion-item text-wrap *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)" [class.core-nav-item-selected]="selected == chapter.id" [class.item-dimmed]="chapter.hidden">
<p [attr.padding-left]="chapter.level == 1 ? true : null">{{chapter.number}} <core-format-text [text]="chapter.title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId"></core-format-text></p>
<p [attr.padding-left]="addPadding && chapter.level == 1 ? true : null">
<span *ngIf="showNumbers" class="addon-mod-book-number">{{chapter.number}}</span>
<span *ngIf="showBullets" class="addon-mod-book-bullet">&bull;</span>
<core-format-text [text]="chapter.title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId"></core-format-text>
</p>
</a>
</ion-list>
</nav>

View File

@ -0,0 +1,7 @@
ion-app.app-root page-addon-mod-book-toc {
.addon-mod-book-bullet {
font-weight: bold;
font-size: 1.5em;
margin-right: 3px;
}
}

View File

@ -14,7 +14,7 @@
import { Component } from '@angular/core';
import { IonicPage, NavParams, ViewController } from 'ionic-angular';
import { AddonModBookTocChapter } from '../../providers/book';
import { AddonModBookTocChapter, AddonModBookBook, AddonModBookNumbering } from '../../providers/book';
/**
* Modal to display the TOC of a book.
@ -29,12 +29,24 @@ export class AddonModBookTocPage {
chapters: AddonModBookTocChapter[];
selected: number;
courseId: number;
showNumbers = true;
addPadding = true;
showBullets = false;
protected book: AddonModBookBook;
constructor(navParams: NavParams, private viewCtrl: ViewController) {
this.moduleId = navParams.get('moduleId');
this.chapters = navParams.get('chapters') || [];
this.selected = navParams.get('selected');
this.courseId = navParams.get('courseId');
this.book = navParams.get('book');
if (this.book) {
this.showNumbers = this.book.numbering == AddonModBookNumbering.NUMBERS;
this.showBullets = this.book.numbering == AddonModBookNumbering.BULLETS;
this.addPadding = this.book.numbering != AddonModBookNumbering.NONE;
}
}
/**

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { CoreFileProvider } from '@providers/file';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreLoggerProvider } from '@providers/logger';
@ -25,7 +24,26 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { CoreSite } from '@classes/site';
import { CoreTagItem } from '@core/tag/providers/tag';
import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
import { CoreWSProvider, CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws';
/**
* Constants to define how the chapters and subchapters of a book should be displayed in that table of contents.
*/
export const enum AddonModBookNumbering {
NONE = 0,
NUMBERS = 1,
BULLETS = 2,
INDENTED = 3,
}
/**
* Constants to define the navigation style used within a book.
*/
export const enum AddonModBookNavStyle {
TOC_ONLY = 0,
IMAGE = 1,
TEXT = 2,
}
/**
* Service that provides some features for books.
@ -37,10 +55,16 @@ export class AddonModBookProvider {
protected ROOT_CACHE_KEY = 'mmaModBook:';
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, private http: Http,
private utils: CoreUtilsProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
private logHelper: CoreCourseLogHelperProvider) {
constructor(logger: CoreLoggerProvider,
protected sitesProvider: CoreSitesProvider,
protected textUtils: CoreTextUtilsProvider,
protected fileProvider: CoreFileProvider,
protected filepoolProvider: CoreFilepoolProvider,
protected wsProvider: CoreWSProvider,
protected utils: CoreUtilsProvider,
protected courseProvider: CoreCourseProvider,
protected domUtils: CoreDomUtilsProvider,
protected logHelper: CoreCourseLogHelperProvider) {
this.logger = logger.getInstance('AddonModBookProvider');
}
@ -128,19 +152,11 @@ export class AddonModBookProvider {
return this.sitesProvider.getCurrentSite().checkAndFixPluginfileURL(indexUrl);
}
return promise.then((url) => {
// Fetch the URL content.
const promise = this.http.get(url).toPromise();
return promise.then(async (url) => {
const content = await this.wsProvider.getText(url);
return promise.then((response: Response): any => {
const content = response.text();
if (typeof content !== 'string') {
return Promise.reject(null);
} else {
// Now that we have the content, we update the SRC to point back to the external resource.
return this.domUtils.restoreSourcesInHtml(content, contentsMap[chapterId].paths);
}
});
});
}
@ -218,15 +234,15 @@ export class AddonModBookProvider {
*
* @param chapters The chapters list.
* @param chapterId The current chapter.
* @return The next chapter id.
* @return The next chapter.
*/
getNextChapter(chapters: AddonModBookTocChapter[], chapterId: string): string {
let next = '0';
getNextChapter(chapters: AddonModBookTocChapter[], chapterId: string): AddonModBookTocChapter {
let next: AddonModBookTocChapter;
for (let i = 0; i < chapters.length; i++) {
if (chapters[i].id == chapterId) {
if (typeof chapters[i + 1] != 'undefined') {
next = chapters[i + 1].id;
next = chapters[i + 1];
break;
}
}
@ -240,16 +256,16 @@ export class AddonModBookProvider {
*
* @param chapters The chapters list.
* @param chapterId The current chapter.
* @return The next chapter id.
* @return The next chapter.
*/
getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: string): string {
let previous = '0';
getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: string): AddonModBookTocChapter {
let previous: AddonModBookTocChapter;
for (let i = 0; i < chapters.length; i++) {
if (chapters[i].id == chapterId) {
break;
}
previous = chapters[i].id;
previous = chapters[i];
}
return previous;

View File

@ -20,7 +20,7 @@
<ion-label id="addon-chat-showalllabel">{{ 'addon.mod_chat.showincompletesessions' | translate }}</ion-label>
<ion-toggle [(ngModel)]="showAll" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-showalllabel"></ion-toggle>
</ion-item>
<ion-card *ngFor="let session of sessions" (click)="openSession(session)"
<ion-card *ngFor="let session of sessions" (click)="openSession(session)" class="core-clickable"
[class.addon-mod-chat-session-selected]="session.sessionstart == selectedSessionStart && groupId == selectedSessionGroupId"
[class.addon-mod-chat-session-show-more]="session.sessionusers.length < session.allsessionusers.length">
<ion-item text-wrap>

View File

@ -182,9 +182,7 @@ export class AddonModChatPrefetchHandler extends CoreCourseActivityPrefetchHandl
});
const userIds = Object.keys(users).map(Number);
return this.userProvider.prefetchProfiles(userIds, courseId, siteId).catch(() => {
// Ignore errors, some users might not exist.
});
return this.userProvider.prefetchProfiles(userIds, courseId, siteId);
});
}
}

View File

@ -26,7 +26,6 @@ import { AddonModChoicePrefetchHandler } from './providers/prefetch-handler';
import { AddonModChoiceSyncProvider } from './providers/sync';
import { AddonModChoiceSyncCronHandler } from './providers/sync-cron-handler';
import { AddonModChoiceOfflineProvider } from './providers/offline';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
export const ADDON_MOD_CHOICE_PROVIDERS: any[] = [
@ -56,7 +55,7 @@ export class AddonModChoiceModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChoiceModuleHandler,
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModChoicePrefetchHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModChoiceLinkHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModChoiceSyncCronHandler, updateManager: CoreUpdateManagerProvider,
cronDelegate: CoreCronDelegate, syncHandler: AddonModChoiceSyncCronHandler,
listLinkHandler: AddonModChoiceListLinkHandler) {
moduleDelegate.registerHandler(moduleHandler);
@ -64,21 +63,5 @@ export class AddonModChoiceModule {
contentLinksDelegate.registerHandler(linkHandler);
contentLinksDelegate.registerHandler(listLinkHandler);
cronDelegate.register(syncHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({
name: 'mma_mod_choice_offline_responses',
newName: AddonModChoiceOfflineProvider.CHOICE_TABLE,
fields: [
{
name: 'responses',
type: 'object'
},
{
name: 'deleting',
type: 'boolean'
}
]
});
}
}

View File

@ -36,7 +36,6 @@ import { AddonModDataDefaultFieldHandler } from './providers/default-field-handl
import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModDataTagAreaHandler } from './providers/tag-area-handler';
import { AddonModDataFieldModule } from './fields/field.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
export const ADDON_MOD_DATA_PROVIDERS: any[] = [
@ -77,7 +76,7 @@ export class AddonModDataModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModDataModuleHandler,
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModDataPrefetchHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModDataLinkHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler, updateManager: CoreUpdateManagerProvider,
cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler,
approveLinkHandler: AddonModDataApproveLinkHandler, deleteLinkHandler: AddonModDataDeleteLinkHandler,
showLinkHandler: AddonModDataShowLinkHandler, editLinkHandler: AddonModDataEditLinkHandler,
listLinkHandler: AddonModDataListLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
@ -93,21 +92,5 @@ export class AddonModDataModule {
contentLinksDelegate.registerHandler(listLinkHandler);
cronDelegate.register(syncHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({
name: 'mma_mod_data_entry',
newName: AddonModDataOfflineProvider.DATA_ENTRY_TABLE,
fields: [
{
name: 'fields',
type: 'object'
},
{
name: 'dataAndEntry',
delete: true
}
]
});
}
}

View File

@ -107,14 +107,6 @@ page-addon-mod-data-edit {
}
}
.input-wp input {
@include padding-horizontal(null, ($item-wp-padding-end / 2));
border-bottom: 1px solid $list-wp-border-color;
&:focus {
border-color: $text-input-wp-highlight-color;
}
}
ion-select {
width: 100%;
@include position(null, null, null, 0);

View File

@ -40,7 +40,7 @@ import { AddonModDataFieldUrlModule } from './url/url.module';
AddonModDataFieldRadiobuttonModule,
AddonModDataFieldTextModule,
AddonModDataFieldTextareaModule,
AddonModDataFieldUrlModule
AddonModDataFieldUrlModule,
],
providers: [
],

View File

@ -10,7 +10,7 @@
<ion-input type="text" [formControlName]="'f_'+field.id+'_1'" maxlength="10"></ion-input>
<span class="placeholder-icon" item-right>°E</span>
</div>
<div *ngIf="mode == 'edit'" class="addon-data-lantlong">
<div *ngIf="mode == 'edit' && showGeolocation" class="addon-data-lantlong">
<button ion-button icon-left (click)="getLocation($event)">
<ion-icon name="locate"></ion-icon>
{{ 'addon.mod_data.mylocation' | translate }}

View File

@ -17,6 +17,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { Platform } from 'ionic-angular';
import { Geolocation, GeolocationOptions } from '@ionic-native/geolocation';
import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component';
import { CoreAppProvider } from '@providers/app';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
@ -30,13 +31,17 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo
north: number;
east: number;
showGeolocation: boolean;
constructor(protected fb: FormBuilder,
protected platform: Platform,
protected geolocation: Geolocation,
protected domUtils: CoreDomUtilsProvider,
protected sanitizer: DomSanitizer) {
protected sanitizer: DomSanitizer,
protected appProvider: CoreAppProvider) {
super(fb);
this.showGeolocation = !this.appProvider.isDesktop();
}
/**

View File

@ -2,7 +2,7 @@
<ion-input *ngIf="mode == 'search'" type="text" [placeholder]="field.name" [formControlName]="'f_'+field.id"></ion-input>
<span *ngIf="mode == 'edit'" [core-mark-required]="field.required" class="core-mark-required"></span>
<core-rich-text-editor *ngIf="mode == 'edit'" item-content [control]="form.controls['f_'+field.id]" [placeholder]="field.name" [formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId"></core-rich-text-editor>
<core-rich-text-editor *ngIf="mode == 'edit'" item-content [control]="form.controls['f_'+field.id]" [placeholder]="field.name" [formControlName]="'f_'+field.id" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="componentId" [elementId]="'field_'+field.id"></core-rich-text-editor>
<core-input-errors *ngIf="error && mode == 'edit'" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
</span>

View File

@ -49,10 +49,10 @@ export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginC
* Initialize field.
*/
protected init(): void {
if (this.isShowOrListMode()) {
this.component = AddonModDataProvider.COMPONENT;
this.componentId = this.database.coursemodule;
if (this.isShowOrListMode()) {
return;
}

View File

@ -20,6 +20,7 @@ import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataFieldTextareaComponent } from './component/textarea';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -30,7 +31,8 @@ import { CoreDirectivesModule } from '@directives/directives.module';
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
CoreDirectivesModule,
CoreEditorComponentsModule,
],
providers: [
AddonModDataFieldTextareaHandler

View File

@ -21,7 +21,7 @@
<div class="addon-data-contents addon-data-entries-{{data.id}}" *ngIf="data">
<core-style [css]="data.csstemplate" prefix=".addon-data-entries-{{data.id}}"></core-style>
<form (ngSubmit)="save($event)" [formGroup]="editForm">
<form (ngSubmit)="save($event)" [formGroup]="editForm" #editFormEl>
<core-compile-html [text]="editFormRender" [jsData]="jsData" [extraImports]="extraImports"></core-compile-html>
</form>
</div>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild } from '@angular/core';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { Content, IonicPage, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { FormGroup } from '@angular/forms';
@ -40,6 +40,7 @@ import { CoreTagProvider } from '@core/tag/providers/tag';
})
export class AddonModDataEditPage {
@ViewChild(Content) content: Content;
@ViewChild('editFormEl') formElement: ElementRef;
protected module: any;
protected courseId: number;
@ -95,28 +96,25 @@ export class AddonModDataEditPage {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave || !this.entry) {
return true;
return;
}
const inputData = this.editForm.value;
return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id,
this.entry.contents).then((changed) => {
if (!changed) {
return Promise.resolve();
const changed = await this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, this.entry.contents);
if (changed) {
// Show confirmation if some data has been modified.
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
// Show confirmation if some data has been modified.
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}).then(() => {
// Delete the local files from the tmp folder.
return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id,
this.entry.contents).then((files) => {
const files = await this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, this.entry.contents);
this.fileUploaderProvider.clearTmpFiles(files);
});
});
this.domUtils.triggerFormCancelledEvent(this.formElement, this.siteId);
}
/**
@ -216,6 +214,9 @@ export class AddonModDataEditPage {
// This is done if entry is updated when editing or creating if not.
if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) {
this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.siteId);
const promises = [];
this.entryId = this.entryId || result.newentryid;

View File

@ -13,7 +13,7 @@
<a class="tab-slide" [attr.aria-selected]="!search.searchingAdvanced" (click)="changeAdvanced(false)">{{ 'addon.mod_data.search' | translate}}</a>
<a class="tab-slide" [attr.aria-selected]="search.searchingAdvanced" (click)="changeAdvanced(true)">{{ 'addon.mod_data.advancedsearch' | translate }}</a>
</div>
<form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm">
<form (ngSubmit)="searchEntries($event)" [formGroup]="searchForm" #searchFormEl>
<ion-list no-margin>
<ion-item [hidden]="search.searchingAdvanced">
<ion-input type="text" placeholder="{{ 'addon.mod_data.search' | translate}}" [(ngModel)]="search.text" name="text" formControlName="text"></ion-input>

View File

@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavParams, ViewController } from 'ionic-angular';
import { FormBuilder, FormGroup } from '@angular/forms';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -32,6 +34,8 @@ import { CoreTagProvider } from '@core/tag/providers/tag';
templateUrl: 'search.html',
})
export class AddonModDataSearchPage {
@ViewChild('searchFormEl') formElement: ElementRef;
search: any;
fields: any;
data: any;
@ -41,10 +45,17 @@ export class AddonModDataSearchPage {
jsData: any;
fieldsArray: any;
constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider,
private tagProvider: CoreTagProvider) {
constructor(params: NavParams,
protected viewCtrl: ViewController,
fb: FormBuilder,
protected utils: CoreUtilsProvider,
protected domUtils: CoreDomUtilsProvider,
protected fieldsDelegate: AddonModDataFieldsDelegate,
protected textUtils: CoreTextUtilsProvider,
protected dataHelper: AddonModDataHelperProvider,
protected tagProvider: CoreTagProvider,
protected eventsProvider: CoreEventsProvider,
protected sitesProvider: CoreSitesProvider) {
this.search = params.get('search');
this.fields = params.get('fields');
this.data = params.get('data');
@ -175,6 +186,12 @@ export class AddonModDataSearchPage {
* @param data Data to return to the page.
*/
closeModal(data?: any): void {
if (typeof data == 'undefined') {
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
} else {
this.domUtils.triggerFormSubmittedEvent(this.formElement, false, this.sitesProvider.getCurrentSiteId());
}
this.viewCtrl.dismiss(data);
}

View File

@ -126,7 +126,8 @@ export class AddonModDataProvider {
.then((entry) => {
return {
// Return provissional entry Id.
newentryid: entry
newentryid: entry,
sent: false,
};
});
};
@ -142,7 +143,11 @@ export class AddonModDataProvider {
return storeOffline();
}
return this.addEntryOnline(dataId, contents, groupId, siteId).catch((error) => {
return this.addEntryOnline(dataId, contents, groupId, siteId).then((result) => {
result.sent = true;
return result;
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
@ -194,7 +199,12 @@ export class AddonModDataProvider {
const storeOffline = (): Promise<any> => {
const action = approve ? 'approve' : 'disapprove';
return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId);
return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId)
.then(() => {
return {
sent: false,
};
});
};
// Get if the opposite action is not synced.
@ -210,7 +220,11 @@ export class AddonModDataProvider {
return storeOffline();
}
return this.approveEntryOnline(entryId, approve, siteId).catch((error) => {
return this.approveEntryOnline(entryId, approve, siteId).then(() => {
return {
sent: true,
};
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
@ -288,7 +302,12 @@ export class AddonModDataProvider {
// Convenience function to store a data to be synchronized later.
const storeOffline = (): Promise<any> => {
return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId);
return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId)
.then(() => {
return {
sent: false,
};
});
};
let justAdded = false;
@ -318,7 +337,11 @@ export class AddonModDataProvider {
return storeOffline();
}
return this.deleteEntryOnline(entryId, siteId).catch((error) => {
return this.deleteEntryOnline(entryId, siteId).then(() => {
return {
sent: true,
};
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);
@ -368,7 +391,8 @@ export class AddonModDataProvider {
return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId)
.then(() => {
return {
updated: true
updated: true,
sent: false,
};
});
};
@ -408,6 +432,7 @@ export class AddonModDataProvider {
return this.addEntry(dataId, entryId, courseId, contents, groupId, fields, siteId, forceOffline)
.then((result) => {
result.updated = true;
result.sent = true;
return result;
});
@ -418,7 +443,11 @@ export class AddonModDataProvider {
return storeOffline();
}
return this.editEntryOnline(entryId, contents, siteId).catch((error) => {
return this.editEntryOnline(entryId, contents, siteId).then((result) => {
result.sent = true;
return result;
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
return Promise.reject(error);

View File

@ -34,7 +34,6 @@ import { AddonModFeedbackPushClickHandler } from './providers/push-click-handler
import { AddonModFeedbackSyncProvider } from './providers/sync';
import { AddonModFeedbackSyncCronHandler } from './providers/sync-cron-handler';
import { AddonModFeedbackOfflineProvider } from './providers/offline';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
export const ADDON_MOD_FEEDBACK_PROVIDERS: any[] = [
@ -73,7 +72,7 @@ export class AddonModFeedbackModule {
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModFeedbackPrefetchHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModFeedbackLinkHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModFeedbackSyncCronHandler,
analysisLinkHandler: AddonModFeedbackAnalysisLinkHandler, updateManager: CoreUpdateManagerProvider,
analysisLinkHandler: AddonModFeedbackAnalysisLinkHandler,
showEntriesLinkHandler: AddonModFeedbackShowEntriesLinkHandler,
showNonRespondentsLinkHandler: AddonModFeedbackShowNonRespondentsLinkHandler,
completeLinkHandler: AddonModFeedbackCompleteLinkHandler,
@ -91,17 +90,5 @@ export class AddonModFeedbackModule {
contentLinksDelegate.registerHandler(listLinkHandler);
cronDelegate.register(syncHandler);
pushNotificationsDelegate.registerClickHandler(pushClickHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({
name: 'mma_mod_feedback_responses',
newName: AddonModFeedbackOfflineProvider.FEEDBACK_TABLE,
fields: [
{
name: 'responses',
type: 'object'
}
]
});
}
}

View File

@ -9,9 +9,6 @@ ion-app.app-root page-addon-mod-feedback-form {
.item-ios .addon-mod_feedback-form-content {
@include margin($item-ios-padding-media-top, $item-ios-padding-start, $item-ios-padding-media-bottom, 0);
}
.item-wp .addon-mod_feedback-form-content {
@include margin($item-wp-padding-media-top, ($item-wp-padding-end / 2), $item-wp-padding-media-bottom, 0);
}
.addon-mod_feedback-postfix {
font-size: 1.4rem;
}

View File

@ -4,7 +4,7 @@
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
<core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="!path" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item>
</core-context-menu>
@ -17,7 +17,7 @@
<ion-list *ngIf="contents && contents.length > 0">
<ng-container *ngFor="let file of contents">
<a *ngIf="file.type === 'folder'" ion-item class="item-media" [navPush]="'AddonModFolderIndexPage'" [navParams]="{path: file.filepath, courseId: courseId, module: file}">
<a *ngIf="file.type === 'folder'" ion-item class="item-media" navPush="AddonModFolderIndexPage" [navParams]="{path: file.filepath, courseId: courseId, module: file}">
<ion-icon name="folder" item-start></ion-icon>
<h2>{{file.name}}</h2>
</a>

View File

@ -26,6 +26,7 @@ import { AddonModForumIndexComponent } from './index/index';
import { AddonModForumPostComponent } from './post/post';
import { AddonForumDiscussionOptionsMenuComponent } from './discussion-options-menu/discussion-options-menu';
import { AddonForumPostOptionsMenuComponent } from './post-options-menu/post-options-menu';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -43,7 +44,8 @@ import { AddonForumPostOptionsMenuComponent } from './post-options-menu/post-opt
CorePipesModule,
CoreCourseComponentsModule,
CoreRatingComponentsModule,
CoreTagComponentsModule
CoreTagComponentsModule,
CoreEditorComponentsModule,
],
providers: [
],

View File

@ -43,7 +43,7 @@
</div>
<ng-container *ngFor="let discussion of offlineDiscussions">
<ion-item text-wrap (click)="openNewDiscussion(discussion.timecreated)" [attr.no-lines]="discussion.groupname" [class.core-split-item-selected]="discussion.timecreated == -selectedDiscussion" class="addon-mod-forum-discussion">
<a ion-item text-wrap (click)="openNewDiscussion(discussion.timecreated)" [attr.no-lines]="discussion.groupname" [class.core-split-item-selected]="discussion.timecreated == -selectedDiscussion" class="addon-mod-forum-discussion">
<div class="addon-mod-forum-discussion-title">
<h2>
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
@ -57,17 +57,17 @@
<p><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
</div>
</div>
</ion-item>
</a>
</ng-container>
<ng-container *ngFor="let discussion of discussions">
<ion-item (click)="openDiscussion(discussion)" [class.core-split-item-selected]="discussion.discussion == selectedDiscussion" class="addon-mod-forum-discussion">
<a ion-item (click)="openDiscussion(discussion)" [class.core-split-item-selected]="discussion.discussion == selectedDiscussion" class="addon-mod-forum-discussion">
<div class="addon-mod-forum-discussion-title">
<h2 text-wrap>
<core-icon name="fa-map-pin" *ngIf="discussion.pinned"></core-icon>
<core-icon name="fa-star" class="addon-forum-star" *ngIf="!discussion.pinned && discussion.starred"></core-icon>
<core-format-text [text]="discussion.subject" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
</h2>
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event, discussion)" *ngIf="canPin || discussion.canlock || discussion.canfavourite">
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event, discussion)" *ngIf="canPin || discussion.canlock || discussion.canfavourite" [attr.aria-label]="('core.displayoptions' | translate)">
<core-icon name="more"></core-icon>
</button>
</div>
@ -94,7 +94,7 @@
</ion-note>
</ion-col>
</ion-row>
</ion-item>
</a>
</ng-container>
<core-infinite-loading [enabled]="canLoadMore" (action)="fetchMoreDiscussions($event)" [error]="loadMoreError"></core-infinite-loading>

View File

@ -10,7 +10,7 @@
<ion-note float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate">
<core-icon name="fa-circle" color="primary"></core-icon>
</ion-note>
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled">
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)">
<core-icon name="more"></core-icon>
</button>
</div>
@ -26,7 +26,7 @@
<ion-note float-end padding-left text-end *ngIf="trackPosts && !post.postread" [attr.aria-label]="'addon.mod_forum.unread' | translate">
<core-icon name="fa-circle" color="primary"></core-icon>
</ion-note>
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled">
<button ion-button icon-only clear color="dark" (click)="showOptionsMenu($event)" *ngIf="optionsMenuEnabled" [attr.aria-label]="('core.displayoptions' | translate)">
<core-icon name="more"></core-icon>
</button>
</ng-container>
@ -57,18 +57,18 @@
</ion-item>
</div>
<ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id && !replyData.isEditing && replyData.replyingTo == post.id) || (!post.id && replyData.isEditing && replyData.replyingTo == post.parent)">
<form ion-list [id]="'addon-forum-reply-edit-form-' + uniqueId" *ngIf="(post.id && !replyData.isEditing && replyData.replyingTo == post.id) || (!post.id && replyData.isEditing && replyData.replyingTo == post.parent)" #replyFormEl>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject"></ion-input>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject" name="subject"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + post.id" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="forum && forum.cmid" elementId="message" [draftExtraParams]="{reply: post.id}"></core-rich-text-editor>
</ion-item>
<ion-item text-wrap *ngIf="accessInfo.canpostprivatereply">
<ion-label>{{ 'addon.mod_forum.privatereply' | translate }}</ion-label>
<ion-checkbox item-end [(ngModel)]="replyData.isprivatereply"></ion-checkbox>
<ion-checkbox item-end [(ngModel)]="replyData.isprivatereply" name="isprivatereply"></ion-checkbox>
</ion-item>
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>
@ -88,5 +88,5 @@
</ion-col>
</ion-row>
</ion-grid>
</ion-list>
</form>
</div>

View File

@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, Optional, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import {
Component, Input, Output, Optional, EventEmitter, OnInit, OnDestroy, ViewChild, ElementRef, OnChanges, SimpleChange
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { Content, PopoverController, ModalController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
@ -37,7 +39,7 @@ import { AddonForumPostOptionsMenuComponent } from '../post-options-menu/post-op
selector: 'addon-mod-forum-post',
templateUrl: 'addon-mod-forum-post.html',
})
export class AddonModForumPostComponent implements OnInit, OnDestroy {
export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges {
@Input() post: any; // Post.
@Input() courseId: number; // Post's course ID.
@Input() discussionId: number; // Post's' discussion ID.
@ -50,8 +52,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
@Input() accessInfo: any; // Forum access information.
@Input() parentSubject?: string; // Subject of parent post.
@Input() ratingInfo?: CoreRatingInfo; // Rating info item.
@Input() leavingPage?: boolean; // Whether the page that contains this post is being left and will be destroyed.
@Output() onPostChange: EventEmitter<void>; // Event emitted when a reply is posted or modified.
@ViewChild('replyFormEl') formElement: ElementRef;
messageControl = new FormControl();
uniqueId: string;
@ -100,6 +105,16 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
(this.forumProvider.isDeletePostAvailable() || this.forumProvider.isUpdatePostAvailable()));
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
if (changes.leavingPage && this.leavingPage) {
// Download all courses is enabled now, initialize it.
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
}
}
/**
* Deletes an online post.
*/
@ -408,6 +423,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
this.onPostChange.emit();
this.domUtils.triggerFormSubmittedEvent(this.formElement, sent, this.sitesProvider.getCurrentSiteId());
if (this.syncId) {
this.syncProvider.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}
@ -426,6 +443,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
// Reset data.
this.setReplyFormData();
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
if (this.syncId) {
this.syncProvider.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}

View File

@ -33,7 +33,6 @@ import { AddonModForumPostLinkHandler } from './providers/post-link-handler';
import { AddonModForumPushClickHandler } from './providers/push-click-handler';
import { AddonModForumTagAreaHandler } from './providers/tag-area-handler';
import { AddonModForumComponentsModule } from './components/components.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
export const ADDON_MOD_FORUM_PROVIDERS: any[] = [
@ -70,7 +69,7 @@ export class AddonModForumModule {
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModForumPrefetchHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModForumSyncCronHandler, linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModForumIndexLinkHandler, discussionHandler: AddonModForumDiscussionLinkHandler,
updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModForumListLinkHandler,
listLinkHandler: AddonModForumListLinkHandler,
pushNotificationsDelegate: CorePushNotificationsDelegate, pushClickHandler: AddonModForumPushClickHandler,
postLinkHandler: AddonModForumPostLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
tagAreaHandler: AddonModForumTagAreaHandler) {
@ -84,41 +83,5 @@ export class AddonModForumModule {
linksDelegate.registerHandler(postLinkHandler);
pushNotificationsDelegate.registerClickHandler(pushClickHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTablesMigration([
{
name: 'mma_mod_forum_offline_discussions',
newName: AddonModForumOfflineProvider.DISCUSSIONS_TABLE,
fields: [
{
name: 'forumAndUser',
delete: true
},
{
name: 'options',
type: 'object'
}
]
},
{
name: 'mma_mod_forum_offline_replies',
newName: AddonModForumOfflineProvider.REPLIES_TABLE,
fields: [
{
name: 'forumAndUser',
delete: true
},
{
name: 'discussionAndUser',
delete: true
},
{
name: 'options',
type: 'object'
}
]
}
]);
}
}

View File

@ -42,13 +42,13 @@
</ion-card>
<div *ngIf="discussion" margin-bottom class="highlight">
<addon-mod-forum-post [post]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post>
<addon-mod-forum-post [post]="discussion" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
</div>
<ion-card *ngIf="sort != 'nested'">
<ng-container *ngFor="let post of posts; first as first">
<ion-item-divider *ngIf="!first"></ion-item-divider>
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parent]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post>
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parent]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
</ng-container>
</ion-card>
@ -60,7 +60,7 @@
<ng-template #nestedPosts let-post="post">
<ion-card>
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parent]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" (onPostChange)="postListChanged()"></addon-mod-forum-post>
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component" [componentId]="cmId" [replyData]="replyData" [originalData]="originalData" [parentSubject]="postSubjects[post.parent]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()"></addon-mod-forum-post>
</ion-card>
<div padding-left *ngIf="post.children.length && post.children[0].subject">
<ng-container *ngFor="let child of post.children">

View File

@ -81,6 +81,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
cmId: number;
canPin = false;
availabilityMessage: string;
leavingPage = false;
protected forumId: number;
protected postId: number;
@ -251,20 +252,17 @@ export class AddonModForumDiscussionPage implements OnDestroy {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
let promise: any;
async ionViewCanLeave(): Promise<void> {
if (this.forumHelper.hasPostDataChanged(this.replyData, this.originalData)) {
// Show confirmation if some data has been modified.
promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} else {
promise = Promise.resolve();
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
return promise.then(() => {
// Delete the local files from the tmp folder.
this.uploaderProvider.clearTmpFiles(this.replyData.files);
});
this.leavingPage = true;
}
/**

View File

@ -9,14 +9,14 @@
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<form ion-list #editFormEl>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject"></ion-input>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="replyData.subject" name="subject"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + replyData.id" [component]="component" [componentId]="componentId"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.replyplaceholder' | translate" [name]="'mod_forum_reply_' + replyData.id" [component]="component" [componentId]="componentId" [autoSave]="true" contextLevel="module" [contextInstanceId]="forum.cmid" elementId="message" [draftExtraParams]="{edit: replyData.id}"></core-rich-text-editor>
</ion-item>
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>
@ -36,5 +36,5 @@
</ion-col>
</ion-row>
</ion-grid>
</ion-list>
</form>
</ion-content>

View File

@ -19,6 +19,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModForumComponentsModule } from '../../components/components.module';
import { AddonModForumEditPostPage } from './edit-post';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -28,6 +29,7 @@ import { AddonModForumEditPostPage } from './edit-post';
CoreComponentsModule,
CoreDirectivesModule,
AddonModForumComponentsModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModForumEditPostPage),
TranslateModule.forChild()
],

View File

@ -12,11 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { FormControl } from '@angular/forms';
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModForumProvider } from '../../providers/forum';
import { AddonModForumHelperProvider } from '../../providers/helper';
@ -30,6 +31,8 @@ import { AddonModForumHelperProvider } from '../../providers/helper';
templateUrl: 'addon-mod-forum-edit-post.html',
})
export class AddonModForumEditPostPage {
@ViewChild('editFormEl') formElement: ElementRef;
component: string; // Component this post belong to.
componentId: number; // Component ID.
forum: any; // The forum the post belongs to. Required for attachments and offline posts.
@ -48,7 +51,8 @@ export class AddonModForumEditPostPage {
protected domUtils: CoreDomUtilsProvider,
protected uploaderProvider: CoreFileUploaderProvider,
protected forumHelper: AddonModForumHelperProvider,
protected translate: TranslateService) {
protected translate: TranslateService,
protected sitesProvider: CoreSitesProvider) {
const post = params.get('post');
this.component = params.get('component');
@ -114,7 +118,13 @@ export class AddonModForumEditPostPage {
*
* @param data Data to return to the page.
*/
closeModal(data: any): void {
closeModal(data: any, ): void {
if (data) {
this.domUtils.triggerFormSubmittedEvent(this.formElement, false, this.sitesProvider.getCurrentSiteId());
} else {
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
}
this.viewCtrl.dismiss(data);
}

View File

@ -12,14 +12,14 @@
</ion-refresher>
<core-loading [hideUntil]="groupsLoaded">
<ion-list *ngIf="showForm">
<form ion-list *ngIf="showForm" #newDiscFormEl>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.subject' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="newDiscussion.subject"></ion-input>
<ion-input type="text" [placeholder]="'addon.mod_forum.subject' | translate" [(ngModel)]="newDiscussion.subject" name="subject"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_forum.message' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" name="addon_mod_forum_new_discussion" [component]="component" [componentId]="forum.cmid"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="messageControl" (contentChanged)="onMessageChange($event)" [placeholder]="'addon.mod_forum.message' | translate" name="addon_mod_forum_new_discussion" [component]="component" [componentId]="forum.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="forum.cmid" elementId="message"></core-rich-text-editor>
</ion-item>
<ion-item-divider text-wrap (click)="toggleAdvanced()" class="core-expandable">
<core-icon *ngIf="!advanced" name="fa-caret-right" item-start></core-icon>
@ -29,21 +29,21 @@
<ng-container *ngIf="advanced">
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
<ion-label>{{ 'addon.mod_forum.posttomygroups' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups"></ion-toggle>
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups"></ion-toggle>
</ion-item>
<ion-item *ngIf="showGroups">
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups" aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet">
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups" aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid">
<ion-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>{{ 'addon.mod_forum.discussionsubscription' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.subscribe"></ion-toggle>
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe"></ion-toggle>
</ion-item>
<ion-item *ngIf="canPin">
<ion-label>{{ 'addon.mod_forum.discussionpinned' | translate }}</ion-label>
<ion-toggle [(ngModel)]="newDiscussion.pin"></ion-toggle>
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin"></ion-toggle>
</ion-item>
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0" [files]="newDiscussion.files" [maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid" [allowOffline]="true"></core-attachments>
</ng-container>
@ -57,6 +57,6 @@
</ion-col>
</ion-row>
</ion-item>
</ion-list>
</form>
</core-loading>
</ion-content>

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModForumNewDiscussionPage } from './new-discussion';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import { AddonModForumNewDiscussionPage } from './new-discussion';
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModForumNewDiscussionPage),
TranslateModule.forChild()
],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnDestroy, Optional, ViewChild } from '@angular/core';
import { Component, OnDestroy, Optional, ViewChild, ElementRef } from '@angular/core';
import { FormControl } from '@angular/forms';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
@ -25,7 +25,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts';
import { CoreEditorRichTextEditorComponent } from '@core/editor/components/rich-text-editor/rich-text-editor.ts';
import { AddonModForumProvider } from '../../providers/forum';
import { AddonModForumOfflineProvider } from '../../providers/offline';
import { AddonModForumHelperProvider } from '../../providers/helper';
@ -41,7 +41,8 @@ import { AddonModForumSyncProvider } from '../../providers/sync';
})
export class AddonModForumNewDiscussionPage implements OnDestroy {
@ViewChild(CoreRichTextEditorComponent) messageEditor: CoreRichTextEditorComponent;
@ViewChild('newDiscFormEl') formElement: ElementRef;
@ViewChild(CoreEditorRichTextEditorComponent) messageEditor: CoreEditorRichTextEditorComponent;
component = AddonModForumProvider.COMPONENT;
messageControl = new FormControl();
@ -74,6 +75,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
protected syncObserver: any;
protected isDestroyed = false;
protected originalData: any;
protected forceLeave = false;
constructor(navParams: NavParams,
private navCtrl: NavController,
@ -408,6 +410,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
this.newDiscussion.postToAllGroups = false;
this.messageEditor.clearText();
this.originalData = this.utils.clone(this.newDiscussion);
this.forceLeave = true; // Avoid asking for confirmation.
// Trigger view event, to highlight the current opened discussion in the split view.
this.eventsProvider.trigger(AddonModForumProvider.VIEW_DISCUSSION_EVENT, {
@ -415,7 +418,7 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
discussion: 0
}, this.sitesProvider.getCurrentSiteId());
} else {
this.originalData = null; // Avoid asking for confirmation.
this.forceLeave = true; // Avoid asking for confirmation.
this.navCtrl.pop();
}
}
@ -477,6 +480,8 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
this.domUtils.showErrorModalDefault(null, 'addon.mod_forum.errorposttoallgroups', true);
}
this.domUtils.triggerFormSubmittedEvent(this.formElement, !!discussionIds, this.sitesProvider.getCurrentSiteId());
this.returnToDiscussions(discussionIds, discTimecreated);
}).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'addon.mod_forum.cannotcreatediscussion', true);
@ -498,6 +503,8 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
}));
return Promise.all(promises).then(() => {
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
this.returnToDiscussions();
});
}).catch(() => {
@ -517,20 +524,22 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
let promise: any;
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return;
}
if (this.forumHelper.hasPostDataChanged(this.newDiscussion, this.originalData)) {
// Show confirmation if some data has been modified.
promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} else {
promise = Promise.resolve();
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
return promise.then(() => {
// Delete the local files from the tmp folder.
this.uploaderProvider.clearTmpFiles(this.newDiscussion.files);
});
if (this.formElement) {
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
}
}
/**

View File

@ -37,6 +37,7 @@ export class AddonModForumProvider {
static VIEW_DISCUSSION_EVENT = 'addon_mod_forum_view_discussion';
static CHANGE_DISCUSSION_EVENT = 'addon_mod_forum_change_discussion_status';
static MARK_READ_EVENT = 'addon_mod_forum_mark_read';
static LEAVING_POSTS_PAGE = 'addon_mod_forum_leaving_posts_page';
static PREFERENCE_SORTORDER = 'forum_discussionlistsortorder';
static SORTORDER_LASTPOST_DESC = 1;
@ -244,13 +245,14 @@ export class AddonModForumProvider {
* Check if a user can post to all groups.
*
* @param forumId Forum ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with an object with the following properties:
* - status (boolean)
* - canpindiscussions (boolean)
* - cancreateattachment (boolean)
*/
canAddDiscussionToAll(forumId: number): Promise<any> {
return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS);
canAddDiscussionToAll(forumId: number, siteId?: string): Promise<any> {
return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, siteId);
}
/**

View File

@ -107,12 +107,13 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
* Get the posts to be prefetched.
*
* @param forum Forum instance.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with array of posts.
*/
protected getPostsForPrefetch(forum: any): Promise<any[]> {
protected getPostsForPrefetch(forum: any, siteId?: string): Promise<any[]> {
const promises = this.forumProvider.getAvailableSortOrders().map((sortOrder) => {
// Get discussions in first 2 pages.
return this.forumProvider.getDiscussionsInPages(forum.id, sortOrder.value, false, 2).then((response) => {
return this.forumProvider.getDiscussionsInPages(forum.id, sortOrder.value, false, 2, 0, siteId).then((response) => {
if (response.error) {
return Promise.reject(null);
}
@ -120,7 +121,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
const promises = [];
response.discussions.forEach((discussion) => {
promises.push(this.forumProvider.getDiscussionPosts(discussion.discussion));
promises.push(this.forumProvider.getDiscussionPosts(discussion.discussion, siteId));
});
return Promise.all(promises);
@ -200,41 +201,31 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
*/
protected prefetchForum(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
// Get the forum data.
return this.forumProvider.getForum(courseId, module.id).then((forum) => {
return this.forumProvider.getForum(courseId, module.id, siteId).then((forum) => {
const promises = [];
// Prefetch the posts.
promises.push(this.getPostsForPrefetch(forum).then((posts) => {
promises.push(this.getPostsForPrefetch(forum, siteId).then((posts) => {
const promises = [];
// Gather user profile images.
const avatars = {}; // List of user avatars, preventing duplicates.
posts.forEach((post) => {
if (post.userpictureurl) {
avatars[post.userpictureurl] = true;
}
});
// Prefetch intro files, attachments, embedded files and user avatars.
const avatarFiles = Object.keys(avatars).map((url) => {
return { fileurl: url };
});
const files = this.getIntroFilesFromInstance(module, forum).concat(this.getPostsFiles(posts)).concat(avatarFiles);
const files = this.getIntroFilesFromInstance(module, forum).concat(this.getPostsFiles(posts));
promises.push(this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id));
// Prefetch groups data.
promises.push(this.prefetchGroupsInfo(forum, courseId, forum.cancreatediscussions));
promises.push(this.prefetchGroupsInfo(forum, courseId, forum.cancreatediscussions, siteId));
// Prefetch avatars.
promises.push(this.userProvider.prefetchUserAvatars(posts, 'userpictureurl', siteId));
return Promise.all(promises);
}));
// Prefetch access information.
promises.push(this.forumProvider.getAccessInformation(forum.id));
promises.push(this.forumProvider.getAccessInformation(forum.id, false, siteId));
// Prefetch sort order preference.
if (this.forumProvider.isDiscussionListSortingAvailable()) {
promises.push(this.userProvider.getUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER));
promises.push(this.userProvider.getUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, siteId));
}
return Promise.all(promises);
@ -247,30 +238,31 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
* @param module The module object returned by WS.
* @param courseI Course ID the module belongs to.
* @param canCreateDiscussions Whether the user can create discussions in the forum.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when group data has been prefetched.
*/
protected prefetchGroupsInfo(forum: any, courseId: number, canCreateDiscussions: boolean): any {
protected prefetchGroupsInfo(forum: any, courseId: number, canCreateDiscussions: boolean, siteId?: string): any {
// Check group mode.
return this.groupsProvider.getActivityGroupMode(forum.cmid).then((mode) => {
return this.groupsProvider.getActivityGroupMode(forum.cmid, siteId).then((mode) => {
if (mode !== CoreGroupsProvider.SEPARATEGROUPS && mode !== CoreGroupsProvider.VISIBLEGROUPS) {
// Activity doesn't use groups. Prefetch canAddDiscussionToAll to determine if user can pin/attach.
return this.forumProvider.canAddDiscussionToAll(forum.id).catch(() => {
return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => {
// Ignore errors.
});
}
// Activity uses groups, prefetch allowed groups.
return this.groupsProvider.getActivityAllowedGroups(forum.cmid).then((result) => {
return this.groupsProvider.getActivityAllowedGroups(forum.cmid, undefined, siteId).then((result) => {
if (mode === CoreGroupsProvider.SEPARATEGROUPS) {
// Groups are already filtered by WS. Prefetch canAddDiscussionToAll to determine if user can pin/attach.
return this.forumProvider.canAddDiscussionToAll(forum.id).catch(() => {
return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => {
// Ignore errors.
});
}
if (canCreateDiscussions) {
// Prefetch data to check the visible groups when creating discussions.
return this.forumProvider.canAddDiscussionToAll(forum.id).catch(() => {
return this.forumProvider.canAddDiscussionToAll(forum.id, siteId).catch(() => {
// The call failed, let's assume he can't.
return {
status: false
@ -284,7 +276,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
// The user can't post to all groups, let's check which groups he can post to.
const groupPromises = [];
result.groups.forEach((group) => {
groupPromises.push(this.forumProvider.canAddDiscussion(forum.id, group.id).catch(() => {
groupPromises.push(this.forumProvider.canAddDiscussion(forum.id, group.id, siteId).catch(() => {
// Ignore errors.
}));
});

View File

@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { CoreSearchComponentsModule } from '@core/search/components/components.module';
import { AddonModGlossaryIndexComponent } from './index/index';
import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-picker';
@ -35,7 +36,8 @@ import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-p
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
CoreCourseComponentsModule
CoreCourseComponentsModule,
CoreSearchComponentsModule,
],
providers: [
],

View File

@ -1,7 +1,10 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons end>
<button *ngIf="glossary" ion-button icon-only (click)="openModePicker($event)" [attr.aria-label]="'addon.mod_glossary.browsemode' | translate">
<ion-icon name="funnel"></ion-icon>
<button *ngIf="glossary && glossary.browsemodes && glossary.browsemodes.length > 1" ion-button icon-only (click)="openModePicker($event)" [attr.aria-label]="'addon.mod_glossary.browsemode' | translate">
<core-icon name="fa-sort"></core-icon>
</button>
<button *ngIf="glossary" ion-button icon-only (click)="toggleSearch()" [attr.aria-label]="'addon.mod_glossary.bysearch' | translate">
<ion-icon name="search"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
@ -29,16 +32,16 @@
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
</ion-card>
<core-search-box *ngIf="viewMode == 'search'" (onSubmit)="search($event)" [placeholder]="'addon.mod_glossary.searchquery' | translate" [autoFocus]="true" [lengthCheck]="2" [showClear]="false"></core-search-box>
<core-search-box *ngIf="isSearch" (onSubmit)="search($event)" [placeholder]="'addon.mod_glossary.searchquery' | translate" [autoFocus]="true" [lengthCheck]="2" (onClear)="toggleSearch($event)" searchArea="AddonModGlossary-{{module.id}}"></core-search-box>
<core-loading [hideUntil]="loaded" class="core-loading-center">
<ion-list *ngIf="viewMode != 'search' && offlineEntries.length > 0">
<ion-list *ngIf="!isSearch && offlineEntries.length > 0">
<ion-item-divider>
{{ 'addon.mod_glossary.entriestobesynced' | translate }}
</ion-item-divider>
<a ion-item *ngFor="let entry of offlineEntries" (click)="openNewEntry(entry)" detail-none>
<p>{{entry.concept}}</p>
<p><core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="glossary.coursemodule" [courseId]="courseId"></core-format-text></p>
</a>
</ion-list>
@ -50,7 +53,7 @@
</ion-item-divider>
<a ion-item (click)="openEntry(entry.id)" [class.core-split-item-selected]="entry.id == selectedEntry" detail-none>
<p>{{entry.concept}}</p>
<p><core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="glossary.coursemodule" [courseId]="courseId"></core-format-text></p>
</a>
</ng-container>
</ion-list>

View File

@ -25,7 +25,7 @@ import { AddonModGlossarySyncProvider } from '../../providers/sync';
import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker';
import { AddonModGlossaryPrefetchHandler } from '../../providers/prefetch-handler';
type FetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'search' | 'letter_all';
type FetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';
/**
* Component that displays a glossary entry page.
@ -41,8 +41,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
component = AddonModGlossaryProvider.COMPONENT;
moduleName = 'glossary';
fetchMode: FetchMode;
viewMode: string;
isSearch = false;
entries = [];
offlineEntries = [];
@ -60,6 +58,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
protected showDivider: (entry: any, previous?: any) => boolean;
protected getDivider: (entry: any) => string;
protected addEntryObserver: any;
protected fetchMode: FetchMode;
protected viewMode: string;
protected fetchedEntriesCanLoadMore = false;
protected fetchedEntries = [];
hasOfflineRatings: boolean;
protected ratingOfflineObserver: any;
@ -187,6 +189,21 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
Array.prototype.push.apply(this.entries, result.entries);
} else {
this.entries = result.entries;
if (this.splitviewCtrl.isOn()) {
// Load the first entry.
if (this.entries.length > 0) {
const found = this.selectedEntry && this.entries.some((entry) => entry.id == this.selectedEntry);
// The current selected entry is not found in the current list, open first item.
if (!found) {
this.openEntry(this.entries[0].id);
}
} else {
this.selectedEntry = null;
this.splitviewCtrl.emptyDetails();
}
}
}
this.canLoadMore = this.entries.length < result.count;
}).catch((error) => {
@ -252,6 +269,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
*/
protected switchMode(mode: FetchMode): void {
this.fetchMode = mode;
this.isSearch = false;
switch (mode) {
case 'author_all':
@ -294,15 +312,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
this.getDivider = null;
this.showDivider = (): boolean => false;
break;
case 'search':
// Search for entries.
this.viewMode = 'search';
this.fetchFunction = this.glossaryProvider.getEntriesBySearch;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesBySearch;
this.fetchArguments = null; // Dynamically set later.
this.getDivider = null;
this.showDivider = (): boolean => false;
break;
case 'letter_all':
default:
// Consider it is 'letter_all'.
@ -311,7 +320,12 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
this.fetchFunction = this.glossaryProvider.getEntriesByLetter;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByLetter;
this.fetchArguments = [this.glossary.id, 'ALL'];
this.getDivider = (entry: any): string => entry.concept.substr(0, 1).toUpperCase();
this.getDivider = (entry: any): string => {
// Try to get the first letter without HTML tags.
const noTags = this.textUtils.cleanTags(entry.concept);
return (noTags || entry.concept).substr(0, 1).toUpperCase();
};
this.showDivider = (entry?: any, previous?: any): boolean => {
return !previous || this.getDivider(entry) != this.getDivider(previous);
};
@ -341,26 +355,15 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
*/
openModePicker(event: MouseEvent): void {
const popover = this.popoverCtrl.create(AddonModGlossaryModePickerPopoverComponent, {
glossary: this.glossary,
selectedMode: this.fetchMode
browsemodes: this.glossary.browsemodes,
selectedMode: this.isSearch ? '' : this.fetchMode
});
popover.onDidDismiss((newMode: FetchMode) => {
if (newMode === this.fetchMode) {
return;
}
this.loadingMessage = this.translate.instant('core.loading');
this.domUtils.scrollToTop(this.content);
this.switchMode(newMode);
if (this.fetchMode === 'search') {
// If it's not an instant search, then we reset the values.
this.entries = [];
this.canLoadMore = false;
} else {
this.loaded = false;
this.loadContent();
popover.onDidDismiss((mode: FetchMode) => {
if (mode !== this.fetchMode) {
this.changeFetchMode(mode);
} else if (this.isSearch) {
this.toggleSearch();
}
});
@ -369,6 +372,44 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
});
}
/**
* Toggles between search and fetch mode.
*/
toggleSearch(): void {
if (this.isSearch) {
this.isSearch = false;
this.entries = this.fetchedEntries;
this.canLoadMore = this.fetchedEntriesCanLoadMore;
this.switchMode(this.fetchMode);
} else {
// Search for entries.
this.fetchFunction = this.glossaryProvider.getEntriesBySearch;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesBySearch;
this.fetchArguments = null; // Dynamically set later.
this.getDivider = null;
this.showDivider = (): boolean => false;
this.isSearch = true;
this.fetchedEntries = this.entries;
this.fetchedEntriesCanLoadMore = this.canLoadMore;
this.canLoadMore = false;
this.entries = [];
}
}
/**
* Change fetch mode
* @param {FetchMode} mode [description]
*/
changeFetchMode(mode: FetchMode): void {
this.isSearch = false;
this.loadingMessage = this.translate.instant('core.loading');
this.domUtils.scrollToTop(this.content);
this.switchMode(mode);
this.loaded = false;
this.loadContent();
}
/**
* Opens an entry.
*

View File

@ -27,14 +27,10 @@ export class AddonModGlossaryModePickerPopoverComponent {
selectedMode: string;
constructor(navParams: NavParams, private viewCtrl: ViewController) {
this.selectedMode = navParams.get('selectedMode');
const glossary = navParams.get('glossary');
this.selectedMode = navParams.get('selectedMode') || '';
const browsemodes = navParams.get('browsemodes');
// Preparing browse modes.
this.modes = [
{key: 'search', langkey: 'addon.mod_glossary.bysearch'}
];
glossary.browsemodes.forEach((mode) => {
browsemodes.forEach((mode) => {
switch (mode) {
case 'letter' :
this.modes.push({key: 'letter_all', langkey: 'addon.mod_glossary.byalphabet'});

View File

@ -31,7 +31,6 @@ import { AddonModGlossaryListLinkHandler } from './providers/list-link-handler';
import { AddonModGlossaryEditLinkHandler } from './providers/edit-link-handler';
import { AddonModGlossaryTagAreaHandler } from './providers/tag-area-handler';
import { AddonModGlossaryComponentsModule } from './components/components.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
export const ADDON_MOD_GLOSSARY_PROVIDERS: any[] = [
@ -67,7 +66,7 @@ export class AddonModGlossaryModule {
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModGlossaryPrefetchHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModGlossarySyncCronHandler, linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModGlossaryIndexLinkHandler, discussionHandler: AddonModGlossaryEntryLinkHandler,
updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModGlossaryListLinkHandler,
listLinkHandler: AddonModGlossaryListLinkHandler,
editLinkHandler: AddonModGlossaryEditLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
tagAreaHandler: AddonModGlossaryTagAreaHandler) {
@ -79,29 +78,5 @@ export class AddonModGlossaryModule {
linksDelegate.registerHandler(listLinkHandler);
linksDelegate.registerHandler(editLinkHandler);
tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({
name: 'mma_mod_glossary_add_entry',
newName: AddonModGlossaryOfflineProvider.ENTRIES_TABLE,
fields: [
{
name: 'glossaryAndConcept',
delete: true
},
{
name: 'glossaryAndUser',
delete: true
},
{
name: 'options',
type: 'object'
},
{
name: 'attachments',
type: 'object'
}
]
});
}
}

View File

@ -8,24 +8,24 @@
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list>
<form ion-list #editFormEl>
<ion-item>
<ion-label stacked>{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept"></ion-input>
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept" name="concept"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="definitionControl" (contentChanged)="onDefinitionChange($event)" [placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component" [componentId]="glossary.cmid"></core-rich-text-editor>
<core-rich-text-editor item-content [control]="definitionControl" (contentChanged)="onDefinitionChange($event)" [placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component" [componentId]="glossary.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="module.id" elementId="definition_editor" [draftExtraParams]="editorExtraParams"></core-rich-text-editor>
</ion-item>
<ion-item *ngIf="categories.length > 0">
<ion-label stacked id="addon-mod-glossary-categories-label">{{ 'addon.mod_glossary.categories' | translate }}</ion-label>
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label" interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate">
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label" interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories">
<ion-option *ngFor="let category of categories" [value]="category.id">{{ category.name }}</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label stacked id="addon-mod-glossary-aliases-label">{{ 'addon.mod_glossary.aliases' | translate }}</ion-label>
<ion-textarea [(ngModel)]="options.aliases" rows="1" core-auto-rows aria-labelledby="addon-mod-glossary-aliases-label"></ion-textarea>
<ion-textarea [(ngModel)]="options.aliases" rows="1" core-auto-rows aria-labelledby="addon-mod-glossary-aliases-label" name="aliases"></ion-textarea>
</ion-item>
<ion-item-divider>{{ 'addon.mod_glossary.attachment' | translate }}</ion-item-divider>
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.cmid" [allowOffline]="true"></core-attachments>
@ -33,17 +33,17 @@
<ion-item-divider>{{ 'addon.mod_glossary.linking' | translate }}</ion-item-divider>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
<ion-toggle [(ngModel)]="options.usedynalink"></ion-toggle>
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
</ion-item>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive"></ion-toggle>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive"></ion-toggle>
</ion-item>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch"></ion-toggle>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle>
</ion-item>
</ng-container>
</ion-list>
</form>
</core-loading>
</ion-content>

View File

@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModGlossaryEditPage } from './edit';
import { CoreEditorComponentsModule } from '@core/editor/components/components.module';
@NgModule({
declarations: [
@ -26,6 +27,7 @@ import { AddonModGlossaryEditPage } from './edit';
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CoreEditorComponentsModule,
IonicPageModule.forChild(AddonModGlossaryEditPage),
TranslateModule.forChild()
],

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { FormControl } from '@angular/forms';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
@ -34,6 +34,8 @@ import { AddonModGlossaryHelperProvider } from '../../providers/helper';
templateUrl: 'edit.html',
})
export class AddonModGlossaryEditPage implements OnInit {
@ViewChild('editFormEl') formElement: ElementRef;
component = AddonModGlossaryProvider.COMPONENT;
loaded = false;
entry = {
@ -51,6 +53,7 @@ export class AddonModGlossaryEditPage implements OnInit {
attachments = [];
definitionControl = new FormControl();
categories = [];
editorExtraParams: {[name: string]: any} = {};
protected courseId: number;
protected module: any;
@ -113,6 +116,10 @@ export class AddonModGlossaryEditPage implements OnInit {
this.originalData.files = files.slice();
});
}
if (entry.id) {
this.editorExtraParams.id = entry.id;
}
}
this.definitionControl.setValue(this.entry.definition);
@ -140,20 +147,20 @@ export class AddonModGlossaryEditPage implements OnInit {
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
let promise: any;
if (!this.saved && this.glossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) {
// Show confirmation if some data has been modified.
promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} else {
promise = Promise.resolve();
async ionViewCanLeave(): Promise<void> {
if (this.saved) {
return;
}
if (this.glossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) {
// Show confirmation if some data has been modified.
await this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
}
return promise.then(() => {
// Delete the local files from the tmp folder.
this.uploaderProvider.clearTmpFiles(this.attachments);
});
this.domUtils.triggerFormCancelledEvent(this.formElement, this.sitesProvider.getCurrentSiteId());
}
/**
@ -246,6 +253,8 @@ export class AddonModGlossaryEditPage implements OnInit {
};
this.eventsProvider.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.domUtils.triggerFormSubmittedEvent(this.formElement, !!entryId, this.sitesProvider.getCurrentSiteId());
this.saved = true;
this.navCtrl.pop();
}).catch((error) => {

Some files were not shown because too many files have changed in this diff Show More