diff --git a/src/app/modules/data/chips/filter/chip-filter.component.html b/src/app/modules/data/chips/filter/chip-filter.component.html new file mode 100644 index 00000000..e76027f7 --- /dev/null +++ b/src/app/modules/data/chips/filter/chip-filter.component.html @@ -0,0 +1,7 @@ + + + {{ displayValue }} + diff --git a/src/app/modules/data/chips/filter/chip-filter.component.scss b/src/app/modules/data/chips/filter/chip-filter.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/modules/data/chips/filter/chip-filter.component.ts b/src/app/modules/data/chips/filter/chip-filter.component.ts new file mode 100644 index 00000000..5c8a258f --- /dev/null +++ b/src/app/modules/data/chips/filter/chip-filter.component.ts @@ -0,0 +1,51 @@ +/* + * 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 . + */ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +/** + * Shows a chip filter + */ +@Component({ + selector: 'stapps-chip-filter', + templateUrl: './chip-filter.component.html', + styleUrls: ['./chip-filter.component.scss'], +}) +export class ChipFilterComponent { + /** + * If the chip (filter) is active + */ + @Input() active: boolean; + + /** + * Text to display on the chip + */ + @Input() displayValue: string; + + /** + * Emits when the chip has been activated/deactivated + */ + @Output() toggle = new EventEmitter(); + + /** + * Value to emit when chip has been activated/deactivated + */ + @Input() value: unknown; + + /** + * Signalize that the chip filter has been activated/deactivated + */ + emitToggle(value: unknown) { + this.toggle.emit(value); + } +} diff --git a/src/app/modules/data/data.provider.ts b/src/app/modules/data/data.provider.ts index 96aa5c65..4c5b8c1d 100644 --- a/src/app/modules/data/data.provider.ts +++ b/src/app/modules/data/data.provider.ts @@ -19,8 +19,10 @@ import { SCMultiSearchResponse, SCSearchRequest, SCSearchResponse, + SCSearchValueFilter, SCThingOriginType, SCThings, + SCThingsField, SCThingType, } from '@openstapps/core'; import {SCSaveableThing} from '@openstapps/core'; @@ -88,6 +90,25 @@ export class DataProvider { */ storageProvider: StorageProvider; + /** + * Simplify creation of a value filter + * + * @param field Database field for apply the filter to + * @param value Value to match with + */ + static createValueFilter( + field: SCThingsField, + value: string, + ): SCSearchValueFilter { + return { + type: 'value', + arguments: { + field: field, + value: value, + }, + }; + } + /** * TODO * diff --git a/src/app/modules/data/detail/data-detail-content.component.ts b/src/app/modules/data/detail/data-detail-content.component.ts index 6dc7d06f..7cd96354 100644 --- a/src/app/modules/data/detail/data-detail-content.component.ts +++ b/src/app/modules/data/detail/data-detail-content.component.ts @@ -20,6 +20,7 @@ import {SCThings} from '@openstapps/core'; */ @Component({ selector: 'stapps-data-detail-content', + styleUrls: ['data-detail-content.scss'], templateUrl: 'data-detail-content.html', }) export class DataDetailContentComponent { diff --git a/src/app/modules/data/detail/data-detail-content.html b/src/app/modules/data/detail/data-detail-content.html index f0e4f9b9..81e3a896 100644 --- a/src/app/modules/data/detail/data-detail-content.html +++ b/src/app/modules/data/detail/data-detail-content.html @@ -6,8 +6,8 @@
-

{{ item.name }}

- {{ item.type }} +

{{ 'name' | thingTranslate: item }}

+ {{ 'type' | thingTranslate: item }}
diff --git a/src/app/modules/data/detail/data-detail-content.scss b/src/app/modules/data/detail/data-detail-content.scss new file mode 100644 index 00000000..64dd7289 --- /dev/null +++ b/src/app/modules/data/detail/data-detail-content.scss @@ -0,0 +1,18 @@ +:host ::ng-deep { + ion-slides.work-locations { + ion-slide { + display: block; + text-align: left; + } + } + ion-card { + ion-card-header { + font-weight: bold; + } + ion-grid, ion-col { + padding-inline-start: 0; + padding-top: 0; + padding-bottom: 0; + } + } +} diff --git a/src/app/modules/data/detail/data-detail.scss b/src/app/modules/data/detail/data-detail.scss index 8be59279..b862d204 100644 --- a/src/app/modules/data/detail/data-detail.scss +++ b/src/app/modules/data/detail/data-detail.scss @@ -12,11 +12,3 @@ } } } - -::ng-deep { - ion-grid, ion-col { - padding-inline-start: 0; - padding-top: 0; - padding-bottom: 0; - } -} diff --git a/src/app/modules/data/elements/skeleton-list-item.scss b/src/app/modules/data/elements/skeleton-list-item.scss index 6d7dd065..e69de29b 100644 --- a/src/app/modules/data/elements/skeleton-list-item.scss +++ b/src/app/modules/data/elements/skeleton-list-item.scss @@ -1,7 +0,0 @@ -::ng-deep { - ion-grid, ion-col { - padding-inline-start: 0; - padding-top: 0; - padding-bottom: 0; - } -} diff --git a/src/app/modules/data/list/data-list-item.scss b/src/app/modules/data/list/data-list-item.scss index 61c0234b..a0be2a73 100644 --- a/src/app/modules/data/list/data-list-item.scss +++ b/src/app/modules/data/list/data-list-item.scss @@ -1,4 +1,4 @@ -.item { +:host ::ng-deep { ion-label { width: 100%; @@ -7,25 +7,20 @@ flex-direction: column; } } -} + ::ng-deep { + ion-note { + ul { + margin: 0; + padding: 0; -::ng-deep { - ion-grid, ion-col { - padding-inline-start: 0 ; - padding-top: 0; - padding-bottom: 0; - } + li { + list-style-type: none; + display: inline; + } - ion-note { - ul { - margin: 0; - padding: 0; - li { - list-style-type: none; - display: inline; - } - li:not(:first-child):before { - content: " • "; + li:not(:first-child):before { + content: " • "; + } } } } diff --git a/src/app/modules/map/item/map-item.component.html b/src/app/modules/map/item/map-item.component.html index 9311e93b..e970c2a0 100644 --- a/src/app/modules/map/item/map-item.component.html +++ b/src/app/modules/map/item/map-item.component.html @@ -1,4 +1,4 @@ - + + + + diff --git a/src/app/modules/news/elements/news-filter-settings/news-settings-filter.component.scss b/src/app/modules/news/elements/news-filter-settings/news-settings-filter.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/modules/news/elements/news-filter-settings/news-settings-filter.component.ts b/src/app/modules/news/elements/news-filter-settings/news-settings-filter.component.ts new file mode 100644 index 00000000..8266a355 --- /dev/null +++ b/src/app/modules/news/elements/news-filter-settings/news-settings-filter.component.ts @@ -0,0 +1,85 @@ +/* + * 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 . + */ +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import { + newsFilterSettingsFieldsMapping, + NewsFilterSettingsNames, +} from '../../news-filter-settings'; +import {SCSearchValueFilter, SCSetting} from '@openstapps/core'; +import {DataProvider} from '../../../data/data.provider'; + +@Component({ + selector: 'stapps-news-settings-filter', + templateUrl: './news-settings-filter.component.html', + styleUrls: ['./news-settings-filter.component.scss'], +}) +export class NewsSettingsFilterComponent implements OnInit { + /** + * A map of the filters where the keys are settings names + */ + filtersMap = new Map(); + + /** + * Emits the current filters + */ + @Output() filtersChanged = new EventEmitter(); + + /** + * Provided settings to show the filters for + */ + @Input() settings: SCSetting[]; + + ngOnInit() { + for (const setting of this.settings) { + this.filtersMap.set( + setting.name as NewsFilterSettingsNames, + DataProvider.createValueFilter( + newsFilterSettingsFieldsMapping[ + setting.name as NewsFilterSettingsNames + ], + setting.value as string, + ), + ); + } + + this.filtersChanged.emit([...this.filtersMap.values()]); + } + + /** + * To be executed when a chip filter has been enabled/disabled + * + * @param setting The value of the filter + */ + stateChanged(setting: SCSetting) { + if ( + typeof this.filtersMap.get(setting.name as NewsFilterSettingsNames) !== + 'undefined' + ) { + this.filtersMap.delete(setting.name as NewsFilterSettingsNames); + } else { + this.filtersMap.set( + setting.name as NewsFilterSettingsNames, + DataProvider.createValueFilter( + newsFilterSettingsFieldsMapping[ + setting.name as NewsFilterSettingsNames + ], + setting.value as string, + ), + ); + } + + this.filtersChanged.emit([...this.filtersMap.values()]); + } +} diff --git a/src/app/modules/news/page/news-item.component.ts b/src/app/modules/news/item/news-item.component.ts similarity index 100% rename from src/app/modules/news/page/news-item.component.ts rename to src/app/modules/news/item/news-item.component.ts diff --git a/src/app/modules/news/page/news-item.html b/src/app/modules/news/item/news-item.html similarity index 100% rename from src/app/modules/news/page/news-item.html rename to src/app/modules/news/item/news-item.html diff --git a/src/app/modules/news/page/news-item.scss b/src/app/modules/news/item/news-item.scss similarity index 100% rename from src/app/modules/news/page/news-item.scss rename to src/app/modules/news/item/news-item.scss diff --git a/src/app/modules/news/page/skeleton-news-item.component.ts b/src/app/modules/news/item/skeleton-news-item.component.ts similarity index 100% rename from src/app/modules/news/page/skeleton-news-item.component.ts rename to src/app/modules/news/item/skeleton-news-item.component.ts diff --git a/src/app/modules/news/page/skeleton-news-item.html b/src/app/modules/news/item/skeleton-news-item.html similarity index 100% rename from src/app/modules/news/page/skeleton-news-item.html rename to src/app/modules/news/item/skeleton-news-item.html diff --git a/src/app/modules/news/news-filter-settings.ts b/src/app/modules/news/news-filter-settings.ts new file mode 100644 index 00000000..8860344b --- /dev/null +++ b/src/app/modules/news/news-filter-settings.ts @@ -0,0 +1,32 @@ +/* + * 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 . + */ +import {SCSettingCategories, SCThingsField} from '@openstapps/core'; +/** + * Category of settings to use for news filter + */ +export const newsFilterSettingsCategory: SCSettingCategories = 'profile'; +/** + * Settings to use for news filter + */ +export type NewsFilterSettingsNames = 'language' | 'group'; +/** + * The mapping between settings and corresponding data fields for building a value filter + */ +export const newsFilterSettingsFieldsMapping: { + [key in NewsFilterSettingsNames]: SCThingsField; +} = { + language: 'inLanguage', + group: 'audiences', +}; diff --git a/src/app/modules/news/news.module.ts b/src/app/modules/news/news.module.ts index ecfed80f..d32be81b 100644 --- a/src/app/modules/news/news.module.ts +++ b/src/app/modules/news/news.module.ts @@ -18,11 +18,15 @@ import {RouterModule, Routes} from '@angular/router'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; import {MomentModule} from 'ngx-moment'; +import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {DataModule} from '../data/data.module'; import {SettingsProvider} from '../settings/settings.provider'; -import {NewsItemComponent} from './page/news-item.component'; +import {NewsItemComponent} from './item/news-item.component'; import {NewsPageComponent} from './page/news-page.component'; -import {SkeletonNewsItemComponent} from './page/skeleton-news-item.component'; +import {SkeletonNewsItemComponent} from './item/skeleton-news-item.component'; +import {ChipFilterComponent} from '../data/chips/filter/chip-filter.component'; +import {SettingsModule} from '../settings/settings.module'; +import {NewsSettingsFilterComponent} from './elements/news-filter-settings/news-settings-filter.component'; const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}]; @@ -31,17 +35,21 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}]; */ @NgModule({ declarations: [ - NewsItemComponent, NewsPageComponent, SkeletonNewsItemComponent, + NewsItemComponent, + ChipFilterComponent, + NewsSettingsFilterComponent, ], imports: [ - CommonModule, - DataModule, IonicModule.forRoot(), - MomentModule, - RouterModule.forChild(newsRoutes), TranslateModule.forChild(), + RouterModule.forChild(newsRoutes), + CommonModule, + MomentModule, + DataModule, + ThingTranslateModule, + SettingsModule, ], providers: [SettingsProvider], }) diff --git a/src/app/modules/news/news.provider.ts b/src/app/modules/news/news.provider.ts index 19e09bd1..37a9cb38 100644 --- a/src/app/modules/news/news.provider.ts +++ b/src/app/modules/news/news.provider.ts @@ -13,7 +13,13 @@ * this program. If not, see . */ import {Injectable} from '@angular/core'; -import {SCMessage} from '@openstapps/core'; +import { + SCBooleanFilterArguments, + SCMessage, + SCSearchBooleanFilter, + SCSearchFilter, + SCSearchQuery, +} from '@openstapps/core'; import {DataProvider} from '../data/data.provider'; /** * Service for providing news messages @@ -22,34 +28,59 @@ import {DataProvider} from '../data/data.provider'; providedIn: 'root', }) export class NewsProvider { - constructor(private dataProvider: DataProvider) { - } + constructor(private dataProvider: DataProvider) {} /** * Get news messages * * @param size How many messages/news to fetch * @param from From which (results) page to start + * @param filters Additional filters to apply */ - async getList(size: number, from: number): Promise { - const result = await this.dataProvider.search({ + async getList( + size: number, + from: number, + filters?: SCSearchFilter[], + ): Promise { + const query: SCSearchQuery = { filter: { - type: 'value', + type: 'boolean', arguments: { - field: 'type', - value: 'message', + filters: [ + { + type: 'value', + arguments: { + field: 'type', + value: 'message', + }, + }, + ], + operation: 'and', }, }, - sort: [{ - type: 'generic', - arguments: { - field: 'datePublished', + sort: [ + { + type: 'generic', + arguments: { + field: 'datePublished', + }, + order: 'desc', }, - order: 'desc', - }], + ], size: size, from: from, - }); + }; + + if (typeof filters !== 'undefined') { + for (const filter of filters) { + ( + (query.filter as SCSearchBooleanFilter) + .arguments as SCBooleanFilterArguments + ).filters.push(filter); + } + } + + const result = await this.dataProvider.search(query); return result.data as SCMessage[]; } diff --git a/src/app/modules/news/page/news-page.component.ts b/src/app/modules/news/page/news-page.component.ts index 59e2609f..f3c6b022 100644 --- a/src/app/modules/news/page/news-page.component.ts +++ b/src/app/modules/news/page/news-page.component.ts @@ -12,9 +12,20 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {IonRefresher} from '@ionic/angular'; -import {SCMessage} from '@openstapps/core'; +import { + SCMessage, + SCSearchFilter, + SCSearchValueFilter, + SCSetting, +} from '@openstapps/core'; +import {SettingsProvider} from '../../settings/settings.provider'; +import { + newsFilterSettingsCategory, + newsFilterSettingsFieldsMapping, + NewsFilterSettingsNames, +} from '../news-filter-settings'; import {NewsProvider} from '../news.provider'; /** * News page component @@ -23,46 +34,86 @@ import {NewsProvider} from '../news.provider'; selector: 'stapps-news-page', templateUrl: 'news-page.html', }) -export class NewsPageComponent { +export class NewsPageComponent implements OnInit { /** * Thing counter to start query the next page from */ from = 0; - /** - * Page size of queries - */ - pageSize = 10; + /** * News (messages) to show */ news: SCMessage[] = []; - constructor(private newsProvider: NewsProvider) {} + /** + * Page size of queries + */ + pageSize = 10; + + /** + * Relevant settings + */ + settings: SCSetting[]; + + /** + * Active filters + */ + filters: SCSearchFilter[]; + + constructor( + private newsProvider: NewsProvider, + private settingsProvider: SettingsProvider, + ) {} /** * Fetch news from the backend */ async fetchNews() { - this.news = await this.newsProvider.getList(this.pageSize, this.from); + this.from = 0; + this.news = await this.newsProvider.getList(this.pageSize, this.from, [ + ...this.filters, + ]); } /** * Loads more news * - * @param event Signal from the infinite scroll to load more data + * @param infiniteScrollElement Infinite scroll element */ - // tslint:disable-next-line:no-any - async loadMore(event: any): Promise { + async loadMore( + infiniteScrollElement: HTMLIonInfiniteScrollElement, + ): Promise { this.from += this.pageSize; - this.news = this.news.concat(await this.newsProvider.getList(this.pageSize, this.from)); - event.target.complete(); + this.news = [ + ...this.news, + ...(await this.newsProvider.getList(this.pageSize, this.from, [ + ...this.filters, + ])), + ]; + await infiniteScrollElement.complete(); } /** * Initialize the local variables on component initialization */ - ngOnInit() { - void this.fetchNews(); + async ngOnInit() { + // Helper method to provide the relevant settings + const getNewsSettings = async (settingNames: NewsFilterSettingsNames[]) => { + const settings = []; + for (const settingName of settingNames) { + settings.push( + await this.settingsProvider.getSetting( + newsFilterSettingsCategory, + settingName, + ), + ); + } + return settings; + }; + + this.settings = await getNewsSettings( + Object.keys(newsFilterSettingsFieldsMapping) as NewsFilterSettingsNames[], + ); } /** @@ -72,12 +123,21 @@ export class NewsPageComponent { */ async refresh(refresher: IonRefresher) { try { - this.from = 0; await this.fetchNews(); - } catch (e) { + } catch { this.news = []; } finally { await refresher.complete(); } } + + /** + * Executed when filters have been changed + * + * @param filters Current filters to be used + */ + toggleFilter(filters: SCSearchValueFilter[]) { + this.filters = filters; + void this.fetchNews(); + } } diff --git a/src/app/modules/news/page/news-page.html b/src/app/modules/news/page/news-page.html index 86eaadf8..38f1e7df 100644 --- a/src/app/modules/news/page/news-page.html +++ b/src/app/modules/news/page/news-page.html @@ -4,19 +4,31 @@ - {{'news.title' | translate}} + {{ 'news.title' | translate }} - + - + + + + + + @@ -27,7 +39,14 @@ - + + {{ 'search.nothing_found' | translate | titlecase }} + + diff --git a/src/app/modules/settings/setting-translate.pipe.ts b/src/app/modules/settings/setting-translate.pipe.ts new file mode 100644 index 00000000..81db28b2 --- /dev/null +++ b/src/app/modules/settings/setting-translate.pipe.ts @@ -0,0 +1,35 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import {SCSetting} from '@openstapps/core'; +import {ThingTranslatePipe} from '../../translation/thing-translate.pipe'; +import {ThingTranslateService} from '../../translation/thing-translate.service'; + +/** + * Translates a setting value (into the display value in current language) + */ +@Pipe({ + name: 'settingValueTranslate', +}) +export class SettingTranslatePipe implements PipeTransform { + constructor( + private readonly translate: TranslateService, + private readonly thingTranslate: ThingTranslateService, + ) {} + + transform(setting: SCSetting) { + const thingTranslatePipe = new ThingTranslatePipe( + this.translate, + this.thingTranslate, + ); + const translatedSettingValues = thingTranslatePipe.transform( + 'values', + setting, + ); + + return translatedSettingValues + ? translatedSettingValues[ + setting.values?.indexOf(setting.value as string) as number + ] + : undefined; + } +} diff --git a/src/app/modules/settings/settings.module.ts b/src/app/modules/settings/settings.module.ts index 3f955911..18ef85b7 100644 --- a/src/app/modules/settings/settings.module.ts +++ b/src/app/modules/settings/settings.module.ts @@ -23,6 +23,7 @@ import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {ConfigProvider} from '../config/config.provider'; import {SettingsItemComponent} from './item/settings-item.component'; import {SettingsPageComponent} from './page/settings-page.component'; +import {SettingTranslatePipe} from './setting-translate.pipe'; import {SettingsProvider} from './settings.provider'; const settingsRoutes: Routes = [ @@ -33,15 +34,19 @@ const settingsRoutes: Routes = [ * Settings Module */ @NgModule({ - declarations: [SettingsPageComponent, SettingsItemComponent], - exports: [SettingsItemComponent], + declarations: [ + SettingsPageComponent, + SettingsItemComponent, + SettingTranslatePipe, + ], + exports: [SettingsItemComponent, SettingTranslatePipe], imports: [ CommonModule, FormsModule, IonicModule.forRoot(), - RouterModule.forChild(settingsRoutes), - ThingTranslateModule.forChild(), TranslateModule.forChild(), + ThingTranslateModule.forChild(), + RouterModule.forChild(settingsRoutes), ], providers: [ConfigProvider, SettingsProvider], }) diff --git a/src/global.scss b/src/global.scss index 7421879a..e35371f7 100644 --- a/src/global.scss +++ b/src/global.scss @@ -32,6 +32,14 @@ ion-item { } } +ion-item, ion-card.compact { + ion-grid, ion-col { + padding-inline-start: 0; + padding-top: 0; + padding-bottom: 0; + } +} + .notFoundContainer { display: flex; flex-direction: column;