From 4a3f79ca20c040d90f573ddf61874a15b37033b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Fri, 23 Sep 2022 12:24:22 +0000 Subject: [PATCH] refactor: move event select popup to a modal --- cypress/integration/ical.spec.ts | 29 +- cypress/integration/schedule.spec.ts | 22 +- src/app/modules/dashboard/dashboard.module.ts | 1 + .../edit-modal/edit-modal.component.ts | 2 +- .../data/chips/add-event-popover.component.ts | 286 ------------------ .../modules/data/chips/add-event-popover.html | 91 ------ .../modules/data/chips/add-event-popover.scss | 26 -- .../data/add-event-action-chip.component.ts | 66 ++-- .../chips/data/add-event-action-chip.html | 55 +++- .../chips/data/add-event-action-chip.scss | 27 ++ .../chips/edit-event-selection.component.ts | 130 ++++++++ .../data/chips/edit-event-selection.html | 86 ++++++ .../data/chips/edit-event-selection.scss | 45 +++ src/app/modules/data/chips/tree-node.ts | 138 +++++++++ src/app/modules/data/data.module.ts | 22 +- src/app/util/edit-modal.component.ts | 97 ++++++ src/app/util/edit-modal.html | 39 +++ src/app/util/pending-changes-action-sheet.ts | 59 ++++ src/app/util/util.module.ts | 25 +- src/assets/i18n/de.json | 7 + src/assets/i18n/en.json | 7 + src/theme/common/_ion-content-parallax.scss | 23 +- 22 files changed, 788 insertions(+), 495 deletions(-) delete mode 100644 src/app/modules/data/chips/add-event-popover.component.ts delete mode 100644 src/app/modules/data/chips/add-event-popover.html delete mode 100644 src/app/modules/data/chips/add-event-popover.scss create mode 100644 src/app/modules/data/chips/edit-event-selection.component.ts create mode 100644 src/app/modules/data/chips/edit-event-selection.html create mode 100644 src/app/modules/data/chips/edit-event-selection.scss create mode 100644 src/app/modules/data/chips/tree-node.ts create mode 100644 src/app/util/edit-modal.component.ts create mode 100644 src/app/util/edit-modal.html create mode 100644 src/app/util/pending-changes-action-sheet.ts diff --git a/cypress/integration/ical.spec.ts b/cypress/integration/ical.spec.ts index 9dbe7470..b48426f4 100644 --- a/cypress/integration/ical.spec.ts +++ b/cypress/integration/ical.spec.ts @@ -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 . + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . */ describe('ical', function () { @@ -34,14 +34,17 @@ describe('ical', function () { .contains('ion-chip', 'Termine Auswählen') .click(); - cy.get('ion-app > ion-popover').within(() => { - cy.get('.download-button > ion-button').should('have.attr', 'disabled'); + cy.get('ion-app > ion-modal').within(() => { + cy.get('ion-footer > ion-toolbar > ion-button').should( + 'have.attr', + 'disabled', + ); cy.contains('ion-item', /eine Stunde um 19. Jan. 2022, \d+:00/).click(); - cy.get('.download-button > ion-button').should( + cy.get('ion-footer > ion-toolbar > ion-button').should( 'not.have.attr', 'disabled', ); - cy.get('.download-button > ion-button').click(); + cy.get('ion-footer > ion-toolbar > ion-button').click(); }); cy.get('add-event-review-modal').within(() => { diff --git a/cypress/integration/schedule.spec.ts b/cypress/integration/schedule.spec.ts index 32e5b4a2..09d71077 100644 --- a/cypress/integration/schedule.spec.ts +++ b/cypress/integration/schedule.spec.ts @@ -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 . + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . */ describe('schedule', function () { @@ -97,10 +97,10 @@ describe('schedule', function () { fixture: 'search/types/date-series/date-series-1.json', }); - cy.get('ion-app > ion-popover').within(() => { + cy.get('ion-app > ion-modal').within(() => { cy.contains('ion-item', /eine Stunde um 19. Jan. 2022, \d+:00/).click(); cy.wait(2000); - cy.contains('ion-button', 'Ok').click(); + cy.contains('ion-button', 'Bestätigen').click(); cy.wait(2000); }); diff --git a/src/app/modules/dashboard/dashboard.module.ts b/src/app/modules/dashboard/dashboard.module.ts index c8057690..629a2864 100644 --- a/src/app/modules/dashboard/dashboard.module.ts +++ b/src/app/modules/dashboard/dashboard.module.ts @@ -71,5 +71,6 @@ const catalogRoutes: Routes = [ UtilModule, ], providers: [SettingsProvider, TranslatePipe], + exports: [EditModalComponent], }) export class DashboardModule {} diff --git a/src/app/modules/dashboard/edit-modal/edit-modal.component.ts b/src/app/modules/dashboard/edit-modal/edit-modal.component.ts index 0980ec57..acb4d47b 100644 --- a/src/app/modules/dashboard/edit-modal/edit-modal.component.ts +++ b/src/app/modules/dashboard/edit-modal/edit-modal.component.ts @@ -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'], }) diff --git a/src/app/modules/data/chips/add-event-popover.component.ts b/src/app/modules/data/chips/add-event-popover.component.ts deleted file mode 100644 index e8a50b6f..00000000 --- a/src/app/modules/data/chips/add-event-popover.component.ts +++ /dev/null @@ -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 . - */ - -/* 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 | SelectionValue> { - /** - * Value of this node - */ - checked: boolean; - - /** - * If items are partially selected - */ - indeterminate: boolean; - - /** - * Parent of this node - */ - parent?: TreeNode>; - - 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).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>; - } - } - } - - /** - * 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>; - - /** - * 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(); - } -} diff --git a/src/app/modules/data/chips/add-event-popover.html b/src/app/modules/data/chips/add-event-popover.html deleted file mode 100644 index 620abbea..00000000 --- a/src/app/modules/data/chips/add-event-popover.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - {{ - 'data.chips.add_events.popover.ALL' | translate - }} - - - - - - {{ - frequency.children[0].item.repeatFrequency - ? (frequency.children[0].item.repeatFrequency - | durationLocalized: true - | sentencecase) - : ('data.chips.add_events.popover.SINGLE' | translate | titlecase) - }} - - - - - - {{ 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' }} - - - - {{ date.item.duration | amDuration: 'hours' }} - {{ 'data.chips.add_events.popover.AT' | translate }} - {{ - date.item.dates[date.item.dates.length - 1] - | amDateFormat: 'll, HH:mm' - }} - - - - - - -
- {{ - 'abort' | translate - }} - {{ - 'ok' | translate - }} -
-
- - - - -
-
diff --git a/src/app/modules/data/chips/add-event-popover.scss b/src/app/modules/data/chips/add-event-popover.scss deleted file mode 100644 index fd9073d3..00000000 --- a/src/app/modules/data/chips/add-event-popover.scss +++ /dev/null @@ -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 . - */ - -::ng-deep ion-item-divider { - cursor: pointer; -} - -.action-buttons { - float: right; -} - -.download-button { - float: left; -} diff --git a/src/app/modules/data/chips/data/add-event-action-chip.component.ts b/src/app/modules/data/chips/data/add-event-action-chip.component.ts index 78a00d1f..fd917d09 100644 --- a/src/app/modules/data/chips/data/add-event-action-chip.component.ts +++ b/src/app/modules/data/chips/data/add-event-action-chip.component.ts @@ -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 . + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . */ /* 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(); - } } diff --git a/src/app/modules/data/chips/data/add-event-action-chip.html b/src/app/modules/data/chips/data/add-event-action-chip.html index fd92c720..b243af1e 100644 --- a/src/app/modules/data/chips/data/add-event-action-chip.html +++ b/src/app/modules/data/chips/data/add-event-action-chip.html @@ -1,16 +1,16 @@
@@ -18,10 +18,45 @@ *ngIf="associatedDateSeries | async as associatedDateSeries; else loading" @chipTransition [disabled]="disabled" - (click)="$event.stopPropagation(); onClick($event)" + (click)="$event.stopPropagation(); editModal.present()" > {{ label | translate }} + + + +
+ +
+
+ + + + {{ + 'schedule.toCalendar.reviewModal.DOWNLOAD' + | translate + | titlecase + }} + + + + +
+
diff --git a/src/app/modules/data/chips/data/add-event-action-chip.scss b/src/app/modules/data/chips/data/add-event-action-chip.scss index 04f80941..bc4eccb8 100644 --- a/src/app/modules/data/chips/data/add-event-action-chip.scss +++ b/src/app/modules/data/chips/data/add-event-action-chip.scss @@ -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 . + */ +@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); +} diff --git a/src/app/modules/data/chips/edit-event-selection.component.ts b/src/app/modules/data/chips/edit-event-selection.component.ts new file mode 100644 index 00000000..89c5af88 --- /dev/null +++ b/src/app/modules/data/chips/edit-event-selection.component.ts @@ -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 . + */ +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>; + + /** + * 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()); + } +} diff --git a/src/app/modules/data/chips/edit-event-selection.html b/src/app/modules/data/chips/edit-event-selection.html new file mode 100644 index 00000000..d7bd5696 --- /dev/null +++ b/src/app/modules/data/chips/edit-event-selection.html @@ -0,0 +1,86 @@ + + + + + {{ 'data.chips.add_events.popover.ALL' | translate }} + + + + + + + + + {{ + frequency.children[0].item.repeatFrequency + ? (frequency.children[0].item.repeatFrequency + | durationLocalized: true + | sentencecase) + : ('data.chips.add_events.popover.SINGLE' | translate | titlecase) + }} + + + + + + + + {{ 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' }} + + + + {{ date.item.duration | amDuration: 'hours' }} + {{ 'data.chips.add_events.popover.AT' | translate }} + {{ + date.item.dates[date.item.dates.length - 1] + | amDateFormat: 'll, HH:mm' + }} + + + + + + diff --git a/src/app/modules/data/chips/edit-event-selection.scss b/src/app/modules/data/chips/edit-event-selection.scss new file mode 100644 index 00000000..a9285207 --- /dev/null +++ b/src/app/modules/data/chips/edit-event-selection.scss @@ -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 . + */ + +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; +} diff --git a/src/app/modules/data/chips/tree-node.ts b/src/app/modules/data/chips/tree-node.ts new file mode 100644 index 00000000..07239bd4 --- /dev/null +++ b/src/app/modules/data/chips/tree-node.ts @@ -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 . + */ + +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 | SelectionValue> { + /** + * Value of this node + */ + checked: boolean; + + /** + * If items are partially selected + */ + indeterminate: boolean; + + /** + * Parent of this node + */ + parent?: TreeNode>; + + 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).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>; + } + } + } + + /** + * 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; +} diff --git a/src/app/modules/data/data.module.ts b/src/app/modules/data/data.module.ts index b83c4ba5..0784c78a 100644 --- a/src/app/modules/data/data.module.ts +++ b/src/app/modules/data/data.module.ts @@ -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 . + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . */ 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, diff --git a/src/app/util/edit-modal.component.ts b/src/app/util/edit-modal.component.ts new file mode 100644 index 00000000..364e841b --- /dev/null +++ b/src/app/util/edit-modal.component.ts @@ -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 . + */ + +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; + + @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; + }; +} diff --git a/src/app/util/edit-modal.html b/src/app/util/edit-modal.html new file mode 100644 index 00000000..8958243f --- /dev/null +++ b/src/app/util/edit-modal.html @@ -0,0 +1,39 @@ + + + + + + + {{ 'modal.TITLE_EDIT' | translate }} + + {{ + 'modal.DISMISS_CONFIRM' | translate + }} + + + {{ + 'modal.DISMISS_CANCEL' | translate + }} + + + + + + diff --git a/src/app/util/pending-changes-action-sheet.ts b/src/app/util/pending-changes-action-sheet.ts new file mode 100644 index 00000000..681b1afa --- /dev/null +++ b/src/app/util/pending-changes-action-sheet.ts @@ -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 . + */ + +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, + }, + ], + }; +} diff --git a/src/app/util/util.module.ts b/src/app/util/util.module.ts index ae9126b4..0e696d25 100644 --- a/src/app/util/util.module.ts +++ b/src/app/util/util.module.ts @@ -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 . + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . */ 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 {} diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 103e0da4..9f15d564 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -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": { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 6cbf1883..078960e7 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -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": { diff --git a/src/theme/common/_ion-content-parallax.scss b/src/theme/common/_ion-content-parallax.scss index c3845589..4e26f1d0 100644 --- a/src/theme/common/_ion-content-parallax.scss +++ b/src/theme/common/_ion-content-parallax.scss @@ -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; } }