diff --git a/scripts/langindex.json b/scripts/langindex.json
index 75ad66948..40cfc905d 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -87,13 +87,19 @@
"addon.calendar.calendarevent": "local_moodlemobileapp",
"addon.calendar.calendarevents": "local_moodlemobileapp",
"addon.calendar.calendarreminders": "local_moodlemobileapp",
+ "addon.calendar.confirmeventdelete": "calendar",
+ "addon.calendar.confirmeventseriesdelete": "calendar",
"addon.calendar.defaultnotificationtime": "local_moodlemobileapp",
+ "addon.calendar.deleteallevents": "calendar",
+ "addon.calendar.deleteevent": "calendar",
+ "addon.calendar.deleteoneevent": "calendar",
"addon.calendar.durationminutes": "calendar",
"addon.calendar.durationnone": "calendar",
"addon.calendar.durationuntil": "calendar",
"addon.calendar.editevent": "calendar",
"addon.calendar.errorloadevent": "local_moodlemobileapp",
"addon.calendar.errorloadevents": "local_moodlemobileapp",
+ "addon.calendar.eventcalendareventdeleted": "calendar",
"addon.calendar.eventduration": "calendar",
"addon.calendar.eventendtime": "calendar",
"addon.calendar.eventkind": "calendar",
diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json
index d5cf329a2..8792b48c5 100644
--- a/src/addon/calendar/lang/en.json
+++ b/src/addon/calendar/lang/en.json
@@ -3,13 +3,19 @@
"calendarevent": "Calendar event",
"calendarevents": "Calendar events",
"calendarreminders": "Calendar reminders",
+ "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?",
+ "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?",
"defaultnotificationtime": "Default notification time",
+ "deleteallevents": "Delete all events",
+ "deleteevent": "Delete event",
+ "deleteoneevent": "Delete this event",
"durationminutes": "Duration in minutes",
"durationnone": "Without duration",
"durationuntil": "Until",
"editevent": "Editing event",
"errorloadevent": "Error loading event.",
"errorloadevents": "Error loading events.",
+ "eventcalendareventdeleted": "Calendar event deleted",
"eventduration": "Duration",
"eventendtime": "End time",
"eventkind": "Type of event",
diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html
index 2608581ae..23e55aac7 100644
--- a/src/addon/calendar/pages/event/event.html
+++ b/src/addon/calendar/pages/event/event.html
@@ -8,8 +8,10 @@
-
-
+
+
+
+
@@ -18,7 +20,7 @@
-
+
{{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }}
@@ -27,6 +29,9 @@
+
+ {{ 'core.deletedoffline' | translate }}
+
{{ 'addon.calendar.eventstarttime' | translate}}
diff --git a/src/addon/calendar/pages/event/event.scss b/src/addon/calendar/pages/event/event.scss
new file mode 100644
index 000000000..6a9913737
--- /dev/null
+++ b/src/addon/calendar/pages/event/event.scss
@@ -0,0 +1,5 @@
+ion-app.app-root page-addon-calendar-event {
+ .card ion-note {
+ font-size: 1.6rem;
+ }
+}
diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts
index 6fb0a16d1..9f312a3e8 100644
--- a/src/addon/calendar/pages/event/event.ts
+++ b/src/addon/calendar/pages/event/event.ts
@@ -68,6 +68,7 @@ export class AddonCalendarEventPage implements OnDestroy {
defaultTime: number;
reminders: any[];
canEdit = false;
+ canDelete = false;
hasOffline = false;
isOnline = false;
syncIcon: string; // Sync icon.
@@ -89,8 +90,9 @@ export class AddonCalendarEventPage implements OnDestroy {
this.currentSiteId = sitesProvider.getCurrentSiteId();
this.isSplitViewOn = this.svComponent && this.svComponent.isOn();
- // Check if site supports editing. No need to check allowed types, event.canedit already does it.
+ // Check if site supports editing and deleting. No need to check allowed types, event.canedit already does it.
this.canEdit = this.calendarProvider.canEditEventsInSite();
+ this.canDelete = this.calendarProvider.canDeleteEventsInSite();
if (this.notificationsEnabled) {
this.calendarProvider.getEventReminders(this.eventId).then((reminders) => {
@@ -123,10 +125,10 @@ export class AddonCalendarEventPage implements OnDestroy {
this.currentSiteId);
// Refresh online status when changes.
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
- this.isOnline = online;
+ this.isOnline = this.appProvider.isOnline();
});
});
}
@@ -150,7 +152,8 @@ export class AddonCalendarEventPage implements OnDestroy {
fetchEvent(sync?: boolean, showErrors?: boolean): Promise {
const currentSite = this.sitesProvider.getCurrentSite(),
canGetById = this.calendarProvider.isGetEventByIdAvailable();
- let promise;
+ let promise,
+ deleted = false;
this.isOnline = this.appProvider.isOnline();
@@ -161,6 +164,11 @@ export class AddonCalendarEventPage implements OnDestroy {
this.domUtils.showErrorModal(result.warnings[0]);
}
+ if (result.deleted && result.deleted.indexOf(this.eventId) != -1) {
+ // This event was deleted during the sync.
+ deleted = true;
+ }
+
if (result.updated) {
// Trigger a manual sync event.
result.source = 'event';
@@ -177,6 +185,10 @@ export class AddonCalendarEventPage implements OnDestroy {
}
return promise.then(() => {
+ if (deleted) {
+ return;
+ }
+
const promises = [];
// Get the event data.
@@ -204,6 +216,10 @@ export class AddonCalendarEventPage implements OnDestroy {
});
}).then((event) => {
+ if (deleted) {
+ return;
+ }
+
const promises = [];
this.calendarHelper.formatEventData(event);
@@ -251,7 +267,7 @@ export class AddonCalendarEventPage implements OnDestroy {
this.title = title;
// If the event belongs to a course, get the course name and the URL to view it.
- if (canGetById && event.course) {
+ if (canGetById && event.course && event.course.id != this.siteHomeId) {
this.courseName = event.course.fullname;
this.courseUrl = event.course.viewurl;
} else if (event.courseid && event.courseid != this.siteHomeId) {
@@ -290,6 +306,11 @@ export class AddonCalendarEventPage implements OnDestroy {
event.encodedLocation = this.textUtils.buildAddressURL(event.location);
}
+ // Check if event was deleted in offine.
+ promises.push(this.calendarOffline.isEventDeleted(this.eventId).then((deleted) => {
+ event.deleted = deleted;
+ }));
+
return Promise.all(promises);
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true);
@@ -391,6 +412,97 @@ export class AddonCalendarEventPage implements OnDestroy {
navCtrl.push('AddonCalendarEditEventPage', {eventId: this.eventId});
}
+ /**
+ * Delete the event.
+ */
+ deleteEvent(): void {
+ const title = this.translate.instant('addon.calendar.deleteevent'),
+ options: any = {};
+ let message: string;
+
+ if (this.event.eventcount > 1) {
+ // It's a repeated event.
+ message = this.translate.instant('addon.calendar.confirmeventseriesdelete',
+ {$a: {name: this.event.name, count: this.event.eventcount}});
+
+ options.inputs = [
+ {
+ type: 'radio',
+ name: 'deleteall',
+ checked: true,
+ value: false,
+ label: this.translate.instant('addon.calendar.deleteoneevent')
+ },
+ {
+ type: 'radio',
+ name: 'deleteall',
+ checked: false,
+ value: true,
+ label: this.translate.instant('addon.calendar.deleteallevents')
+ }
+ ];
+ } else {
+ // Not repeated, display a simple confirm.
+ message = this.translate.instant('addon.calendar.confirmeventdelete', {$a: this.event.name});
+ }
+
+ this.domUtils.showConfirm(message, title, undefined, undefined, options).then((deleteAll) => {
+
+ const modal = this.domUtils.showModalLoading('core.sending', true);
+
+ this.calendarProvider.deleteEvent(this.event.id, this.event.name, deleteAll).then((sent) => {
+
+ // Trigger an event.
+ this.eventsProvider.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, {
+ eventId: this.eventId,
+ sent: sent
+ }, this.sitesProvider.getCurrentSiteId());
+
+ if (sent) {
+ this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false);
+
+ // Event deleted, close the view.
+ if (!this.svComponent || !this.svComponent.isOn()) {
+ this.navCtrl.pop();
+ }
+ } else {
+ // Event deleted in offline, just mark it as deleted.
+ this.event.deleted = true;
+ }
+
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error deleting event.');
+ }).finally(() => {
+ modal.dismiss();
+ });
+ }, () => {
+ // User canceled.
+ });
+ }
+
+ /**
+ * Undo delete the event.
+ */
+ undoDelete(): void {
+ const modal = this.domUtils.showModalLoading('core.sending', true);
+
+ this.calendarOffline.unmarkDeleted(this.event.id).then(() => {
+
+ // Trigger an event.
+ this.eventsProvider.trigger(AddonCalendarProvider.UNDELETED_EVENT_EVENT, {
+ eventId: this.eventId
+ }, this.sitesProvider.getCurrentSiteId());
+
+ this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false);
+ this.event.deleted = false;
+
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error undeleting event.');
+ }).finally(() => {
+ modal.dismiss();
+ });
+ }
+
/**
* Check the result of an automatic sync or a manual sync not done by this page.
*
@@ -398,7 +510,18 @@ export class AddonCalendarEventPage implements OnDestroy {
* @param {any} data Sync result.
*/
protected checkSyncResult(isManual: boolean, data: any): void {
- if (data && data.events && (!isManual || data.source != 'event')) {
+ if (!data) {
+ return;
+ }
+
+ if (data.deleted && data.deleted.indexOf(this.eventId) != -1) {
+ this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false);
+
+ // Event was deleted, close the view.
+ if (!this.svComponent || !this.svComponent.isOn()) {
+ this.navCtrl.pop();
+ }
+ } else if (data.events && (!isManual || data.source != 'event')) {
const event = data.events.find((ev) => {
return ev.id == this.eventId;
});
diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html
index a224599f7..bd5c848ad 100644
--- a/src/addon/calendar/pages/list/list.html
+++ b/src/addon/calendar/pages/list/list.html
@@ -40,9 +40,13 @@
- {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimetime" }}
- {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}
-
+
- {{ 'core.notsent' | translate }}
+ {{ 'core.notsent' | translate }}
+
+
+
+ {{ 'core.deletedoffline' | translate }}
diff --git a/src/addon/calendar/pages/list/list.scss b/src/addon/calendar/pages/list/list.scss
new file mode 100644
index 000000000..9f40d9746
--- /dev/null
+++ b/src/addon/calendar/pages/list/list.scss
@@ -0,0 +1,5 @@
+ion-app.app-root page-addon-calendar-list {
+ ion-note {
+ max-width: 30%;
+ }
+}
diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts
index e3ce88a44..0dac7f74b 100644
--- a/src/addon/calendar/pages/list/list.ts
+++ b/src/addon/calendar/pages/list/list.ts
@@ -63,16 +63,19 @@ export class AddonCalendarListPage implements OnDestroy {
protected newEventObserver: any;
protected discardedObserver: any;
protected editEventObserver: any;
+ protected deleteEventObserver: any;
+ protected undeleteEventObserver: any;
protected syncObserver: any;
protected manualSyncObserver: any;
protected onlineObserver: any;
protected currentSiteId: string;
+ protected onlineEvents = [];
+ protected offlineEvents = [];
+ protected deletedEvents = [];
courses: any[];
eventsLoaded = false;
events = []; // Events (both online and offline).
- onlineEvents = [];
- offlineEvents = [];
notificationsEnabled = false;
filteredEvents = [];
canLoadMore = false;
@@ -149,6 +152,11 @@ export class AddonCalendarListPage implements OnDestroy {
this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => {
this.eventsLoaded = false;
this.refreshEvents();
+
+ if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) {
+ // Current selected event was deleted. Clear details.
+ this.splitviewCtrl.emptyDetails();
+ }
}, this.currentSiteId);
// Refresh data if calendar events are synchronized manually but not by this page.
@@ -157,13 +165,51 @@ export class AddonCalendarListPage implements OnDestroy {
this.eventsLoaded = false;
this.refreshEvents();
}
+
+ if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) {
+ // Current selected event was deleted. Clear details.
+ this.splitviewCtrl.emptyDetails();
+ }
+ }, this.currentSiteId);
+
+ // Update the list when an event is deleted.
+ this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => {
+ if (data && !data.sent) {
+ // Event was deleted in offline. Just mark it as deleted, no need to refresh.
+ this.markAsDeleted(data.eventId, true);
+ this.hasOffline = true;
+ } else {
+ // Event deleted, clear the details if needed and refresh the view.
+ if (this.splitviewCtrl.isOn()) {
+ this.splitviewCtrl.emptyDetails();
+ }
+
+ this.eventsLoaded = false;
+ this.refreshEvents();
+ }
+ }, this.currentSiteId);
+
+ // Listen for events "undeleted" (offline).
+ this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => {
+ if (data && data.eventId) {
+ // Mark it as undeleted, no need to refresh.
+ this.markAsDeleted(data.eventId, false);
+
+ // Remove it from the list of deleted events if it's there.
+ const index = this.deletedEvents.indexOf(data.eventId);
+ if (index != -1) {
+ this.deletedEvents.splice(index, 1);
+ }
+
+ this.hasOffline = !!this.offlineEvents.length || !!this.deletedEvents.length;
+ }
}, this.currentSiteId);
// Refresh online status when changes.
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
- this.isOnline = online;
+ this.isOnline = this.appProvider.isOnline();
});
});
}
@@ -234,6 +280,8 @@ export class AddonCalendarListPage implements OnDestroy {
const promises = [];
const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined;
+ this.hasOffline = false;
+
promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => {
this.canCreate = canEdit;
}));
@@ -254,8 +302,8 @@ export class AddonCalendarListPage implements OnDestroy {
}));
// Get offline events.
- promises.push(this.calendarOffline.getAllEvents().then((events) => {
- this.hasOffline = !!events.length;
+ promises.push(this.calendarOffline.getAllEditedEvents().then((events) => {
+ this.hasOffline = this.hasOffline || !!events.length;
// Format data and sort by timestart.
events.forEach((event) => {
@@ -265,6 +313,12 @@ export class AddonCalendarListPage implements OnDestroy {
this.offlineEvents = this.sortEvents(events);
}));
+ // Get events deleted in offline.
+ promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => {
+ this.hasOffline = this.hasOffline || !!ids.length;
+ this.deletedEvents = ids;
+ }));
+
return Promise.all(promises);
}).finally(() => {
this.eventsLoaded = true;
@@ -457,22 +511,32 @@ export class AddonCalendarListPage implements OnDestroy {
* @return {any[]} Merged events.
*/
protected mergeEvents(onlineEvents: any[]): any[] {
- if (!this.offlineEvents || !this.offlineEvents.length) {
+ if (!this.offlineEvents.length && !this.deletedEvents.length) {
// No offline events, nothing to merge.
return onlineEvents;
}
const start = this.initialTime + (CoreConstants.SECONDS_DAY * this.daysLoaded),
end = start + (CoreConstants.SECONDS_DAY * AddonCalendarProvider.DAYS_INTERVAL) - 1;
+ let result = onlineEvents;
- // First of all, remove the online events that were modified in offline.
- let result = onlineEvents.filter((event) => {
- const offlineEvent = this.offlineEvents.find((ev) => {
- return ev.id == event.id;
+ if (this.deletedEvents.length) {
+ // Mark as deleted the events that were deleted in offline.
+ result.forEach((event) => {
+ event.deleted = this.deletedEvents.indexOf(event.id) != -1;
});
+ }
- return !offlineEvent;
- });
+ if (this.offlineEvents.length) {
+ // Remove the online events that were modified in offline.
+ result = result.filter((event) => {
+ const offlineEvent = this.offlineEvents.find((ev) => {
+ return ev.id == event.id;
+ });
+
+ return !offlineEvent;
+ });
+ }
// Now get the offline events that belong to this period.
const periodOfflineEvents = this.offlineEvents.filter((event) => {
@@ -658,6 +722,22 @@ export class AddonCalendarListPage implements OnDestroy {
}
}
+ /**
+ * Find an event and mark it as deleted.
+ *
+ * @param {number} eventId Event ID.
+ * @param {boolean} deleted Whether to mark it as deleted or not.
+ */
+ protected markAsDeleted(eventId: number, deleted: boolean): void {
+ const event = this.onlineEvents.find((event) => {
+ return event.id == eventId;
+ });
+
+ if (event) {
+ event.deleted = deleted;
+ }
+ }
+
/**
* Page destroyed.
*/
@@ -666,6 +746,8 @@ export class AddonCalendarListPage implements OnDestroy {
this.newEventObserver && this.newEventObserver.off();
this.discardedObserver && this.discardedObserver.off();
this.editEventObserver && this.editEventObserver.off();
+ this.deleteEventObserver && this.deleteEventObserver.off();
+ this.undeleteEventObserver && this.undeleteEventObserver.off();
this.syncObserver && this.syncObserver.off();
this.manualSyncObserver && this.manualSyncObserver.off();
this.onlineObserver && this.onlineObserver.unsubscribe();
diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts
index a929113bc..3d48baa38 100644
--- a/src/addon/calendar/providers/calendar-offline.ts
+++ b/src/addon/calendar/providers/calendar-offline.ts
@@ -14,6 +14,7 @@
import { Injectable } from '@angular/core';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
+import { CoreUtilsProvider } from '@providers/utils/utils';
/**
* Service to handle offline calendar events.
@@ -23,6 +24,7 @@ export class AddonCalendarOfflineProvider {
// Variables for database.
static EVENTS_TABLE = 'addon_calendar_offline_events';
+ static DELETED_EVENTS_TABLE = 'addon_calendar_deleted_events';
protected siteSchema: CoreSiteSchema = {
name: 'AddonCalendarOfflineProvider',
@@ -34,6 +36,7 @@ export class AddonCalendarOfflineProvider {
{
name: 'id', // Negative for offline entries.
type: 'INTEGER',
+ primaryKey: true
},
{
name: 'name',
@@ -110,13 +113,35 @@ export class AddonCalendarOfflineProvider {
name: 'timecreated',
type: 'INTEGER',
}
- ],
- primaryKeys: ['id']
+ ]
+ },
+ {
+ name: AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE,
+ columns: [
+ {
+ name: 'id',
+ type: 'INTEGER',
+ primaryKey: true
+ },
+ {
+ name: 'name', // Save the name to be able to notify the user.
+ type: 'TEXT',
+ notNull: true
+ },
+ {
+ name: 'repeat',
+ type: 'INTEGER'
+ },
+ {
+ name: 'timemodified',
+ type: 'INTEGER',
+ }
+ ]
}
]
};
- constructor(private sitesProvider: CoreSitesProvider) {
+ constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) {
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
@@ -138,17 +163,91 @@ export class AddonCalendarOfflineProvider {
}
/**
- * Get all offline events.
+ * Get the IDs of all the events created/edited/deleted in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the IDs.
+ */
+ getAllEventsIds(siteId?: string): Promise {
+ const promises = [];
+
+ promises.push(this.getAllDeletedEventsIds(siteId));
+ promises.push(this.getAllEditedEventsIds(siteId));
+
+ return Promise.all(promises).then((result) => {
+ return this.utils.mergeArraysWithoutDuplicates(result[0], result[1]);
+ });
+ }
+
+ /**
+ * Get all the events deleted in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with all the events deleted in offline.
+ */
+ getAllDeletedEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.getDb().getRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE);
+ });
+ }
+
+ /**
+ * Get the IDs of all the events deleted in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the IDs of all the events deleted in offline.
+ */
+ getAllDeletedEventsIds(siteId?: string): Promise {
+ return this.getAllDeletedEvents(siteId).then((events) => {
+ return events.map((event) => {
+ return event.id;
+ });
+ });
+ }
+
+ /**
+ * Get all the events created/edited in offline.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved with events.
*/
- getAllEvents(siteId?: string): Promise {
+ getAllEditedEvents(siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonCalendarOfflineProvider.EVENTS_TABLE);
});
}
+ /**
+ * Get the IDs of all the events created/edited in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with events IDs.
+ */
+ getAllEditedEventsIds(siteId?: string): Promise {
+ return this.getAllEditedEvents(siteId).then((events) => {
+ return events.map((event) => {
+ return event.id;
+ });
+ });
+ }
+
+ /**
+ * Get an event deleted in offline.
+ *
+ * @param {number} eventId Event ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the deleted event.
+ */
+ getDeletedEvent(eventId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const conditions: any = {
+ id: eventId
+ };
+
+ return site.getDb().getRecord(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions);
+ });
+ }
+
/**
* Get an offline event.
*
@@ -172,8 +271,8 @@ export class AddonCalendarOfflineProvider {
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved with boolean: true if has offline events, false otherwise.
*/
- hasEvents(siteId?: string): Promise {
- return this.getAllEvents(siteId).then((events) => {
+ hasEditedEvents(siteId?: string): Promise {
+ return this.getAllEditedEvents(siteId).then((events) => {
return !!events.length;
}).catch(() => {
// No offline data found, return false.
@@ -181,6 +280,43 @@ export class AddonCalendarOfflineProvider {
});
}
+ /**
+ * Check if an event is deleted.
+ *
+ * @param {number} eventId Event ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with boolean: whether the event is deleted.
+ */
+ isEventDeleted(eventId: number, siteId?: string): Promise {
+ return this.getDeletedEvent(eventId, siteId).then((event) => {
+ return !!event;
+ }).catch(() => {
+ return false;
+ });
+ }
+
+ /**
+ * Mark an event as deleted.
+ *
+ * @param {number} eventId Event ID to delete.
+ * @param {number} name Name of the event to delete.
+ * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ markDeleted(eventId: number, name: string, deleteAll?: boolean, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const event = {
+ id: eventId,
+ name: name || '',
+ repeat: deleteAll ? 1 : 0,
+ timemodified: Date.now()
+ };
+
+ return site.getDb().insertRecord(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, event);
+ });
+ }
+
/**
* Offline version for adding a new discussion to a forum.
*
@@ -221,4 +357,21 @@ export class AddonCalendarOfflineProvider {
});
});
}
+
+ /**
+ * Unmark an event as deleted.
+ *
+ * @param {number} eventId Event ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if deleted, rejected if failure.
+ */
+ unmarkDeleted(eventId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const conditions: any = {
+ id: eventId
+ };
+
+ return site.getDb().deleteRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions);
+ });
+ }
}
diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts
index 834d1ac34..6b81d39ae 100644
--- a/src/addon/calendar/providers/calendar-sync.ts
+++ b/src/addon/calendar/providers/calendar-sync.ts
@@ -81,7 +81,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
// Sync successful, send event.
this.eventsProvider.trigger(AddonCalendarSyncProvider.AUTO_SYNCED, {
warnings: result.warnings,
- events: result.events
+ events: result.events,
+ deleted: result.deleted
}, siteId);
}
});
@@ -122,18 +123,19 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
const result = {
warnings: [],
events: [],
+ deleted: [],
updated: false
};
- let offlineEvents;
+ let offlineEventIds: number[];
// Get offline events.
- const syncPromise = this.calendarOffline.getAllEvents(siteId).catch(() => {
+ const syncPromise = this.calendarOffline.getAllEventsIds(siteId).catch(() => {
// No offline data found, return empty list.
return [];
- }).then((events) => {
- offlineEvents = events;
+ }).then((eventIds) => {
+ offlineEventIds = eventIds;
- if (!events.length) {
+ if (!eventIds.length) {
// Nothing to sync.
return;
} else if (!this.appProvider.isOnline()) {
@@ -143,8 +145,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
const promises = [];
- events.forEach((event) => {
- promises.push(this.syncOfflineEvent(event, result, siteId));
+ offlineEventIds.forEach((eventId) => {
+ promises.push(this.syncOfflineEvent(eventId, result, siteId));
});
return this.utils.allPromises(promises);
@@ -155,10 +157,10 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
this.calendarProvider.invalidateEventsList(siteId),
];
- offlineEvents.forEach((event) => {
- if (event.id > 0) {
+ offlineEventIds.forEach((eventId) => {
+ if (eventId > 0) {
// An event was edited, invalidate its data too.
- promises.push(this.calendarProvider.invalidateEvent(event.id, siteId));
+ promises.push(this.calendarProvider.invalidateEvent(eventId, siteId));
}
});
@@ -182,47 +184,95 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
/**
* Synchronize an offline event.
*
- * @param {any} event The event to sync.
+ * @param {number} eventId The event ID to sync.
* @param {any} result Object where to store the result of the sync.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved if sync is successful, rejected otherwise.
*/
- protected syncOfflineEvent(event: any, result: any, siteId?: string): Promise {
+ protected syncOfflineEvent(eventId: number, result: any, siteId?: string): Promise {
// Verify that event isn't blocked.
- if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, event.id, siteId)) {
- this.logger.debug('Cannot sync event ' + event.name + ' because it is blocked.');
+ if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) {
+ this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.');
return Promise.reject(this.translate.instant('core.errorsyncblocked',
{$a: this.translate.instant('addon.calendar.calendarevent')}));
}
- // Try to send the data.
- const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function.
-
- return this.calendarProvider.submitEventOnline(event.id > 0 ? event.id : undefined, data, siteId).then((newEvent) => {
- result.updated = true;
- result.events.push(newEvent);
-
- // Event sent, delete the offline data.
- return this.calendarOffline.deleteEvent(event.id, siteId);
- }).catch((error) => {
- if (this.utils.isWebServiceError(error)) {
- // The WebService has thrown an error, this means that the event cannot be created. Delete it.
+ // First of all, check if the event has been deleted.
+ return this.calendarOffline.getDeletedEvent(eventId, siteId).then((data) => {
+ // Delete the event.
+ return this.calendarProvider.deleteEventOnline(data.id, data.repeat, siteId).then(() => {
result.updated = true;
+ result.deleted.push(eventId);
- return this.calendarOffline.deleteEvent(event.id, siteId).then(() => {
- // Event deleted, add a warning.
- result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
- component: this.translate.instant('addon.calendar.calendarevent'),
- name: event.name,
- error: this.textUtils.getErrorMessageFromError(error)
+ // Event sent, delete the offline data.
+ const promises = [];
+
+ promises.push(this.calendarOffline.unmarkDeleted(eventId, siteId));
+ promises.push(this.calendarOffline.deleteEvent(eventId, siteId).catch(() => {
+ // Ignore errors, maybe there was no edit data.
+ }));
+
+ return Promise.all(promises);
+ }).catch((error) => {
+
+ if (this.utils.isWebServiceError(error)) {
+ // The WebService has thrown an error, this means that the event cannot be created. Delete it.
+ result.updated = true;
+
+ const promises = [];
+
+ promises.push(this.calendarOffline.unmarkDeleted(eventId, siteId));
+ promises.push(this.calendarOffline.deleteEvent(eventId, siteId).catch(() => {
+ // Ignore errors, maybe there was no edit data.
}));
- });
- }
- // Local error, reject.
- return Promise.reject(error);
+ return Promise.all(promises).then(() => {
+ // Event deleted, add a warning.
+ result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
+ component: this.translate.instant('addon.calendar.calendarevent'),
+ name: data.name,
+ error: this.textUtils.getErrorMessageFromError(error)
+ }));
+ });
+ }
+
+ // Local error, reject.
+ return Promise.reject(error);
+ });
+ }, () => {
+
+ // Not deleted. Now get the event data.
+ return this.calendarOffline.getEvent(eventId, siteId).then((event) => {
+ // Try to send the data.
+ const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function.
+
+ return this.calendarProvider.submitEventOnline(eventId > 0 ? eventId : undefined, data, siteId).then((newEvent) => {
+ result.updated = true;
+ result.events.push(newEvent);
+
+ // Event sent, delete the offline data.
+ return this.calendarOffline.deleteEvent(event.id, siteId);
+ }).catch((error) => {
+ if (this.utils.isWebServiceError(error)) {
+ // The WebService has thrown an error, this means that the event cannot be created. Delete it.
+ result.updated = true;
+
+ return this.calendarOffline.deleteEvent(event.id, siteId).then(() => {
+ // Event deleted, add a warning.
+ result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
+ component: this.translate.instant('addon.calendar.calendarevent'),
+ name: event.name,
+ error: this.textUtils.getErrorMessageFromError(error)
+ }));
+ });
+ }
+
+ // Local error, reject.
+ return Promise.reject(error);
+ });
+ });
});
}
}
diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts
index bfd3a03df..9e34520a1 100644
--- a/src/addon/calendar/providers/calendar.ts
+++ b/src/addon/calendar/providers/calendar.ts
@@ -42,6 +42,8 @@ export class AddonCalendarProvider {
static NEW_EVENT_EVENT = 'addon_calendar_new_event';
static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded';
static EDIT_EVENT_EVENT = 'addon_calendar_edit_event';
+ static DELETED_EVENT_EVENT = 'addon_calendar_deleted_event';
+ static UNDELETED_EVENT_EVENT = 'addon_calendar_undeleted_event';
static TYPE_CATEGORY = 'category';
static TYPE_COURSE = 'course';
static TYPE_GROUP = 'group';
@@ -225,11 +227,38 @@ export class AddonCalendarProvider {
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
+ /**
+ * Check if a certain site allows deleting events.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved with true if can delete.
+ * @since 3.3
+ */
+ canDeleteEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return this.canDeleteEventsInSite(site);
+ });
+ }
+
+ /**
+ * Check if a certain site allows deleting events.
+ *
+ * @param {CoreSite} [site] Site. If not defined, use current site.
+ * @return {boolean} Whether events can be deleted.
+ * @since 3.3
+ */
+ canDeleteEventsInSite(site?: CoreSite): boolean {
+ site = site || this.sitesProvider.getCurrentSite();
+
+ return site.wsAvailable('core_calendar_delete_calendar_events');
+ }
+
/**
* Check if a certain site allows creating and editing events.
*
* @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise} Promise resolved with true if can create/edit.
+ * @since 3.7.1
*/
canEditEvents(siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
@@ -242,6 +271,7 @@ export class AddonCalendarProvider {
*
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether events can be created and edited.
+ * @since 3.7.1
*/
canEditEventsInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite();
@@ -261,20 +291,89 @@ export class AddonCalendarProvider {
return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?',
[this.timeUtils.timestamp()]).then((events) => {
return Promise.all(events.map((event) => {
- return this.deleteEvent(event.id, siteId);
+ return this.deleteLocalEvent(event.id, siteId);
}));
});
});
}
/**
- * Delete event cancelling all the reminders and notifications.
+ * Delete an event.
+ *
+ * @param {number} eventId Event ID to delete.
+ * @param {string} name Name of the event to delete.
+ * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series.
+ * @param {boolean} [forceOffline] True to always save it in offline.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ deleteEvent(eventId: number, name: string, deleteAll?: boolean, forceOffline?: boolean, siteId?: string): Promise {
+
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ // Function to store the submission to be synchronized later.
+ const storeOffline = (): Promise => {
+ return this.calendarOffline.markDeleted(eventId, name, deleteAll, siteId).then(() => {
+ return false;
+ });
+ };
+
+ if (forceOffline || !this.appProvider.isOnline()) {
+ // App is offline, store the action.
+ return storeOffline();
+ }
+
+ // If the event is already stored, discard it first.
+ return this.calendarOffline.unmarkDeleted(eventId, siteId).then(() => {
+ return this.deleteEventOnline(eventId, deleteAll, siteId).then(() => {
+ return true;
+ }).catch((error) => {
+ if (error && !this.utils.isWebServiceError(error)) {
+ // Couldn't connect to server, store in offline.
+ return storeOffline();
+ } else {
+ // The WebService has thrown an error, reject.
+ return Promise.reject(error);
+ }
+ });
+ });
+ }
+
+ /**
+ * Delete an event. It will fail if offline or cannot connect.
+ *
+ * @param {number} eventId Event ID to delete.
+ * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ deleteEventOnline(eventId: number, deleteAll?: boolean, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+
+ const params = {
+ events: [
+ {
+ eventid: eventId,
+ repeat: deleteAll ? 1 : 0
+ }
+ ]
+ },
+ preSets = {
+ responseExpected: false
+ };
+
+ return site.write('core_calendar_delete_calendar_events', params, preSets);
+ });
+ }
+
+ /**
+ * Delete a locally stored event cancelling all the reminders and notifications.
*
* @param {number} eventId Event ID.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise} Resolved when done.
*/
- protected deleteEvent(eventId: number, siteId?: string): Promise {
+ protected deleteLocalEvent(eventId: number, siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId();
@@ -833,7 +932,7 @@ export class AddonCalendarProvider {
if (timeEnd <= new Date().getTime()) {
// The event has finished already, don't schedule it.
- return this.deleteEvent(event.id, siteId);
+ return this.deleteLocalEvent(event.id, siteId);
}
return this.getEventReminders(event.id, siteId).then((reminders) => {
diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts
index 99bcbec94..41bd0e1ca 100644
--- a/src/addon/mod/chat/pages/chat/chat.ts
+++ b/src/addon/mod/chat/pages/chat/chat.ts
@@ -65,7 +65,7 @@ export class AddonModChatChatPage {
this.logger = logger.getInstance('AddonModChoiceChoicePage');
this.currentUserBeep = 'beep ' + sitesProvider.getCurrentSiteUserId();
this.isOnline = this.appProvider.isOnline();
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
this.isOnline = this.appProvider.isOnline();
diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts
index a9f4f175a..90e59df6d 100644
--- a/src/addon/mod/chat/pages/users/users.ts
+++ b/src/addon/mod/chat/pages/users/users.ts
@@ -44,7 +44,7 @@ export class AddonModChatUsersPage {
this.sessionId = navParams.get('sessionId');
this.isOnline = this.appProvider.isOnline();
this.currentUserId = this.sitesProvider.getCurrentSiteUserId();
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
this.isOnline = this.appProvider.isOnline();
diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts
index 65a21bec8..f1da26ca8 100644
--- a/src/addon/mod/feedback/pages/form/form.ts
+++ b/src/addon/mod/feedback/pages/form/form.ts
@@ -82,10 +82,10 @@ export class AddonModFeedbackFormPage implements OnDestroy {
this.currentSite = sitesProvider.getCurrentSite();
// Refresh online status when changes.
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
- this.offline = !online;
+ this.offline = !this.appProvider.isOnline();
});
});
}
diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts
index 68598a18e..7c395fc57 100644
--- a/src/addon/mod/forum/pages/discussion/discussion.ts
+++ b/src/addon/mod/forum/pages/discussion/discussion.ts
@@ -115,7 +115,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.postId = navParams.get('postId');
this.isOnline = this.appProvider.isOnline();
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
this.isOnline = this.appProvider.isOnline();
diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json
index 63974e503..19cb0008d 100644
--- a/src/assets/lang/en.json
+++ b/src/assets/lang/en.json
@@ -87,13 +87,19 @@
"addon.calendar.calendarevent": "Calendar event",
"addon.calendar.calendarevents": "Calendar events",
"addon.calendar.calendarreminders": "Calendar reminders",
+ "addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?",
+ "addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?",
"addon.calendar.defaultnotificationtime": "Default notification time",
+ "addon.calendar.deleteallevents": "Delete all events",
+ "addon.calendar.deleteevent": "Delete event",
+ "addon.calendar.deleteoneevent": "Delete this event",
"addon.calendar.durationminutes": "Duration in minutes",
"addon.calendar.durationnone": "Without duration",
"addon.calendar.durationuntil": "Until",
"addon.calendar.editevent": "Editing event",
"addon.calendar.errorloadevent": "Error loading event.",
"addon.calendar.errorloadevents": "Error loading events.",
+ "addon.calendar.eventcalendareventdeleted": "Calendar event deleted",
"addon.calendar.eventduration": "Duration",
"addon.calendar.eventendtime": "End time",
"addon.calendar.eventkind": "Type of event",
diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts
index 925f9109b..752a140e2 100644
--- a/src/core/course/classes/main-activity-component.ts
+++ b/src/core/course/classes/main-activity-component.ts
@@ -63,10 +63,10 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
const zone = injector.get(NgZone);
// Refresh online status when changes.
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
- this.isOnline = online;
+ this.isOnline = this.appProvider.isOnline();
});
});
}
diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts
index 4f4fdab17..9cbfb77f5 100644
--- a/src/providers/utils/dom.ts
+++ b/src/providers/utils/dom.ts
@@ -1344,9 +1344,12 @@ export class CoreDomUtilsProvider {
* @param {boolean} [needsTranslate] Whether the 'text' needs to be translated.
* @param {number} [duration=2000] Duration in ms of the dimissable toast.
* @param {string} [cssClass=""] Class to add to the toast.
+ * @param {boolean} [dismissOnPageChange=true] Dismiss the Toast on page change.
* @return {Toast} Toast instance.
*/
- showToast(text: string, needsTranslate?: boolean, duration: number = 2000, cssClass: string = ''): Toast {
+ showToast(text: string, needsTranslate?: boolean, duration: number = 2000, cssClass: string = '',
+ dismissOnPageChange: boolean = true): Toast {
+
if (needsTranslate) {
text = this.translate.instant(text);
}
@@ -1356,7 +1359,7 @@ export class CoreDomUtilsProvider {
duration: duration,
position: 'bottom',
cssClass: cssClass,
- dismissOnPageChange: true
+ dismissOnPageChange: dismissOnPageChange
});
loader.present();