feat: add action chips to search results

This commit is contained in:
Wieland Schöbl
2021-06-22 15:09:40 +00:00
parent 0aa26020be
commit 67fb4a43c9
27 changed files with 771 additions and 303 deletions

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCDateSeries, SCThings, SCThingType} from '@openstapps/core';
/**
* Shows a horizontal list of action chips
*/
@Component({
selector: 'stapps-action-chip-list',
templateUrl: 'action-chip-list.html',
styleUrls: ['action-chip-list.scss'],
})
export class ActionChipListComponent {
/**
* If chips are applicable
*/
applicable: Record<string, () => boolean> = {
'locate': () => this.item.hasOwnProperty('inPlace'),
'event': () => this.item.type === SCThingType.AcademicEvent ||
(this.item.type === SCThingType.DateSeries && (this.item as SCDateSeries).dates.length !== 0),
};
/**
* The item the action belongs to
*/
@Input() item: SCThings;
}

View File

@@ -0,0 +1,4 @@
<div>
<stapps-locate-action-chip *ngIf='applicable["locate"]()' [item]='item'></stapps-locate-action-chip>
<stapps-add-event-action-chip *ngIf='applicable["event"]()' [item]='item'></stapps-add-event-action-chip>
</div>

View File

@@ -0,0 +1,4 @@
div {
display: flex;
flex-direction: row;
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {SCDateSeries} from '@openstapps/core';
import {every, groupBy, some, sortBy, values} from 'lodash-es';
import {capitalize, last} from 'lodash-es';
enum Selection {
ON = 2,
PARTIAL = 1,
OFF = 1,
}
/**
* A tree
*
* The generic is to preserve type safety of how deep the tree goes.
*/
// tslint:disable-next-line:no-any
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 = every(selections, it => it === Selection.ON);
this.indeterminate = this.checked ? false : some(selections, 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 {
/**
* Lodash alias
*/
capitalize: (item: string) => string = capitalize;
/**
* The item the action belongs to
*/
@Input() items: SCDateSeries[];
/**
* Lodash alias
*/
last: <T>(item: T[] | null | undefined) => T | undefined = last;
/**
* Selection of the item
*/
selection: TreeNode<TreeNode<SelectionValue>>;
constructor(readonly ref: ChangeDetectorRef) {
}
/**
* Init
*/
ngOnInit() {
this.selection =
new TreeNode(values(groupBy(sortBy(this.items.map(item => ({
selected: false,
item: item,
})), it => it.item.frequency), it => it.item.frequency))
.map(item => new TreeNode(item, this.ref)), this.ref);
}
}

View File

@@ -0,0 +1,40 @@
<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>{{('frequency' | thingTranslate: frequency.children[0].item) | 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 *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}}
{{last(date.item.dates) | amDateFormat: 'll'}}
</ion-label>
<ng-template #single_event>
<ion-label>
{{date.item.duration | amDuration: 'hours'}}
{{'data.chips.add_events.popover.AT' | translate}}
{{last(date.item.dates) | 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>
</ion-card-content>

View File

@@ -0,0 +1,7 @@
::ng-deep ion-item-divider {
cursor: pointer;
}
ion-card-content {
width: fit-content;
}

View File

@@ -0,0 +1,165 @@
/* tslint:disable:prefer-function-over-method */
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input, OnInit} from '@angular/core';
import {PopoverController} from '@ionic/angular';
import {SCDateSeries, SCThing, SCThingType} from '@openstapps/core';
import {DataProvider} from '../../data.provider';
import {AddEventPopoverComponent} from '../add-event-popover.component';
enum AddEventStates {
ADDED_ALL,
ADDED_SOME,
REMOVED_ALL,
UNAVAILABLE,
}
/**
* Shows a horizontal list of action chips
*/
@Component({
selector: 'stapps-add-event-action-chip',
templateUrl: 'add-event-action-chip.html',
styleUrls: ['add-event-action-chip.scss'],
})
export class AddEventActionChipComponent implements OnInit {
/**
* Associated date series
*/
associatedDateSeries: Promise<SCDateSeries[]>;
/**
* Disabled
*/
disabled: boolean;
/**
* Icon
*/
icon: string;
/**
* Item
*/
@Input() item: SCThing;
/**
* Label
*/
label: string;
/**
* State
*/
state: AddEventStates;
/**
* States
*/
states = {
[AddEventStates.ADDED_ALL]: {
icon: 'events-all',
label: 'data.chips.add_events.ADDED_ALL',
disabled: false,
},
[AddEventStates.ADDED_SOME]: {
icon: 'events-partial',
label: 'data.chips.add_events.ADDED_SOME',
disabled: false,
},
[AddEventStates.REMOVED_ALL]: {
icon: 'events',
label: 'data.chips.add_events.REMOVED_ALL',
disabled: false,
},
[AddEventStates.UNAVAILABLE]: {
icon: 'close',
label: 'data.chips.add_events.UNAVAILABLE',
disabled: true,
},
};
constructor(readonly popoverController: PopoverController,
readonly dataProvider: DataProvider) {
}
/**
* Apply state
*/
applyState(state: AddEventStates) {
this.state = state;
const {label, icon, disabled} = this.states[state];
this.label = label;
this.icon = icon;
this.disabled = disabled;
}
/**
* Init
*/
ngOnInit() {
this.associatedDateSeries = this.item.type === SCThingType.DateSeries ?
Promise.resolve([this.item as SCDateSeries]) :
this.dataProvider.search({
filter: {
arguments: {
filters: [
{
arguments: {
field: 'type',
value: SCThingType.DateSeries,
},
type: 'value',
},
{
arguments: {
field: 'event.uid',
value: this.item.uid,
},
type: 'value',
},
],
operation: 'and',
},
type: 'boolean',
},
})
.then((it) => it.data as SCDateSeries[]);
this.associatedDateSeries.then((it) => this.applyState(
it.length < 1 ? AddEventStates.UNAVAILABLE : AddEventStates.REMOVED_ALL));
}
/**
* Action
*/
// @Override
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();
// TODO: replace dummy implementation
await popover.onDidDismiss();
this.applyState(this.state === AddEventStates.ADDED_ALL ?
AddEventStates.REMOVED_ALL : AddEventStates.ADDED_ALL);
}
}

View File

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

View File

@@ -0,0 +1,3 @@
::ng-deep ion-skeleton-text {
width: 50px;
}

View File

@@ -0,0 +1,39 @@
/* tslint:disable:prefer-function-over-method */
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCThing} from '@openstapps/core';
/**
* Shows a horizontal list of action chips
*/
@Component({
selector: 'stapps-locate-action-chip',
templateUrl: 'locate-action-chip.html',
})
export class LocateActionChipComponent {
/**
* Item
*/
@Input() item: SCThing;
/**
* Click
*/
onClick(/*event: MouseEvent*/) {
// TODO
}
}

View File

@@ -0,0 +1,7 @@
<ion-chip class='chip-class' (click)='$event.stopPropagation(); onClick()'>
<ion-icon name='location'></ion-icon>
<ion-label>{{'Locate' | translate}}</ion-label>
<ng-template #loading>
<ion-skeleton-text animated='true'></ion-skeleton-text>
</ng-template>
</ion-chip>