Merge pull request #1340 from dpalou/MOBILE-2428

Mobile 2428
main
Juan Leyva 2018-06-15 11:08:38 +02:00 committed by GitHub
commit 78a92e5509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 435 additions and 178 deletions

View File

@ -33,7 +33,7 @@
<preference name="AndroidPersistentFileLocation" value="Compatibility" /> <preference name="AndroidPersistentFileLocation" value="Compatibility" />
<preference name="iosPersistentFileLocation" value="Compatibility" /> <preference name="iosPersistentFileLocation" value="Compatibility" />
<preference name="SplashScreen" value="screen" /> <preference name="SplashScreen" value="screen" />
<preference name="SplashScreenDelay" value="10000" /> <preference name="SplashScreenDelay" value="15000" />
<preference name="SplashMaintainAspectRatio" value="true" /> <preference name="SplashMaintainAspectRatio" value="true" />
<feature name="StatusBar"> <feature name="StatusBar">
<param name="ios-package" onload="true" value="CDVStatusBar" /> <param name="ios-package" onload="true" value="CDVStatusBar" />

View File

@ -73,5 +73,8 @@ export class AddonCalendarModule {
'categoryid', 'groupid', 'userid', 'instance', 'modulename', 'timemodified', 'repeatid', 'visible', 'uuid', 'categoryid', 'groupid', 'userid', 'instance', 'modulename', 'timemodified', 'repeatid', 'visible', 'uuid',
'sequence', 'subscriptionid', 'notificationtime'] 'sequence', 'subscriptionid', 'notificationtime']
}); });
// Migrate the component name.
updateManager.registerLocalNotifComponentMigration('mmaCalendarComponent', AddonCalendarProvider.COMPONENT);
} }
} }

View File

@ -31,9 +31,9 @@ export class AddonCalendarProvider {
static DAYS_INTERVAL = 30; static DAYS_INTERVAL = 30;
static COMPONENT = 'AddonCalendarEvents'; static COMPONENT = 'AddonCalendarEvents';
static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent'; static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent';
protected DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime';
static DEFAULT_NOTIFICATION_TIME = 60;
protected ROOT_CACHE_KEY = 'mmaCalendar:'; protected ROOT_CACHE_KEY = 'mmaCalendar:';
protected DEFAULT_NOTIFICATION_TIME = 60;
// Variables for database. // Variables for database.
static EVENTS_TABLE = 'addon_calendar_events'; static EVENTS_TABLE = 'addon_calendar_events';
@ -136,6 +136,18 @@ export class AddonCalendarProvider {
this.sitesProvider.createTablesFromSchema(this.tablesSchema); this.sitesProvider.createTablesFromSchema(this.tablesSchema);
} }
/**
* Get all calendar events from local Db.
*
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<any[]>} Promise resolved with all the events.
*/
getAllEventsFromLocalDb(siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getAllRecords(AddonCalendarProvider.EVENTS_TABLE);
});
}
/** /**
* Get the configured default notification time. * Get the configured default notification time.
* *
@ -145,9 +157,9 @@ export class AddonCalendarProvider {
getDefaultNotificationTime(siteId?: string): Promise<number> { getDefaultNotificationTime(siteId?: string): Promise<number> {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
const key = this.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
return this.configProvider.get(key, this.DEFAULT_NOTIFICATION_TIME); return this.configProvider.get(key, AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME);
} }
/** /**
@ -496,11 +508,24 @@ export class AddonCalendarProvider {
setDefaultNotificationTime(time: number, siteId?: string): Promise<any[]> { setDefaultNotificationTime(time: number, siteId?: string): Promise<any[]> {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
const key = this.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; const key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId;
return this.configProvider.set(key, time); return this.configProvider.set(key, time);
} }
/**
* Store an event in local DB as it is.
*
* @param {any} event Event to store.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<any>} Promise resolved when stored.
*/
storeEventInLocalDb(event: any, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, event);
});
}
/** /**
* Store events in local DB. * Store events in local DB.
* *

View File

@ -131,5 +131,9 @@ export class AddonMessagesModule {
} }
] ]
}); });
// Migrate the component name.
updateManager.registerLocalNotifComponentMigration('mmaMessagesPushSimulation',
AddonMessagesProvider.PUSH_SIMULATION_COMPONENT);
} }
} }

View File

@ -15,9 +15,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label stacked>{{ 'addon.mod_glossary.definition' | translate }}</ion-label> <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"></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"></core-rich-text-editor>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="glossary.cmid" -->
</ion-item> </ion-item>
<ion-item *ngIf="categories.length > 0"> <ion-item *ngIf="categories.length > 0">
<ion-label stacked id="addon-mod-glossary-categories-label">{{ 'addon.mod_glossary.categories' | translate }}</ion-label> <ion-label stacked id="addon-mod-glossary-categories-label">{{ 'addon.mod_glossary.categories' | translate }}</ion-label>

View File

@ -37,7 +37,7 @@
<ion-item> <ion-item>
<core-show-password item-content [name]="'password'"> <core-show-password item-content [name]="'password'">
<ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> <ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" core-auto-focus #passwordinput></ion-input> <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" [core-auto-focus] #passwordinput></ion-input>
</core-show-password> </core-show-password>
</ion-item> </ion-item>
<ion-item> <ion-item>

View File

@ -13,7 +13,7 @@
<ion-item> <ion-item>
<core-show-password item-content [name]="'password'"> <core-show-password item-content [name]="'password'">
<ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label> <ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" core-auto-focus #passwordinput></ion-input> <ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" [core-auto-focus] #passwordinput></ion-input>
</core-show-password> </core-show-password>
</ion-item> </ion-item>
<ion-item> <ion-item>

View File

@ -20,7 +20,9 @@
<core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content" [component]="component" [componentId]="componentId"></core-rich-text-editor> <core-rich-text-editor item-content [control]="contentControl" [placeholder]="'core.content' | translate" name="wiki_page_content" [component]="component" [componentId]="componentId"></core-rich-text-editor>
</ion-item> </ion-item>
<ion-badge color="danger" *ngIf="wrongVersionLock" item-end>{{ 'addon.mod_wiki.wrongversionlock' | translate }}</ion-badge> <!-- @todo: Check this. --> <ion-item *ngIf="wrongVersionLock" text-center class="addon-mod_wiki-wrongversionlock" >
<ion-badge color="danger" padding>{{ 'addon.mod_wiki.wrongversionlock' | translate }}</ion-badge>
</ion-item>
</form> </form>
</core-loading> </core-loading>
</ion-content> </ion-content>

View File

@ -0,0 +1,5 @@
page-addon-mod-wiki-edit {
.addon-mod_wiki-wrongversionlock .label {
margin: 0;
}
}

View File

@ -83,5 +83,8 @@ export class AddonPushNotificationsModule {
} }
] ]
}); });
// Migrate the component name.
updateManager.registerLocalNotifComponentMigration('mmaPushNotifications', AddonPushNotificationsProvider.COMPONENT);
} }
} }

View File

@ -10,8 +10,10 @@
</ion-item> </ion-item>
<!-- Edit. --> <!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname" [formGroup]="form"> <ion-item *ngIf="edit && field && field.shortname" [formGroup]="form">
<ion-label [core-mark-required]="field.required">{{ field.name }}</ion-label> <ion-label>
<span [core-mark-required]="field.required">{{ field.name }}</span>
<core-input-errors [control]="form.controls[field.modelName]"></core-input-errors>
</ion-label>
<ion-checkbox item-end [formControlName]="field.modelName"> <ion-checkbox item-end [formControlName]="field.modelName">
</ion-checkbox> </ion-checkbox>
<core-input-errors [control]="form.controls[field.modelName]" [errorMessages]="errors"></core-input-errors>
</ion-item> </ion-item>

View File

@ -7,4 +7,5 @@
<ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form"> <ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form">
<ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label> <ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label>
<ion-datetime [formControlName]="field.modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="field.format" [max]="field.max" [min]="field.min"></ion-datetime> <ion-datetime [formControlName]="field.modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="field.format" [max]="field.max" [min]="field.min"></ion-datetime>
<core-input-errors item-content [control]="form.controls[field.modelName]"></core-input-errors>
</ion-item> </ion-item>

View File

@ -10,4 +10,5 @@
<ion-option value="">{{ 'core.choosedots' | translate }}</ion-option> <ion-option value="">{{ 'core.choosedots' | translate }}</ion-option>
<ion-option *ngFor="let option of field.options" [value]="option">{{option}}</ion-option> <ion-option *ngFor="let option of field.options" [value]="option">{{option}}</ion-option>
</ion-select> </ion-select>
<core-input-errors item-content [control]="form.controls[field.modelName]"></core-input-errors>
</ion-item> </ion-item>

View File

@ -7,4 +7,5 @@
<ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form"> <ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form">
<ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label> <ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label>
<ion-input [type]="field.inputType" [formControlName]="field.modelName" [placeholder]="field.name" maxlength="{{field.maxlength}}"></ion-input> <ion-input [type]="field.inputType" [formControlName]="field.modelName" [placeholder]="field.name" maxlength="{{field.maxlength}}"></ion-input>
<core-input-errors item-content [control]="form.controls[field.modelName]"></core-input-errors>
</ion-item> </ion-item>

View File

@ -5,6 +5,9 @@
</ion-item> </ion-item>
<!-- Edit. --> <!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form"> <ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form">
<ion-label stacked [core-mark-required]="field.required">{{ field.name }}</ion-label> <ion-label stacked>
<span [core-mark-required]="field.required">{{ field.name }}</span>
<core-input-errors [control]="control"></core-input-errors>
</ion-label>
<core-rich-text-editor item-content [control]="control" [placeholder]="field.name"></core-rich-text-editor> <core-rich-text-editor item-content [control]="control" [placeholder]="field.name"></core-rich-text-editor>
</ion-item> </ion-item>

View File

@ -15,7 +15,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Platform } from 'ionic-angular'; import { Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar'; import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreLangProvider } from '@providers/lang'; import { CoreLangProvider } from '@providers/lang';
@ -33,7 +32,7 @@ export class MoodleMobileApp implements OnInit {
protected logger; protected logger;
protected lastUrls = {}; protected lastUrls = {};
constructor(private platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, logger: CoreLoggerProvider, constructor(private platform: Platform, statusBar: StatusBar, logger: CoreLoggerProvider,
private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider, private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider,
private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider) { private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider) {
this.logger = logger.getInstance('AppComponent'); this.logger = logger.getInstance('AppComponent');
@ -46,8 +45,6 @@ export class MoodleMobileApp implements OnInit {
} else { } else {
statusBar.styleDefault(); statusBar.styleDefault();
} }
splashScreen.hide();
}); });
} }

View File

@ -1226,7 +1226,7 @@ export class CoreSite {
} }
if (alertMessage) { if (alertMessage) {
const alert = this.domUtils.showAlert(this.translate.instant('core.notice'), alertMessage, undefined, 3000); this.domUtils.showAlert(this.translate.instant('core.notice'), alertMessage, undefined, 3000).then((alert) => {
alert.onDidDismiss(() => { alert.onDidDismiss(() => {
if (inApp) { if (inApp) {
resolve(this.utils.openInApp(url, options)); resolve(this.utils.openInApp(url, options));
@ -1234,6 +1234,7 @@ export class CoreSite {
resolve(this.utils.openInBrowser(url)); resolve(this.utils.openInBrowser(url));
} }
}); });
});
} else { } else {
if (inApp) { if (inApp) {
resolve(this.utils.openInApp(url, options)); resolve(this.utils.openInApp(url, options));

View File

@ -1,7 +1,15 @@
<div class="core-input-error-container" role="alert" *ngIf="(formControl && formControl.dirty && !formControl.valid) || errorText"> <div class="core-input-error-container" role="alert" *ngIf="(formControl && formControl.dirty && !formControl.valid) || errorText">
<ng-container *ngIf="formControl && formControl.dirty && !formControl.valid"> <ng-container *ngIf="formControl && formControl.dirty && !formControl.valid">
<ng-container *ngFor="let error of errorKeys"> <ng-container *ngFor="let error of errorKeys">
<div *ngIf="formControl.hasError(error)" class="core-input-error">{{errorMessages[error]}}</div> <div *ngIf="formControl.hasError(error)" class="core-input-error">
<span *ngIf="errorMessages[error]">{{errorMessages[error]}}</span>
<span *ngIf="!errorMessages[error] && error == 'max' && formControl.errors.max">
{{ 'core.login.invalidvaluemax' | translate:{$a: formControl.errors.max.max} }}
</span>
<span *ngIf="!errorMessages[error] && error == 'min' && formControl.errors.min">
{{ 'core.login.invalidvaluemin' | translate:{$a: formControl.errors.min.min} }}
</span>
</div>
</ng-container> </ng-container>
</ng-container> </ng-container>
<div *ngIf="errorText" class="core-input-error">{{ errorText }}</div> <div *ngIf="errorText" class="core-input-error">{{ errorText }}</div>

View File

@ -75,7 +75,9 @@ export class CoreInputErrorsComponent implements OnInit, OnChanges {
this.errorMessages.time = this.errorMessages.time || this.translate.instant('core.login.invalidtime'); this.errorMessages.time = this.errorMessages.time || this.translate.instant('core.login.invalidtime');
this.errorMessages.url = this.errorMessages.url || this.translate.instant('core.login.invalidurl'); this.errorMessages.url = this.errorMessages.url || this.translate.instant('core.login.invalidurl');
// @todo: Check how to handle min/max errors once we have a test case to use. Also, review previous errors. // Set empty values by default, the default error messages will be built in the template when needed.
this.errorMessages.max = this.errorMessages.max || '';
this.errorMessages.min = this.errorMessages.min || '';
} }
/** /**

View File

@ -197,6 +197,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
} else { } else {
// Don't emit event so our valueChanges doesn't get notified by this change. // Don't emit event so our valueChanges doesn't get notified by this change.
this.control.setValue(this.editorElement.innerHTML, {emitEvent: false}); this.control.setValue(this.editorElement.innerHTML, {emitEvent: false});
this.control.markAsDirty();
this.textarea.value = this.editorElement.innerHTML; this.textarea.value = this.editorElement.innerHTML;
} }
} else { } else {
@ -205,6 +206,7 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
} else { } else {
// Don't emit event so our valueChanges doesn't get notified by this change. // Don't emit event so our valueChanges doesn't get notified by this change.
this.control.setValue(this.textarea.value, {emitEvent: false}); this.control.setValue(this.textarea.value, {emitEvent: false});
this.control.markAsDirty();
} }
} }
@ -376,6 +378,11 @@ export class CoreRichTextEditorComponent implements AfterContentInit, OnDestroy
* Treating videos and audios in here is complex, so if a user manually adds one he won't be able to play it in the editor. * Treating videos and audios in here is complex, so if a user manually adds one he won't be able to play it in the editor.
*/ */
protected treatExternalContent(): void { protected treatExternalContent(): void {
if (!this.sitesProvider.isLoggedIn()) {
// Only treat external content if the user is logged in.
return;
}
const elements = Array.from(this.editorElement.querySelectorAll('img')), const elements = Array.from(this.editorElement.querySelectorAll('img')),
siteId = this.sitesProvider.getCurrentSiteId(), siteId = this.sitesProvider.getCurrentSiteId(),
canDownloadFiles = this.sitesProvider.getCurrentSite().canDownloadFiles(); canDownloadFiles = this.sitesProvider.getCurrentSite().canDownloadFiles();

View File

@ -41,7 +41,6 @@ import { NavController, Nav } from 'ionic-angular';
templateUrl: 'core-split-view.html' templateUrl: 'core-split-view.html'
}) })
export class CoreSplitViewComponent implements OnInit { export class CoreSplitViewComponent implements OnInit {
// @todo Mix both panels header buttons
@ViewChild('detailNav') detailNav: Nav; @ViewChild('detailNav') detailNav: Nav;
@Input() when?: string | boolean = 'md'; @Input() when?: string | boolean = 'md';

View File

@ -83,7 +83,7 @@ import { CoreSitePluginsQuizAccessRuleComponent } from '@core/siteplugins/compon
import { CoreSitePluginsAssignFeedbackComponent } from '@core/siteplugins/components/assign-feedback/assign-feedback'; import { CoreSitePluginsAssignFeedbackComponent } from '@core/siteplugins/components/assign-feedback/assign-feedback';
import { CoreSitePluginsAssignSubmissionComponent } from '@core/siteplugins/components/assign-submission/assign-submission'; import { CoreSitePluginsAssignSubmissionComponent } from '@core/siteplugins/components/assign-submission/assign-submission';
// Import addon providers. Do not import database module because it causes circular dependencies. @todo workshop // Import addon providers. Do not import database module because it causes circular dependencies.
import { ADDON_BADGES_PROVIDERS } from '@addon/badges/badges.module'; import { ADDON_BADGES_PROVIDERS } from '@addon/badges/badges.module';
import { ADDON_CALENDAR_PROVIDERS } from '@addon/calendar/calendar.module'; import { ADDON_CALENDAR_PROVIDERS } from '@addon/calendar/calendar.module';
import { ADDON_COMPETENCY_PROVIDERS } from '@addon/competency/competency.module'; import { ADDON_COMPETENCY_PROVIDERS } from '@addon/competency/competency.module';

View File

@ -819,8 +819,6 @@ export class CoreCourseHelperProvider {
moduleInfo.sizeReadable = this.textUtils.bytesToSize(moduleSize, 2); moduleInfo.sizeReadable = this.textUtils.bytesToSize(moduleSize, 2);
})); }));
// @todo: Decide what to display instead of timemodified. Last check_updates?
promises.push(this.prefetchDelegate.getModuleStatus(module, courseId).then((moduleStatus) => { promises.push(this.prefetchDelegate.getModuleStatus(module, courseId).then((moduleStatus) => {
moduleInfo.status = moduleStatus; moduleInfo.status = moduleStatus;
switch (moduleStatus) { switch (moduleStatus) {

View File

@ -128,9 +128,11 @@
<p><a [href]="settings.sitepolicy" core-link capture="false">{{ 'core.login.policyagreementclick' | translate }}</a></p> <p><a [href]="settings.sitepolicy" core-link capture="false">{{ 'core.login.policyagreementclick' | translate }}</a></p>
</ion-item> </ion-item>
<ion-item text-wrap> <ion-item text-wrap>
<ion-label core-mark-required="true">{{ 'core.login.policyaccept' | translate }}</ion-label> <ion-label>
<ion-checkbox item-end name="policyagreed" formControlName="policyagreed"></ion-checkbox> <span [core-mark-required]="true">{{ 'core.login.policyaccept' | translate }}</span>
<core-input-errors [control]="signupForm.controls.policyagreed" [errorMessages]="policyErrors"></core-input-errors> <core-input-errors [control]="signupForm.controls.policyagreed" [errorMessages]="policyErrors"></core-input-errors>
</ion-label>
<ion-checkbox item-end name="policyagreed" formControlName="policyagreed"></ion-checkbox>
</ion-item> </ion-item>
</ng-container> </ng-container>

View File

@ -25,7 +25,7 @@
</ion-item> </ion-item>
</div> </div>
<ion-item> <ion-item>
<ion-input type="text" name="value" placeholder="{{ 'core.login.usernameoremail' | translate }}" formControlName="value" autocapitalize="none" autocorrect="off" core-auto-focus></ion-input> <ion-input type="text" name="value" placeholder="{{ 'core.login.usernameoremail' | translate }}" formControlName="value" autocapitalize="none" autocorrect="off" [core-auto-focus]></ion-input>
</ion-item> </ion-item>
<ion-item> <ion-item>
<button text-wrap ion-button block [disabled]="!myForm.valid">{{ 'core.courses.search' | translate }}</button> <button text-wrap ion-button block [disabled]="!myForm.valid">{{ 'core.courses.search' | translate }}</button>

View File

@ -14,6 +14,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular'; import { IonicPage, NavController } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreInitDelegate } from '@providers/init'; import { CoreInitDelegate } from '@providers/init';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
@ -31,7 +32,8 @@ import { CoreLoginHelperProvider } from '../../providers/helper';
export class CoreLoginInitPage { export class CoreLoginInitPage {
constructor(private navCtrl: NavController, private appProvider: CoreAppProvider, private initDelegate: CoreInitDelegate, constructor(private navCtrl: NavController, private appProvider: CoreAppProvider, private initDelegate: CoreInitDelegate,
private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider) { } private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider,
private splashScreen: SplashScreen) { }
/** /**
* View loaded. * View loaded.
@ -51,11 +53,11 @@ export class CoreLoginInitPage {
// The redirect is pointing to a site, load it. // The redirect is pointing to a site, load it.
return this.sitesProvider.loadSite(redirectData.siteId).then(() => { return this.sitesProvider.loadSite(redirectData.siteId).then(() => {
if (!this.loginHelper.isSiteLoggedOut(redirectData.page, redirectData.params)) { if (!this.loginHelper.isSiteLoggedOut(redirectData.page, redirectData.params)) {
this.navCtrl.setRoot(redirectData.page, redirectData.params, { animate: false }); return this.navCtrl.setRoot(redirectData.page, redirectData.params, { animate: false });
} }
}).catch(() => { }).catch(() => {
// Site doesn't exist. // Site doesn't exist.
this.loadPage(); return this.loadPage();
}); });
} else { } else {
// No site to load, just open the state. // No site to load, just open the state.
@ -64,24 +66,37 @@ export class CoreLoginInitPage {
} }
} }
this.loadPage(); return this.loadPage();
}).then(() => {
// If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen.
setTimeout(() => {
this.splashScreen.hide();
}, 100);
}); });
} }
/** /**
* Load the right page. * Load the right page.
*
* @return {Promise<any>} Promise resolved when done.
*/ */
protected loadPage(): void { protected loadPage(): Promise<any> {
if (this.sitesProvider.isLoggedIn()) { if (this.sitesProvider.isLoggedIn()) {
if (!this.loginHelper.isSiteLoggedOut()) { if (!this.loginHelper.isSiteLoggedOut()) {
this.loginHelper.goToSiteInitialPage(); // User is logged in, go to site initial page.
return this.loginHelper.goToSiteInitialPage();
} else {
// The site is marked as logged out. Logout and try again.
return this.sitesProvider.logout().then(() => {
return this.loadPage();
});
} }
} else { } else {
this.sitesProvider.hasSites().then((hasSites) => { return this.sitesProvider.hasSites().then((hasSites) => {
if (hasSites) { if (hasSites) {
this.navCtrl.setRoot('CoreLoginSitesPage'); return this.navCtrl.setRoot('CoreLoginSitesPage');
} else { } else {
this.loginHelper.goToAddSite(true); return this.loginHelper.goToAddSite(true);
} }
}); });
} }

View File

@ -33,7 +33,7 @@
<form [formGroup]="credForm" (ngSubmit)="login()"> <form [formGroup]="credForm" (ngSubmit)="login()">
<ion-item> <ion-item>
<core-show-password item-content [name]="'password'"> <core-show-password item-content [name]="'password'">
<ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-auto-focus></ion-input> <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [core-auto-focus]></ion-input>
</core-show-password> </core-show-password>
</ion-item> </ion-item>
<ion-grid> <ion-grid>

View File

@ -19,7 +19,7 @@
<div *ngIf="!fixedSites"> <div *ngIf="!fixedSites">
<p padding>{{ 'core.login.newsitedescription' | translate }}</p> <p padding>{{ 'core.login.newsitedescription' | translate }}</p>
<ion-item> <ion-item>
<ion-input type="url" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" formControlName="siteUrl" [core-auto-focus]></ion-input> <ion-input type="url" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" formControlName="siteUrl" [core-auto-focus]="showKeyboard"></ion-input>
</ion-item> </ion-item>
</div> </div>

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { IonicPage, NavController, ModalController } from 'ionic-angular'; import { IonicPage, NavController, ModalController, NavParams } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -33,10 +33,14 @@ export class CoreLoginSitePage {
siteForm: FormGroup; siteForm: FormGroup;
fixedSites: any[]; fixedSites: any[];
displayAsButtons = false; displayAsButtons = false;
showKeyboard = false;
constructor(private navCtrl: NavController, fb: FormBuilder, private appProvider: CoreAppProvider, constructor(navParams: NavParams, private navCtrl: NavController, fb: FormBuilder, private appProvider: CoreAppProvider,
private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider,
private modalCtrl: ModalController, private domUtils: CoreDomUtilsProvider) { private modalCtrl: ModalController, private domUtils: CoreDomUtilsProvider) {
this.showKeyboard = !!navParams.get('showKeyboard');
let url = ''; let url = '';
// Load fixed sites if they're set. // Load fixed sites if they're set.

View File

@ -67,7 +67,7 @@ export class CoreLoginSitesPage {
* Go to the page to add a site. * Go to the page to add a site.
*/ */
add(): void { add(): void {
this.loginHelper.goToAddSite(false); this.loginHelper.goToAddSite(false, true);
} }
/** /**
@ -91,7 +91,7 @@ export class CoreLoginSitesPage {
// If there are no sites left, go to add site. // If there are no sites left, go to add site.
this.sitesProvider.hasSites().then((hasSites) => { this.sitesProvider.hasSites().then((hasSites) => {
if (!hasSites) { if (!hasSites) {
this.loginHelper.goToAddSite(true); this.loginHelper.goToAddSite(true, true);
} }
}); });
}).catch((error) => { }).catch((error) => {

View File

@ -380,9 +380,10 @@ export class CoreLoginHelperProvider {
* If a fixed URL is configured, go to credentials instead. * If a fixed URL is configured, go to credentials instead.
* *
* @param {boolean} [setRoot] True to set the new page as root, false to add it to the stack. * @param {boolean} [setRoot] True to set the new page as root, false to add it to the stack.
* @param {boolean} [showKeyboard] Whether to show keyboard in the new page. Only if no fixed URL set.
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
goToAddSite(setRoot?: boolean): Promise<any> { goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise<any> {
let pageName, let pageName,
params; params;
@ -395,6 +396,9 @@ export class CoreLoginHelperProvider {
params = { siteUrl: url }; params = { siteUrl: url };
} else { } else {
pageName = 'CoreLoginSitePage'; pageName = 'CoreLoginSitePage';
params = {
showKeyboard: showKeyboard
};
} }
if (setRoot) { if (setRoot) {
@ -685,10 +689,11 @@ export class CoreLoginHelperProvider {
* @param {string} error Error message. * @param {string} error Error message.
*/ */
openChangePassword(siteUrl: string, error: string): void { openChangePassword(siteUrl: string, error: string): void {
const alert = this.domUtils.showAlert(this.translate.instant('core.notice'), error, undefined, 3000); this.domUtils.showAlert(this.translate.instant('core.notice'), error, undefined, 3000).then((alert) => {
alert.onDidDismiss(() => { alert.onDidDismiss(() => {
this.utils.openInApp(siteUrl + '/login/change_password.php'); this.utils.openInApp(siteUrl + '/login/change_password.php');
}); });
});
} }
/** /**

View File

@ -1 +1 @@
<core-site-plugins-plugin-content *ngIf="component && method" [component]="component" [method]="method" [args]="args" [initResult]="initResult"></core-site-plugins-plugin-content> <core-site-plugins-plugin-content *ngIf="component && method" [component]="component" [method]="method" [args]="args" [initResult]="initResult" [data]="data"></core-site-plugins-plugin-content>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, Input, ViewChild } from '@angular/core'; import { Component, OnChanges, Input, ViewChild } from '@angular/core';
import { CoreSitePluginsProvider } from '../../providers/siteplugins'; import { CoreSitePluginsProvider } from '../../providers/siteplugins';
import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content';
@ -23,10 +23,12 @@ import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-
selector: 'core-site-plugins-course-format', selector: 'core-site-plugins-course-format',
templateUrl: 'core-siteplugins-course-format.html', templateUrl: 'core-siteplugins-course-format.html',
}) })
export class CoreSitePluginsCourseFormatComponent implements OnInit { export class CoreSitePluginsCourseFormatComponent implements OnChanges {
@Input() course: any; // The course to render. @Input() course: any; // The course to render.
@Input() sections: any[]; // List of course sections. @Input() sections: any[]; // List of course sections.
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
@Input() initialSectionId?: number; // The section to load first (by ID).
@Input() initialSectionNumber?: number; // The section to load first (by number).
@ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent;
@ -34,14 +36,17 @@ export class CoreSitePluginsCourseFormatComponent implements OnInit {
method: string; method: string;
args: any; args: any;
initResult: any; initResult: any;
data: any;
constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { }
/** /**
* Component being initialized. * Detect changes on input properties.
*/ */
ngOnInit(): void { ngOnChanges(): void {
if (this.course && this.course.format) { if (this.course && this.course.format) {
if (!this.component) {
// Initialize the data.
const handler = this.sitePluginsProvider.getSitePluginHandler(this.course.format); const handler = this.sitePluginsProvider.getSitePluginHandler(this.course.format);
if (handler) { if (handler) {
this.component = handler.plugin.component; this.component = handler.plugin.component;
@ -53,6 +58,16 @@ export class CoreSitePluginsCourseFormatComponent implements OnInit {
this.initResult = handler.initResult; this.initResult = handler.initResult;
} }
} }
// Pass input data to the component.
this.data = {
course: this.course,
sections: this.sections,
downloadEnabled: this.downloadEnabled,
initialSectionId: this.initialSectionId,
initialSectionNumber: this.initialSectionNumber
};
}
} }
/** /**

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, Input, Output, EventEmitter, Optional } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, Optional, DoCheck, KeyValueDiffers } from '@angular/core';
import { NavController } from 'ionic-angular'; import { NavController } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreSitePluginsProvider } from '../../providers/siteplugins'; import { CoreSitePluginsProvider } from '../../providers/siteplugins';
@ -25,11 +25,12 @@ import { Subject } from 'rxjs';
selector: 'core-site-plugins-plugin-content', selector: 'core-site-plugins-plugin-content',
templateUrl: 'core-siteplugins-plugin-content.html', templateUrl: 'core-siteplugins-plugin-content.html',
}) })
export class CoreSitePluginsPluginContentComponent implements OnInit { export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
@Input() component: string; @Input() component: string;
@Input() method: string; @Input() method: string;
@Input() args: any; @Input() args: any;
@Input() initResult: any; // Result of the init WS call of the handler. @Input() initResult: any; // Result of the init WS call of the handler.
@Input() data: any; // Data to pass to the component.
@Output() onContentLoaded?: EventEmitter<boolean>; // Emits an event when the content is loaded. @Output() onContentLoaded?: EventEmitter<boolean>; // Emits an event when the content is loaded.
@Output() onLoadingContent?: EventEmitter<boolean>; // Emits an event when starts to load the content. @Output() onLoadingContent?: EventEmitter<boolean>; // Emits an event when starts to load the content.
@ -40,11 +41,14 @@ export class CoreSitePluginsPluginContentComponent implements OnInit {
invalidateObservable: Subject<void>; // An observable to notify observers when to invalidate data. invalidateObservable: Subject<void>; // An observable to notify observers when to invalidate data.
jsData: any; // Data to pass to the component. jsData: any; // Data to pass to the component.
protected differ: any; // To detect changes in the data input.
constructor(protected domUtils: CoreDomUtilsProvider, protected sitePluginsProvider: CoreSitePluginsProvider, constructor(protected domUtils: CoreDomUtilsProvider, protected sitePluginsProvider: CoreSitePluginsProvider,
@Optional() protected navCtrl: NavController) { @Optional() protected navCtrl: NavController, differs: KeyValueDiffers) {
this.onContentLoaded = new EventEmitter(); this.onContentLoaded = new EventEmitter();
this.onLoadingContent = new EventEmitter(); this.onLoadingContent = new EventEmitter();
this.invalidateObservable = new Subject<void>(); this.invalidateObservable = new Subject<void>();
this.differ = differs.find([]).create();
} }
/** /**
@ -54,6 +58,21 @@ export class CoreSitePluginsPluginContentComponent implements OnInit {
this.fetchContent(); this.fetchContent();
} }
/**
* Detect and act upon changes that Angular cant or wont detect on its own (objects and arrays).
*/
ngDoCheck(): void {
if (!this.data || !this.jsData) {
return;
}
// Check if there's any change in the data object.
const changes = this.differ.diff(this.data);
if (changes) {
this.jsData = Object.assign(this.jsData, this.data);
}
}
/** /**
* Fetches the content to render. * Fetches the content to render.
* *
@ -67,7 +86,9 @@ export class CoreSitePluginsPluginContentComponent implements OnInit {
this.content = result.templates.length ? result.templates[0].html : ''; // Load first template. this.content = result.templates.length ? result.templates[0].html : ''; // Load first template.
this.javascript = result.javascript; this.javascript = result.javascript;
this.otherData = result.otherdata; this.otherData = result.otherdata;
this.jsData = this.sitePluginsProvider.createDataForJS(this.initResult, result); this.data = this.data || {};
this.jsData = Object.assign(this.data, this.sitePluginsProvider.createDataForJS(this.initResult, result));
// Pass some methods as jsData so they can be called from the template too. // Pass some methods as jsData so they can be called from the template too.
this.jsData.openContent = this.openContent.bind(this); this.jsData.openContent = this.openContent.bind(this);

View File

@ -187,7 +187,6 @@ export class CoreExternalContentDirective implements AfterViewInit {
this.logger.debug('Using URL ' + finalUrl + ' for ' + url); this.logger.debug('Using URL ' + finalUrl + ' for ' + url);
if (tagName === 'SOURCE') { if (tagName === 'SOURCE') {
// The browser does not catch changes in SRC, we need to add a new source. // The browser does not catch changes in SRC, we need to add a new source.
// @todo: Check if changing src works in Android 4.4, maybe the problem was only in 4.1-4.3.
this.addSource(finalUrl); this.addSource(finalUrl);
} else { } else {
this.element.setAttribute(targetAttr, finalUrl); this.element.setAttribute(targetAttr, finalUrl);

View File

@ -186,7 +186,6 @@ export class CoreFormatTextDirective implements OnChanges {
this.element.innerHTML = ''; // Remove current contents. this.element.innerHTML = ''; // Remove current contents.
if (this.maxHeight && div.innerHTML != '') { if (this.maxHeight && div.innerHTML != '') {
// Move the children to the current element to be able to calculate the height. // Move the children to the current element to be able to calculate the height.
// @todo: Display the element?
this.domUtils.moveChildren(div, this.element); this.domUtils.moveChildren(div, this.element);
// Height cannot be calculated if the element is not shown while calculating. // Height cannot be calculated if the element is not shown while calculating.

View File

@ -90,8 +90,8 @@ export class CoreLinkDirective implements OnInit {
href = href.substr(1); href = href.substr(1);
// In site links // In site links
if (href.charAt(0) == '/') { if (href.charAt(0) == '/') {
// @todo: Investigate how to achieve this behaviour. // @todo: This cannot be achieved with push/pop navigation, location.go() doesn't update the state, only the URL.
// $location.url(href); // In Ionic 4 the navigation will change, so maybe it can be done by then.
} else { } else {
// Look for id or name. // Look for id or name.
this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']');

View File

@ -47,9 +47,6 @@ export interface CoreILocalNotification extends ILocalNotification {
* *
* See https://angular.io/guide/dependency-injection for more info on providers * See https://angular.io/guide/dependency-injection for more info on providers
* and Angular DI. * and Angular DI.
*
* @todo We might have to translate the old component name to the new one.
* Otherwise the unique ID of local notifications could change.
*/ */
@Injectable() @Injectable()
export class CoreLocalNotificationsProvider { export class CoreLocalNotificationsProvider {
@ -502,4 +499,18 @@ export class CoreLocalNotificationsProvider {
return this.appDB.insertRecord(this.TRIGGERED_TABLE, entry); return this.appDB.insertRecord(this.TRIGGERED_TABLE, entry);
} }
/**
* Update a component name.
*
* @param {string} oldName The old name.
* @param {string} newName The new name.
* @return {Promise<any>} Promise resolved when done.
*/
updateComponentName(oldName: string, newName: string): Promise<any> {
const oldId = this.COMPONENTS_TABLE + '#' + oldName,
newId = this.COMPONENTS_TABLE + '#' + newName;
return this.appDB.updateRecords(this.COMPONENTS_TABLE, {id: newId}, {id: oldId});
}
} }

View File

@ -22,6 +22,7 @@ import { CoreLoggerProvider } from './logger';
import { CoreSitesProvider } from './sites'; import { CoreSitesProvider } from './sites';
import { CoreUtilsProvider } from './utils/utils'; import { CoreUtilsProvider } from './utils/utils';
import { CoreConfigConstants } from '../configconstants'; import { CoreConfigConstants } from '../configconstants';
import { AddonCalendarProvider } from '@addon/calendar/providers/calendar';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
/** /**
@ -71,6 +72,7 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
protected VERSION_APPLIED = 'version_applied'; protected VERSION_APPLIED = 'version_applied';
protected logger; protected logger;
protected localNotificationsComponentsMigrate: {[old: string]: string} = {};
/** /**
* Tables to migrate from app DB ('MoodleMobile'). Include all the core ones to decrease the dependencies. * Tables to migrate from app DB ('MoodleMobile'). Include all the core ones to decrease the dependencies.
@ -323,7 +325,8 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
constructor(logger: CoreLoggerProvider, private configProvider: CoreConfigProvider, private sitesProvider: CoreSitesProvider, constructor(logger: CoreLoggerProvider, private configProvider: CoreConfigProvider, private sitesProvider: CoreSitesProvider,
private filepoolProvider: CoreFilepoolProvider, private notifProvider: CoreLocalNotificationsProvider, private filepoolProvider: CoreFilepoolProvider, private notifProvider: CoreLocalNotificationsProvider,
private utils: CoreUtilsProvider, private appProvider: CoreAppProvider) { private utils: CoreUtilsProvider, private appProvider: CoreAppProvider,
private calendarProvider: AddonCalendarProvider) {
this.logger = logger.getInstance('CoreUpdateManagerProvider'); this.logger = logger.getInstance('CoreUpdateManagerProvider');
} }
@ -341,6 +344,9 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
if (!versionApplied) { if (!versionApplied) {
// No version applied, either the app was just installed or it's being updated from Ionic 1. // No version applied, either the app was just installed or it's being updated from Ionic 1.
return this.migrateAllDBs().then(() => { return this.migrateAllDBs().then(() => {
// Now that the DBs have been migrated, migrate the local notification components names.
return this.migrateLocalNotificationsComponents();
}).then(() => {
// DBs migrated, get the version applied again. // DBs migrated, get the version applied again.
return this.configProvider.get(this.VERSION_APPLIED, 0); return this.configProvider.get(this.VERSION_APPLIED, 0);
}); });
@ -358,9 +364,8 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
promises.push(this.setSitesConfig()); promises.push(this.setSitesConfig());
} }
if (versionCode >= 2018 && versionApplied < 2018 && versionApplied > 0) { // In version 2018 we adapted the forum offline stores to match a new schema.
promises.push(this.adaptForumOfflineStores()); // However, due to the migration of data to SQLite we can no longer do that.
}
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
return this.configProvider.set(this.VERSION_APPLIED, versionCode); return this.configProvider.set(this.VERSION_APPLIED, versionCode);
@ -410,6 +415,16 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
this.siteDBTables.push(table); this.siteDBTables.push(table);
} }
/**
* Register a migration of component name for local notifications.
*
* @param {string} oldName The old name.
* @param {string} newName The new name.
*/
registerLocalNotifComponentMigration(oldName: string, newName: string): void {
this.localNotificationsComponentsMigrate[oldName] = newName;
}
/** /**
* Migrate all DBs and tables from the old format to SQLite. * Migrate all DBs and tables from the old format to SQLite.
* *
@ -555,6 +570,30 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
}); });
} }
/**
* Migrate local notifications components from the old nomenclature to the new one.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected migrateLocalNotificationsComponents(): Promise<any> {
if (!this.notifProvider.isAvailable()) {
// Local notifications not available, nothing to do.
return Promise.resolve();
}
const promises = [];
for (const oldName in this.localNotificationsComponentsMigrate) {
const newName = this.localNotificationsComponentsMigrate[oldName];
promises.push(this.notifProvider.updateComponentName(oldName, newName).catch((error) => {
this.logger.error('Error migrating local notif component from ' + oldName + ' to ' + newName + ': ', error);
}));
}
return Promise.all(promises);
}
/** /**
* Calendar default notification time is configurable from version 3.2.1, and a new option "Default" is added. * Calendar default notification time is configurable from version 3.2.1, and a new option "Default" is added.
* All events that were configured to use the fixed default time should now be configured to use "Default" option. * All events that were configured to use the fixed default time should now be configured to use "Default" option.
@ -567,8 +606,27 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
return Promise.resolve(); return Promise.resolve();
} }
// @todo: Implement it once Calendar addon is implemented. return this.sitesProvider.getSitesIds().then((siteIds) => {
return Promise.resolve();
const promises = [];
siteIds.forEach((siteId) => {
// Get stored events.
promises.push(this.calendarProvider.getAllEventsFromLocalDb(siteId).then((events) => {
const eventPromises = [];
events.forEach((event) => {
if (event.notificationtime == AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME) {
event.notificationtime = -1;
eventPromises.push(this.calendarProvider.storeEventInLocalDb(event, siteId));
}
});
return Promise.all(eventPromises);
}));
});
return Promise.all(promises);
});
} }
/** /**
@ -626,15 +684,4 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
}); });
}); });
} }
/**
* The data stored for offline discussions and posts changed its format. Adapt the entries already stored.
* Since it can be slow, we'll only block migrating the db of current site, the rest will be in background.
*
* @return {Promise<any>} Promise resolved when the db is migrated.
*/
protected adaptForumOfflineStores(): Promise<any> {
// @todo: Implement it once Forum addon is implemented.
return Promise.resolve();
}
} }

View File

@ -44,22 +44,6 @@ export class CoreDomUtilsProvider {
private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider, private platform: Platform, private configProvider: CoreConfigProvider, private urlUtils: CoreUrlUtilsProvider,
private modalCtrl: ModalController) { } private modalCtrl: ModalController) { }
/**
* Wraps a message with core-format-text if the message contains HTML tags.
* @todo Finish the adaptation
*
* @param {string} message Message to wrap.
* @return {string} Result message.
*/
private addFormatTextIfNeeded(message: string): string {
// @todo
if (this.textUtils.hasHTMLTags(message)) {
return '<core-format-text watch="true">' + message + '</core-format-text>';
}
return message;
}
/** /**
* Equivalent to element.closest(). If the browser doesn't support element.closest, it will * Equivalent to element.closest(). If the browser doesn't support element.closest, it will
* traverse the parents to achieve the same functionality. * traverse the parents to achieve the same functionality.
@ -776,16 +760,34 @@ export class CoreDomUtilsProvider {
* @param {string} message Message to show. * @param {string} message Message to show.
* @param {string} [buttonText] Text of the button. * @param {string} [buttonText] Text of the button.
* @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
* @return {Alert} The alert modal. * @return {Promise<Alert>} Promise resolved with the alert modal.
*/ */
showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Alert { showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise<Alert> {
const hasHTMLTags = this.textUtils.hasHTMLTags(message);
let promise;
if (hasHTMLTags) {
// Format the text.
promise = this.textUtils.formatText(message);
} else {
promise = Promise.resolve(message);
}
return promise.then((message) => {
const alert = this.alertCtrl.create({ const alert = this.alertCtrl.create({
title: title, title: title,
message: this.addFormatTextIfNeeded(message), // Add format-text to handle links. message: message,
buttons: [buttonText || this.translate.instant('core.ok')] buttons: [buttonText || this.translate.instant('core.ok')]
}); });
alert.present(); alert.present().then(() => {
if (hasHTMLTags) {
// Treat all anchors so they don't override the app.
const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message');
this.treatAnchors(alertMessageEl);
}
});
if (autocloseTime > 0) { if (autocloseTime > 0) {
setTimeout(() => { setTimeout(() => {
@ -794,6 +796,7 @@ export class CoreDomUtilsProvider {
} }
return alert; return alert;
});
} }
/** /**
@ -803,9 +806,9 @@ export class CoreDomUtilsProvider {
* @param {string} message Message to show. * @param {string} message Message to show.
* @param {string} [buttonText] Text of the button. * @param {string} [buttonText] Text of the button.
* @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
* @return {Alert} The alert modal. * @return {Promise<Alert>} Promise resolved with the alert modal.
*/ */
showAlertTranslated(title: string, message: string, buttonText?: string, autocloseTime?: number): Alert { showAlertTranslated(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise<Alert> {
title = title ? this.translate.instant(title) : title; title = title ? this.translate.instant(title) : title;
message = message ? this.translate.instant(message) : message; message = message ? this.translate.instant(message) : message;
buttonText = buttonText ? this.translate.instant(buttonText) : buttonText; buttonText = buttonText ? this.translate.instant(buttonText) : buttonText;
@ -825,9 +828,20 @@ export class CoreDomUtilsProvider {
*/ */
showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options?: any): Promise<void> { showConfirm(message: string, title?: string, okText?: string, cancelText?: string, options?: any): Promise<void> {
return new Promise<void>((resolve, reject): void => { return new Promise<void>((resolve, reject): void => {
const hasHTMLTags = this.textUtils.hasHTMLTags(message);
let promise;
if (hasHTMLTags) {
// Format the text.
promise = this.textUtils.formatText(message);
} else {
promise = Promise.resolve(message);
}
promise.then((message) => {
options = options || {}; options = options || {};
options.message = this.addFormatTextIfNeeded(message); // Add format-text to handle links. options.message = message;
options.title = title; options.title = title;
if (!title) { if (!title) {
options.cssClass = 'core-nohead'; options.cssClass = 'core-nohead';
@ -848,7 +862,16 @@ export class CoreDomUtilsProvider {
} }
]; ];
this.alertCtrl.create(options).present(); const alert = this.alertCtrl.create(options);
alert.present().then(() => {
if (hasHTMLTags) {
// Treat all anchors so they don't override the app.
const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message');
this.treatAnchors(alertMessageEl);
}
});
});
}); });
} }
@ -858,9 +881,9 @@ export class CoreDomUtilsProvider {
* @param {any} error Message to show. * @param {any} error Message to show.
* @param {boolean} [needsTranslate] Whether the error needs to be translated. * @param {boolean} [needsTranslate] Whether the error needs to be translated.
* @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
* @return {Alert} The alert modal. * @return {Promise<Alert>} Promise resolved with the alert modal.
*/ */
showErrorModal(error: any, needsTranslate?: boolean, autocloseTime?: number): Alert { showErrorModal(error: any, needsTranslate?: boolean, autocloseTime?: number): Promise<Alert> {
if (typeof error == 'object') { if (typeof error == 'object') {
// We received an object instead of a string. Search for common properties. // We received an object instead of a string. Search for common properties.
if (error.coreCanceled) { if (error.coreCanceled) {
@ -903,9 +926,9 @@ export class CoreDomUtilsProvider {
* @param {any} [defaultError] Message to show if the error is not a string. * @param {any} [defaultError] Message to show if the error is not a string.
* @param {boolean} [needsTranslate] Whether the error needs to be translated. * @param {boolean} [needsTranslate] Whether the error needs to be translated.
* @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
* @return {Alert} The alert modal. * @return {Promise<Alert>} Promise resolved with the alert modal.
*/ */
showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Alert { showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise<Alert> {
if (error && error.coreCanceled) { if (error && error.coreCanceled) {
// It's a canceled error, don't display an error. // It's a canceled error, don't display an error.
return; return;
@ -927,9 +950,9 @@ export class CoreDomUtilsProvider {
* @param {any} [defaultError] Message to show if the error is not a string. * @param {any} [defaultError] Message to show if the error is not a string.
* @param {boolean} [needsTranslate] Whether the error needs to be translated. * @param {boolean} [needsTranslate] Whether the error needs to be translated.
* @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @param {number} [autocloseTime] Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
* @return {Alert} The alert modal. * @return {Promise<Alert>} Promise resolved with the alert modal.
*/ */
showErrorModalFirstWarning(warnings: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Alert { showErrorModalFirstWarning(warnings: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number): Promise<Alert> {
const error = warnings && warnings.length && warnings[0].message; const error = warnings && warnings.length && warnings[0].message;
return this.showErrorModalDefault(error, defaultError, needsTranslate, autocloseTime); return this.showErrorModalDefault(error, defaultError, needsTranslate, autocloseTime);
@ -974,8 +997,19 @@ export class CoreDomUtilsProvider {
*/ */
showPrompt(message: string, title?: string, placeholder?: string, type: string = 'password'): Promise<any> { showPrompt(message: string, title?: string, placeholder?: string, type: string = 'password'): Promise<any> {
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
this.alertCtrl.create({ const hasHTMLTags = this.textUtils.hasHTMLTags(message);
message: this.addFormatTextIfNeeded(message), // Add format-text to handle links. let promise;
if (hasHTMLTags) {
// Format the text.
promise = this.textUtils.formatText(message);
} else {
promise = Promise.resolve(message);
}
promise.then((message) => {
const alert = this.alertCtrl.create({
message: message,
title: title, title: title,
inputs: [ inputs: [
{ {
@ -999,7 +1033,16 @@ export class CoreDomUtilsProvider {
} }
} }
] ]
}).present(); });
alert.present().then(() => {
if (hasHTMLTags) {
// Treat all anchors so they don't override the app.
const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message');
this.treatAnchors(alertMessageEl);
}
});
});
}); });
} }
@ -1069,6 +1112,42 @@ export class CoreDomUtilsProvider {
return element.children; return element.children;
} }
/**
* Treat anchors inside alert/modals.
*
* @param {HTMLElement} container The HTMLElement that can contain anchors.
*/
protected treatAnchors(container: HTMLElement): void {
const anchors = Array.from(container.querySelectorAll('a'));
anchors.forEach((anchor) => {
anchor.addEventListener('click', (event) => {
if (event.defaultPrevented) {
// Stop.
return;
}
const href = anchor.getAttribute('href');
if (href) {
event.preventDefault();
event.stopPropagation();
// We cannot use CoreDomUtilsProvider.openInBrowser due to circular dependencies.
if (this.appProvider.isDesktop()) {
// It's a desktop app, use Electron shell library to open the browser.
const shell = require('electron').shell;
if (!shell.openExternal(href)) {
// Open browser failed, open a new window in the app.
window.open(href, '_system');
}
} else {
window.open(href, '_system');
}
}
});
});
}
/** /**
* View an image in a new page or modal. * View an image in a new page or modal.
* *