mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 00:23:03 +00:00
feat: timetable module - schedule and calendar
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
*ngIf="applicable['locate']()"
|
||||
[item]="item"
|
||||
></stapps-locate-action-chip>
|
||||
<!-- Add Event Chip needs to load data and should be the last -->
|
||||
<stapps-add-event-action-chip
|
||||
*ngIf="applicable['event']()"
|
||||
[item]="item"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
@@ -12,10 +13,29 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
|
||||
import {SCDateSeries} from '@openstapps/core';
|
||||
import {every, groupBy, some, sortBy, values} from 'lodash-es';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {PopoverController} from '@ionic/angular';
|
||||
import {SCDateSeries, SCUuid} from '@openstapps/core';
|
||||
import {
|
||||
difference,
|
||||
every,
|
||||
flatMap,
|
||||
groupBy,
|
||||
mapValues,
|
||||
some,
|
||||
sortBy,
|
||||
union,
|
||||
values,
|
||||
} from 'lodash-es';
|
||||
import {capitalize, last} from 'lodash-es';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {ScheduleProvider} from '../../schedule/schedule.provider';
|
||||
|
||||
enum Selection {
|
||||
ON = 2,
|
||||
@@ -28,7 +48,6 @@ enum Selection {
|
||||
*
|
||||
* The generic is to preserve type safety of how deep the tree goes.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
class TreeNode<T extends TreeNode<any> | SelectionValue> {
|
||||
/**
|
||||
* Value of this node
|
||||
@@ -54,19 +73,16 @@ class TreeNode<T extends TreeNode<any> | SelectionValue> {
|
||||
* Accumulate values of children to set current value
|
||||
*/
|
||||
private accumulateApplyValues() {
|
||||
const selections: number[] = this.children.map(
|
||||
it =>
|
||||
/* eslint-disable unicorn/no-nested-ternary */
|
||||
it instanceof TreeNode
|
||||
? it.checked
|
||||
? Selection.ON
|
||||
: it.indeterminate
|
||||
? Selection.PARTIAL
|
||||
: Selection.OFF
|
||||
: (it as SelectionValue).selected
|
||||
const selections: number[] = this.children.map(it =>
|
||||
it instanceof TreeNode
|
||||
? it.checked
|
||||
? Selection.ON
|
||||
: Selection.OFF,
|
||||
/* eslint-enable unicorn/no-nested-ternary */
|
||||
: it.indeterminate
|
||||
? Selection.PARTIAL
|
||||
: Selection.OFF
|
||||
: (it as SelectionValue).selected
|
||||
? Selection.ON
|
||||
: Selection.OFF,
|
||||
);
|
||||
|
||||
this.checked = every(selections, it => it === Selection.ON);
|
||||
@@ -83,7 +99,7 @@ class TreeNode<T extends TreeNode<any> | SelectionValue> {
|
||||
if (child instanceof TreeNode) {
|
||||
child.checked = this.checked;
|
||||
child.indeterminate = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// tslint:disable-next-line:no-any
|
||||
(child as TreeNode<any>).applyValueDownwards();
|
||||
} else {
|
||||
(child as SelectionValue).selected = this.checked;
|
||||
@@ -149,7 +165,7 @@ interface SelectionValue {
|
||||
templateUrl: 'add-event-popover.html',
|
||||
styleUrls: ['add-event-popover.scss'],
|
||||
})
|
||||
export class AddEventPopoverComponent implements OnInit {
|
||||
export class AddEventPopoverComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Lodash alias
|
||||
*/
|
||||
@@ -170,23 +186,70 @@ export class AddEventPopoverComponent implements OnInit {
|
||||
*/
|
||||
selection: TreeNode<TreeNode<SelectionValue>>;
|
||||
|
||||
constructor(readonly ref: ChangeDetectorRef) {}
|
||||
/**
|
||||
* Uuids
|
||||
*/
|
||||
uuids: SCUuid[];
|
||||
|
||||
/**
|
||||
* Uuid Subscription
|
||||
*/
|
||||
uuidSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
readonly ref: ChangeDetectorRef,
|
||||
readonly scheduleProvider: ScheduleProvider,
|
||||
readonly popoverController: PopoverController,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Destroy
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.uuidSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.selection = new TreeNode(
|
||||
values(
|
||||
groupBy(
|
||||
sortBy(
|
||||
this.items.map(item => ({selected: false, item: item})),
|
||||
it => it.item.frequency,
|
||||
),
|
||||
it => it.item.frequency,
|
||||
),
|
||||
).map(item => new TreeNode(item, this.ref)),
|
||||
this.ref,
|
||||
this.uuidSubscription = this.scheduleProvider.uuids$.subscribe(
|
||||
async result => {
|
||||
this.uuids = result;
|
||||
|
||||
this.selection = new TreeNode(
|
||||
values(
|
||||
groupBy(
|
||||
sortBy(
|
||||
this.items.map(item => ({
|
||||
selected: this.uuids.includes(item.uid),
|
||||
item: item,
|
||||
})),
|
||||
it => it.item.frequency,
|
||||
),
|
||||
it => it.item.frequency,
|
||||
),
|
||||
).map(item => new TreeNode(item, this.ref)),
|
||||
this.ref,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* On selection change
|
||||
*/
|
||||
async onCommit(save: boolean) {
|
||||
if (save) {
|
||||
const {false: unselected, true: selected} = mapValues(
|
||||
groupBy(flatMap(this.selection.children, 'children'), 'selected'),
|
||||
value => value.map(it => it.item.uid),
|
||||
);
|
||||
this.scheduleProvider.uuids$.next(
|
||||
union(difference(this.uuids, unselected), selected),
|
||||
);
|
||||
}
|
||||
|
||||
await this.popoverController.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,12 @@
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-item-group>
|
||||
<div class="action-buttons">
|
||||
<ion-button (click)="onCommit(false)" fill="clear">{{
|
||||
'abort' | translate
|
||||
}}</ion-button>
|
||||
<ion-button (click)="onCommit(true)" fill="clear">{{
|
||||
'ok' | translate
|
||||
}}</ion-button>
|
||||
</div>
|
||||
</ion-card-content>
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
ion-card-content {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
/* tslint:disable:prefer-function-over-method */
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
@@ -13,11 +13,18 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {PopoverController} from '@ionic/angular';
|
||||
import {SCDateSeries, SCThing, SCThingType} from '@openstapps/core';
|
||||
import {DataProvider} from '../../data.provider';
|
||||
import {SCDateSeries, SCThing, SCThingType, SCUuid} from '@openstapps/core';
|
||||
import {difference, map} from 'lodash-es';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {ScheduleProvider} from '../../../schedule/schedule.provider';
|
||||
import {AddEventPopoverComponent} from '../add-event-popover.component';
|
||||
import {CoordinatedSearchProvider} from '../../coordinated-search.provider';
|
||||
import {
|
||||
chipSkeletonTransition,
|
||||
chipTransition,
|
||||
} from '../../../../animation/skeleton-transitions/chip-loading-transition';
|
||||
|
||||
enum AddEventStates {
|
||||
ADDED_ALL,
|
||||
@@ -33,8 +40,9 @@ enum AddEventStates {
|
||||
selector: 'stapps-add-event-action-chip',
|
||||
templateUrl: 'add-event-action-chip.html',
|
||||
styleUrls: ['add-event-action-chip.scss'],
|
||||
animations: [chipSkeletonTransition, chipTransition],
|
||||
})
|
||||
export class AddEventActionChipComponent implements OnInit {
|
||||
export class AddEventActionChipComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Associated date series
|
||||
*/
|
||||
@@ -91,9 +99,20 @@ export class AddEventActionChipComponent implements OnInit {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* UUIDs
|
||||
*/
|
||||
uuids: SCUuid[];
|
||||
|
||||
/**
|
||||
* UUID Subscription
|
||||
*/
|
||||
uuidSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
readonly popoverController: PopoverController,
|
||||
readonly dataProvider: DataProvider,
|
||||
readonly dataProvider: CoordinatedSearchProvider,
|
||||
readonly scheduleProvider: ScheduleProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -107,6 +126,13 @@ export class AddEventActionChipComponent implements OnInit {
|
||||
this.disabled = disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.uuidSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init
|
||||
*/
|
||||
@@ -115,7 +141,7 @@ export class AddEventActionChipComponent implements OnInit {
|
||||
this.item.type === SCThingType.DateSeries
|
||||
? Promise.resolve([this.item as SCDateSeries])
|
||||
: this.dataProvider
|
||||
.search({
|
||||
.coordinatedSearch({
|
||||
filter: {
|
||||
arguments: {
|
||||
filters: [
|
||||
@@ -140,19 +166,36 @@ export class AddEventActionChipComponent implements OnInit {
|
||||
},
|
||||
})
|
||||
.then(it => it.data as SCDateSeries[]);
|
||||
this.associatedDateSeries.then(it =>
|
||||
this.applyState(
|
||||
it.length === 0
|
||||
? AddEventStates.UNAVAILABLE
|
||||
: AddEventStates.REMOVED_ALL,
|
||||
),
|
||||
|
||||
this.uuidSubscription = this.scheduleProvider.uuids$.subscribe(
|
||||
async result => {
|
||||
this.uuids = result;
|
||||
const associatedDateSeries = await this.associatedDateSeries;
|
||||
if (associatedDateSeries.length === 0) {
|
||||
this.applyState(AddEventStates.UNAVAILABLE);
|
||||
|
||||
return;
|
||||
}
|
||||
switch (
|
||||
difference(map(associatedDateSeries, 'uid'), this.uuids).length
|
||||
) {
|
||||
case 0:
|
||||
this.applyState(AddEventStates.ADDED_ALL);
|
||||
break;
|
||||
case associatedDateSeries.length:
|
||||
this.applyState(AddEventStates.REMOVED_ALL);
|
||||
break;
|
||||
default:
|
||||
this.applyState(AddEventStates.ADDED_SOME);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action
|
||||
*/
|
||||
// @Override
|
||||
async onClick(event: MouseEvent) {
|
||||
const associatedDateSeries = await this.associatedDateSeries;
|
||||
const popover = await this.popoverController.create({
|
||||
@@ -165,12 +208,5 @@ export class AddEventActionChipComponent implements OnInit {
|
||||
event: event,
|
||||
});
|
||||
await popover.present();
|
||||
// TODO: replace dummy implementation
|
||||
await popover.onDidDismiss();
|
||||
this.applyState(
|
||||
this.state === AddEventStates.ADDED_ALL
|
||||
? AddEventStates.REMOVED_ALL
|
||||
: AddEventStates.ADDED_ALL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<div *ngIf="associatedDateSeries | async as associatedDateSeries; else loading">
|
||||
<div class="stack-children">
|
||||
<ion-chip
|
||||
*ngIf="associatedDateSeries | async as associatedDateSeries; else loading"
|
||||
@chipTransition
|
||||
[disabled]="disabled"
|
||||
(click)="$event.stopPropagation(); onClick($event)"
|
||||
>
|
||||
<ion-icon [name]="icon"></ion-icon>
|
||||
<ion-label>{{ label | translate }}</ion-label>
|
||||
</ion-chip>
|
||||
<ng-template #loading>
|
||||
<ion-chip @chipSkeletonTransition>
|
||||
<ion-skeleton-text animated="true"></ion-skeleton-text>
|
||||
</ion-chip>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template #loading>
|
||||
<ion-chip>
|
||||
<ion-skeleton-text animated="true"></ion-skeleton-text>
|
||||
</ion-chip>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
::ng-deep ion-skeleton-text {
|
||||
:host ::ng-deep ion-skeleton-text {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.stack-children {
|
||||
display: grid;
|
||||
align-items: start;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.stack-children > * {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
}
|
||||
|
||||
22
src/app/modules/data/coordinated-search.provider.spec.ts
Normal file
22
src/app/modules/data/coordinated-search.provider.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 {arrayToIndexMap} from './coordinated-search.provider';
|
||||
|
||||
describe('CoordinatedSearchProvider', () => {
|
||||
it('transform arrays correctly', () => {
|
||||
expect(arrayToIndexMap(['a', 'b', 'c'])).toEqual({0: 'a', 1: 'b', 2: 'c'});
|
||||
});
|
||||
});
|
||||
102
src/app/modules/data/coordinated-search.provider.ts
Normal file
102
src/app/modules/data/coordinated-search.provider.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 {SCSearchRequest, SCSearchResponse} from '@openstapps/core';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {DataProvider} from './data.provider';
|
||||
|
||||
/**
|
||||
* Delay execution for (at least) a set amount of time
|
||||
*/
|
||||
async function delay(ms: number): Promise<void> {
|
||||
return await new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an array to an object with the indices as keys
|
||||
*
|
||||
* ['a', 'b', 'c'] => {0: 'a', 1: 'b', 2: 'c'}
|
||||
*/
|
||||
export function arrayToIndexMap<T>(array: T[]): Record<number, T> {
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
return array.reduce((previous, current, index) => {
|
||||
previous[index] = current;
|
||||
return previous;
|
||||
}, {} as Record<number, T>);
|
||||
}
|
||||
|
||||
interface OngoingQuery {
|
||||
request: SCSearchRequest;
|
||||
response?: Promise<SCSearchResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinated search request that bundles requests from multiple modules into a single one
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoordinatedSearchProvider {
|
||||
constructor(readonly dataProvider: DataProvider) {}
|
||||
|
||||
/**
|
||||
* Queue of ongoing queries
|
||||
*/
|
||||
queue: OngoingQuery[] = [];
|
||||
|
||||
/**
|
||||
* Default latency of search requests
|
||||
*/
|
||||
latencyMs = 50;
|
||||
|
||||
/**
|
||||
* Start a coordinated search that merges requests across components
|
||||
*
|
||||
* This method collects the request, then:
|
||||
* 1. If the queue is full, dispatches all immediately
|
||||
* 2. If not, waits a set amount of time for other requests to come in
|
||||
*/
|
||||
async coordinatedSearch(
|
||||
query: SCSearchRequest,
|
||||
latencyMs?: number,
|
||||
): Promise<SCSearchResponse> {
|
||||
const ongoingQuery: OngoingQuery = {request: query};
|
||||
this.queue.push(ongoingQuery);
|
||||
|
||||
if (this.queue.length < this.dataProvider.backendQueriesLimit) {
|
||||
await delay(latencyMs ?? this.latencyMs);
|
||||
}
|
||||
|
||||
if (this.queue.length > 0) {
|
||||
// because we are guaranteed to have limited our queue size to be
|
||||
// <= to the backendQueriesLimite as of above, we can bypass the wrapper
|
||||
// in the data provider that usually would be responsible for splitting up the requests
|
||||
const responses = this.dataProvider.client.multiSearch(
|
||||
arrayToIndexMap(this.queue.map(it => it.request)),
|
||||
);
|
||||
|
||||
for (const [index, request] of this.queue.entries()) {
|
||||
request.response = new Promise(resolve =>
|
||||
responses.then(it => resolve(it[index])),
|
||||
);
|
||||
}
|
||||
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
// Response is guaranteed to be defined here
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return await ongoingQuery.response!;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {MarkdownModule} from 'ngx-markdown';
|
||||
import {MomentModule} from 'ngx-moment';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {MenuModule} from '../menu/menu.module';
|
||||
import {ScheduleProvider} from '../schedule/schedule.provider';
|
||||
import {StorageModule} from '../storage/storage.module';
|
||||
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
||||
import {AddEventPopoverComponent} from './chips/add-event-popover.component';
|
||||
@@ -39,7 +40,6 @@ import {AddressDetailComponent} from './elements/address-detail.component';
|
||||
import {OffersDetailComponent} from './elements/offers-detail.component';
|
||||
import {OffersInListComponent} from './elements/offers-in-list.component';
|
||||
import {OriginDetailComponent} from './elements/origin-detail.component';
|
||||
import {OriginInListComponent} from './elements/origin-in-list.component';
|
||||
import {SimpleCardComponent} from './elements/simple-card.component';
|
||||
import {DataListComponent} from './list/data-list.component';
|
||||
import {FoodDataListComponent} from './list/food-data-list.component';
|
||||
@@ -59,7 +59,6 @@ import {PlaceDetailContentComponent} from './types/place/place-detail-content.co
|
||||
import {PlaceListItemComponent} from './types/place/place-list-item.component';
|
||||
import {PlaceMensaDetailComponent} from './types/place/special/mensa/place-mensa-detail.component';
|
||||
import {SemesterDetailContentComponent} from './types/semester/semester-detail-content.component';
|
||||
import {VideoDetailContentComponent} from './types/video/video-detail-content.component';
|
||||
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
||||
import {LeafletModule} from '@asymmetrik/ngx-leaflet';
|
||||
import {ArticleListItemComponent} from './types/article/article-list-item.component';
|
||||
@@ -73,29 +72,30 @@ import {LongInlineTextComponent} from './elements/long-inline-text.component';
|
||||
import {MessageListItemComponent} from './types/message/message-list-item.component';
|
||||
import {OrganizationListItemComponent} from './types/organization/organization-list-item.component';
|
||||
import {PersonListItemComponent} from './types/person/person-list-item.component';
|
||||
import {SemesterListItemComponent} from './types/semester/semester-list-item.component';
|
||||
import {SkeletonListItemComponent} from './elements/skeleton-list-item.component';
|
||||
import {SkeletonSegmentComponent} from './elements/skeleton-segment-button.component';
|
||||
import {VideoDetailContentComponent} from './types/video/video-detail-content.component';
|
||||
import {SemesterListItemComponent} from './types/semester/semester-list-item.component';
|
||||
import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
import {OriginInListComponent} from './elements/origin-in-list.component';
|
||||
import {CoordinatedSearchProvider} from './coordinated-search.provider';
|
||||
|
||||
/**
|
||||
* Module for handling data
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ActionChipListComponent,
|
||||
AddEventActionChipComponent,
|
||||
AddEventPopoverComponent,
|
||||
OffersDetailComponent,
|
||||
OffersInListComponent,
|
||||
AddressDetailComponent,
|
||||
ArticleDetailContentComponent,
|
||||
ArticleListItemComponent,
|
||||
SimpleCardComponent,
|
||||
SkeletonSimpleCardComponent,
|
||||
CatalogDetailContentComponent,
|
||||
CatalogListItemComponent,
|
||||
DataDetailComponent,
|
||||
DataDetailContentComponent,
|
||||
FoodDataListComponent,
|
||||
DataIconPipe,
|
||||
DataListComponent,
|
||||
DataListItemComponent,
|
||||
DateSeriesDetailContentComponent,
|
||||
@@ -106,10 +106,14 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
EventListItemComponent,
|
||||
FavoriteDetailContentComponent,
|
||||
FavoriteListItemComponent,
|
||||
FoodDataListComponent,
|
||||
LocateActionChipComponent,
|
||||
LongInlineTextComponent,
|
||||
MapWidgetComponent,
|
||||
MessageDetailContentComponent,
|
||||
MessageListItemComponent,
|
||||
OffersDetailComponent,
|
||||
OffersInListComponent,
|
||||
OrganizationDetailContentComponent,
|
||||
OrganizationListItemComponent,
|
||||
OriginDetailComponent,
|
||||
@@ -122,21 +126,20 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
SearchPageComponent,
|
||||
SemesterDetailContentComponent,
|
||||
SemesterListItemComponent,
|
||||
SimpleCardComponent,
|
||||
SkeletonListItemComponent,
|
||||
SkeletonSegmentComponent,
|
||||
SkeletonSimpleCardComponent,
|
||||
VideoDetailContentComponent,
|
||||
VideoListItemComponent,
|
||||
DataIconPipe,
|
||||
ActionChipListComponent,
|
||||
AddEventActionChipComponent,
|
||||
LocateActionChipComponent,
|
||||
],
|
||||
entryComponents: [DataListComponent],
|
||||
imports: [
|
||||
IonicModule.forRoot(),
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
DataRoutingModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
IonicModule.forRoot(),
|
||||
LeafletModule,
|
||||
MarkdownModule.forRoot(),
|
||||
MenuModule,
|
||||
@@ -150,16 +153,26 @@ import {VideoListItemComponent} from './types/video/video-list-item.component';
|
||||
TranslateModule.forChild(),
|
||||
ThingTranslateModule.forChild(),
|
||||
],
|
||||
providers: [DataProvider, DataFacetsProvider, Network, StAppsWebHttpClient],
|
||||
providers: [
|
||||
CoordinatedSearchProvider,
|
||||
DataProvider,
|
||||
DataFacetsProvider,
|
||||
Network,
|
||||
ScheduleProvider,
|
||||
StAppsWebHttpClient,
|
||||
],
|
||||
exports: [
|
||||
DataDetailComponent,
|
||||
DataDetailContentComponent,
|
||||
DataIconPipe,
|
||||
DataListComponent,
|
||||
DataListItemComponent,
|
||||
DataDetailComponent,
|
||||
SkeletonSimpleCardComponent,
|
||||
SkeletonListItemComponent,
|
||||
DataIconPipe,
|
||||
DateSeriesListItemComponent,
|
||||
PlaceListItemComponent,
|
||||
DataDetailContentComponent,
|
||||
SimpleCardComponent,
|
||||
SkeletonListItemComponent,
|
||||
SkeletonSimpleCardComponent,
|
||||
SearchPageComponent,
|
||||
],
|
||||
})
|
||||
export class DataModule {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2021 StApps
|
||||
* 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.
|
||||
|
||||
@@ -49,6 +49,11 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@Input() forcedFilter?: SCSearchFilter;
|
||||
|
||||
/**
|
||||
* If routing should be done if the user clicks on an item
|
||||
*/
|
||||
@Input() itemRouting? = true;
|
||||
|
||||
/**
|
||||
* Thing counter to start query the next page from
|
||||
*/
|
||||
@@ -120,46 +125,6 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
protected router: Router,
|
||||
) {
|
||||
this.initialize();
|
||||
|
||||
combineLatest([
|
||||
this.queryTextChanged.pipe(
|
||||
debounceTime(this.searchQueryDueTime),
|
||||
distinctUntilChanged(),
|
||||
startWith(this.queryText),
|
||||
),
|
||||
this.contextMenuService.filterQueryChanged$.pipe(
|
||||
startWith(this.filterQuery),
|
||||
),
|
||||
this.contextMenuService.sortQueryChanged$.pipe(startWith(this.sortQuery)),
|
||||
]).subscribe(async query => {
|
||||
this.queryText = query[0];
|
||||
this.filterQuery = query[1];
|
||||
this.sortQuery = query[2];
|
||||
this.from = 0;
|
||||
await this.fetchAndUpdateItems();
|
||||
this.queryChanged.next();
|
||||
});
|
||||
|
||||
this.fetchAndUpdateItems();
|
||||
|
||||
/**
|
||||
* Subscribe to 'settings.changed' events
|
||||
*/
|
||||
this.subscriptions.push(
|
||||
this.settingsProvider.settingsActionChanged$.subscribe(
|
||||
({type, payload}) => {
|
||||
if (type === 'stapps.settings.changed') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const {category, name, value} = payload!;
|
||||
this.logger.log(`received event "settings.changed" with category:
|
||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
||||
}
|
||||
},
|
||||
),
|
||||
this.dataRoutingService.itemSelectListener().subscribe(item => {
|
||||
void this.router.navigate(['data-detail', item.uid]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,6 +226,48 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
* Initialises the possible sort options in ContextMenuService
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
combineLatest([
|
||||
this.queryTextChanged.pipe(
|
||||
debounceTime(this.searchQueryDueTime),
|
||||
distinctUntilChanged(),
|
||||
startWith(this.queryText),
|
||||
),
|
||||
this.contextMenuService.filterQueryChanged$.pipe(
|
||||
startWith(this.filterQuery),
|
||||
),
|
||||
this.contextMenuService.sortQueryChanged$.pipe(startWith(this.sortQuery)),
|
||||
]).subscribe(async query => {
|
||||
this.queryText = query[0];
|
||||
this.filterQuery = query[1];
|
||||
this.sortQuery = query[2];
|
||||
this.from = 0;
|
||||
await this.fetchAndUpdateItems();
|
||||
this.queryChanged.next();
|
||||
});
|
||||
|
||||
void this.fetchAndUpdateItems();
|
||||
|
||||
/**
|
||||
* Subscribe to 'settings.changed' events
|
||||
*/
|
||||
this.subscriptions.push(
|
||||
this.settingsProvider.settingsActionChanged$.subscribe(
|
||||
({type, payload}) => {
|
||||
if (type === 'stapps.settings.changed') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const {category, name, value} = payload!;
|
||||
this.logger.log(`received event "settings.changed" with category:
|
||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
||||
}
|
||||
},
|
||||
),
|
||||
this.dataRoutingService.itemSelectListener().subscribe(item => {
|
||||
if (this.itemRouting) {
|
||||
void this.router.navigate(['data-detail', item.uid]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.contextMenuService.setContextSort({
|
||||
name: 'sort',
|
||||
reversed: false,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{ 'inPlace' | propertyNameTranslate: item | titlecase }}
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ion-icon name="location"></ion-icon>
|
||||
<ion-icon name="location"> </ion-icon>
|
||||
<a [routerLink]="['/data-detail', item.inPlace.uid]">{{
|
||||
'name' | thingTranslate: item.inPlace
|
||||
}}</a>
|
||||
@@ -17,6 +17,10 @@
|
||||
[title]="'Duration'"
|
||||
[content]="[item.duration | amDuration: 'minutes']"
|
||||
></stapps-simple-card>
|
||||
<stapps-simple-card
|
||||
[title]="'Time'"
|
||||
[content]="[item.dates[0] | amDateFormat]"
|
||||
></stapps-simple-card>
|
||||
<stapps-simple-card
|
||||
*ngIf="item.performers"
|
||||
[title]="'performers' | propertyNameTranslate: item | titlecase"
|
||||
|
||||
@@ -22,8 +22,14 @@ import {DataListItemComponent} from '../../list/data-list-item.component';
|
||||
@Component({
|
||||
selector: 'stapps-date-series-list-item',
|
||||
templateUrl: 'date-series-list-item.html',
|
||||
styleUrls: ['date-series-list-item.scss'],
|
||||
})
|
||||
export class DateSeriesListItemComponent extends DataListItemComponent {
|
||||
/**
|
||||
* Compact view for schedule
|
||||
*/
|
||||
@Input() compact = false;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.remove-button {
|
||||
&:hover {
|
||||
--color-hover: var(--ion-color-danger);
|
||||
--border-color: var(--ion-color-danger);
|
||||
}
|
||||
--color: var(--ion-color-success);
|
||||
--border-color: var(--ion-color-success);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<ng-container *ngIf="item.type === 'academic event'">
|
||||
<stapps-add-event-action-chip [item]="item" style="margin: 2px">
|
||||
</stapps-add-event-action-chip>
|
||||
<stapps-simple-card
|
||||
*ngIf="item.categories"
|
||||
[title]="'Categories'"
|
||||
|
||||
Reference in New Issue
Block a user