feat: timetable module - schedule and calendar

This commit is contained in:
Wieland Schöbl
2021-08-13 12:27:40 +00:00
parent e81b2e161d
commit d8ede006df
59 changed files with 3287 additions and 555 deletions

View File

@@ -0,0 +1,324 @@
/*
* 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/>.
*/
import {
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewChild,
} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {IonDatetime, Platform} from '@ionic/angular';
import {SCUuid} from '@openstapps/core';
import {last} from 'lodash-es';
import moment, {Moment} from 'moment';
import {DateFormatPipe} from 'ngx-moment';
import {Subscription} from 'rxjs';
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
import {
materialFade,
materialManualFade,
materialSharedAxisX,
} from '../../../animation/material-motion';
import {ScheduleProvider} from '../schedule.provider';
import {ScheduleEvent, ScheduleResponsiveBreakpoint} from './schema/schema';
/**
* Component that displays the schedule
*/
@Component({
selector: 'stapps-calendar-view',
templateUrl: 'calendar-view.html',
styleUrls: ['calendar-view.scss'],
animations: [materialFade, materialSharedAxisX, materialManualFade],
})
export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
/**
* UUID subscription
*/
private _uuidSubscription: Subscription;
/**
* The day that is routed to
*/
protected routeDate: Moment;
/**
* @see {blockDateTimeChange}
*/
anticipateDatetimeChangeBlocked = false;
/**
* @see {blockDateTimeChange}
*/
// tslint:disable-next-line:no-magic-numbers
readonly anticipateDatetimeChangeTimeoutMs: 100;
/**
* Animation state for cards
*/
cardsAnimationState: 'in' | 'out' = 'out';
/**
* The cursor
*/
@ViewChild('cursor', {read: HTMLElement}) cursor?: HTMLElement;
/**
* Choreographer
*/
dateLabelsChoreographer: SharedAxisChoreographer<Moment[]>;
/**
* The date range to initially display
*/
displayDates: Moment[][] = [];
/**
* Hours for grid
*/
readonly hours: number[];
/**
* Height of the slides based on the displayed hours
*/
readonly hoursAmount: number;
/**
* Range of hours to display
*/
@Input() readonly hoursRange = {
from: 5,
to: 22,
};
/**
* Layout of the schedule
*/
@Input() layout: ScheduleResponsiveBreakpoint;
/**
* Get the date format for the date field
*/
// tslint:disable-next-line:prefer-function-over-method
localDateFormat = moment.localeData().longDateFormat('L');
/**
* Route fragment
*/
routeFragment = 'schedule/calendar';
/**
* Vertical scale of the schedule (distance between hour lines)
*/
scale = 60;
/**
* date -> (uid -> event)
*/
testSchedule: Record<string, Record<SCUuid, ScheduleEvent>> = {};
/**
* UUIDs
*/
uuids: SCUuid[];
constructor(
protected readonly scheduleProvider: ScheduleProvider,
protected readonly activatedRoute: ActivatedRoute,
protected readonly datePipe: DateFormatPipe,
protected readonly platform: Platform,
) {
// This could be done directly on the properties too instead of
// here in the constructor, but because of TSLint member ordering,
// some properties wouldn't be initialized, and if you disable
// member ordering, auto-fixing the file can still cause reordering
// of properties.
this.hoursAmount = this.hoursRange.to - this.hoursRange.from + 1;
this.hours = [...Array.from({length: this.hoursAmount}).keys()];
}
/**
* Because of some stupid Ionic implementation, there is no
* way to wait for the datetime picker to be dismissed without
* listening for (ionChange). Unfortunately that also includes
* changes caused by a page change, so whenever we do that,
* we have to block the event for a few milliseconds.
*/
blockDateTimeChange() {
this.anticipateDatetimeChangeBlocked = true;
setTimeout(() => {
this.anticipateDatetimeChangeBlocked = false;
}, this.anticipateDatetimeChangeTimeoutMs);
}
/**
* Determine displayed dates according to display size
*/
determineDisplayDates() {
// let's boldly assume that we at least display one day
const out = [moment(this.routeDate).startOf(this.layout.startOf)];
for (let i = 1; i < this.layout.days; i++) {
out.push(out[0].clone().add(i, 'day'));
}
this.displayDates = [
out.map(it => it.clone().subtract(this.layout.days, 'days')),
out,
out.map(it => it.clone().add(this.layout.days, 'days')),
];
this.dateLabelsChoreographer?.changeViewForState(this.getDateLabels(), 0);
// void this.mainSlides.slideTo(this.mode === 'schedule' ? 0 : 1, 0, false);
}
/**
* Get date labels
*/
getDateLabels(): Moment[] {
return (this.displayDates[1] ?? this.displayDates[0]).map(it => it.clone());
}
/**
* Jump to a date
*/
jumpToDate(alt: IonDatetime, offset = 0, date?: Moment) {
if (this.anticipateDatetimeChangeBlocked) {
return;
}
const newDate = (date ?? moment(alt.value)).subtract(offset, 'days');
const direction = this.routeDate.isBefore(newDate)
? 1
: this.routeDate.isAfter(newDate)
? -1
: 0;
this.blockDateTimeChange();
this.routeDate = newDate;
this.determineDisplayDates();
this.dateLabelsChoreographer.changeViewForState(
this.getDateLabels(),
direction,
);
}
/**
* Load events
*/
async loadEvents(): Promise<void> {
this.cardsAnimationState = 'out';
const dateSeries = await this.scheduleProvider.getDateSeries(this.uuids);
this.testSchedule = {};
for (const series of dateSeries) {
for (const date of series.dates) {
const parsedDate = moment(date).startOf('day').unix();
// fall back to default
(this.testSchedule[parsedDate] ?? (this.testSchedule[parsedDate] = {}))[
series.uid
] = {
dateSeries: series,
time: {
start: moment(date).hours(),
duration: series.duration,
},
};
}
}
this.cursor?.scrollIntoView();
this.cardsAnimationState = 'in';
}
/**
* On Changes
*/
ngOnChanges(changes: SimpleChanges) {
const layout = changes.layout?.currentValue as
| ScheduleResponsiveBreakpoint
| undefined;
if (layout) {
this.determineDisplayDates();
}
}
/**
* OnDestroy
*/
ngOnDestroy(): void {
this._uuidSubscription.unsubscribe();
}
/**
* Initialize
*/
ngOnInit() {
this._uuidSubscription = this.scheduleProvider.uuids$.subscribe(
async result => {
this.uuids = result;
await this.loadEvents();
},
);
let dayString: string | number | null =
this.activatedRoute.snapshot.paramMap.get('date');
if (dayString == undefined || dayString === 'now') {
const urlFragment: string = last(window.location.href.split('/')) ?? '';
dayString = /^\d{4}-\d{2}-\d{2}$/.test(urlFragment)
? urlFragment
: moment.now();
}
this.routeDate = moment(dayString).startOf('day');
this.dateLabelsChoreographer = new SharedAxisChoreographer(
this.getDateLabels(),
);
this.determineDisplayDates();
}
/**
* Change page
*/
async onPageChange(direction: number) {
this.blockDateTimeChange();
const amount = direction * this.displayDates[0].length;
this.routeDate.add(amount, 'days');
window.history.replaceState(
{},
'',
`#/${this.routeFragment}/${this.routeDate.format('YYYY-MM-DD')}`,
);
for (const slide of this.displayDates) {
for (const date of slide) {
date.add(amount, 'days');
}
}
this.dateLabelsChoreographer.changeViewForState(
this.getDateLabels(),
direction > 0 ? 1 : direction < 0 ? -1 : 0,
);
}
}

View File

@@ -0,0 +1,86 @@
<div>
<ion-button fill="clear" class="left-button" (click)="mainSlides.prevPage()">
<ion-icon slot="icon-only" name="chevron-back-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" class="right-button" (click)="mainSlides.nextPage()">
<ion-icon slot="icon-only" name="chevron-forward-outline"></ion-icon>
</ion-button>
<ion-item>
<ion-grid
class="day-labels"
[@materialSharedAxisX]="dateLabelsChoreographer.animationState"
(@materialSharedAxisX.done)="dateLabelsChoreographer.animationDone()"
>
<ion-row>
<ion-col
*ngFor="
let item of dateLabelsChoreographer.currentValue;
let idx = index
"
>
<ion-button expand="block" fill="clear" (click)="datetime.open()">
<ion-label>
{{
item
| amDateFormat: ((item | dateIsThis: 'week') ? 'dddd' : 'll')
}}
</ion-label>
</ion-button>
<!-- This poor datetime element is a phantom element to provide us with a date picker -->
<ion-datetime
#datetime
[displayFormat]="localDateFormat"
[value]="item.toISOString()"
(ionChange)="jumpToDate(datetime, idx)"
>
</ion-datetime>
</ion-col>
</ion-row>
</ion-grid>
</ion-item>
</div>
<ion-content>
<stapps-infinite-slides
#mainSlides
(pageChangeCallback)="onPageChange($event.direction)"
>
<ion-slide class="slide" *ngFor="let slide of displayDates">
<ion-grid>
<ion-row>
<ion-col *ngFor="let item of slide">
<div
class="vertical-line"
[style.height.px]="hoursAmount * scale"
></div>
<stapps-schedule-cursor
*ngIf="item | dateIsThis: 'date'"
[hoursRange]="hoursRange"
[scale]="scale"
#cursor
>
</stapps-schedule-cursor>
<div [@materialManualFade]="cardsAnimationState">
<stapps-schedule-card
*ngFor="let entry of testSchedule[item.unix()] | keyvalue"
[scheduleEvent]="entry.value"
[fromHour]="hoursRange.from"
[scale]="scale"
>
</stapps-schedule-card>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-slide>
</stapps-infinite-slides>
<div
class="hour-lines"
*ngFor="let i of hours"
[style.marginTop.px]="i * scale"
>
<ion-label>{{ i + hoursRange.from }}:00</ion-label>
<hr class="horizontal-line" />
</div>
</ion-content>

View File

@@ -0,0 +1,97 @@
div {
position: relative;
.left-button, .right-button {
position: absolute;
top: 0;
bottom: 0;
margin: auto;
z-index: 5;
}
.left-button {
left: 0;
}
.right-button {
right: 0;
}
}
.day-labels {
position: absolute;
top: 0;
left: 0;
padding: 8px 20px 8px 30px;
width: 100%;
ion-row {
padding-right: 20px;
ion-col {
ion-button {
position: absolute;
top: -8px;
font-size: large;
text-align: center;
width: 100%;
}
// phantom element
ion-datetime {
position: absolute;
visibility: hidden;
height: 0 !important;
}
}
}
}
.slide {
ion-grid {
position: absolute;
top: 0;
height: fit-content;
width: 100%;
padding-top: 8px;
ion-row {
ion-col {
width: 100%;
.vertical-line {
position: absolute;
top: 0;
left: 0;
border-left: 1px solid #dbdbdb;
z-index: 1;
}
stapps-schedule-card {
z-index: 4;
position: absolute;
left: 0;
width: 100%;
}
}
}
}
}
.hour-lines {
top: 0;
position: absolute;
display: flex;
flex-direction: row;
width: 100%;
ion-label {
padding: 0 20px 20px;
}
.horizontal-line {
width: 100%;
top: 0;
border-top: 1px solid #dbdbdb;
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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/>.
*/
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
import {IonSlides} from '@ionic/angular';
/**
* Component that can display infinite slides
*/
@Component({
selector: 'stapps-infinite-slides',
templateUrl: 'infinite-slides.html',
styleUrls: ['infinite-slides.scss'],
})
export class InfiniteSlidesComponent {
/**
* If the view was initialized
*/
initialized = false;
/**
* Callback for when the page has changed
*
* The caller needs to replace the component here
*/
@Output() pageChangeCallback: EventEmitter<{
/**
* The current page
*/
currentPage: number;
/**
* The direction that was scrolled
*/
direction: number;
}> = new EventEmitter();
/**
* The virtual page we are currently on
*/
page = 0;
/**
* Options for IonSlides
*/
@Input() slideOpts = {
initialSlide: 1,
speed: 200,
loop: false,
};
/**
* Slider element
*/
@ViewChild('slides') slides: IonSlides;
/**
* Slide to next page
*/
async nextPage() {
await this.slides.slideNext(this.slideOpts.speed);
}
/**
* Change page
*/
async onPageChange(direction: number) {
if (!this.initialized) {
// setting the initial page to 1 causes a page change to
// be emitted initially, which intern would cause the
// page to actually change one to far, so we listen for
// that first page change and skip it
this.initialized = true;
return;
}
this.page += direction;
this.pageChangeCallback.emit({
currentPage: this.page,
direction: direction,
});
// tslint:disable-next-line:no-magic-numbers
this.slides.slideTo(1, 0, false).then();
}
/**
* Slide to previous page
*/
async prevPage() {
await this.slides.slidePrev(this.slideOpts.speed);
}
}

View File

@@ -0,0 +1,9 @@
<ion-slides
#slides
pager="false"
[options]="slideOpts"
(ionSlideNextEnd)="onPageChange(1)"
(ionSlidePrevEnd)="onPageChange(-1)"
>
<ng-content></ng-content>
</ion-slides>

View File

@@ -0,0 +1,4 @@
ion-slides {
width: 100%;
height: 1100px; // BIG TODO: This is completely bypasses the scale parameter
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2020 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/>.
*/
import {Component, Input, OnInit} from '@angular/core';
import moment from 'moment';
import {ScheduleProvider} from '../../schedule.provider';
import {ScheduleEvent} from '../schema/schema';
/**
* Component that can display a schedule event
*/
@Component({
selector: 'stapps-schedule-card',
templateUrl: 'schedule-card.html',
styleUrls: ['../../../data/list/data-list-item.scss', 'schedule-card.scss'],
})
export class ScheduleCardComponent implements OnInit {
/**
* The hour from which on the schedule is displayed
*/
@Input() fromHour = 0;
/**
* Card Y start position
*/
fromY = 0;
/**
* Card Y end position
*/
height = 0;
/**
* Show the card without a top offset
*/
@Input() noOffset = false;
/**
* The scale of the schedule
*/
@Input() scale = 1;
/**
* The event
*/
@Input() scheduleEvent: ScheduleEvent;
/**
* The title of the event
*/
title: string;
constructor(private readonly scheduleProvider: ScheduleProvider) {}
/**
* Get the note text
*/
getNote(): string | undefined {
return 'categories' in this.scheduleEvent.dateSeries.event
? this.scheduleEvent.dateSeries.event.categories?.join(', ')
: undefined;
}
/**
* Initializer
*/
ngOnInit() {
this.fromY = this.noOffset ? 0 : this.scheduleEvent.time.start;
this.height = moment.duration(this.scheduleEvent.time.duration).asHours();
this.title = this.scheduleEvent.dateSeries.event.name;
}
/**
* Remove the event
*/
removeEvent(): false {
if (confirm('Remove event?')) {
this.scheduleProvider.uuids$.next(
this.scheduleProvider.uuids$.value.filter(
it => it !== this.scheduleEvent.dateSeries.uid,
),
);
}
// to prevent event propagation
return false;
}
}

View File

@@ -0,0 +1,30 @@
<ion-card
[style.height.px]="height * scale"
[style.marginTop.px]="(fromY - fromHour) * scale - 5"
[routerLink]="['/data-detail', scheduleEvent.dateSeries.event.uid]"
>
<ion-card-header mode="md">
<ion-card-title>
{{
this.scheduleEvent.dateSeries.event.name
| nullishCoalesce: this.scheduleEvent.dateSeries.name
}}
</ion-card-title>
<ion-card-subtitle>
<ion-icon name="calendar"></ion-icon>
<span class="repetitions">
{{ scheduleEvent.dateSeries.frequency }}
until
{{
scheduleEvent.dateSeries.dates | last | amDateFormat: 'DD. MMM YYYY'
}}
</span>
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-note>
{{ getNote() }}
</ion-note>
</ion-card-content>
<div></div>
</ion-card>

View File

@@ -0,0 +1,37 @@
ion-card {
z-index: 2;
ion-grid {
padding: 0;
margin: 0;
ion-row {
ion-col {
height: 5px;
padding: 0;
margin: 0;
}
}
}
ion-card-header {
height: available;
width: 100%;
ion-card-title {
height: 50px;
width: 100%;
}
}
div {
position: absolute;
bottom: 0;
height: 20px;
width: 100%;
background: linear-gradient(
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 255) 100%
);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2018, 2019 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/>.
*/
import {Component, Input, OnInit} from '@angular/core';
import moment from 'moment';
import {HoursRange} from '../schema/schema';
/**
* Component that displays the schedule
*/
@Component({
selector: 'stapps-schedule-cursor',
templateUrl: 'schedule-cursor.html',
styleUrls: ['schedule-cursor.scss'],
})
export class ScheduleCursorComponent implements OnInit {
/**
* Cursor update
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore unused
private cursorInterval: NodeJS.Timeout;
/**
* Range of hours to display
*/
@Input() readonly hoursRange: HoursRange;
/**
* Cursor
*/
now = ScheduleCursorComponent.getCursorTime();
/**
* Vertical scale of the schedule (distance between hour lines)
*/
@Input() readonly scale: number;
/**
* Get a floating point time 0..24
*/
static getCursorTime(): number {
const mnt = moment(moment.now());
const hh = mnt.hours();
const mm = mnt.minutes();
// tslint:disable-next-line:no-magic-numbers
return hh + mm / 60;
}
/**
* Initialize
*/
ngOnInit() {
this.cursorInterval = setInterval(async () => {
this.now = ScheduleCursorComponent.getCursorTime();
// tslint:disable-next-line:no-magic-numbers
}, 1000 * 60 /*1 Minute*/);
}
}

View File

@@ -0,0 +1,6 @@
<div [style.marginTop.px]="(now - hoursRange.from) * scale">
<div>
<hr />
<div></div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
div {
padding: 0;
margin: 0;
position: absolute;
display: flex;
flex-direction: row;
width: 100%;
top: 0;
z-index: 0;
div {
width: 100%;
height: fit-content;
hr {
width: calc(100% - 8px);
position: absolute;
margin-left: 4px;
margin-right: 16px;
margin-top: 8px;
height: 2px;
border-top: 2px solid var(--ion-color-primary);
margin-block-start: 0;
margin-block-end: 0;
}
div {
width: 8px;
height: 8px;
position: absolute;
top: -3px;
left: -4px;
border-radius: 50% 0 50% 50%;
transform: rotateZ(45deg);
background-color: var(--ion-color-primary);
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2020 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/>.
*/
import {Component, Input} from '@angular/core';
import {SCSearchFilter, SCThingType} from '@openstapps/core';
/**
* TODO
*/
@Component({
selector: 'modal-event-creator',
templateUrl: 'modal-event-creator.html',
styleUrls: ['modal-event-creator.scss'],
})
export class ModalEventCreatorComponent {
/**
* Action when close is pressed
*/
@Input() dismissAction: () => void;
/**
* Forced filter
*/
filter: SCSearchFilter = {
arguments: {
field: 'type',
value: SCThingType.AcademicEvent,
},
type: 'value',
};
}

View File

@@ -0,0 +1,15 @@
<ion-card-header>
<ion-card-title>{{
'schedule.addEventModal.addEvent' | translate
}}</ion-card-title>
<ion-button fill="clear" (click)="dismissAction()">
<ion-label>{{ 'schedule.addEventModal.close' | translate }}</ion-label>
</ion-button>
</ion-card-header>
<ion-card-content>
<stapps-search-page
[forcedFilter]="filter"
[itemRouting]="false"
></stapps-search-page>
</ion-card-content>

View File

@@ -0,0 +1,17 @@
ion-card-header {
ion-button {
position: absolute;
right: 0;
top: 0;
}
}
ion-card-content {
height: 100%;
padding-left: 0;
padding-right: 0;
stapps-data-list {
height: available;
}
}

View File

@@ -0,0 +1,243 @@
/*
* 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/>.
*/
import {
AfterViewInit,
Component,
HostListener,
OnInit,
ViewChild,
} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {AnimationController, ModalController} from '@ionic/angular';
import {last} from 'lodash-es';
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
import {materialSharedAxisX} from '../../../animation/material-motion';
import {ModalEventCreatorComponent} from './modal/modal-event-creator.component';
import {ScheduleResponsiveBreakpoint} from './schema/schema';
import {animate, style, transition, trigger} from '@angular/animations';
/**
* This needs to be sorted by break point low -> high
*
* Last entry must have `until: Infinity`
*/
const responsiveConfig: ScheduleResponsiveBreakpoint[] = [
{
until: 768,
days: 1,
startOf: 'day',
},
{
until: 1700,
days: 3,
startOf: 'day',
},
{
until: Number.POSITIVE_INFINITY,
days: 7,
startOf: 'week',
},
];
const fabAnimations = trigger('fabAnimation', [
transition(':leave', [
style({opacity: 1, transform: 'translate(0, 0) scale(1)'}),
animate(
'100ms ease-in',
style({opacity: 0, transform: 'translate(-5vw, -5vh) scale(200%)'}),
),
]),
transition(':enter', [
style({opacity: 0, transform: 'translate(-5vw, -5vh) scale(200%)'}),
animate(
'200ms ease-out',
style({opacity: 1, transform: 'translate(0, 0) scale(1)'}),
),
]),
]);
/**
* Component that displays the schedule
*/
@Component({
selector: 'stapps-schedule-page',
templateUrl: 'schedule-page.html',
styleUrls: ['schedule-page.scss'],
animations: [materialSharedAxisX, fabAnimations],
})
export class SchedulePageComponent implements OnInit, AfterViewInit {
/**
* Current width of the window
*/
private currentWindowWidth: number = window.innerWidth;
/**
* Actual Segment Tab
*/
actualSegmentValue?: string | null;
fabVisible = true;
/**
* Layout
*/
layout: ScheduleResponsiveBreakpoint = SchedulePageComponent.getDaysToDisplay(
this.currentWindowWidth,
);
/**
* Vertical scale of the schedule (distance between hour lines)
*/
scale = 60;
@ViewChild('segment') segmentView!: HTMLIonSegmentElement;
/**
* Choreographer for the tab switching
*/
tabChoreographer: SharedAxisChoreographer<string | null | undefined>;
/**
* Weekly config for schedule
*/
weeklyConfig: ScheduleResponsiveBreakpoint = {
until: Number.POSITIVE_INFINITY,
days: 7,
startOf: 'week',
};
/**
* Amount of days that should be shown according to current display width
*/
static getDaysToDisplay(width: number): ScheduleResponsiveBreakpoint {
// the search could be optimized, but probably would have little
// actual effect with five entries.
// we can be sure we get an hit when the last value.until is infinity
// (unless someone has a display that reaches across the universe)
return (
responsiveConfig.find(value => width < value.until) ??
responsiveConfig[responsiveConfig.length - 1]
);
}
constructor(
private readonly modalController: ModalController,
private readonly activatedRoute: ActivatedRoute,
private readonly animationController: AnimationController,
) {}
ngOnInit() {
this.tabChoreographer = new SharedAxisChoreographer(
this.activatedRoute.snapshot.paramMap.get('mode'),
['calendar', 'recurring', 'single'],
);
}
ngAfterViewInit() {
this.segmentView.value = this.tabChoreographer.currentValue;
}
/**
* Resize callback
*
* Note: this may not fire when the browser transfers from full screen to windowed
* (Firefox & Chrome tested)
*/
@HostListener('window:resize', ['$event'])
onResize(_: UIEvent) {
const current = SchedulePageComponent.getDaysToDisplay(
this.currentWindowWidth,
);
const next = SchedulePageComponent.getDaysToDisplay(window.innerWidth);
this.currentWindowWidth = window.innerWidth;
if (current.days === next.days) {
this.layout = next;
}
}
/**
* When the segment changes
*/
onSegmentChange() {
window.history.replaceState(
{},
'',
`/#/schedule/${this.segmentView.value}/${last(
window.location.href.split('/'),
)}`,
);
this.tabChoreographer.changeViewForState(this.segmentView.value);
}
/**
* Add event modal sheet
*/
async showCreateEventModal() {
this.fabVisible = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any,unicorn/consistent-function-scoping
const enterAnimation = (baseElement: any) => {
const backdropAnimation = this.animationController
.create()
.addElement(baseElement.querySelector('.modal-wrapper'))
.fromTo('opacity', '0', 'var(--backdrop-opacity)');
const wrapperAnimation = this.animationController
.create()
.addElement(baseElement.querySelector('.modal-wrapper'))
.keyframes([
{
opacity: '0',
transform: 'translate(30vw, 30vh) scale(0.5)',
},
{
opacity: '1',
transform: 'translate(0, 0) scale(1)',
},
]);
return this.animationController
.create()
.addElement(baseElement)
.easing('ease-out')
.duration(150)
.addAnimation([backdropAnimation, wrapperAnimation]);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any,unicorn/consistent-function-scoping
const leaveAnimation = (baseElement: any) => {
return enterAnimation(baseElement).direction('reverse');
};
const modal = await this.modalController.create({
component: ModalEventCreatorComponent,
swipeToClose: true,
cssClass: 'add-modal',
componentProps: {
dismissAction: () => {
modal.dismiss();
},
},
enterAnimation,
leaveAnimation,
});
await modal.present();
await modal.onWillDismiss();
this.fabVisible = true;
}
}

View File

@@ -0,0 +1,57 @@
<!--
~ 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/>.
-->
<ion-header>
<ion-segment #segment value="calendar" (ionChange)="onSegmentChange()">
<ion-segment-button value="calendar">
<ion-label>{{ 'schedule.calendar' | translate }}</ion-label>
</ion-segment-button>
<ion-segment-button value="recurring">
<ion-label>{{ 'schedule.recurring' | translate }}</ion-label>
</ion-segment-button>
<ion-segment-button value="single">
<ion-label>{{ 'schedule.single' | translate }}</ion-label>
</ion-segment-button>
</ion-segment>
</ion-header>
<div
[ngSwitch]="tabChoreographer.currentValue"
[@materialSharedAxisX]="tabChoreographer.animationState"
(@materialSharedAxisX.done)="tabChoreographer.animationDone()"
>
<stapps-calendar-view
*ngSwitchCase="'calendar'"
[layout]="layout"
></stapps-calendar-view>
<!-- Schedule view needs full week -->
<stapps-schedule-view
*ngSwitchCase="'recurring'"
[layout]="weeklyConfig"
></stapps-schedule-view>
<stapps-single-events *ngSwitchCase="'single'"></stapps-single-events>
</div>
<ion-fab
*ngIf="fabVisible"
@fabAnimation
vertical="bottom"
horizontal="end"
slot="fixed"
>
<ion-fab-button (click)="showCreateEventModal()">
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab>

View File

@@ -0,0 +1,26 @@
ion-header {
padding: 8px;
}
div {
height: 100%;
}
.content-container {
display: grid;
}
.content {
grid-column: 1;
grid-row: 1;
}
.add-modal {
align-items: flex-end !important;
justify-content: flex-end !important;
.modal-wrapper {
transform-origin: bottom right !important;
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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/>.
*/
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {SCDateSeries, SCUuid} from '@openstapps/core';
import {flatMap, groupBy, omit, sortBy} from 'lodash-es';
import moment from 'moment';
import {Subscription} from 'rxjs';
import {materialFade} from '../../../animation/material-motion';
import {ScheduleProvider} from '../schedule.provider';
import {ScheduleEvent} from './schema/schema';
/**
* A single event
*/
export interface ScheduleSingleEvent {
/**
* Day the event is on
*/
day: string;
/**
* Event the date is referring to
*/
event: ScheduleEvent;
}
/**
* Component that displays single events one after each other
*/
@Component({
selector: 'stapps-single-events',
templateUrl: 'schedule-single-events.html',
styleUrls: ['schedule-single-events.scss'],
animations: [materialFade],
})
export class ScheduleSingleEventsComponent implements OnInit, OnDestroy {
/**
* UUID subscription
*/
private _uuidSubscription: Subscription;
/**
* The events to display
*/
private uuids: SCUuid[];
/**
* Events that are displayed
*/
events: Promise<ScheduleSingleEvent[][]>;
/**
* Scale of the view
*/
@Input() scale = 60;
/**
* Sorts dates to a list of days with events on each
*/
static groupDateSeriesToDays(
dateSeries: SCDateSeries[],
): ScheduleSingleEvent[][] {
return sortBy(
groupBy(
flatMap(dateSeries, event =>
event.dates.map(date => ({
dateUnix: moment(date).unix(),
day: moment(date).startOf('day').toISOString(),
event: {
dateSeries: event,
time: {
start:
moment(date).hour() +
moment(date)
// tslint:disable-next-line:no-magic-numbers
.minute() /
60,
duration: event.duration,
},
},
})),
)
.sort((a, b) => a.dateUnix - b.dateUnix)
.map(event => omit(event, 'dateUnix')),
'day',
),
'day',
);
}
constructor(protected readonly scheduleProvider: ScheduleProvider) {}
/**
* Fetch date series items
*/
async fetchDateSeries(): Promise<ScheduleSingleEvent[][]> {
// TODO: only single events
const dateSeries = await this.scheduleProvider.getDateSeries(
this.uuids,
undefined /*TODO*/,
moment(moment.now()).startOf('week').toISOString(),
);
// TODO: replace with filter
return ScheduleSingleEventsComponent.groupDateSeriesToDays(
dateSeries.filter(it => it.frequency === 'single'),
);
}
/**
* OnDestroy
*/
ngOnDestroy(): void {
this._uuidSubscription.unsubscribe();
}
/**
* Initialize
*/
ngOnInit() {
this._uuidSubscription = this.scheduleProvider.uuids$.subscribe(
async result => {
this.uuids = result;
this.events = this.fetchDateSeries();
},
);
}
}

View File

@@ -0,0 +1,21 @@
<ion-content @materialFade>
<ion-list lines="none">
<ion-item-group *ngFor="let day of events | async" @materialFade>
<ion-label class="day-label" color="medium">
{{ day[0].day | amDateFormat: 'LL' }}
</ion-label>
<ion-item *ngFor="let event of day" lines="none">
<ion-avatar class="hour-label">
{{ event.event.dateSeries.dates[0] | amDateFormat: 'HH:mm' }}
</ion-avatar>
<stapps-schedule-card
class="event-card"
[scheduleEvent]="event.event"
[noOffset]="true"
[scale]="scale"
>
</stapps-schedule-card>
</ion-item>
</ion-item-group>
</ion-list>
</ion-content>

View File

@@ -0,0 +1,17 @@
ion-content {
height: 100%;
}
.hour-label {
width: fit-content;
}
.day-label {
padding: 16px;
font-size: large;
font-weight: bold;
}
.event-card {
width: 100%
}

View File

@@ -0,0 +1,64 @@
/*
* 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/>.
*/
import {SCDateSeries} from '@openstapps/core';
import {ScheduleSingleEventsComponent} from './schedule-single-events.component';
import moment from 'moment';
describe('ScheduleSingleEvents', () => {
it('should group date series to days', () => {
const events: Partial<SCDateSeries>[] = [
{
dates: ['2021-12-24T10:00Z', '2021-12-24T12:00Z'],
duration: 'A',
},
{
dates: ['2021-12-20T10:00Z'],
duration: 'B',
},
{
dates: ['2021-12-24T10:15Z'],
duration: 'C',
},
];
const grouped = ScheduleSingleEventsComponent.groupDateSeriesToDays(
events as SCDateSeries[],
);
const seriesToDate = (series: Partial<SCDateSeries>, index: number) => {
const time = moment(series.dates?.[index]);
return {
day: time.clone().startOf('day').toISOString(),
event: {
dateSeries: series as SCDateSeries,
time: {
start: time.hour() + time.minute() / 60,
duration: series.duration as string,
},
},
};
};
expect(grouped).toEqual([
[seriesToDate(events[1], 0)],
[
seriesToDate(events[0], 0),
seriesToDate(events[2], 0),
seriesToDate(events[0], 1),
],
]);
});
});

View File

@@ -0,0 +1,108 @@
/*
* 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/>.
*/
import {Component} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Platform} from '@ionic/angular';
import moment from 'moment';
import {DateFormatPipe} from 'ngx-moment';
import {
materialFade,
materialManualFade,
materialSharedAxisX,
} from '../../../animation/material-motion';
import {ScheduleProvider} from '../schedule.provider';
import {CalendarViewComponent} from './calendar-view.component';
/**
* Component that displays the schedule
*/
@Component({
selector: 'stapps-schedule-view',
// this is intentional for extending
templateUrl: 'calendar-view.html',
styleUrls: ['calendar-view.scss'],
animations: [materialFade, materialSharedAxisX, materialManualFade],
})
export class ScheduleViewComponent extends CalendarViewComponent {
/**
* Route Fragment
*/
// @Override
routeFragment = 'schedule/recurring';
constructor(
scheduleProvider: ScheduleProvider,
activatedRoute: ActivatedRoute,
datePipe: DateFormatPipe,
platform: Platform,
) {
super(scheduleProvider, activatedRoute, datePipe, platform);
}
/**
* Determine displayed dates according to display size
*/
// @Override
determineDisplayDates() {
// let's boldly assume that we at least display one day
const out = [moment(moment.now()).startOf(this.layout.startOf)];
for (let i = 1; i < this.layout.days; i++) {
out.push(out[0].clone().add(i, 'day'));
}
this.displayDates = [out];
// void this.mainSlides.slideTo(this.mode === 'schedule' ? 0 : 1, 0, false);
}
/**
* Load events
*/
// @Override
async loadEvents(): Promise<void> {
this.cardsAnimationState = 'out';
const dateSeries = await this.scheduleProvider.getDateSeries(
this.uuids,
undefined,
moment(moment.now()).startOf('week').toISOString(),
);
this.testSchedule = {};
for (const series of dateSeries) {
if (series.dates.length > 0) {
const date = moment(moment.now())
.startOf('week')
.day(moment(series.dates[0]).day())
.unix();
// fall back to default
(this.testSchedule[date] ?? (this.testSchedule[date] = {}))[
series.uid
] = {
dateSeries: series,
time: {
start: moment(series.dates[0]).hours(),
duration: series.duration,
},
};
}
}
this.cursor?.scrollIntoView();
this.cardsAnimationState = 'in';
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (C) 2020 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/>.
*/
import {Injectable} from '@angular/core';
/**
* MenuService provides bidirectional communication of context menu options and search queries
*/
@Injectable()
export class ScheduleService {}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2020 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/>.
*/
import {SCDateSeries, SCISO8601Duration} from '@openstapps/core';
import {unitOfTime} from 'moment';
interface DateRange {
duration: SCISO8601Duration;
start: number;
}
/**
* Minimal interface to provide information about a custom event
*/
export interface ScheduleEvent {
/**
* UUIDs of things related to the event
*/
dateSeries: SCDateSeries;
/**
* How long the event goes
*/
time: DateRange;
}
/**
* Range of hours
*/
export interface HoursRange {
/**
* Start hour
*/
from: number;
/**
* End hour
*/
to: number;
}
export interface ScheduleResponsiveBreakpoint {
/**
* Number of days to display
*/
days: number;
/**
* When the first day should start
*/
startOf: unitOfTime.StartOf;
/**
* Width until next breakpoint is hit
*/
until: number;
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2018, 2019 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/>.
*/
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {RouterModule, Routes} from '@angular/router';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {ScheduleCardComponent} from './page/grid/schedule-card.component';
import {DateFormatPipe, MomentModule} from 'ngx-moment';
import {UtilModule} from '../../util/util.module';
import {DataModule} from '../data/data.module';
import {DataProvider} from '../data/data.provider';
import {CalendarViewComponent} from './page/calendar-view.component';
import {InfiniteSlidesComponent} from './page/grid/infinite-slides.component';
import {ScheduleCursorComponent} from './page/grid/schedule-cursor.component';
import {ModalEventCreatorComponent} from './page/modal/modal-event-creator.component';
import {SchedulePageComponent} from './page/schedule-page.component';
import {ScheduleSingleEventsComponent} from './page/schedule-single-events.component';
import {ScheduleViewComponent} from './page/schedule-view.component';
import {ScheduleProvider} from './schedule.provider';
const settingsRoutes: Routes = [
{path: 'schedule', redirectTo: 'schedule/calendar/now'},
{path: 'schedule/calendar', redirectTo: 'schedule/calendar/now'},
{path: 'schedule/recurring', redirectTo: 'schedule/recurring/now'},
{path: 'schedule/single', redirectTo: 'schedule/single/now'},
// calendar | recurring | single
{path: 'schedule/:mode/:date', component: SchedulePageComponent},
];
/**
* Schedule Module
*/
@NgModule({
declarations: [
CalendarViewComponent,
InfiniteSlidesComponent,
ModalEventCreatorComponent,
ScheduleCardComponent,
ScheduleCursorComponent,
SchedulePageComponent,
ScheduleSingleEventsComponent,
ScheduleViewComponent,
],
imports: [
CommonModule,
FormsModule,
UtilModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
RouterModule.forChild(settingsRoutes),
DataModule,
MomentModule,
],
providers: [ScheduleProvider, DataProvider, DateFormatPipe],
})
export class ScheduleModule {}

View File

@@ -0,0 +1,195 @@
/* eslint-disable unicorn/no-null */
/*
* Copyright (C) 2018-2020 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/>.
*/
import {Injectable, OnDestroy} from '@angular/core';
import {
Bounds,
SCDateSeries,
SCISO8601Date,
SCSearchFilter,
SCThingType,
SCUuid,
} from '@openstapps/core';
import {BehaviorSubject, Subscription} from 'rxjs';
import {DataProvider} from '../data/data.provider';
/**
* Provider for app settings
*/
@Injectable()
export class ScheduleProvider implements OnDestroy {
// tslint:disable:prefer-function-over-method
/**
* Storage key for event UUIDs
*/
private static uuidStorageKey = 'schedule::event_uuids';
private _uuids$?: BehaviorSubject<SCUuid[]>;
private _uuidSubscription?: Subscription;
constructor(private readonly dataProvider: DataProvider) {
window.addEventListener('storage', this.storageEventListener.bind(this));
}
/**
* 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);
}
}
/**
* TODO
*/
public get uuids$(): BehaviorSubject<SCUuid[]> {
if (!this._uuids$) {
this._uuids$ = new BehaviorSubject(
ScheduleProvider.get<SCUuid>(ScheduleProvider.uuidStorageKey),
);
this._uuidSubscription = this._uuids$.subscribe(result => {
ScheduleProvider.set(ScheduleProvider.uuidStorageKey, result);
});
}
return this._uuids$;
}
/**
* Listen to updates in local storage
*/
private storageEventListener(event: StorageEvent) {
if (
event.newValue &&
event.storageArea === localStorage &&
event.key === ScheduleProvider.uuidStorageKey
) {
this._uuids$?.next(JSON.parse(event.newValue));
}
}
/**
* Load Date Series
*/
async getDateSeries(
uuids: SCUuid[],
frequencies?: Array<'single' | 'weekly' | 'biweekly'>,
from?: SCISO8601Date | 'now',
to?: SCISO8601Date | 'now',
): Promise<SCDateSeries[]> {
if (uuids.length === 0) {
return [];
}
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: 'frequency',
value: frequency,
},
type: 'value',
})),
operation: 'or',
},
type: 'boolean',
});
}
if (from || to) {
const bounds: Bounds<string> = {};
if (from) {
bounds.lowerBound = {
limit: from,
mode: 'inclusive',
};
}
if (to) {
bounds.upperBound = {
limit: to,
mode: 'inclusive',
};
}
filters.push({
arguments: {
field: 'dates',
bounds: bounds,
},
type: 'date range',
});
}
return (
await this.dataProvider.search({
filter: {
arguments: {
filters: filters,
operation: 'and',
},
type: 'boolean',
},
})
).data as SCDateSeries[];
}
/**
* TODO
*/
ngOnDestroy(): void {
this._uuidSubscription?.unsubscribe();
window.removeEventListener('storage', this.storageEventListener.bind(this));
}
}