MOBILE-1927 calendar: Allow creating events in offline

main
Dani Palou 2019-06-21 09:17:46 +02:00
parent 5a9a7b1a11
commit 7ccfade21e
13 changed files with 437 additions and 44 deletions

View File

@ -14,6 +14,7 @@
import { NgModule } from '@angular/core';
import { AddonCalendarProvider } from './providers/calendar';
import { AddonCalendarOfflineProvider } from './providers/calendar-offline';
import { AddonCalendarHelperProvider } from './providers/helper';
import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
@ -25,6 +26,7 @@ import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
export const ADDON_CALENDAR_PROVIDERS: any[] = [
AddonCalendarProvider,
AddonCalendarOfflineProvider,
AddonCalendarHelperProvider
];
@ -35,6 +37,7 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [
],
providers: [
AddonCalendarProvider,
AddonCalendarOfflineProvider,
AddonCalendarHelperProvider,
AddonCalendarMainMenuHandler
]

View File

@ -44,7 +44,7 @@
<ion-item text-wrap *ngIf="eventTypeControl.value == 'course'">
<ion-label id="addon-calendar-course-label"><h2 [core-mark-required]="true">{{ 'core.course' | translate }}</h2></ion-label>
<ion-select [formControlName]="'courseid'" aria-labelledby="addon-calendar-course-label" interface="action-sheet" [placeholder]="'core.noselection' | translate">
<ion-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-option>
<ion-option *ngFor="let course of courses" [value]="course.id"><core-format-text [text]="course.fullname"></core-format-text></ion-option>
</ion-select>
</ion-item>
@ -54,7 +54,7 @@
<ion-item text-wrap>
<ion-label id="addon-calendar-groupcourse-label"><h2 [core-mark-required]="true">{{ 'core.course' | translate }}</h2></ion-label>
<ion-select [formControlName]="'groupcourseid'" aria-labelledby="addon-calendar-groupcourse-label" interface="action-sheet" [placeholder]="'core.noselection' | translate" (ionChange)="groupCourseSelected($event)">
<ion-option *ngFor="let course of courses" [value]="course.id">{{ course.fullname }}</ion-option>
<ion-option *ngFor="let course of courses" [value]="course.id"><core-format-text [text]="course.fullname"></core-format-text></ion-option>
</ion-select>
</ion-item>
<!-- The course has no groups. -->

View File

@ -26,6 +26,7 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts';
import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper';
import { CoreSite } from '@classes/site';
@ -79,6 +80,7 @@ export class AddonCalendarEditEventPage implements OnInit {
private coursesProvider: CoreCoursesProvider,
private utils: CoreUtilsProvider,
private calendarProvider: AddonCalendarProvider,
private calendarOffline: AddonCalendarOfflineProvider,
private calendarHelper: AddonCalendarHelperProvider,
private fb: FormBuilder,
@Optional() private svComponent: CoreSplitViewComponent) {
@ -102,8 +104,10 @@ export class AddonCalendarEditEventPage implements OnInit {
this.groupControl = this.fb.control('');
this.descriptionControl = this.fb.control('');
const currentDate = this.timeUtils.toDatetimeFormat();
this.eventForm.addControl('name', this.fb.control('', Validators.required));
this.eventForm.addControl('timestart', this.fb.control(new Date().toISOString(), Validators.required));
this.eventForm.addControl('timestart', this.fb.control(currentDate, Validators.required));
this.eventForm.addControl('eventtype', this.eventTypeControl);
this.eventForm.addControl('categoryid', this.fb.control(''));
this.eventForm.addControl('courseid', this.fb.control(this.courseId));
@ -112,7 +116,7 @@ export class AddonCalendarEditEventPage implements OnInit {
this.eventForm.addControl('description', this.descriptionControl);
this.eventForm.addControl('location', this.fb.control(''));
this.eventForm.addControl('duration', this.fb.control(0));
this.eventForm.addControl('timedurationuntil', this.fb.control(new Date().toISOString()));
this.eventForm.addControl('timedurationuntil', this.fb.control(currentDate));
this.eventForm.addControl('timedurationminutes', this.fb.control(''));
this.eventForm.addControl('repeat', this.fb.control(false));
this.eventForm.addControl('repeats', this.fb.control('1'));
@ -131,9 +135,10 @@ export class AddonCalendarEditEventPage implements OnInit {
/**
* Fetch the data needed to render the form.
*
* @param {boolean} [refresh] Whether it's refreshing data.
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchData(): Promise<any> {
protected fetchData(refresh?: boolean): Promise<any> {
let accessInfo;
// Get access info.
@ -151,6 +156,33 @@ export class AddonCalendarEditEventPage implements OnInit {
return Promise.reject(this.translate.instant('addon.calendar.nopermissiontoupdatecalendar'));
}
if (this.eventId && !refresh) {
// Get the event data if there's any.
promises.push(this.calendarOffline.getEvent(this.eventId).then((event) => {
this.hasOffline = true;
// Load the data in the form.
this.eventForm.controls.name.setValue(event.name);
this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000));
this.eventForm.controls.eventtype.setValue(event.eventtype);
this.eventForm.controls.categoryid.setValue(event.categoryid || '');
this.eventForm.controls.courseid.setValue(event.courseid || '');
this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || '');
this.eventForm.controls.groupid.setValue(event.groupid || '');
this.eventForm.controls.description.setValue(event.description);
this.eventForm.controls.location.setValue(event.location);
this.eventForm.controls.duration.setValue(event.duration);
this.eventForm.controls.timedurationuntil.setValue(
this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now()));
this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || '');
this.eventForm.controls.repeat.setValue(!!event.repeat);
this.eventForm.controls.repeats.setValue(event.repeats || '1');
}).catch(() => {
// No offline data.
this.hasOffline = false;
}));
}
if (types.category) {
// Get the categories.
promises.push(this.coursesProvider.getCategories(0, true).then((cats) => {
@ -185,12 +217,14 @@ export class AddonCalendarEditEventPage implements OnInit {
}
return Promise.all(promises).then(() => {
// Set event types. If course is allowed, select it first.
if (!this.eventTypeControl.value) {
// Initialize event type value. If course is allowed, select it first.
if (types.course) {
this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE);
} else {
this.eventTypeControl.setValue(eventTypes[0].value);
}
}
this.eventTypes = eventTypes;
});
@ -227,7 +261,7 @@ export class AddonCalendarEditEventPage implements OnInit {
}
Promise.all(promises).finally(() => {
this.fetchData().finally(() => {
this.fetchData(true).finally(() => {
refresher.complete();
});
});
@ -333,8 +367,8 @@ export class AddonCalendarEditEventPage implements OnInit {
// Send the data.
const modal = this.domUtils.showModalLoading('core.sending');
this.calendarProvider.submitEvent(this.eventId, data).then((event) => {
this.returnToList(event);
this.calendarProvider.submitEvent(this.eventId, data).then((result) => {
this.returnToList(result.event);
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error sending data.');
}).finally(() => {
@ -348,10 +382,14 @@ export class AddonCalendarEditEventPage implements OnInit {
* @param {number} [event] Event.
*/
protected returnToList(event?: any): void {
if (event) {
const data: any = {
event: event
};
this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId());
} else {
this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, this.currentSite.getId());
}
if (this.svComponent && this.svComponent.isOn()) {
// Empty form.
@ -369,7 +407,12 @@ export class AddonCalendarEditEventPage implements OnInit {
*/
discard(): void {
this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
// @todo.
this.calendarOffline.deleteEvent(this.eventId).then(() => {
this.returnToList();
}).catch(() => {
// Shouldn't happen.
this.domUtils.showErrorModal('Error discarding event.');
});
}).catch(() => {
// Cancelled.
});

View File

@ -17,9 +17,28 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="eventsLoaded">
<!-- There is data to be synchronized -->
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline">
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }}
</ion-card>
<core-empty-box *ngIf="!filteredEvents || !filteredEvents.length" icon="calendar" [message]="'addon.calendar.noevents' | translate">
</core-empty-box>
<ion-list *ngIf="offlineEvents && offlineEvents.length" no-margin>
<ng-container *ngFor="let event of offlineEvents">
<ion-item-divider> {{ 'core.notsent' | translate }}</ion-item-divider>
<a ion-item text-wrap [title]="event.name" (click)="gotoEvent(event.id)" [class.core-split-item-selected]="event.id == eventId">
<core-icon *ngIf="event.icon" [name]="event.icon" item-start></core-icon>
<h2><core-format-text [text]="event.name"></core-format-text></h2>
<p>
{{ event.timestart * 1000 | coreFormatDate: "strftimedatetimeshort" }}
<span *ngIf="event.timeduration"> - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}</span>
</p>
</a>
</ng-container>
</ion-list>
<ion-list *ngIf="filteredEvents && filteredEvents.length" no-margin>
<ng-container *ngFor="let event of filteredEvents">
<ion-item-divider *ngIf="event.showDate">
@ -43,7 +62,7 @@
<!-- Create a calendar event. -->
<ion-fab core-fab bottom end *ngIf="canCreate">
<button ion-fab (click)="openCreate()" [attr.aria-label]="'addon.calendar.newevent' | translate">
<button ion-fab (click)="openEdit()" [attr.aria-label]="'addon.calendar.newevent' | translate">
<ion-icon name="add"></ion-icon>
</button>
</ion-fab>

View File

@ -16,6 +16,7 @@ import { Component, ViewChild, OnDestroy } from '@angular/core';
import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -55,10 +56,12 @@ export class AddonCalendarListPage implements OnDestroy {
protected eventId: number;
protected preSelectedCourseId: number;
protected newEventObserver: any;
protected discardedObserver: any;
courses: any[];
eventsLoaded = false;
events = [];
offlineEvents = [];
notificationsEnabled = false;
filteredEvents = [];
canLoadMore = false;
@ -67,12 +70,14 @@ export class AddonCalendarListPage implements OnDestroy {
course: this.allCourses
};
canCreate = false;
hasOffline = false;
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider,
private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider,
localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController,
eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider) {
eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider,
private calendarOffline: AddonCalendarOfflineProvider) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
this.notificationsEnabled = localNotificationsProvider.isAvailable();
@ -87,7 +92,7 @@ export class AddonCalendarListPage implements OnDestroy {
this.eventId = navParams.get('eventId') || false;
this.preSelectedCourseId = navParams.get('courseId') || null;
// Listen for events added. When an event is added, we reload the data.
// Listen for events added. When an event is added, reload the data.
this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => {
if (data && data.event) {
if (this.splitviewCtrl.isOn()) {
@ -97,19 +102,25 @@ export class AddonCalendarListPage implements OnDestroy {
this.eventsLoaded = false;
this.refreshEvents(false).finally(() => {
this.eventsLoaded = true;
// In tablet mode try to open the event.
if (this.splitviewCtrl.isOn()) {
if (data.event.id) {
// In tablet mode try to open the event (only if it's an online event).
if (this.splitviewCtrl.isOn() && data.event.id > 0) {
this.gotoEvent(data.event.id);
} else {
// It's an offline event.
}
}
});
}
}, sitesProvider.getCurrentSiteId());
// Listen for new event discarded event. When it does, reload the data.
this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
if (this.splitviewCtrl.isOn()) {
// Discussion added, clear details page.
this.splitviewCtrl.emptyDetails();
}
this.eventsLoaded = false;
this.refreshEvents(false);
}, sitesProvider.getCurrentSiteId());
}
/**
@ -126,8 +137,6 @@ export class AddonCalendarListPage implements OnDestroy {
// Take first and load it.
this.gotoEvent(this.events[0].id);
}
}).finally(() => {
this.eventsLoaded = true;
});
}
@ -167,7 +176,18 @@ export class AddonCalendarListPage implements OnDestroy {
return this.fetchEvents(refresh);
}));
return Promise.all(promises);
// Get offline events.
promises.push(this.calendarOffline.getAllEvents().then((events) => {
this.hasOffline = !!events.length;
// Format data and sort by timestart.
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart);
}));
return Promise.all(promises).finally(() => {
this.eventsLoaded = true;
});
}
/**
@ -194,6 +214,8 @@ export class AddonCalendarListPage implements OnDestroy {
return this.fetchEvents();
}
} else {
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
// Sort the events by timestart, they're ordered by id.
events.sort((a, b) => {
if (a.timestart == b.timestart) {
@ -203,7 +225,6 @@ export class AddonCalendarListPage implements OnDestroy {
return a.timestart - b.timestart;
});
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
this.getCategories = this.shouldLoadCategories(events);
if (refresh) {
@ -427,10 +448,16 @@ export class AddonCalendarListPage implements OnDestroy {
}
/**
* Open page to create an event.
* Open page to create/edit an event.
*
* @param {number} [eventId] Event ID to edit.
*/
openCreate(): void {
openEdit(eventId?: number): void {
const params: any = {};
if (eventId) {
params.eventId = eventId;
}
if (this.filter.course.id != this.allCourses.id) {
params.courseId = this.filter.course.id;
}
@ -452,7 +479,15 @@ export class AddonCalendarListPage implements OnDestroy {
*/
gotoEvent(eventId: number): void {
this.eventId = eventId;
this.splitviewCtrl.push('AddonCalendarEventPage', { id: eventId });
if (eventId < 0) {
// It's an offline event, go to the edit page.
this.openEdit(eventId);
} else {
this.splitviewCtrl.push('AddonCalendarEventPage', {
id: eventId
});
}
}
/**

View File

@ -0,0 +1,215 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { AddonCalendarProvider } from './calendar';
/**
* Service to handle offline calendar events.
*/
@Injectable()
export class AddonCalendarOfflineProvider {
// Variables for database.
static EVENTS_TABLE = 'addon_calendar_offline_events';
protected siteSchema: CoreSiteSchema = {
name: 'AddonCalendarOfflineProvider',
version: 1,
tables: [
{
name: AddonCalendarOfflineProvider.EVENTS_TABLE,
columns: [
{
name: 'id', // Negative for offline entries.
type: 'INTEGER',
},
{
name: 'name',
type: 'TEXT',
notNull: true
},
{
name: 'timestart',
type: 'INTEGER',
notNull: true
},
{
name: 'eventtype',
type: 'TEXT',
notNull: true
},
{
name: 'categoryid',
type: 'INTEGER',
},
{
name: 'courseid',
type: 'INTEGER',
},
{
name: 'groupcourseid',
type: 'INTEGER',
},
{
name: 'groupid',
type: 'INTEGER',
},
{
name: 'description',
type: 'TEXT',
},
{
name: 'location',
type: 'TEXT',
},
{
name: 'duration',
type: 'INTEGER',
},
{
name: 'timedurationuntil',
type: 'INTEGER',
},
{
name: 'timedurationminutes',
type: 'INTEGER',
},
{
name: 'repeat',
type: 'INTEGER',
},
{
name: 'repeats',
type: 'TEXT',
},
{
name: 'userid',
type: 'INTEGER',
},
{
name: 'timecreated',
type: 'INTEGER',
}
],
primaryKeys: ['id']
}
]
};
constructor(private sitesProvider: CoreSitesProvider) {
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
/**
* Delete an offline event.
*
* @param {number} eventId Event ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
*/
deleteEvent(eventId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions: any = {
id: eventId
};
return site.getDb().deleteRecords(AddonCalendarOfflineProvider.EVENTS_TABLE, conditions);
});
}
/**
* Get all offline events.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with events.
*/
getAllEvents(siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonCalendarOfflineProvider.EVENTS_TABLE);
});
}
/**
* Get an offline event.
*
* @param {number} eventId Event ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the event.
*/
getEvent(eventId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions: any = {
id: eventId
};
return site.getDb().getRecord(AddonCalendarOfflineProvider.EVENTS_TABLE, conditions);
});
}
/**
* Check if there are offline events to send.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline events, false otherwise.
*/
hasEvents(siteId?: string): Promise<boolean> {
return this.getAllEvents(siteId).then((events) => {
return !!events.length;
}).catch(() => {
// No offline data found, return false.
return false;
});
}
/**
* Offline version for adding a new discussion to a forum.
*
* @param {number} eventId Event ID. If it's a new event, set it to undefined/null.
* @param {any} data Event data.
* @param {number} [timeCreated] The time the event was created. If not defined, current time.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the stored event.
*/
saveEvent(eventId: number, data: any, timeCreated?: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
timeCreated = timeCreated || Date.now();
const event = {
id: eventId || -timeCreated,
name: data.name,
timestart: data.timestart,
eventtype: data.eventtype,
categoryid: data.categoryid || null,
courseid: data.courseid || null,
groupcourseid: data.groupcourseid || null,
groupid: data.groupid || null,
description: data.description && data.description.text,
location: data.location,
duration: data.duration,
timedurationuntil: data.timedurationuntil,
timedurationminutes: data.timedurationminutes,
repeat: data.repeat ? 1 : 0,
repeats: data.repeats,
timecreated: timeCreated,
userid: site.getUserId()
};
return site.getDb().insertRecord(AddonCalendarOfflineProvider.EVENTS_TABLE, event).then(() => {
return event;
});
});
}
}

View File

@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreSite } from '@classes/site';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreAppProvider } from '@providers/app';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreGroupsProvider } from '@providers/groups';
@ -25,6 +26,7 @@ import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreConfigProvider } from '@providers/config';
import { ILocalNotification } from '@ionic-native/local-notifications';
import { SQLiteDB } from '@classes/sqlitedb';
import { AddonCalendarOfflineProvider } from './calendar-offline';
/**
* Service to handle calendar events.
@ -37,6 +39,7 @@ export class AddonCalendarProvider {
static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime';
static DEFAULT_NOTIFICATION_TIME = 60;
static NEW_EVENT_EVENT = 'addon_calendar_new_event';
static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded';
static TYPE_CATEGORY = 'category';
static TYPE_COURSE = 'course';
static TYPE_GROUP = 'group';
@ -214,7 +217,8 @@ export class AddonCalendarProvider {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider,
private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider,
private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider,
private utils: CoreUtilsProvider) {
private utils: CoreUtilsProvider, private calendarOffline: AddonCalendarOfflineProvider,
private appProvider: CoreAppProvider) {
this.logger = logger.getInstance('AddonCalendarProvider');
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
@ -918,14 +922,58 @@ export class AddonCalendarProvider {
}
/**
* Submit an event, either to create it or to edit it.
* Submit a calendar event.
*
* @param {number} eventId ID of the event. If undefined/null, create a new event.
* @param {any} formData Form data.
* @param {number} [timeCreated] The time the event was created. Only if modifying a new offline event.
* @param {boolean} [forceOffline] True to always save it in offline.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<{sent: boolean, event: any}>} Promise resolved with the event and a boolean indicating if data was
* sent to server or stored in offline.
*/
submitEvent(eventId: number, formData: any, timeCreated?: number, forceOffline?: boolean, siteId?: string):
Promise<{sent: boolean, event: any}> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Function to store the event to be synchronized later.
const storeOffline = (): Promise<{sent: boolean, event: any}> => {
return this.calendarOffline.saveEvent(eventId, formData, timeCreated, siteId).then((event) => {
return {sent: false, event: event};
});
};
if (forceOffline || !this.appProvider.isOnline()) {
// App is offline, store the event.
return storeOffline();
}
// If the event is already stored, discard it first.
return this.calendarOffline.deleteEvent(eventId, siteId).then(() => {
return this.submitEventOnline(eventId, formData, siteId).then((event) => {
return {sent: true, event: event};
}).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);
}
});
});
}
/**
* Submit an event, either to create it or to edit it. It will fail if offline or cannot connect.
*
* @param {number} eventId ID of the event. If undefined/null, create a new event.
* @param {any} formData Form data.
* @param {string} [siteId] Site ID. If not provided, current site.
* @return {Promise<any>} Promise resolved when done.
*/
submitEvent(eventId: number, formData: any, siteId?: string): Promise<any> {
submitEventOnline(eventId: number, formData: any, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
// Add data that is "hidden" in web.
formData.id = eventId || 0;
@ -940,7 +988,7 @@ export class AddonCalendarProvider {
return site.write('core_calendar_submit_create_update_form', params).then((result) => {
if (result.validationerror) {
return Promise.reject(null);
return Promise.reject(this.utils.createFakeWSError(''));
}
return result.event;

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreCourseProvider } from '@core/course/providers/course';
import { AddonCalendarProvider } from './calendar';
import { CoreConstants } from '@core/constants';
/**
* Service that provides some features regarding lists of courses and categories.
@ -47,6 +48,20 @@ export class AddonCalendarHelperProvider {
e.icon = this.courseProvider.getModuleIconSrc(e.modulename);
e.moduleIcon = e.icon;
}
if (e.id < 0) {
// It's an offline event, add some calculated data.
e.format = 1;
e.visible = 1;
if (e.duration == 1) {
e.timeduration = e.timedurationuntil - e.timestart;
} else if (e.duration == 2) {
e.timeduration = e.timedurationminutes * CoreConstants.SECONDS_MINUTE;
} else {
e.timeduration = 0;
}
}
}
/**

View File

@ -51,10 +51,10 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo
val = this.search['f_' + this.field.id + '_y'] ? new Date(this.search['f_' + this.field.id + '_y'] + '-' +
this.search['f_' + this.field.id + '_m'] + '-' + this.search['f_' + this.field.id + '_d']) : new Date();
this.search['f_' + this.field.id] = val.toISOString();
this.search['f_' + this.field.id] = this.timeUtils.toDatetimeFormat(val.getTime());
} else {
val = this.value && this.value.content ? new Date(parseInt(this.value.content, 10) * 1000) : new Date();
val = val.toISOString();
val = this.timeUtils.toDatetimeFormat(val.getTime());
}
this.addControl('f_' + this.field.id, val);

View File

@ -15,6 +15,7 @@ import { Injector, Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { AddonModDataFieldHandler } from '../../../providers/fields-delegate';
import { AddonModDataFieldDateComponent } from '../component/date';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
/**
* Handler for date data field plugin.
@ -24,7 +25,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldDateHandler';
type = 'date';
constructor(private translate: TranslateService) { }
constructor(private translate: TranslateService, private timeUtils: CoreTimeUtilsProvider) { }
/**
* Return the Component to use to display the plugin data.
@ -129,7 +130,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler {
input = inputData[fieldName] && inputData[fieldName].substr(0, 10) || '';
originalFieldData = (originalFieldData && originalFieldData.content &&
new Date(originalFieldData.content * 1000).toISOString().substr(0, 10)) || '';
this.timeUtils.toDatetimeFormat(originalFieldData.content * 1000).substr(0, 10)) || '';
return input != originalFieldData;
}

View File

@ -1389,6 +1389,7 @@
"core.deleteduser": "Deleted user",
"core.deleting": "Deleting",
"core.description": "Description",
"core.dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS",
"core.dfdaymonthyear": "MM-DD-YYYY",
"core.dfdayweekmonth": "ddd, D MMM",
"core.dffulldate": "dddd, D MMMM YYYY h[:]mm A",

View File

@ -64,6 +64,7 @@
"deleteduser": "Deleted user",
"deleting": "Deleting",
"description": "Description",
"dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS",
"dfdaymonthyear": "MM-DD-YYYY",
"dfdayweekmonth": "ddd, D MMM",
"dffulldate": "dddd, D MMMM YYYY h[:]mm A",

View File

@ -299,6 +299,18 @@ export class CoreTimeUtilsProvider {
return moment(timestamp).format(format);
}
/**
* Convert a timestamp to the format to set to a datetime input.
*
* @param {number} [timestamp] Timestamp to convert (in ms). If not provided, current time.
* @return {string} Formatted time.
*/
toDatetimeFormat(timestamp?: number): string {
timestamp = timestamp || Date.now();
return this.userDate(timestamp, 'core.dfdatetimeinput', false);
}
/**
* Convert a text into user timezone timestamp.
*