mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-02-28 11:52:13 +00:00
refactor: move event select popup to a modal
This commit is contained in:
committed by
Rainer Killinger
parent
8a04a43903
commit
4a3f79ca20
@@ -34,14 +34,17 @@ describe('ical', function () {
|
|||||||
.contains('ion-chip', 'Termine Auswählen')
|
.contains('ion-chip', 'Termine Auswählen')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('ion-app > ion-popover').within(() => {
|
cy.get('ion-app > ion-modal').within(() => {
|
||||||
cy.get('.download-button > ion-button').should('have.attr', 'disabled');
|
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.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',
|
'not.have.attr',
|
||||||
'disabled',
|
'disabled',
|
||||||
);
|
);
|
||||||
cy.get('.download-button > ion-button').click();
|
cy.get('ion-footer > ion-toolbar > ion-button').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get('add-event-review-modal').within(() => {
|
cy.get('add-event-review-modal').within(() => {
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ describe('schedule', function () {
|
|||||||
fixture: 'search/types/date-series/date-series-1.json',
|
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.contains('ion-item', /eine Stunde um 19. Jan. 2022, \d+:00/).click();
|
||||||
cy.wait(2000);
|
cy.wait(2000);
|
||||||
cy.contains('ion-button', 'Ok').click();
|
cy.contains('ion-button', 'Bestätigen').click();
|
||||||
cy.wait(2000);
|
cy.wait(2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,5 +71,6 @@ const catalogRoutes: Routes = [
|
|||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider, TranslatePipe],
|
providers: [SettingsProvider, TranslatePipe],
|
||||||
|
exports: [EditModalComponent],
|
||||||
})
|
})
|
||||||
export class DashboardModule {}
|
export class DashboardModule {}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {EditModalItem, EditModalTypeEnum} from './edit-modal-type.enum';
|
|||||||
* Shows a modal window to sort and enable/disable menu items
|
* Shows a modal window to sort and enable/disable menu items
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-edit-modal',
|
selector: 'stapps-dashboard-edit-modal',
|
||||||
templateUrl: 'edit-modal.component.html',
|
templateUrl: 'edit-modal.component.html',
|
||||||
styleUrls: ['edit-modal.component.scss'],
|
styleUrls: ['edit-modal.component.scss'],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -14,12 +14,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* tslint:disable:prefer-function-over-method */
|
/* tslint:disable:prefer-function-over-method */
|
||||||
import {Component, Input, OnDestroy} from '@angular/core';
|
import {Component, Input, OnDestroy, ViewChild} from '@angular/core';
|
||||||
import {PopoverController} from '@ionic/angular';
|
import {IonRouterOutlet, ModalController} from '@ionic/angular';
|
||||||
import {SCDateSeries, SCThing, SCThingType, SCUuid} from '@openstapps/core';
|
import {SCDateSeries, SCThing, SCThingType, SCUuid} from '@openstapps/core';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {ScheduleProvider} from '../../../calendar/schedule.provider';
|
import {ScheduleProvider} from '../../../calendar/schedule.provider';
|
||||||
import {AddEventPopoverComponent} from '../add-event-popover.component';
|
|
||||||
import {CoordinatedSearchProvider} from '../../coordinated-search.provider';
|
import {CoordinatedSearchProvider} from '../../coordinated-search.provider';
|
||||||
import {
|
import {
|
||||||
chipSkeletonTransition,
|
chipSkeletonTransition,
|
||||||
@@ -29,6 +28,8 @@ import {
|
|||||||
AddEventStates,
|
AddEventStates,
|
||||||
AddEventStatesMap,
|
AddEventStatesMap,
|
||||||
} from './add-event-action-chip.config';
|
} 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
|
* Shows a horizontal list of action chips
|
||||||
@@ -82,10 +83,14 @@ export class AddEventActionChipComponent implements OnDestroy {
|
|||||||
*/
|
*/
|
||||||
uuidSubscription: Subscription;
|
uuidSubscription: Subscription;
|
||||||
|
|
||||||
|
@ViewChild('selection', {static: false})
|
||||||
|
selection: EditEventSelectionComponent;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly popoverController: PopoverController,
|
|
||||||
readonly dataProvider: CoordinatedSearchProvider,
|
readonly dataProvider: CoordinatedSearchProvider,
|
||||||
|
readonly modalController: ModalController,
|
||||||
readonly scheduleProvider: ScheduleProvider,
|
readonly scheduleProvider: ScheduleProvider,
|
||||||
|
readonly routerOutlet: IonRouterOutlet,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +112,24 @@ export class AddEventActionChipComponent implements OnDestroy {
|
|||||||
this.uuidSubscription?.unsubscribe();
|
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
|
* 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,45 @@
|
|||||||
*ngIf="associatedDateSeries | async as associatedDateSeries; else loading"
|
*ngIf="associatedDateSeries | async as associatedDateSeries; else loading"
|
||||||
@chipTransition
|
@chipTransition
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
(click)="$event.stopPropagation(); onClick($event)"
|
(click)="$event.stopPropagation(); editModal.present()"
|
||||||
>
|
>
|
||||||
<ion-icon [name]="icon" [fill]="iconFill"></ion-icon>
|
<ion-icon [name]="icon" [fill]="iconFill"></ion-icon>
|
||||||
<ion-label>{{ label | translate }}</ion-label>
|
<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>
|
</ion-chip>
|
||||||
<ng-template #loading>
|
<ng-template #loading>
|
||||||
<ion-chip @chipSkeletonTransition>
|
<ion-chip @chipSkeletonTransition>
|
||||||
|
|||||||
@@ -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 {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
padding: var(--spacing-sm);
|
padding: var(--spacing-sm);
|
||||||
@@ -17,3 +33,14 @@
|
|||||||
grid-column-start: 1;
|
grid-column-start: 1;
|
||||||
grid-row-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);
|
||||||
|
}
|
||||||
|
|||||||
130
src/app/modules/data/chips/edit-event-selection.component.ts
Normal file
130
src/app/modules/data/chips/edit-event-selection.component.ts
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/app/modules/data/chips/edit-event-selection.html
Normal file
86
src/app/modules/data/chips/edit-event-selection.html
Normal 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>
|
||||||
45
src/app/modules/data/chips/edit-event-selection.scss
Normal file
45
src/app/modules/data/chips/edit-event-selection.scss
Normal 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;
|
||||||
|
}
|
||||||
138
src/app/modules/data/chips/tree-node.ts
Normal file
138
src/app/modules/data/chips/tree-node.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ import {MenuModule} from '../menu/menu.module';
|
|||||||
import {ScheduleProvider} from '../calendar/schedule.provider';
|
import {ScheduleProvider} from '../calendar/schedule.provider';
|
||||||
import {StorageModule} from '../storage/storage.module';
|
import {StorageModule} from '../storage/storage.module';
|
||||||
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
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 {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
|
||||||
import {LocateActionChipComponent} from './chips/data/locate-action-chip.component';
|
import {LocateActionChipComponent} from './chips/data/locate-action-chip.component';
|
||||||
import {DataFacetsProvider} from './data-facets.provider';
|
import {DataFacetsProvider} from './data-facets.provider';
|
||||||
@@ -99,7 +99,7 @@ import {ExternalLinkComponent} from './elements/external-link.component';
|
|||||||
declarations: [
|
declarations: [
|
||||||
ActionChipListComponent,
|
ActionChipListComponent,
|
||||||
AddEventActionChipComponent,
|
AddEventActionChipComponent,
|
||||||
AddEventPopoverComponent,
|
EditEventSelectionComponent,
|
||||||
AddressDetailComponent,
|
AddressDetailComponent,
|
||||||
ArticleDetailContentComponent,
|
ArticleDetailContentComponent,
|
||||||
ArticleListItemComponent,
|
ArticleListItemComponent,
|
||||||
|
|||||||
97
src/app/util/edit-modal.component.ts
Normal file
97
src/app/util/edit-modal.component.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
39
src/app/util/edit-modal.html
Normal file
39
src/app/util/edit-modal.html
Normal 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>
|
||||||
59
src/app/util/pending-changes-action-sheet.ts
Normal file
59
src/app/util/pending-changes-action-sheet.ts
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21,8 +21,13 @@ import {DateFromIndexPipe} from './date-from-index.pipe';
|
|||||||
import {DaytimeKeyPipe} from './daytime-key.pipe';
|
import {DaytimeKeyPipe} from './daytime-key.pipe';
|
||||||
import {LazyPipe} from './lazy.pipe';
|
import {LazyPipe} from './lazy.pipe';
|
||||||
import {NextDateInListPipe} from './next-date-in-list.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({
|
@NgModule({
|
||||||
|
imports: [BrowserModule, IonicModule, TranslateModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
ArrayLastPipe,
|
ArrayLastPipe,
|
||||||
DateIsThisPipe,
|
DateIsThisPipe,
|
||||||
@@ -31,6 +36,7 @@ import {NextDateInListPipe} from './next-date-in-list.pipe';
|
|||||||
DateFromIndexPipe,
|
DateFromIndexPipe,
|
||||||
DaytimeKeyPipe,
|
DaytimeKeyPipe,
|
||||||
NextDateInListPipe,
|
NextDateInListPipe,
|
||||||
|
EditModalComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ArrayLastPipe,
|
ArrayLastPipe,
|
||||||
@@ -40,6 +46,7 @@ import {NextDateInListPipe} from './next-date-in-list.pipe';
|
|||||||
DateFromIndexPipe,
|
DateFromIndexPipe,
|
||||||
DaytimeKeyPipe,
|
DaytimeKeyPipe,
|
||||||
NextDateInListPipe,
|
NextDateInListPipe,
|
||||||
|
EditModalComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UtilModule {}
|
export class UtilModule {}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
"DISMISS_CANCEL": "Abbrechen",
|
"DISMISS_CANCEL": "Abbrechen",
|
||||||
"DISMISS_CONFIRM": "Bestätigen",
|
"DISMISS_CONFIRM": "Bestätigen",
|
||||||
"DISMISS": "Schließen",
|
"DISMISS": "Schließen",
|
||||||
|
"TITLE_EDIT": "Bearbeiten",
|
||||||
|
"dismiss_warn_pending_changes": {
|
||||||
|
"TITLE": "Ausstehende Änderungen",
|
||||||
|
"SAVE": "Speichern",
|
||||||
|
"DISCARD": "Verwerfen",
|
||||||
|
"CANCEL": "Abbrechen"
|
||||||
|
},
|
||||||
"settings": "Einstellungen"
|
"settings": "Einstellungen"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
"DISMISS_CANCEL": "Cancel",
|
"DISMISS_CANCEL": "Cancel",
|
||||||
"DISMISS_CONFIRM": "Confirm",
|
"DISMISS_CONFIRM": "Confirm",
|
||||||
"DISMISS": "Close",
|
"DISMISS": "Close",
|
||||||
|
"TITLE_EDIT": "Edit",
|
||||||
|
"dismiss_warn_pending_changes": {
|
||||||
|
"TITLE": "Pending changes",
|
||||||
|
"SAVE": "Save",
|
||||||
|
"DISCARD": "Discard",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
},
|
||||||
"settings": "Settings"
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|||||||
@@ -17,14 +17,15 @@
|
|||||||
$parallax-background: var(--ion-color-primary),
|
$parallax-background: var(--ion-color-primary),
|
||||||
$background: var(--ion-color-light),
|
$background: var(--ion-color-light),
|
||||||
$parallax-strength: 2,
|
$parallax-strength: 2,
|
||||||
$overscroll-padding: 50vh,
|
$overscroll-padding: 720px,
|
||||||
$content-size: 0px,
|
$content-size: 230px,
|
||||||
) {
|
) {
|
||||||
&::part(background) {
|
&::part(background) {
|
||||||
background: $background;
|
background: $background;
|
||||||
}
|
}
|
||||||
&::part(scroll) {
|
&::part(scroll) {
|
||||||
perspective: 2px;
|
perspective: 2px;
|
||||||
|
perspective-origin: center top;
|
||||||
}
|
}
|
||||||
> div {
|
> div {
|
||||||
transform-style: preserve-3d;
|
transform-style: preserve-3d;
|
||||||
@@ -35,13 +36,21 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: calc(#{$content-size} + #{$parallax-strength} * #{$overscroll-padding});
|
left: 0;
|
||||||
width: 200%;
|
|
||||||
|
$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:
|
transform:
|
||||||
translateY(calc(-#{$overscroll-padding} * #{$parallax-strength}))
|
translate3d(0px, $translateY, $translateZ)
|
||||||
translateZ(calc(-1px * #{$parallax-strength}))
|
scale($parallax-strength);
|
||||||
scale(1.5);
|
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
|
||||||
background: $parallax-background;
|
background: $parallax-background;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user