Compare commits

...

1 Commits

Author SHA1 Message Date
9ef3527429 feat: data filter chips 2024-03-27 09:00:43 +00:00
9 changed files with 205 additions and 46 deletions

View File

@@ -107,6 +107,7 @@ import {SemesterListItemComponent} from './types/semester/semester-list-item.com
import {VideoDetailContentComponent} from './types/video/video-detail-content.component';
import {VideoListItemComponent} from './types/video/video-list-item.component';
import {ShareButtonComponent} from './elements/share-button.component';
import {DataFilterComponent} from './filter/data-filter.component';
/**
* Module for handling data
@@ -184,6 +185,7 @@ import {ShareButtonComponent} from './elements/share-button.component';
ShareButtonComponent,
],
imports: [
DataFilterComponent,
CommonModule,
DataRoutingModule,
FormsModule,

View File

@@ -0,0 +1,28 @@
import {AsyncPipe, CommonModule} from '@angular/common';
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {IonicModule} from '@ionic/angular';
import {IonIconModule} from 'src/app/util/ion-icon/ion-icon.module';
import {DataFilterProvider} from './data-filter.provider';
import {ThingTranslateModule} from 'src/app/translation/thing-translate.module';
import {PropertyValueTranslatePipe} from 'src/app/translation/property-value-translate.pipe';
@Component({
selector: 'stapps-data-filter',
templateUrl: 'data-filter.html',
styleUrls: ['data-filter.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
IonicModule,
IonIconModule,
AsyncPipe,
ThingTranslateModule,
PropertyValueTranslatePipe,
],
})
export class DataFilterComponent {
@Input() expandBucketCount = 3;
constructor(readonly filterProvider: DataFilterProvider) {}
}

View File

@@ -0,0 +1,75 @@
<ion-chip (click)="sortMenu.present($event)">
<ion-label>Relevance</ion-label>
<ion-icon name="expand_more"></ion-icon>
</ion-chip>
<ion-popover #sortMenu side="bottom" alignment="start">
<ng-template>
<ion-content class="ion-padding">
<ion-radio-group value="relevance">
<ion-radio value="relevance">Relevance</ion-radio>
<ion-radio value="name">Name</ion-radio>
<ion-radio value="type">Type</ion-radio>
</ion-radio-group>
</ion-content>
</ng-template>
</ion-popover>
<div class="separator"></div>
<ng-container *ngIf="filterProvider.context | async as context">
<ng-container *ngFor="let facet of context.facets">
<ng-container *ngIf="facet.buckets.length > 1">
<ng-template #facetLabel>
<ng-container *ngIf="facet.onlyOnType; else notOnlyOnType">
<strong>{{facet.onlyOnType | propertyValueTranslate: 'type': facet.onlyOnType | titlecase}}</strong>
/ {{facet.field | propertyNameTranslate: facet.onlyOnType | titlecase}}
</ng-container>
<ng-template #notOnlyOnType>
{{facet.field | propertyNameTranslate: $any('building') | titlecase}}
</ng-template>
</ng-template>
<ng-template #bucketLabel let-bucket>
{{bucket.key | propertyValueTranslate: facet.field: (facet.onlyOnType ?? $any(bucket.key)) |
titlecase}}
</ng-template>
<ng-container *ngIf="facet.buckets.length <= expandBucketCount; else expandableFacet">
<div class="separator"></div>
<div class="expanded-facet">
<div class="buckets">
<ion-chip *ngFor="let bucket of facet.buckets" [outline]="true">
<ion-label>
<ng-container *ngTemplateOutlet="bucketLabel; context: {$implicit: bucket}"></ng-container>
</ion-label>
</ion-chip>
</div>
<div class="facet-label">
<ng-container *ngTemplateOutlet="facetLabel"></ng-container>
</div>
</div>
<div class="separator"></div>
</ng-container>
<ng-template #expandableFacet>
<ion-chip (click)="filterMenu.present($event)" [outline]="true">
<ion-label>
<ng-container *ngTemplateOutlet="facetLabel"></ng-container>
</ion-label>
<ion-icon name="expand_more"></ion-icon>
</ion-chip>
<ion-popover #filterMenu side="bottom" alignment="start">
<ng-template>
<ion-content>
<ion-list>
<ion-item *ngFor="let bucket of facet.buckets">
<ion-checkbox slot="start"></ion-checkbox>
<ion-label>{{bucket.key}}</ion-label>
<ion-note slot="end">{{bucket.count}}</ion-note>
</ion-item>
</ion-list>
</ion-content>
</ng-template>
</ion-popover>
</ng-template>
</ng-container>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,12 @@
import {Injectable} from '@angular/core';
import {SCSearchResult} from '@openstapps/core';
import {BehaviorSubject} from 'rxjs';
@Injectable()
export class DataFilterProvider {
readonly context = new BehaviorSubject<SCSearchResult | undefined>(undefined);
readonly userSortOption = new BehaviorSubject<string | undefined>(undefined);
readonly userFilterOption = new BehaviorSubject(new Map<string, string | number>());
}

View File

@@ -0,0 +1,36 @@
:host {
overflow-x: auto;
display: flex;
align-items: flex-start;
margin-block: var(--spacing-xs);
> * {
flex-shrink: 0;
}
}
.expanded-facet {
display: flex;
flex-direction: column;
> .facet-label {
margin-inline: var(--spacing-md);
font-size: 0.7em;
}
}
.separator {
align-self: center;
width: 1px;
height: 1.25em;
margin-inline: var(--spacing-xs);
opacity: 0.2;
background: currentcolor;
&:last-child,
+ .separator {
display: none;
}
}

View File

@@ -12,7 +12,7 @@
* 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, DestroyRef, inject, Input, OnInit} from '@angular/core';
import {Component, DestroyRef, Inject, inject, Input, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Keyboard} from '@capacitor/keyboard';
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
@@ -36,6 +36,7 @@ import {PositionService} from '../../map/position.service';
import {ConfigProvider} from '../../config/config.provider';
import {searchPageSwitchAnimation} from './search-page-switch-animation';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {DataFilterProvider} from '../filter/data-filter.provider';
/**
* SearchPageComponent queries things and shows list of things as search results and filter as context menu
@@ -44,7 +45,7 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
selector: 'stapps-search-page',
templateUrl: 'search-page.html',
styleUrls: ['search-page.scss'],
providers: [ContextMenuService],
providers: [ContextMenuService, DataFilterProvider],
})
export class SearchPageComponent implements OnInit {
@Input() title = 'search.title';
@@ -144,6 +145,8 @@ export class SearchPageComponent implements OnInit {
destroy$ = inject(DestroyRef);
dataFilterProvider = inject(DataFilterProvider);
routeAnimation: AnimationBuilder;
/**
@@ -216,6 +219,7 @@ export class SearchPageComponent implements OnInit {
try {
const result = await this.dataProvider.search(searchOptions);
this.dataFilterProvider.context.next(result);
this.singleTypeResponse = result.facets.find(facet => facet.field === 'type')?.buckets.length === 1;
if (append) {
// append results

View File

@@ -16,62 +16,41 @@
<stapps-context contentId="data-list"></stapps-context>
<ion-header>
@if (showDrawer && showTopToolbar) {
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start">
<ion-back-button [defaultHref]="backUrl"></ion-back-button>
</ion-buttons>
<ion-title>{{ title | translate }}</ion-title>
</ion-toolbar>
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start">
<ion-back-button [defaultHref]="backUrl"></ion-back-button>
</ion-buttons>
<ion-title>{{ title | translate }}</ion-title>
</ion-toolbar>
}
<ion-toolbar color="primary">
<ion-searchbar
(ngModelChange)="searchStringChanged($event)"
(keyup.enter)="hideKeyboard()"
(search)="hideKeyboard()"
[(ngModel)]="queryText"
showClearButton="always"
placeholder="{{ placeholder | translate }}"
mode="md"
type="search"
enterkeyhint="search"
class="filterable"
[autofocus]="!showDefaultData"
>
<ion-searchbar (ngModelChange)="searchStringChanged($event)" (keyup.enter)="hideKeyboard()"
(search)="hideKeyboard()" [(ngModel)]="queryText" showClearButton="always"
placeholder="{{ placeholder | translate }}" mode="md" type="search" enterkeyhint="search" class="filterable"
[autofocus]="!showDefaultData">
<ion-menu-button menu="context" auto-hide="false">
<ion-icon name="tune"></ion-icon>
</ion-menu-button>
</ion-searchbar>
</ion-toolbar>
@if (showNavigation && isHebisAvailable) {
<ion-toolbar color="primary" class="category-tab">
<ion-buttons class="ion-justify-content-between">
<ion-button class="button-active" size="large">{{ 'search.type' | translate }}</ion-button>
<ion-button
[routerLink]="['/hebis-search']"
queryParamsHandling="merge"
[routerAnimation]="routeAnimation"
fill="outline"
size="large"
>{{ 'hebisSearch.type' | translate }}
</ion-button>
</ion-buttons>
</ion-toolbar>
<ion-toolbar color="primary" class="category-tab">
<ion-buttons class="ion-justify-content-between">
<ion-button class="button-active" size="large">{{ 'search.type' | translate }}</ion-button>
<ion-button [routerLink]="['/hebis-search']" queryParamsHandling="merge" [routerAnimation]="routeAnimation"
fill="outline" size="large">{{ 'hebisSearch.type' | translate }}
</ion-button>
</ion-buttons>
</ion-toolbar>
}
</ion-header>
<ion-content class="content">
<div
[class.no-results]="!showDefaultData && !items && !loading"
[style.display]="!showDefaultData && !items && !loading ? 'block' : 'none'"
>
<div [class.no-results]="!showDefaultData && !items && !loading"
[style.display]="!showDefaultData && !items && !loading ? 'block' : 'none'">
<ion-label class="centered-message-container"> {{ searchInstruction | translate }} </ion-label>
</div>
<stapps-data-list
id="data-list"
[items]="items | async"
[singleType]="singleTypeResponse"
(loadmore)="loadMore()"
[resetToTop]="queryChanged.asObservable()"
[loading]="loading"
></stapps-data-list>
<stapps-data-filter></stapps-data-filter>
<stapps-data-list id="data-list" [items]="items | async" [singleType]="singleTypeResponse" (loadmore)="loadMore()"
[resetToTop]="queryChanged.asObservable()" [loading]="loading"></stapps-data-list>
</ion-content>

View File

@@ -0,0 +1,19 @@
import {Pipe, PipeTransform} from '@angular/core';
import {ThingTranslateService} from './thing-translate.service';
import {SCThingType, SCThings} from '@openstapps/core';
@Pipe({
name: 'propertyValueTranslate',
standalone: true,
})
export class PropertyValueTranslatePipe implements PipeTransform {
constructor(private readonly thingTranslate: ThingTranslateService) {}
transform<
K extends string | undefined,
T extends SCThingType,
U extends keyof Extract<SCThings, {type: T}>,
>(value: K, field: (U & string) | string, type: T): K {
return this.thingTranslate.getPropertyValue(type, field, value) as K;
}
}

View File

@@ -113,4 +113,8 @@ export class ThingTranslateService {
return this.getParsedResult(translatedPropertyNames, keyPath);
}
public getPropertyValue(type: SCThingType, field: string, key: string | undefined): string | undefined {
return this.translator.translatedPropertyValue(type, field, key);
}
}