mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 09:03:02 +00:00
feat: calendar plugin
This commit is contained in:
committed by
Rainer Killinger
parent
080e6fa3e8
commit
a57c3029df
226
src/app/modules/calendar/add-event-review-modal.component.ts
Normal file
226
src/app/modules/calendar/add-event-review-modal.component.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (C) 2022 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {
|
||||
getICalExport,
|
||||
getNativeCalendarExport,
|
||||
ICalEvent,
|
||||
serializeICal,
|
||||
toICal,
|
||||
toICalUpdates,
|
||||
} from './ical/ical';
|
||||
import moment from 'moment';
|
||||
import {Share} from '@capacitor/share';
|
||||
import {Directory, Encoding, Filesystem} from '@capacitor/filesystem';
|
||||
import {Device} from '@capacitor/device';
|
||||
import {CalendarService} from './calendar.service';
|
||||
import {Dialog} from '@capacitor/dialog';
|
||||
import {SCDateSeries} from '@openstapps/core';
|
||||
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {NewShareData, NewShareNavigator} from './new-share';
|
||||
|
||||
interface ICalInfo {
|
||||
title: string;
|
||||
events: ICalEvent[];
|
||||
cancelledEvents: ICalEvent[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'add-event-review-modal',
|
||||
templateUrl: 'add-event-review-modal.html',
|
||||
styleUrls: ['add-event-review-modal.scss'],
|
||||
})
|
||||
export class AddEventReviewModalComponent implements OnInit {
|
||||
moment = moment;
|
||||
|
||||
@Input() dismissAction: () => void;
|
||||
|
||||
@Input() dateSeries: SCDateSeries[];
|
||||
|
||||
iCalEvents: ICalInfo[];
|
||||
|
||||
includeCancelled = true;
|
||||
|
||||
isWeb = true;
|
||||
|
||||
constructor(
|
||||
readonly calendarService: CalendarService,
|
||||
readonly translator: ThingTranslateService,
|
||||
readonly translateService: TranslateService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
Device.getInfo().then(it => {
|
||||
this.isWeb = it.platform === 'web';
|
||||
});
|
||||
|
||||
this.iCalEvents = this.dateSeries.map(event => ({
|
||||
title:
|
||||
this.translator.translator.translatedAccess(event).event.name() ??
|
||||
'error',
|
||||
events: toICal(event, this.translator.translator, {
|
||||
allowRRuleExceptions: true,
|
||||
excludeCancelledEvents: false,
|
||||
}),
|
||||
cancelledEvents: toICalUpdates(event, this.translator.translator),
|
||||
}));
|
||||
}
|
||||
|
||||
async toCalendar() {
|
||||
await Dialog.confirm({
|
||||
title: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.dialogs.toCalendarConfirm.TITLE',
|
||||
),
|
||||
message: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.dialogs.toCalendarConfirm.DESCRIPTION',
|
||||
),
|
||||
});
|
||||
|
||||
await this.calendarService.syncEvents(
|
||||
getNativeCalendarExport(this.dateSeries, this.translator.translator),
|
||||
);
|
||||
|
||||
this.dismissAction();
|
||||
}
|
||||
|
||||
async download() {
|
||||
const blob = new Blob(
|
||||
[
|
||||
serializeICal(
|
||||
getICalExport(
|
||||
this.dateSeries,
|
||||
this.translator.translator,
|
||||
this.includeCancelled,
|
||||
),
|
||||
),
|
||||
],
|
||||
{
|
||||
type: 'text/calendar',
|
||||
},
|
||||
);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${
|
||||
this.dateSeries.length === 1
|
||||
? this.dateSeries[0].event.name
|
||||
: 'stapps_calendar'
|
||||
}.ics`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
async export() {
|
||||
const info = await Device.getInfo();
|
||||
|
||||
if (info.platform === 'web') {
|
||||
const blob = new Blob(
|
||||
[
|
||||
serializeICal(
|
||||
getICalExport(
|
||||
this.dateSeries,
|
||||
this.translator.translator,
|
||||
this.includeCancelled,
|
||||
),
|
||||
),
|
||||
],
|
||||
{
|
||||
type: 'text/calendar',
|
||||
},
|
||||
);
|
||||
const file = new File([blob], 'calendar.ics', {type: blob.type});
|
||||
const shareData: NewShareData = {
|
||||
files: [file],
|
||||
title: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.shareData.TITLE',
|
||||
),
|
||||
text: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.shareData.TEXT',
|
||||
),
|
||||
};
|
||||
|
||||
if (!(navigator as unknown as NewShareNavigator).canShare) {
|
||||
return Dialog.alert({
|
||||
title: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.dialogs.cannotShare.TITLE',
|
||||
),
|
||||
message: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.dialogs.cannotShare.DESCRIPTION',
|
||||
),
|
||||
});
|
||||
}
|
||||
console.log(
|
||||
(navigator as unknown as NewShareNavigator).canShare(shareData),
|
||||
);
|
||||
|
||||
if (!(navigator as unknown as NewShareNavigator).canShare(shareData)) {
|
||||
return Dialog.alert({
|
||||
title: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.dialogs.unsupportedFileType.TITLE',
|
||||
),
|
||||
message: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.dialogs.unsupportedFileType.DESCRIPTION',
|
||||
),
|
||||
});
|
||||
}
|
||||
try {
|
||||
await (navigator as unknown as NewShareNavigator).share(shareData);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return Dialog.alert({
|
||||
title: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.dialogs.failedShare.TITLE',
|
||||
),
|
||||
message: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.dialogs.failedShare.DESCRIPTION',
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const result = await Filesystem.writeFile({
|
||||
path: `${
|
||||
this.dateSeries.length === 1
|
||||
? this.dateSeries[0].event.name
|
||||
: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.shareData.FILE_TYPE',
|
||||
)
|
||||
}.ics`,
|
||||
data: serializeICal(
|
||||
getICalExport(
|
||||
this.dateSeries,
|
||||
this.translator.translator,
|
||||
this.includeCancelled,
|
||||
),
|
||||
),
|
||||
encoding: Encoding.UTF8,
|
||||
directory: Directory.Cache,
|
||||
});
|
||||
|
||||
await Share.share({
|
||||
title: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.shareData.TITLE',
|
||||
),
|
||||
text: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.shareData.TEXT',
|
||||
),
|
||||
url: result.uri,
|
||||
dialogTitle: this.translateService.instant(
|
||||
'schedule.toCalendar.reviewModal.shareData.TITLE',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/app/modules/calendar/add-event-review-modal.html
Normal file
81
src/app/modules/calendar/add-event-review-modal.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!--
|
||||
~ Copyright (C) 2022 StApps
|
||||
~ This program is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU General Public License as published by the Free
|
||||
~ Software Foundation, version 3.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for
|
||||
~ more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with
|
||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<div>
|
||||
<ion-card-header>
|
||||
<ion-card-title>{{
|
||||
'schedule.toCalendar.reviewModal.TITLE' | translate
|
||||
}}</ion-card-title>
|
||||
<ion-button fill="clear" (click)="dismissAction()">
|
||||
<ion-label>{{ 'modal.DISMISS' | translate }}</ion-label>
|
||||
</ion-button>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content>
|
||||
<ion-list lines="none">
|
||||
<ion-item-group *ngFor="let event of iCalEvents">
|
||||
<ion-item-divider>
|
||||
<ion-label>{{ event.title }}</ion-label>
|
||||
<ion-note slot="start" *ngIf="event.events.length > 1">
|
||||
<ion-icon name="warning"></ion-icon>
|
||||
</ion-note>
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let iCalEvent of event.events">
|
||||
<ion-label>
|
||||
<s *ngIf="iCalEvent.cancelled; else date"
|
||||
><ng-container [ngTemplateOutlet]="date"></ng-container>
|
||||
</s>
|
||||
<ng-template #date>
|
||||
{{ moment(iCalEvent.start) | amDateFormat: 'll, HH:mm' }}
|
||||
</ng-template>
|
||||
</ion-label>
|
||||
<ion-note *ngIf="iCalEvent.rrule">
|
||||
{{ iCalEvent.rrule.interval }}
|
||||
{{ iCalEvent.rrule.freq | sentencecase }}
|
||||
</ion-note>
|
||||
<ion-icon *ngIf="iCalEvent.rrule" name="repeat"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-list>
|
||||
</ion-card-content>
|
||||
|
||||
<div class="horizontal-flex">
|
||||
<ion-item lines="none">
|
||||
<ion-label>{{
|
||||
'schedule.toCalendar.reviewModal.INCLUDE_CANCELLED' | translate
|
||||
}}</ion-label>
|
||||
<ion-checkbox [(ngModel)]="includeCancelled" slot="end"></ion-checkbox>
|
||||
</ion-item>
|
||||
</div>
|
||||
<div class="horizontal-flex">
|
||||
<ion-button fill="clear" (click)="export()">
|
||||
{{ 'share' | translate }}
|
||||
<ion-icon slot="end" name="share"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
fill="outline"
|
||||
(click)="download()"
|
||||
*ngIf="isWeb; else exportButton"
|
||||
>
|
||||
{{ 'schedule.toCalendar.reviewModal.DOWNLOAD' | translate }}
|
||||
<ion-icon slot="end" name="download"></ion-icon>
|
||||
</ion-button>
|
||||
<ng-template #exportButton>
|
||||
<ion-button fill="outline" (click)="toCalendar()">
|
||||
{{ 'schedule.toCalendar.reviewModal.EXPORT' | translate }}
|
||||
<ion-icon slot="end" name="calendar"></ion-icon>
|
||||
</ion-button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
29
src/app/modules/calendar/add-event-review-modal.scss
Normal file
29
src/app/modules/calendar/add-event-review-modal.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
div {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
ion-card-header {
|
||||
ion-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-flex {
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
21
src/app/modules/calendar/calendar-info.ts
Normal file
21
src/app/modules/calendar/calendar-info.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2022 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export interface CalendarInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
displayname: string;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
41
src/app/modules/calendar/calendar.module.ts
Normal file
41
src/app/modules/calendar/calendar.module.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2022 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
import {AddEventReviewModalComponent} from './add-event-review-modal.component';
|
||||
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
||||
import {CalendarService} from './calendar.service';
|
||||
import {ScheduleProvider} from './schedule.provider';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {MomentModule} from 'ngx-moment';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AddEventReviewModalComponent],
|
||||
imports: [
|
||||
IonicModule.forRoot(),
|
||||
TranslateModule.forChild(),
|
||||
ThingTranslateModule.forChild(),
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
MomentModule,
|
||||
],
|
||||
exports: [],
|
||||
providers: [Calendar, CalendarService, ScheduleProvider],
|
||||
})
|
||||
export class CalendarModule {}
|
||||
101
src/app/modules/calendar/calendar.service.ts
Normal file
101
src/app/modules/calendar/calendar.service.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2022 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ICalEvent} from './ical/ical';
|
||||
import moment, {duration, unitOfTime} from 'moment';
|
||||
import {Dialog} from '@capacitor/dialog';
|
||||
import {CalendarInfo} from './calendar-info';
|
||||
|
||||
const CALENDAR_NAME = 'StApps';
|
||||
|
||||
const RECURRENCE_PATTERNS: Partial<
|
||||
Record<unitOfTime.Diff, string | undefined>
|
||||
> = {
|
||||
year: 'yearly',
|
||||
month: 'monthly',
|
||||
week: 'weekly',
|
||||
day: 'daily',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CalendarService {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
constructor(readonly calendar: Calendar) {}
|
||||
|
||||
async createCalendar(): Promise<CalendarInfo | undefined> {
|
||||
await this.calendar.createCalendar({
|
||||
calendarName: CALENDAR_NAME,
|
||||
calendarColor: '#ff8740',
|
||||
});
|
||||
return this.findCalendar(CALENDAR_NAME);
|
||||
}
|
||||
|
||||
async listCalendars(): Promise<CalendarInfo[] | undefined> {
|
||||
return this.calendar.listCalendars();
|
||||
}
|
||||
|
||||
async findCalendar(name: string): Promise<CalendarInfo | undefined> {
|
||||
return (await this.listCalendars())?.find(
|
||||
(calendar: CalendarInfo) => calendar.name === name,
|
||||
);
|
||||
}
|
||||
|
||||
async purge(): Promise<CalendarInfo | undefined> {
|
||||
if (await this.findCalendar(CALENDAR_NAME)) {
|
||||
await this.calendar.deleteCalendar(CALENDAR_NAME);
|
||||
}
|
||||
return await this.createCalendar();
|
||||
}
|
||||
|
||||
async syncEvents(events: ICalEvent[]) {
|
||||
const calendar = await this.purge();
|
||||
if (!calendar) {
|
||||
return Dialog.alert({
|
||||
title: 'Error',
|
||||
message: 'Could not create calendar',
|
||||
});
|
||||
}
|
||||
|
||||
for (const iCalEvent of events) {
|
||||
// TODO: change to use non-interactive version after testing is complete
|
||||
const start = iCalEvent.rrule ? iCalEvent.rrule.from : iCalEvent.start;
|
||||
|
||||
await this.calendar.createEventWithOptions(
|
||||
iCalEvent.recurrenceSequence
|
||||
? `(${iCalEvent.recurrenceSequence}/${iCalEvent.recurrenceSequenceAmount}) ${iCalEvent.name}`
|
||||
: iCalEvent.name,
|
||||
iCalEvent.geo,
|
||||
iCalEvent.description,
|
||||
new Date(start),
|
||||
moment(start).add(duration(iCalEvent.duration)).toDate(),
|
||||
{
|
||||
id: `${iCalEvent.uuid}-${start}`,
|
||||
url: iCalEvent.url,
|
||||
calendarName: calendar.name,
|
||||
calendarId: calendar.id,
|
||||
...(iCalEvent.rrule
|
||||
? {
|
||||
recurrence: RECURRENCE_PATTERNS[iCalEvent.rrule.freq],
|
||||
recurrenceInterval: iCalEvent.rrule.interval,
|
||||
recurrenceEndDate: new Date(iCalEvent.rrule.until),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/app/modules/calendar/ical/ical.spec.ts
Normal file
82
src/app/modules/calendar/ical/ical.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {findRRules, RRule} from './ical';
|
||||
import moment, {unitOfTime} from 'moment';
|
||||
import {shuffle} from 'lodash-es';
|
||||
import {SCISO8601Date} from '@openstapps/core';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function expandRRule(rule: RRule): SCISO8601Date[] {
|
||||
const initial = moment(rule.from);
|
||||
const interval = rule.interval ?? 1;
|
||||
|
||||
return shuffle(
|
||||
Array.from({
|
||||
length:
|
||||
Math.floor(
|
||||
moment(rule.until).diff(initial, rule.freq, true) / interval,
|
||||
) + 1,
|
||||
}).map((_, i) =>
|
||||
initial
|
||||
.clone()
|
||||
.add(interval * i, rule.freq ?? 'day')
|
||||
.toISOString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe('iCal', () => {
|
||||
it('should find simple recurrence patterns', () => {
|
||||
for (const freq of ['day', 'week', 'month', 'year'] as unitOfTime.Diff[]) {
|
||||
for (const interval of [1, 2, 3]) {
|
||||
const pattern: RRule = {
|
||||
freq: freq,
|
||||
interval: interval,
|
||||
from: moment('2021-09-01T10:00').toISOString(),
|
||||
until: moment('2021-09-01T10:00')
|
||||
.add(4 * interval, freq)
|
||||
.toISOString(),
|
||||
};
|
||||
|
||||
expect(findRRules(expandRRule(pattern))).toEqual([pattern]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should find missing recurrence patterns', () => {
|
||||
const pattern: SCISO8601Date = moment('2021-09-01T10:00').toISOString();
|
||||
|
||||
expect(findRRules([pattern])).toEqual([pattern]);
|
||||
});
|
||||
|
||||
it('should find mixed recurrence patterns', () => {
|
||||
const singlePattern: SCISO8601Date =
|
||||
moment('2021-09-01T09:00').toISOString();
|
||||
|
||||
const weeklyPattern: RRule = {
|
||||
freq: 'week',
|
||||
interval: 1,
|
||||
from: moment('2021-09-03T10:00').toISOString(),
|
||||
until: moment('2021-09-03T10:00').add(4, 'weeks').toISOString(),
|
||||
};
|
||||
|
||||
expect(
|
||||
findRRules(shuffle([singlePattern, ...expandRRule(weeklyPattern)])),
|
||||
).toEqual([singlePattern, weeklyPattern]);
|
||||
});
|
||||
});
|
||||
414
src/app/modules/calendar/ical/ical.ts
Normal file
414
src/app/modules/calendar/ical/ical.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {
|
||||
SCDateSeries,
|
||||
SCISO8601Date,
|
||||
SCISO8601Duration,
|
||||
SCThingTranslator,
|
||||
SCThingWithCategories,
|
||||
SCUuid,
|
||||
} from '@openstapps/core';
|
||||
import {
|
||||
difference,
|
||||
flatMap,
|
||||
isObject,
|
||||
last,
|
||||
mapValues,
|
||||
minBy,
|
||||
size,
|
||||
} from 'lodash-es';
|
||||
import moment, {unitOfTime} from 'moment';
|
||||
|
||||
export interface ICalEvent {
|
||||
name?: string;
|
||||
uuid: SCUuid;
|
||||
categories?: string[];
|
||||
description?: string;
|
||||
cancelled?: boolean;
|
||||
recurrenceId?: SCISO8601Date;
|
||||
geo?: string;
|
||||
/**
|
||||
* The sequence index if the series had to be split into multiple rrules
|
||||
*/
|
||||
recurrenceSequence?: number;
|
||||
recurrenceSequenceAmount?: number;
|
||||
rrule?: RRule;
|
||||
dates?: SCISO8601Date[];
|
||||
exceptionDates?: SCISO8601Date[];
|
||||
start: SCISO8601Date;
|
||||
sequence?: number;
|
||||
duration?: SCISO8601Duration;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export type ICalKeyValuePair = `${Uppercase<string>}${':' | '='}${string}`;
|
||||
|
||||
export type ICalLike = ICalKeyValuePair[];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function timeDist(
|
||||
current: SCISO8601Date,
|
||||
next: SCISO8601Date | undefined,
|
||||
recurrence: unitOfTime.Diff,
|
||||
): number | undefined {
|
||||
if (!next) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const diff = moment(next).diff(moment(current), recurrence, true);
|
||||
|
||||
return Math.floor(diff) === diff ? diff : undefined;
|
||||
}
|
||||
|
||||
export interface RRule {
|
||||
freq: unitOfTime.Diff; // 'SECONDLY' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||
interval: number;
|
||||
from: SCISO8601Date;
|
||||
until: SCISO8601Date;
|
||||
}
|
||||
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||
|
||||
export interface MergedRRule {
|
||||
rrule?: RRule;
|
||||
exceptions?: SCISO8601Date[];
|
||||
date?: SCISO8601Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge compatible RRules to a single RRule with exceptions
|
||||
*/
|
||||
export function mergeRRules(
|
||||
rules: Array<RRule | SCISO8601Date>,
|
||||
allowExceptions = true,
|
||||
): MergedRRule[] {
|
||||
if (!allowExceptions)
|
||||
return rules.map(it => (typeof it === 'string' ? {date: it} : {rrule: it}));
|
||||
/*map(groupBy(rules, it => `${it.freq}@${it.interval}`), it => {
|
||||
|
||||
});*/
|
||||
|
||||
return rules.map(it =>
|
||||
typeof it === 'string' ? {date: it} : {rrule: it},
|
||||
) /* TODO */;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find RRules in a list of dates
|
||||
*/
|
||||
export function findRRules(
|
||||
dates: SCISO8601Date[],
|
||||
): Array<RRule | SCISO8601Date> {
|
||||
const sorted = dates.sort((a, b) => moment(a).unix() - moment(b).unix());
|
||||
|
||||
const output: Optional<RRule, 'freq'>[] = [
|
||||
{
|
||||
from: sorted[0],
|
||||
until: sorted[0],
|
||||
interval: -1,
|
||||
},
|
||||
];
|
||||
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const current = sorted[i];
|
||||
const next = sorted[i + 1] as SCISO8601Date | undefined;
|
||||
const element = last(output);
|
||||
|
||||
const units: unitOfTime.Diff[] = element?.freq
|
||||
? [element.freq]
|
||||
: ['day', 'week', 'month', 'year'];
|
||||
const freq = minBy(
|
||||
units.map(recurrence => ({
|
||||
recurrence: recurrence,
|
||||
dist: timeDist(current, next, recurrence),
|
||||
})),
|
||||
it => it.dist,
|
||||
)?.recurrence;
|
||||
const interval = freq ? timeDist(current, next, freq) : undefined;
|
||||
|
||||
if (element?.interval === -1) {
|
||||
element.freq = freq;
|
||||
element.interval = interval ?? -1;
|
||||
}
|
||||
|
||||
if (!freq || element?.freq !== freq || element.interval !== interval) {
|
||||
if (element) {
|
||||
element.until = current;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
output.push({
|
||||
from: next,
|
||||
until: next,
|
||||
interval: -1,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
element.until = current;
|
||||
}
|
||||
}
|
||||
|
||||
return output.map(it => (it.freq ? (it as RRule) : it.from));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function strikethrough(text: string): string {
|
||||
return `\u274C ${[...text].join('\u0336')}\u0336`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function getICalData(
|
||||
dateSeries: SCDateSeries,
|
||||
translator: SCThingTranslator,
|
||||
): Pick<ICalEvent, 'name' | 'uuid' | 'categories' | 'description' | 'geo'> {
|
||||
const translated = translator.translatedAccess(dateSeries);
|
||||
|
||||
return {
|
||||
name: translated.event()?.name,
|
||||
uuid: dateSeries.uid,
|
||||
categories: [
|
||||
'stapps',
|
||||
...((translated.event() as SCThingWithCategories<string, never>)
|
||||
?.categories ?? []),
|
||||
],
|
||||
description: translated.event()?.description ?? translated.description(),
|
||||
geo: translated.inPlace()?.name,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToICalOptions {
|
||||
allowRRuleExceptions?: boolean;
|
||||
excludeCancelledEvents?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function toICal(
|
||||
dateSeries: SCDateSeries,
|
||||
translator: SCThingTranslator,
|
||||
options: ToICalOptions = {},
|
||||
): ICalEvent[] {
|
||||
const rrules = findRRules(
|
||||
options.excludeCancelledEvents
|
||||
? difference(dateSeries.dates, dateSeries.exceptions ?? [])
|
||||
: dateSeries.dates,
|
||||
);
|
||||
|
||||
return mergeRRules(rrules, options.allowRRuleExceptions).map(
|
||||
(it, i, array) => ({
|
||||
...getICalData(dateSeries, translator),
|
||||
dates: dateSeries.dates,
|
||||
rrule: it.rrule,
|
||||
recurrenceSequence: array.length > 1 ? i + 1 : undefined,
|
||||
recurrenceSequenceAmount: array.length > 1 ? array.length : undefined,
|
||||
exceptionDates: it.exceptions,
|
||||
start: it.rrule?.from ?? it.date ?? dateSeries.dates[0],
|
||||
sequence: 0,
|
||||
duration: dateSeries.duration,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function toICalUpdates(
|
||||
dateSeries: SCDateSeries,
|
||||
translator: SCThingTranslator,
|
||||
): ICalEvent[] {
|
||||
return (
|
||||
dateSeries.exceptions?.map(exception => ({
|
||||
...getICalData(dateSeries, translator),
|
||||
sequence: 1,
|
||||
recurrenceId: exception,
|
||||
cancelled: true,
|
||||
start: exception,
|
||||
})) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ISO8601 date to a string in the format YYYYMMDDTHHMMSSZ
|
||||
*/
|
||||
export function iso8601ToICalDateTime<T extends SCISO8601Date | undefined>(
|
||||
date: T,
|
||||
): T extends SCISO8601Date ? string : undefined {
|
||||
return (
|
||||
date ? `${moment(date).utc().format('YYYYMMDDTHHmmss')}Z` : undefined
|
||||
) as never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ISO8601 date to a string in the format YYYYMMDD
|
||||
*/
|
||||
export function iso8601ToICalDate(date: SCISO8601Date): string {
|
||||
return `${moment(date).utc().format('YYYYMMDD')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively stringify all linebreaks to \n strings
|
||||
*/
|
||||
function stringifyLinebreaks<T extends string | unknown[] | unknown>(
|
||||
value: T,
|
||||
): T {
|
||||
if (typeof value === 'string') {
|
||||
return value.replace(/\r?\n|\r/g, '\\n') as T;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(stringifyLinebreaks) as T;
|
||||
}
|
||||
if (isObject(value)) {
|
||||
return mapValues(value, stringifyLinebreaks) as T;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize an ICal object to not contain line breaks and convert dates to iCal format
|
||||
*/
|
||||
export function normalizeICalDates(iCal: ICalEvent): ICalEvent {
|
||||
return {
|
||||
...iCal,
|
||||
dates: iCal.dates?.filter(it => it !== iCal.start).map(iso8601ToICalDate),
|
||||
exceptionDates: iCal.exceptionDates?.map(iso8601ToICalDate),
|
||||
start: iso8601ToICalDateTime(iCal.start),
|
||||
recurrenceId: iso8601ToICalDateTime(iCal.recurrenceId),
|
||||
};
|
||||
}
|
||||
|
||||
const REPEAT_FREQUENCIES: Partial<Record<unitOfTime.Diff, string>> = {
|
||||
day: 'DAILY',
|
||||
week: 'WEEKLY',
|
||||
month: 'MONTHLY',
|
||||
year: 'YEARLY',
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function serializeICalLike(iCal: ICalLike): string {
|
||||
return iCal.map(stringifyLinebreaks).join('\r\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all strings that are either undefined or end with 'undefined'
|
||||
*/
|
||||
function withoutNullishStrings<T extends string>(
|
||||
array: Array<T | `${string}${undefined}` | undefined>,
|
||||
): T[] {
|
||||
return array.filter(it => it && !it.endsWith('undefined')) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function serializeRRule(rrule?: RRule): string | undefined {
|
||||
return rrule
|
||||
? `FREQ=${
|
||||
REPEAT_FREQUENCIES[rrule.freq ?? 's']
|
||||
};UNTIL=${iso8601ToICalDateTime(rrule.until)};INTERVAL=${rrule.interval}`
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an iCal event to a string
|
||||
*/
|
||||
export function serializeICalEvent(iCal: ICalEvent): ICalLike {
|
||||
const normalized = normalizeICalDates(iCal);
|
||||
|
||||
return withoutNullishStrings<ICalKeyValuePair>([
|
||||
'BEGIN:VEVENT',
|
||||
`DTSTART:${normalized.start}`,
|
||||
`DURATION:${normalized.duration}`,
|
||||
`DTSTAMP:${moment().utc().format('YYYYMMDDTHHmmss')}Z`,
|
||||
`UID:${normalized.uuid}`,
|
||||
`RECURRENCE-ID:${normalized.recurrenceId}`,
|
||||
`CATEGORIES:${normalized.categories?.join(',')}`,
|
||||
`SUMMARY:${normalized.name}`,
|
||||
`DESCRIPTION:${normalized.description}`,
|
||||
`STATUS:${normalized.cancelled === true ? 'CANCELLED' : 'CONFIRMED'}`,
|
||||
`URL:${normalized.url}`,
|
||||
// `RDATE;VALUE=DATE:${normalized.dates.join(',')}`,
|
||||
size(normalized.exceptionDates) > 0
|
||||
? `EXDATE;VALUE=DATE:${normalized.exceptionDates?.join(',')}`
|
||||
: undefined,
|
||||
`RRULE:${serializeRRule(normalized.rrule)}`,
|
||||
'END:VEVENT',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an iCal object to a string
|
||||
*/
|
||||
export function serializeICal(iCal: ICalEvent[]): string {
|
||||
return serializeICalLike([
|
||||
'BEGIN:VCALENDAR',
|
||||
'VERSION:2.0',
|
||||
'PRODID:-//StApps//NONSGML StApps Calendar//EN',
|
||||
'NAME:StApps',
|
||||
'X-WR-CALNAME:StApps',
|
||||
'X-WR-CALDESC:StApps Calendar',
|
||||
'X-WR-TIMEZONE:Europe/Berlin',
|
||||
'LOCATION;LANGUAGE=en:Germany',
|
||||
'CALSCALE:GREGORIAN',
|
||||
'COLOR:#FF0000',
|
||||
'METHOD:PUBLISH',
|
||||
...flatMap(iCal, serializeICalEvent),
|
||||
'END:VCALENDAR',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transform date series for purpose of native calendar export
|
||||
*/
|
||||
export function getNativeCalendarExport(
|
||||
dateSeries: SCDateSeries[],
|
||||
translator: SCThingTranslator,
|
||||
): ICalEvent[] {
|
||||
return flatMap(dateSeries, event =>
|
||||
toICal(event, translator, {
|
||||
allowRRuleExceptions: false,
|
||||
excludeCancelledEvents: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transform date series for purpose of iCal file export
|
||||
*/
|
||||
export function getICalExport(
|
||||
dateSeries: SCDateSeries[],
|
||||
translator: SCThingTranslator,
|
||||
includeCancelled: boolean,
|
||||
): ICalEvent[] {
|
||||
return [
|
||||
...flatMap(dateSeries, event =>
|
||||
toICal(event, translator, {
|
||||
allowRRuleExceptions: false,
|
||||
excludeCancelledEvents: !includeCancelled,
|
||||
}),
|
||||
),
|
||||
...(includeCancelled
|
||||
? flatMap(dateSeries, event => toICalUpdates(event, translator))
|
||||
: []),
|
||||
];
|
||||
}
|
||||
28
src/app/modules/calendar/new-share.ts
Normal file
28
src/app/modules/calendar/new-share.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2022 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export interface NewShareData {
|
||||
files?: File[];
|
||||
title?: string;
|
||||
text?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
// web share api is relatively new
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
|
||||
export interface NewShareNavigator {
|
||||
canShare: (options: NewShareData) => boolean;
|
||||
share: (options: NewShareData) => Promise<void>;
|
||||
}
|
||||
282
src/app/modules/calendar/schedule.provider.ts
Normal file
282
src/app/modules/calendar/schedule.provider.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable unicorn/no-null */
|
||||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {
|
||||
Bounds,
|
||||
SCDateSeries,
|
||||
SCISO8601Date,
|
||||
SCISO8601Duration,
|
||||
SCSearchFilter,
|
||||
SCThingType,
|
||||
SCUuid,
|
||||
} from '@openstapps/core';
|
||||
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
|
||||
import {DataProvider} from '../data/data.provider';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {pick} from 'lodash-es';
|
||||
import {DateFormatPipe, DurationPipe} from 'ngx-moment';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function toDateSeriesRelevantData(
|
||||
dateSeries: SCDateSeries,
|
||||
): DateSeriesRelevantData {
|
||||
return pick(dateSeries, ...dateSeriesRelevantKeys);
|
||||
}
|
||||
|
||||
export type DateSeriesRelevantKeys =
|
||||
| 'uid'
|
||||
| 'dates'
|
||||
| 'exceptions'
|
||||
| 'repeatFrequency'
|
||||
| 'duration';
|
||||
|
||||
export const dateSeriesRelevantKeys: Array<DateSeriesRelevantKeys> = [
|
||||
'uid',
|
||||
'dates',
|
||||
'exceptions',
|
||||
'repeatFrequency',
|
||||
'duration',
|
||||
];
|
||||
|
||||
export const formatRelevantKeys: {
|
||||
[key in DateSeriesRelevantKeys]: (
|
||||
value: SCDateSeries[key],
|
||||
dateFormatter: DateFormatPipe,
|
||||
durationFormatter: DurationPipe,
|
||||
) => string;
|
||||
} = {
|
||||
uid: value => value,
|
||||
dates: (value, dateFormatter) =>
|
||||
`[${value.map(it => dateFormatter.transform(it)).join(', ')}]`,
|
||||
exceptions: (value, dateFormatter) =>
|
||||
`[${value?.map(it => dateFormatter.transform(it)).join(', ') ?? ''}]`,
|
||||
repeatFrequency: (value, _, durationFormatter) =>
|
||||
durationFormatter.transform(value),
|
||||
duration: (value, _, durationFormatter) => durationFormatter.transform(value),
|
||||
};
|
||||
|
||||
export type DateSeriesRelevantData = Pick<SCDateSeries, DateSeriesRelevantKeys>;
|
||||
|
||||
/**
|
||||
* Provider for app settings
|
||||
*/
|
||||
@Injectable()
|
||||
export class ScheduleProvider implements OnDestroy {
|
||||
// tslint:disable:prefer-function-over-method
|
||||
|
||||
private static partialEventsStorageKey = 'schedule::partial_events';
|
||||
|
||||
private _partialEvents$?: BehaviorSubject<DateSeriesRelevantData[]>;
|
||||
|
||||
private _partialEventsSubscription?: Subscription;
|
||||
|
||||
constructor(private readonly dataProvider: DataProvider) {
|
||||
window.addEventListener('storage', this.storageListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push one or more values to local storage
|
||||
*/
|
||||
private static get<T>(key: string): T[] {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item == undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(item) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Push one or more values to local storage
|
||||
*/
|
||||
private static set<T>(key: string, item: T[]) {
|
||||
const newValue = JSON.stringify(item);
|
||||
// prevent feedback loop from storageEvent -> _uuids$.next() -> set -> storageEvent
|
||||
if (newValue !== localStorage.getItem(key)) {
|
||||
localStorage.setItem(key, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
public async restore(uuids: SCUuid[]): Promise<SCDateSeries[] | undefined> {
|
||||
if (uuids.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const dateSeries = (await this.getDateSeries(uuids)).dates;
|
||||
|
||||
this._partialEvents$?.next(dateSeries.map(toDateSeriesRelevantData));
|
||||
|
||||
return dateSeries;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
public get uuids$(): Observable<SCUuid[]> {
|
||||
return this.partialEvents$.pipe(map(events => events.map(it => it.uid)));
|
||||
}
|
||||
|
||||
public get partialEvents$(): BehaviorSubject<DateSeriesRelevantData[]> {
|
||||
if (!this._partialEvents$) {
|
||||
const data = ScheduleProvider.get<DateSeriesRelevantData>(
|
||||
ScheduleProvider.partialEventsStorageKey,
|
||||
);
|
||||
|
||||
this._partialEvents$ = new BehaviorSubject(data ?? []);
|
||||
this._partialEventsSubscription = this._partialEvents$.subscribe(
|
||||
result => {
|
||||
ScheduleProvider.set(
|
||||
ScheduleProvider.partialEventsStorageKey,
|
||||
result,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this._partialEvents$;
|
||||
}
|
||||
|
||||
/**
|
||||
* What to do when local storage updates
|
||||
*/
|
||||
private storageEventHandler(event: StorageEvent) {
|
||||
if (
|
||||
event.newValue &&
|
||||
event.storageArea === localStorage &&
|
||||
event.key === ScheduleProvider.partialEventsStorageKey
|
||||
) {
|
||||
this._partialEvents$?.next(JSON.parse(event.newValue));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to updates in local storage
|
||||
*/
|
||||
private storageListener = this.storageEventHandler.bind(this);
|
||||
|
||||
/**
|
||||
* Load Date Series
|
||||
*/
|
||||
async getDateSeries(
|
||||
uuids: SCUuid[],
|
||||
frequencies?: Array<SCISO8601Duration>,
|
||||
from?: SCISO8601Date | 'now',
|
||||
to?: SCISO8601Date | 'now',
|
||||
): Promise<{
|
||||
dates: SCDateSeries[];
|
||||
min: SCISO8601Date;
|
||||
max: SCISO8601Date;
|
||||
}> {
|
||||
if (uuids.length === 0) {
|
||||
return {
|
||||
dates: [],
|
||||
min: '',
|
||||
max: '',
|
||||
};
|
||||
}
|
||||
|
||||
const filters: SCSearchFilter[] = [
|
||||
{
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.DateSeries,
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
filters: uuids.map(uid => ({
|
||||
arguments: {
|
||||
field: 'uid',
|
||||
value: uid,
|
||||
},
|
||||
type: 'value',
|
||||
})),
|
||||
operation: 'or',
|
||||
},
|
||||
type: 'boolean',
|
||||
},
|
||||
];
|
||||
|
||||
if (frequencies) {
|
||||
filters.push({
|
||||
arguments: {
|
||||
filters: frequencies.map(frequency => ({
|
||||
arguments: {
|
||||
field: 'repeatFrequency',
|
||||
value: frequency,
|
||||
},
|
||||
type: 'value',
|
||||
})),
|
||||
operation: 'or',
|
||||
},
|
||||
type: 'boolean',
|
||||
});
|
||||
}
|
||||
|
||||
if (from || to) {
|
||||
const bounds: Bounds<string> = {};
|
||||
if (from) {
|
||||
console.log(from);
|
||||
bounds.lowerBound = {
|
||||
limit: from,
|
||||
mode: 'inclusive',
|
||||
};
|
||||
}
|
||||
if (to) {
|
||||
console.log(to);
|
||||
bounds.upperBound = {
|
||||
limit: to,
|
||||
mode: 'inclusive',
|
||||
};
|
||||
}
|
||||
filters.push({
|
||||
arguments: {
|
||||
field: 'dates',
|
||||
bounds: bounds,
|
||||
},
|
||||
type: 'date range',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.dataProvider.search({
|
||||
filter: {
|
||||
arguments: {
|
||||
filters: filters,
|
||||
operation: 'and',
|
||||
},
|
||||
type: 'boolean',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
dates: result.data as SCDateSeries[],
|
||||
// TODO: https://gitlab.com/openstapps/backend/-/issues/100
|
||||
min: new Date(2021, 11, 1).toISOString(),
|
||||
max: new Date(2022, 1, 24).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this._partialEventsSubscription?.unsubscribe();
|
||||
window.removeEventListener('storage', this.storageListener);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user