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

@@ -3,6 +3,7 @@
*ngIf="applicable['locate']()"
[item]="item"
></stapps-locate-action-chip>
<!-- Add Event Chip needs to load data and should be the last -->
<stapps-add-event-action-chip
*ngIf="applicable['event']()"
[item]="item"

View File

@@ -1,4 +1,5 @@
div {
display: flex;
flex-direction: row;
width: fit-content;
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -12,10 +13,29 @@
* 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 {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {SCDateSeries} from '@openstapps/core';
import {every, groupBy, some, sortBy, values} from 'lodash-es';
import {
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import {PopoverController} from '@ionic/angular';
import {SCDateSeries, SCUuid} from '@openstapps/core';
import {
difference,
every,
flatMap,
groupBy,
mapValues,
some,
sortBy,
union,
values,
} from 'lodash-es';
import {capitalize, last} from 'lodash-es';
import {Subscription} from 'rxjs';
import {ScheduleProvider} from '../../schedule/schedule.provider';
enum Selection {
ON = 2,
@@ -28,7 +48,6 @@ enum Selection {
*
* The generic is to preserve type safety of how deep the tree goes.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
class TreeNode<T extends TreeNode<any> | SelectionValue> {
/**
* Value of this node
@@ -54,19 +73,16 @@ class TreeNode<T extends TreeNode<any> | SelectionValue> {
* Accumulate values of children to set current value
*/
private accumulateApplyValues() {
const selections: number[] = this.children.map(
it =>
/* eslint-disable unicorn/no-nested-ternary */
it instanceof TreeNode
? it.checked
? Selection.ON
: it.indeterminate
? Selection.PARTIAL
: Selection.OFF
: (it as SelectionValue).selected
const selections: number[] = this.children.map(it =>
it instanceof TreeNode
? it.checked
? Selection.ON
: Selection.OFF,
/* eslint-enable unicorn/no-nested-ternary */
: it.indeterminate
? Selection.PARTIAL
: Selection.OFF
: (it as SelectionValue).selected
? Selection.ON
: Selection.OFF,
);
this.checked = every(selections, it => it === Selection.ON);
@@ -83,7 +99,7 @@ class TreeNode<T extends TreeNode<any> | SelectionValue> {
if (child instanceof TreeNode) {
child.checked = this.checked;
child.indeterminate = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// tslint:disable-next-line:no-any
(child as TreeNode<any>).applyValueDownwards();
} else {
(child as SelectionValue).selected = this.checked;
@@ -149,7 +165,7 @@ interface SelectionValue {
templateUrl: 'add-event-popover.html',
styleUrls: ['add-event-popover.scss'],
})
export class AddEventPopoverComponent implements OnInit {
export class AddEventPopoverComponent implements OnInit, OnDestroy {
/**
* Lodash alias
*/
@@ -170,23 +186,70 @@ export class AddEventPopoverComponent implements OnInit {
*/
selection: TreeNode<TreeNode<SelectionValue>>;
constructor(readonly ref: ChangeDetectorRef) {}
/**
* Uuids
*/
uuids: SCUuid[];
/**
* Uuid Subscription
*/
uuidSubscription: Subscription;
constructor(
readonly ref: ChangeDetectorRef,
readonly scheduleProvider: ScheduleProvider,
readonly popoverController: PopoverController,
) {}
/**
* Destroy
*/
ngOnDestroy() {
this.uuidSubscription.unsubscribe();
}
/**
* Init
*/
ngOnInit() {
this.selection = new TreeNode(
values(
groupBy(
sortBy(
this.items.map(item => ({selected: false, item: item})),
it => it.item.frequency,
),
it => it.item.frequency,
),
).map(item => new TreeNode(item, this.ref)),
this.ref,
this.uuidSubscription = this.scheduleProvider.uuids$.subscribe(
async result => {
this.uuids = result;
this.selection = new TreeNode(
values(
groupBy(
sortBy(
this.items.map(item => ({
selected: this.uuids.includes(item.uid),
item: item,
})),
it => it.item.frequency,
),
it => it.item.frequency,
),
).map(item => new TreeNode(item, this.ref)),
this.ref,
);
},
);
}
/**
* On selection change
*/
async onCommit(save: boolean) {
if (save) {
const {false: unselected, true: selected} = mapValues(
groupBy(flatMap(this.selection.children, 'children'), 'selected'),
value => value.map(it => it.item.uid),
);
this.scheduleProvider.uuids$.next(
union(difference(this.uuids, unselected), selected),
);
}
await this.popoverController.dismiss();
}
}

View File

@@ -45,4 +45,12 @@
</ion-item>
</ion-item-group>
</ion-item-group>
<div class="action-buttons">
<ion-button (click)="onCommit(false)" fill="clear">{{
'abort' | translate
}}</ion-button>
<ion-button (click)="onCommit(true)" fill="clear">{{
'ok' | translate
}}</ion-button>
</div>
</ion-card-content>

View File

@@ -5,3 +5,7 @@
ion-card-content {
width: fit-content;
}
.action-buttons {
float: right;
}

View File

@@ -1,4 +1,4 @@
/* eslint-disable class-methods-use-this */
/* tslint:disable:prefer-function-over-method */
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -13,11 +13,18 @@
* 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 {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {PopoverController} from '@ionic/angular';
import {SCDateSeries, SCThing, SCThingType} from '@openstapps/core';
import {DataProvider} from '../../data.provider';
import {SCDateSeries, SCThing, SCThingType, SCUuid} from '@openstapps/core';
import {difference, map} from 'lodash-es';
import {Subscription} from 'rxjs';
import {ScheduleProvider} from '../../../schedule/schedule.provider';
import {AddEventPopoverComponent} from '../add-event-popover.component';
import {CoordinatedSearchProvider} from '../../coordinated-search.provider';
import {
chipSkeletonTransition,
chipTransition,
} from '../../../../animation/skeleton-transitions/chip-loading-transition';
enum AddEventStates {
ADDED_ALL,
@@ -33,8 +40,9 @@ enum AddEventStates {
selector: 'stapps-add-event-action-chip',
templateUrl: 'add-event-action-chip.html',
styleUrls: ['add-event-action-chip.scss'],
animations: [chipSkeletonTransition, chipTransition],
})
export class AddEventActionChipComponent implements OnInit {
export class AddEventActionChipComponent implements OnInit, OnDestroy {
/**
* Associated date series
*/
@@ -91,9 +99,20 @@ export class AddEventActionChipComponent implements OnInit {
},
};
/**
* UUIDs
*/
uuids: SCUuid[];
/**
* UUID Subscription
*/
uuidSubscription: Subscription;
constructor(
readonly popoverController: PopoverController,
readonly dataProvider: DataProvider,
readonly dataProvider: CoordinatedSearchProvider,
readonly scheduleProvider: ScheduleProvider,
) {}
/**
@@ -107,6 +126,13 @@ export class AddEventActionChipComponent implements OnInit {
this.disabled = disabled;
}
/**
* TODO
*/
ngOnDestroy() {
this.uuidSubscription?.unsubscribe();
}
/**
* Init
*/
@@ -115,7 +141,7 @@ export class AddEventActionChipComponent implements OnInit {
this.item.type === SCThingType.DateSeries
? Promise.resolve([this.item as SCDateSeries])
: this.dataProvider
.search({
.coordinatedSearch({
filter: {
arguments: {
filters: [
@@ -140,19 +166,36 @@ export class AddEventActionChipComponent implements OnInit {
},
})
.then(it => it.data as SCDateSeries[]);
this.associatedDateSeries.then(it =>
this.applyState(
it.length === 0
? AddEventStates.UNAVAILABLE
: AddEventStates.REMOVED_ALL,
),
this.uuidSubscription = this.scheduleProvider.uuids$.subscribe(
async result => {
this.uuids = result;
const associatedDateSeries = await this.associatedDateSeries;
if (associatedDateSeries.length === 0) {
this.applyState(AddEventStates.UNAVAILABLE);
return;
}
switch (
difference(map(associatedDateSeries, 'uid'), this.uuids).length
) {
case 0:
this.applyState(AddEventStates.ADDED_ALL);
break;
case associatedDateSeries.length:
this.applyState(AddEventStates.REMOVED_ALL);
break;
default:
this.applyState(AddEventStates.ADDED_SOME);
break;
}
},
);
}
/**
* Action
*/
// @Override
async onClick(event: MouseEvent) {
const associatedDateSeries = await this.associatedDateSeries;
const popover = await this.popoverController.create({
@@ -165,12 +208,5 @@ export class AddEventActionChipComponent implements OnInit {
event: event,
});
await popover.present();
// TODO: replace dummy implementation
await popover.onDidDismiss();
this.applyState(
this.state === AddEventStates.ADDED_ALL
? AddEventStates.REMOVED_ALL
: AddEventStates.ADDED_ALL,
);
}
}

View File

@@ -1,14 +1,16 @@
<div *ngIf="associatedDateSeries | async as associatedDateSeries; else loading">
<div class="stack-children">
<ion-chip
*ngIf="associatedDateSeries | async as associatedDateSeries; else loading"
@chipTransition
[disabled]="disabled"
(click)="$event.stopPropagation(); onClick($event)"
>
<ion-icon [name]="icon"></ion-icon>
<ion-label>{{ label | translate }}</ion-label>
</ion-chip>
<ng-template #loading>
<ion-chip @chipSkeletonTransition>
<ion-skeleton-text animated="true"></ion-skeleton-text>
</ion-chip>
</ng-template>
</div>
<ng-template #loading>
<ion-chip>
<ion-skeleton-text animated="true"></ion-skeleton-text>
</ion-chip>
</ng-template>

View File

@@ -1,3 +1,14 @@
::ng-deep ion-skeleton-text {
:host ::ng-deep ion-skeleton-text {
width: 50px;
}
.stack-children {
display: grid;
align-items: start;
justify-items: start;
}
.stack-children > * {
grid-column-start: 1;
grid-row-start: 1;
}