refactor: move event select popup to a modal

This commit is contained in:
Thea Schöbl
2022-09-23 12:24:22 +00:00
committed by Rainer Killinger
parent 8a04a43903
commit 4a3f79ca20
22 changed files with 788 additions and 495 deletions

View File

@@ -71,5 +71,6 @@ const catalogRoutes: Routes = [
UtilModule,
],
providers: [SettingsProvider, TranslatePipe],
exports: [EditModalComponent],
})
export class DashboardModule {}

View File

@@ -21,7 +21,7 @@ import {EditModalItem, EditModalTypeEnum} from './edit-modal-type.enum';
* Shows a modal window to sort and enable/disable menu items
*/
@Component({
selector: 'stapps-edit-modal',
selector: 'stapps-dashboard-edit-modal',
templateUrl: 'edit-modal.component.html',
styleUrls: ['edit-modal.component.scss'],
})

View File

@@ -1,286 +0,0 @@
/*
* 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 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 @typescript-eslint/no-explicit-any */
import {
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import {ModalController, PopoverController} from '@ionic/angular';
import {SCDateSeries} from '@openstapps/core';
import {Subscription} from 'rxjs';
import {
DateSeriesRelevantData,
ScheduleProvider,
toDateSeriesRelevantData,
} from '../../calendar/schedule.provider';
import {CalendarService} from '../../calendar/calendar.service';
import {AddEventReviewModalComponent} from '../../calendar/add-event-review-modal.component';
import {ThingTranslatePipe} from '../../../translation/thing-translate.pipe';
import {groupBy, groupByProperty} from '../../../_helpers/collections/group-by';
import {mapValues} from '../../../_helpers/collections/map-values';
import {stringSortBy} from '../../../_helpers/collections/string-sort';
import {uniqBy} from '../../../_helpers/collections/uniq';
import {differenceBy} from '../../../_helpers/collections/difference';
enum Selection {
ON = 2,
PARTIAL = 1,
OFF = 1,
}
/**
* A tree
*
* The generic is to preserve type safety of how deep the tree goes.
*/
class TreeNode<T extends TreeNode<any> | SelectionValue> {
/**
* Value of this node
*/
checked: boolean;
/**
* If items are partially selected
*/
indeterminate: boolean;
/**
* Parent of this node
*/
parent?: TreeNode<TreeNode<T>>;
constructor(readonly children: T[], readonly ref: ChangeDetectorRef) {
this.updateParents();
this.accumulateApplyValues();
}
/**
* Accumulate values of children to set current value
*/
private accumulateApplyValues() {
const selections: number[] = this.children.map(it =>
it instanceof TreeNode
? it.checked
? Selection.ON
: it.indeterminate
? Selection.PARTIAL
: Selection.OFF
: (it as SelectionValue).selected
? Selection.ON
: Selection.OFF,
);
this.checked = selections.every(it => it === Selection.ON);
this.indeterminate = this.checked
? false
: selections.some(it => it > Selection.OFF);
}
/**
* Apply the value of this node to all child nodes
*/
private applyValueDownwards() {
for (const child of this.children) {
if (child instanceof TreeNode) {
child.checked = this.checked;
child.indeterminate = false;
// tslint:disable-next-line:no-any
(child as TreeNode<any>).applyValueDownwards();
} else {
(child as SelectionValue).selected = this.checked;
}
}
}
/**
* Set all children's parent to this
*/
private updateParents() {
for (const child of this.children) {
if (child instanceof TreeNode) {
child.parent = this as TreeNode<TreeNode<T>>;
}
}
}
/**
* Update values to all parents upwards
*/
private updateValueUpwards() {
this.parent?.accumulateApplyValues();
this.parent?.updateValueUpwards();
}
/**
* Click on this node
*/
click() {
this.checked = !this.checked;
this.indeterminate = false;
this.applyValueDownwards();
this.updateValueUpwards();
}
/**
* Notify that a child's value has changed
*/
notifyChildChanged() {
this.accumulateApplyValues();
this.updateValueUpwards();
}
}
interface SelectionValue {
/**
* Item that was selected
*/
item: SCDateSeries;
/**
* Selection
*/
selected: boolean;
}
/**
* Shows a horizontal list of action chips
*/
@Component({
selector: 'stapps-add-event-popover-component',
templateUrl: 'add-event-popover.html',
styleUrls: ['add-event-popover.scss'],
})
export class AddEventPopoverComponent implements OnInit, OnDestroy {
/**
* The item the action belongs to
*/
@Input() items: SCDateSeries[];
/**
* Selection of the item
*/
selection: TreeNode<TreeNode<SelectionValue>>;
/**
* Uuids
*/
partialDateSeries: DateSeriesRelevantData[];
/**
* Uuid Subscription
*/
uuidSubscription: Subscription;
constructor(
readonly ref: ChangeDetectorRef,
readonly scheduleProvider: ScheduleProvider,
readonly popoverController: PopoverController,
readonly calendar: CalendarService,
readonly modalController: ModalController,
readonly thingTranslatePipe: ThingTranslatePipe,
) {}
/**
* Destroy
*/
ngOnDestroy() {
this.uuidSubscription.unsubscribe();
}
/**
* Init
*/
ngOnInit() {
this.uuidSubscription = this.scheduleProvider.partialEvents$.subscribe(
async result => {
this.partialDateSeries = result;
this.selection = new TreeNode(
Object.values(
groupBy(
this.items
.map(item => ({
selected: this.partialDateSeries.some(
it => it.uid === item.uid,
),
item: item,
}))
.sort(stringSortBy(it => it.item.repeatFrequency)),
it => it.item.repeatFrequency,
),
).map(item => new TreeNode(item, this.ref)),
this.ref,
);
},
);
}
getSelection(): {
selected: DateSeriesRelevantData[];
unselected: DateSeriesRelevantData[];
} {
const selection = mapValues(
groupByProperty(
this.selection.children.flatMap(it => it.children),
'selected',
),
value => value.map(it => toDateSeriesRelevantData(it.item)),
);
return {selected: selection.true ?? [], unselected: selection.false ?? []};
}
async export() {
const modal = await this.modalController.create({
component: AddEventReviewModalComponent,
canDismiss: true,
cssClass: 'add-modal',
componentProps: {
dismissAction: () => {
modal.dismiss();
},
dateSeries: this.items,
},
});
await modal.present();
await modal.onWillDismiss();
}
/**
* On selection change
*/
async onCommit(save: boolean) {
if (save) {
const {selected, unselected} = this.getSelection();
this.scheduleProvider.partialEvents$.next(
uniqBy(
[
...differenceBy(this.partialDateSeries, unselected, it => it.uid),
...selected,
],
it => it.uid,
),
);
}
await this.popoverController.dismiss();
}
}

View File

@@ -1,91 +0,0 @@
<!--
~ 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 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-card-content>
<ion-item-group>
<ion-item-divider (click)="selection.click()">
<ion-label>{{
'data.chips.add_events.popover.ALL' | translate
}}</ion-label>
<ion-checkbox
slot="start"
[checked]="selection.checked"
[indeterminate]="selection.indeterminate"
>
</ion-checkbox>
</ion-item-divider>
<ion-item-group *ngFor="let frequency of selection.children">
<ion-item-divider (click)="frequency.click()">
<ion-label class="ion-text-wrap">{{
frequency.children[0].item.repeatFrequency
? (frequency.children[0].item.repeatFrequency
| durationLocalized: true
| sentencecase)
: ('data.chips.add_events.popover.SINGLE' | translate | titlecase)
}}</ion-label>
<ion-checkbox
slot="start"
[checked]="frequency.checked"
[indeterminate]="frequency.indeterminate"
>
</ion-checkbox>
</ion-item-divider>
<ion-item
*ngFor="let date of frequency.children"
(click)="date.selected = !date.selected; frequency.notifyChildChanged()"
>
<ion-label
class="ion-text-wrap"
*ngIf="date.item.dates.length > 1; else single_event"
>
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{ date.item.dates[0] | amDateFormat: 'HH:mm ddd' }}
{{ 'data.chips.add_events.popover.UNTIL' | translate }}
{{ date.item.dates[date.item.dates.length - 1] | amDateFormat: 'll' }}
</ion-label>
<ng-template #single_event>
<ion-label class="ion-text-wrap">
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{
date.item.dates[date.item.dates.length - 1]
| amDateFormat: 'll, HH:mm'
}}
</ion-label>
</ng-template>
<ion-checkbox slot="start" [checked]="date.selected"> </ion-checkbox>
</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>
<div class="download-button">
<ion-button
fill="clear"
(click)="export()"
[disabled]="!(selection.indeterminate || selection.checked)"
>
<ion-icon slot="icon-only" name="download"></ion-icon>
<!-- {{ 'export' | translate }} -->
</ion-button>
</div>
</ion-card-content>

View File

@@ -1,26 +0,0 @@
/*!
* 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/>.
*/
::ng-deep ion-item-divider {
cursor: pointer;
}
.action-buttons {
float: right;
}
.download-button {
float: left;
}

View File

@@ -1,25 +1,24 @@
/*
* 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 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.
* 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* tslint:disable:prefer-function-over-method */
import {Component, Input, OnDestroy} from '@angular/core';
import {PopoverController} from '@ionic/angular';
import {Component, Input, OnDestroy, ViewChild} from '@angular/core';
import {IonRouterOutlet, ModalController} from '@ionic/angular';
import {SCDateSeries, SCThing, SCThingType, SCUuid} from '@openstapps/core';
import {Subscription} from 'rxjs';
import {ScheduleProvider} from '../../../calendar/schedule.provider';
import {AddEventPopoverComponent} from '../add-event-popover.component';
import {CoordinatedSearchProvider} from '../../coordinated-search.provider';
import {
chipSkeletonTransition,
@@ -29,6 +28,8 @@ import {
AddEventStates,
AddEventStatesMap,
} from './add-event-action-chip.config';
import {EditEventSelectionComponent} from '../edit-event-selection.component';
import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-modal.component';
/**
* Shows a horizontal list of action chips
@@ -82,10 +83,14 @@ export class AddEventActionChipComponent implements OnDestroy {
*/
uuidSubscription: Subscription;
@ViewChild('selection', {static: false})
selection: EditEventSelectionComponent;
constructor(
readonly popoverController: PopoverController,
readonly dataProvider: CoordinatedSearchProvider,
readonly modalController: ModalController,
readonly scheduleProvider: ScheduleProvider,
readonly routerOutlet: IonRouterOutlet,
) {}
/**
@@ -107,6 +112,24 @@ export class AddEventActionChipComponent implements OnDestroy {
this.uuidSubscription?.unsubscribe();
}
async export() {
const modal = await this.modalController.create({
component: AddEventReviewModalComponent,
canDismiss: true,
cssClass: 'add-modal',
presentingElement: await this.modalController.getTop(),
componentProps: {
dismissAction: () => {
modal.dismiss();
},
dateSeries: this.selection.items,
},
});
await modal.present();
await modal.onWillDismiss();
}
/**
* Init
*/
@@ -176,21 +199,4 @@ export class AddEventActionChipComponent implements OnDestroy {
},
);
}
/**
* Action
*/
async onClick(event: MouseEvent) {
const associatedDateSeries = await this.associatedDateSeries;
const popover = await this.popoverController.create({
component: AddEventPopoverComponent,
translucent: true,
cssClass: 'add-event-popover',
componentProps: {
items: associatedDateSeries,
},
event: event,
});
await popover.present();
}
}

View File

@@ -1,16 +1,16 @@
<!--
~ 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 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.
~ 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/>.
~ 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 class="stack-children">
@@ -18,10 +18,45 @@
*ngIf="associatedDateSeries | async as associatedDateSeries; else loading"
@chipTransition
[disabled]="disabled"
(click)="$event.stopPropagation(); onClick($event)"
(click)="$event.stopPropagation(); editModal.present()"
>
<ion-icon [name]="icon" [fill]="iconFill"></ion-icon>
<ion-label>{{ label | translate }}</ion-label>
<stapps-edit-modal #editModal (save)="selection.save()">
<ng-template>
<ion-content class="ion-padding modal-content">
<div>
<stapps-edit-event-selection
#selection
[items]="associatedDateSeries"
(modified)="editModal.pendingChanges = true"
></stapps-edit-event-selection>
</div>
</ion-content>
<ion-footer mode="ios">
<ion-toolbar color="light">
<ion-button
slot="end"
fill="clear"
(click)="export()"
[disabled]="
!(
selection.selection.indeterminate ||
selection.selection.checked
)
"
>
{{
'schedule.toCalendar.reviewModal.DOWNLOAD'
| translate
| titlecase
}}
<ion-icon slot="end" name="download"></ion-icon>
</ion-button>
</ion-toolbar>
</ion-footer>
</ng-template>
</stapps-edit-modal>
</ion-chip>
<ng-template #loading>
<ion-chip @chipSkeletonTransition>

View File

@@ -1,3 +1,19 @@
/*!
* 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 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 "src/theme/common/ion-content-parallax";
:host {
display: block;
padding: var(--spacing-sm);
@@ -17,3 +33,14 @@
grid-column-start: 1;
grid-row-start: 1;
}
.modal-content {
--background: var(--ion-color-primary);
--color: var(--ion-color-primary-contrast);
@include ion-content-parallax($content-size: 160px)
}
ion-footer > ion-toolbar {
--border-color: var(--ion-color-light-shade);
}

View File

@@ -0,0 +1,130 @@
/*
* 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 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 {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import {ModalController, PopoverController} from '@ionic/angular';
import {SCDateSeries} from '@openstapps/core';
import {
DateSeriesRelevantData,
ScheduleProvider,
toDateSeriesRelevantData,
} from '../../calendar/schedule.provider';
import {CalendarService} from '../../calendar/calendar.service';
import {ThingTranslatePipe} from '../../../translation/thing-translate.pipe';
import {groupBy, groupByProperty} from '../../../_helpers/collections/group-by';
import {mapValues} from '../../../_helpers/collections/map-values';
import {stringSortBy} from '../../../_helpers/collections/string-sort';
import {uniqBy} from '../../../_helpers/collections/uniq';
import {differenceBy} from '../../../_helpers/collections/difference';
import {SelectionValue, TreeNode} from './tree-node';
/**
* Shows a horizontal list of action chips
*/
@Component({
selector: 'stapps-edit-event-selection',
templateUrl: 'edit-event-selection.html',
styleUrls: ['edit-event-selection.scss'],
})
export class EditEventSelectionComponent implements OnInit {
/**
* The item the action belongs to
*/
@Input() items: SCDateSeries[];
/**
* Selection of the item
*/
selection: TreeNode<TreeNode<SelectionValue>>;
/**
* Uuids
*/
partialDateSeries: DateSeriesRelevantData[];
@Output()
modified = new EventEmitter();
constructor(
readonly ref: ChangeDetectorRef,
readonly scheduleProvider: ScheduleProvider,
readonly popoverController: PopoverController,
readonly calendar: CalendarService,
readonly modalController: ModalController,
readonly thingTranslatePipe: ThingTranslatePipe,
) {}
ngOnInit() {
this.partialDateSeries = this.scheduleProvider.partialEvents$.value;
this.reset();
}
private getSelection(): {
selected: DateSeriesRelevantData[];
unselected: DateSeriesRelevantData[];
} {
const selection = mapValues(
groupByProperty(
this.selection.children.flatMap(it => it.children),
'selected',
),
value => value.map(it => toDateSeriesRelevantData(it.item)),
);
return {selected: selection.true ?? [], unselected: selection.false ?? []};
}
getModifiedEvents(): DateSeriesRelevantData[] {
const {selected, unselected} = this.getSelection();
return uniqBy(
[
...differenceBy(this.partialDateSeries, unselected, it => it.uid),
...selected,
],
it => it.uid,
);
}
reset() {
this.selection = new TreeNode(
Object.values(
groupBy(
this.items
.map(item => ({
selected: this.partialDateSeries.some(it => it.uid === item.uid),
item: item,
}))
.sort(stringSortBy(it => it.item.repeatFrequency)),
it => it.item.repeatFrequency,
),
).map(item => new TreeNode(item, this.ref)),
this.ref,
);
}
/**
* Save selection
*/
save() {
this.scheduleProvider.partialEvents$.next(this.getModifiedEvents());
}
}

View File

@@ -0,0 +1,86 @@
<!--
~ 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 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-item
(click)="modified.emit(); selection.click()"
class="list-header"
lines="none"
>
<ion-list-header>
<ion-label>{{ 'data.chips.add_events.popover.ALL' | translate }}</ion-label>
</ion-list-header>
<ion-checkbox
slot="end"
[checked]="selection.checked"
[indeterminate]="selection.indeterminate"
>
</ion-checkbox>
</ion-item>
<ng-container *ngFor="let frequency of selection.children">
<ion-list inset="true" lines="full">
<ion-item
lines="none"
(click)="modified.emit(); frequency.click()"
class="list-header"
>
<ion-list-header>
<ion-label>{{
frequency.children[0].item.repeatFrequency
? (frequency.children[0].item.repeatFrequency
| durationLocalized: true
| sentencecase)
: ('data.chips.add_events.popover.SINGLE' | translate | titlecase)
}}</ion-label>
<ion-button></ion-button>
</ion-list-header>
<ion-checkbox
slot="end"
[checked]="frequency.checked"
[indeterminate]="frequency.indeterminate"
>
</ion-checkbox>
</ion-item>
<ion-item
*ngFor="let date of frequency.children"
(click)="
modified.emit();
date.selected = !date.selected;
frequency.notifyChildChanged()
"
>
<ion-label
*ngIf="date.item.dates.length > 1; else single_event"
class="ion-text-wrap"
>
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{ date.item.dates[0] | amDateFormat: 'HH:mm ddd' }}
{{ 'data.chips.add_events.popover.UNTIL' | translate }}
{{ date.item.dates[date.item.dates.length - 1] | amDateFormat: 'll' }}
</ion-label>
<ng-template #single_event>
<ion-label class="ion-text-wrap">
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{
date.item.dates[date.item.dates.length - 1]
| amDateFormat: 'll, HH:mm'
}}
</ion-label>
</ng-template>
<ion-checkbox slot="end" [checked]="date.selected"> </ion-checkbox>
</ion-item>
</ion-list>
</ng-container>

View File

@@ -0,0 +1,45 @@
/*!
* 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 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-item-divider.ios > ion-checkbox {
margin-right: 8px;
}
.list-header {
--padding-start: 0;
--background: var(--ion-color-primary-shade);
> ion-list-header {
--color: var(--ion-color-primary-contrast);
--background: none;
}
> ion-checkbox {
--background: none;
--border-color: rgba(var(--ion-color-primary-contrast-rgb), 0.77);
--background-checked: var(--ion-color-primary-contrast);
--border-color-checked: var(--ion-color-primary-contrast);
--checkmark-color: var(--ion-color-primary)
}
}
:host > .list-header {
--background: none;
}
ion-list.md {
padding-top: 0;
}

View File

@@ -0,0 +1,138 @@
/*
* 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 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 {ChangeDetectorRef} from '@angular/core';
import {SCDateSeries} from '@openstapps/core';
export enum Selection {
ON = 2,
PARTIAL = 1,
OFF = 1,
}
/**
* A tree
*
* The generic is to preserve type safety of how deep the tree goes.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class TreeNode<T extends TreeNode<any> | SelectionValue> {
/**
* Value of this node
*/
checked: boolean;
/**
* If items are partially selected
*/
indeterminate: boolean;
/**
* Parent of this node
*/
parent?: TreeNode<TreeNode<T>>;
constructor(readonly children: T[], readonly ref: ChangeDetectorRef) {
this.updateParents();
this.accumulateApplyValues();
}
/**
* Accumulate values of children to set current value
*/
private accumulateApplyValues() {
const selections: number[] = this.children.map(it =>
it instanceof TreeNode
? it.checked
? Selection.ON
: it.indeterminate
? Selection.PARTIAL
: Selection.OFF
: (it as SelectionValue).selected
? Selection.ON
: Selection.OFF,
);
this.checked = selections.every(it => it === Selection.ON);
this.indeterminate = this.checked
? false
: selections.some(it => it > Selection.OFF);
}
/**
* Apply the value of this node to all child nodes
*/
private applyValueDownwards() {
for (const child of this.children) {
if (child instanceof TreeNode) {
child.checked = this.checked;
child.indeterminate = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(child as TreeNode<any>).applyValueDownwards();
} else {
(child as SelectionValue).selected = this.checked;
}
}
}
/**
* Set all children's parent to this
*/
private updateParents() {
for (const child of this.children) {
if (child instanceof TreeNode) {
child.parent = this as TreeNode<TreeNode<T>>;
}
}
}
/**
* Update values to all parents upwards
*/
private updateValueUpwards() {
this.parent?.accumulateApplyValues();
this.parent?.updateValueUpwards();
}
/**
* Click on this node
*/
click() {
this.checked = !this.checked;
this.indeterminate = false;
this.applyValueDownwards();
this.updateValueUpwards();
}
/**
* Notify that a child's value has changed
*/
notifyChildChanged() {
this.accumulateApplyValues();
this.updateValueUpwards();
}
}
export interface SelectionValue {
/**
* Item that was selected
*/
item: SCDateSeries;
/**
* Selection
*/
selected: boolean;
}

View File

@@ -1,16 +1,16 @@
/*
* 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 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.
* 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/>.
* 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 {ScrollingModule} from '@angular/cdk/scrolling';
import {CommonModule} from '@angular/common';
@@ -26,7 +26,7 @@ import {MenuModule} from '../menu/menu.module';
import {ScheduleProvider} from '../calendar/schedule.provider';
import {StorageModule} from '../storage/storage.module';
import {ActionChipListComponent} from './chips/action-chip-list.component';
import {AddEventPopoverComponent} from './chips/add-event-popover.component';
import {EditEventSelectionComponent} from './chips/edit-event-selection.component';
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
import {LocateActionChipComponent} from './chips/data/locate-action-chip.component';
import {DataFacetsProvider} from './data-facets.provider';
@@ -99,7 +99,7 @@ import {ExternalLinkComponent} from './elements/external-link.component';
declarations: [
ActionChipListComponent,
AddEventActionChipComponent,
AddEventPopoverComponent,
EditEventSelectionComponent,
AddressDetailComponent,
ArticleDetailContentComponent,
ArticleListItemComponent,

View File

@@ -0,0 +1,97 @@
/*
* 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 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,
ContentChild,
EventEmitter,
Input,
OnInit,
Output,
TemplateRef,
ViewChild,
} from '@angular/core';
import {
ActionSheetController,
AlertController,
Config,
IonModal,
IonRouterOutlet,
ModalController,
} from '@ionic/angular';
import {
pendingChangesActionSheet,
PendingChangesRole,
} from './pending-changes-action-sheet';
import {TranslatePipe} from '@ngx-translate/core';
@Component({
selector: 'stapps-edit-modal',
templateUrl: 'edit-modal.html',
})
export class EditModalComponent implements OnInit {
@ContentChild(TemplateRef) content: TemplateRef<unknown>;
@ViewChild('modal') modal: IonModal;
@Input() pendingChanges = false;
@Output() save = new EventEmitter();
presentingElement: HTMLElement;
constructor(
readonly modalController: ModalController,
readonly routerOutlet: IonRouterOutlet,
readonly alertController: AlertController,
readonly actionSheetController: ActionSheetController,
readonly translatePipe: TranslatePipe,
readonly config: Config,
) {}
async ngOnInit() {
this.presentingElement =
(await this.modalController.getTop()) || this.routerOutlet.nativeEl;
}
present() {
this.modal.present();
this.pendingChanges = false;
}
dismiss(skipChanges = false) {
this.pendingChanges = skipChanges ? false : this.pendingChanges;
setTimeout(() => this.modal.dismiss());
}
canDismissModal = async () => {
const alert =
this.config.get('mode') === 'ios'
? await this.actionSheetController.create(
pendingChangesActionSheet(this.translatePipe),
)
: await this.alertController.create(
pendingChangesActionSheet(this.translatePipe, false),
);
alert.present().then();
const {role} = await alert.onWillDismiss();
if (role === PendingChangesRole.SAVE) {
this.save.emit();
}
return role !== 'backdrop' && role !== PendingChangesRole.CANCEL;
};
}

View File

@@ -0,0 +1,39 @@
<!--
~ 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 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-modal
#modal
[presentingElement]="presentingElement"
[canDismiss]="!pendingChanges || canDismissModal"
>
<ng-template>
<ion-header mode="ios">
<ion-toolbar>
<ion-title>{{ 'modal.TITLE_EDIT' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="save.emit(); dismiss(true)">{{
'modal.DISMISS_CONFIRM' | translate
}}</ion-button>
</ion-buttons>
<ion-buttons slot="start">
<ion-button (click)="dismiss(true)">{{
'modal.DISMISS_CANCEL' | translate
}}</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ng-container *ngTemplateOutlet="content"> </ng-container>
</ng-template>
</ion-modal>

View File

@@ -0,0 +1,59 @@
/*
* 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 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 {TranslatePipe} from '@ngx-translate/core';
import {ActionSheetOptions, AlertOptions} from '@ionic/angular';
export enum PendingChangesRole {
SAVE = 'save',
DISCARD = 'discard',
CANCEL = 'cancel',
}
/**
*
*/
export function pendingChangesActionSheet(
translatePipe: TranslatePipe,
includeSaveOption = true,
): ActionSheetOptions & AlertOptions {
return {
header: translatePipe.transform('modal.dismiss_warn_pending_changes.TITLE'),
buttons: [
...(includeSaveOption
? [
{
text: translatePipe.transform(
'modal.dismiss_warn_pending_changes.SAVE',
),
role: PendingChangesRole.SAVE,
},
]
: []),
{
text: translatePipe.transform(
'modal.dismiss_warn_pending_changes.CANCEL',
),
role: PendingChangesRole.CANCEL,
},
{
text: translatePipe.transform(
'modal.dismiss_warn_pending_changes.DISCARD',
),
role: PendingChangesRole.DISCARD,
},
],
};
}

View File

@@ -1,16 +1,16 @@
/*
* 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 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.
* 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/>.
* 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';
@@ -21,8 +21,13 @@ import {DateFromIndexPipe} from './date-from-index.pipe';
import {DaytimeKeyPipe} from './daytime-key.pipe';
import {LazyPipe} from './lazy.pipe';
import {NextDateInListPipe} from './next-date-in-list.pipe';
import {EditModalComponent} from './edit-modal.component';
import {BrowserModule} from '@angular/platform-browser';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
@NgModule({
imports: [BrowserModule, IonicModule, TranslateModule],
declarations: [
ArrayLastPipe,
DateIsThisPipe,
@@ -31,6 +36,7 @@ import {NextDateInListPipe} from './next-date-in-list.pipe';
DateFromIndexPipe,
DaytimeKeyPipe,
NextDateInListPipe,
EditModalComponent,
],
exports: [
ArrayLastPipe,
@@ -40,6 +46,7 @@ import {NextDateInListPipe} from './next-date-in-list.pipe';
DateFromIndexPipe,
DaytimeKeyPipe,
NextDateInListPipe,
EditModalComponent,
],
})
export class UtilModule {}

View File

@@ -11,6 +11,13 @@
"DISMISS_CANCEL": "Abbrechen",
"DISMISS_CONFIRM": "Bestätigen",
"DISMISS": "Schließen",
"TITLE_EDIT": "Bearbeiten",
"dismiss_warn_pending_changes": {
"TITLE": "Ausstehende Änderungen",
"SAVE": "Speichern",
"DISCARD": "Verwerfen",
"CANCEL": "Abbrechen"
},
"settings": "Einstellungen"
},
"app": {

View File

@@ -11,6 +11,13 @@
"DISMISS_CANCEL": "Cancel",
"DISMISS_CONFIRM": "Confirm",
"DISMISS": "Close",
"TITLE_EDIT": "Edit",
"dismiss_warn_pending_changes": {
"TITLE": "Pending changes",
"SAVE": "Save",
"DISCARD": "Discard",
"CANCEL": "Cancel"
},
"settings": "Settings"
},
"app": {

View File

@@ -17,14 +17,15 @@
$parallax-background: var(--ion-color-primary),
$background: var(--ion-color-light),
$parallax-strength: 2,
$overscroll-padding: 50vh,
$content-size: 0px,
$overscroll-padding: 720px,
$content-size: 230px,
) {
&::part(background) {
background: $background;
}
&::part(scroll) {
perspective: 2px;
perspective-origin: center top;
}
> div {
transform-style: preserve-3d;
@@ -35,13 +36,21 @@
position: absolute;
top: 0;
right: 0;
height: calc(#{$content-size} + #{$parallax-strength} * #{$overscroll-padding});
width: 200%;
left: 0;
$height: calc($content-size + $overscroll-padding);
$translateY: calc($overscroll-padding * $parallax-strength);
$translateZ: calc(-1px * $parallax-strength);
$transform-origin: calc($parallax-strength * $parallax-strength * $overscroll-padding);
height: $height;
width: 150%;
transform-origin: 50% $transform-origin;
transform:
translateY(calc(-#{$overscroll-padding} * #{$parallax-strength}))
translateZ(calc(-1px * #{$parallax-strength}))
scale(1.5);
translate3d(0px, $translateY, $translateZ)
scale($parallax-strength);
z-index: -1;
background: $parallax-background;
}
}