From 7a525930db9fbf68757814472230a164381b2f2d Mon Sep 17 00:00:00 2001 From: James Fusia Date: Tue, 10 Sep 2024 21:58:24 -0400 Subject: [PATCH] Added sorting, started on paging --- src/components/metadata/metadata.component.ts | 4 +- src/components/query/query.component.ts | 205 +++++++++++++----- src/components/result/result.component.html | 6 +- src/components/result/result.component.ts | 44 ++-- src/components/table/table.component.html | 69 +++++- src/components/table/table.component.scss | 61 ++++++ src/components/table/table.component.ts | 34 ++- src/enums/milestone.ts | 2 + src/enums/sort.ts | 5 + src/models/header.ts | 4 + src/models/page.ts | 10 +- src/models/query.ts | 16 +- src/models/result.ts | 16 ++ src/models/sort.ts | 19 ++ src/services/data.service.ts | 33 ++- src/services/execute.service.ts | 109 ++++++---- src/services/meta.service.ts | 73 ++++--- src/services/query.service.ts | 25 ++- 18 files changed, 558 insertions(+), 177 deletions(-) create mode 100644 src/enums/sort.ts create mode 100644 src/models/result.ts create mode 100644 src/models/sort.ts diff --git a/src/components/metadata/metadata.component.ts b/src/components/metadata/metadata.component.ts index 865b98f..095260e 100644 --- a/src/components/metadata/metadata.component.ts +++ b/src/components/metadata/metadata.component.ts @@ -20,8 +20,8 @@ export class MetadataComponent { constructor(metaService: MetaService, private queryService: QueryService) { combineLatest({ - meta: metaService.Data, - query: queryService.Query, + meta: metaService.Data$, + query: queryService.Query$, }).subscribe((d: { meta: Partial[]; query: Query }) => { const inuse = d.query.fields; const expanded = this.getExpanded(this.node); diff --git a/src/components/query/query.component.ts b/src/components/query/query.component.ts index e2d667e..58fbf06 100644 --- a/src/components/query/query.component.ts +++ b/src/components/query/query.component.ts @@ -8,9 +8,14 @@ import { faArrowDown, faArrowUp, faRemove, + faSort, + faSortAsc, + faSortDesc, + faSortDown, } from '@fortawesome/free-solid-svg-icons'; import { ExecuteService } from '../../services/execute.service'; import { Header } from '../../models/header'; +import { SORT } from '../../enums/sort'; @Component({ selector: 'fbi-query', @@ -26,54 +31,34 @@ export class QueryComponent { private queryService: QueryService, executeService: ExecuteService ) { - queryService.Query.subscribe((query: Query) => { - executeService.headers(query).subscribe((headers: Header[]) => { - this.node = new TreeNode({ - hidden: true, - expanded: true, - children: [ - { - label: 'Fields', - leaf: false, - expanded: true, - children: (query?.fields ?? []).map( - (field: string, index: number, array: string[]) => { - const actions = []; - if (index !== 0) { - actions.push({ - label: 'Move Up', - icon: faArrowUp, - data: { cmd: ACTIONS.UP, data: index - 1 }, - }); - } - if (index !== array.length - 1) { - actions.push({ - label: 'Move Down', - icon: faArrowDown, - data: { cmd: ACTIONS.DOWN, data: index + 1 }, - }); - } - actions.push({ - label: 'Remove', - icon: faRemove, - data: { cmd: ACTIONS.REMOVE }, - }); - - const label = - headers.find((header: Header) => header.source === field) - ?.label ?? 'unknown'; - - return { - label: label, - data: field, - actions: actions, - }; - } - ), - }, - { label: 'Filters', expanded: true, leaf: false }, - ] as TreeNode[], - }); + queryService.Query$.subscribe((query: Query) => { + const headers = executeService.headers(query); + const fields = this.getFields(query, headers); + const filters = this.getFilters(query, headers); + const sort = this.getSorts(query, headers); + this.node = new TreeNode({ + hidden: true, + expanded: true, + children: [ + { + label: 'Fields', + leaf: false, + expanded: true, + children: fields, + }, + { + label: 'Filters', + expanded: true, + leaf: false, + children: filters, + }, + { + label: 'Sort', + expanded: true, + leaf: false, + children: sort, + }, + ] as TreeNode[], }); }); } @@ -85,19 +70,139 @@ export class QueryComponent { this.queryService.remove(event.node.data); break; case ACTIONS.UP: - this.queryService.add(event.node.data, data.data); - break; case ACTIONS.DOWN: this.queryService.add(event.node.data, data.data); break; + case ACTIONS.SORTASC: + case ACTIONS.SORTDSC: + case ACTIONS.SORTDEL: + this.queryService.sort(data.data); + break; } } + + private getFields(query: Query, headers: Header[]): Partial[] { + return (query?.fields ?? []).map( + (field: string, index: number, array: string[]) => { + const sort = query?.sort?.field ?? ''; + const sdir = query?.sort?.dir ?? SORT.NONE; + + const actions = []; + // move + if (index !== 0) { + actions.push({ + label: 'Move Up', + icon: faArrowUp, + data: { cmd: ACTIONS.UP, data: index - 1 }, + }); + } + if (index !== array.length - 1) { + actions.push({ + label: 'Move Down', + icon: faArrowDown, + data: { cmd: ACTIONS.DOWN, data: index + 1 }, + }); + } + + // sort + if (sort === field) { + switch (sdir) { + case SORT.ASC: + actions.push({ + label: 'Sort Dsc', + icon: faSortDown, + data: { cmd: ACTIONS.SORTDSC, data: field }, + }); + break; + case SORT.DSC: + actions.push({ + label: 'Clear Sort', + icon: faSort, + data: { cmd: ACTIONS.SORTDEL, data: field }, + }); + break; + default: + actions.push({ + label: 'Sort Asc', + icon: faSortAsc, + data: { cmd: ACTIONS.SORTASC, data: field }, + }); + break; + } + } else { + actions.push({ + label: 'Sort Asc', + icon: faSortAsc, + data: { cmd: ACTIONS.SORTASC, data: field }, + }); + } + + // remove + actions.push({ + label: 'Remove', + icon: faRemove, + data: { cmd: ACTIONS.REMOVE }, + }); + + const label = + headers.find((header: Header) => header.source === field)?.label ?? + 'unknown'; + + return { + label: label, + data: field, + actions: actions, + }; + } + ); + } + + private getFilters(query: Query, headers: Header[]): Partial[] { + return []; + } + + private getSorts(query: Query, headers: Header[]): Partial[] { + const result: Partial[] = []; + + if (query?.sort?.isValid?.()) { + const s = query.sort; + const label = + headers.find((header: Header) => header.source === s.field)?.label ?? + 'unknown'; + const actions = []; + + if (s.dir === SORT.ASC) { + actions.push({ + label: 'Sort Dsc', + icon: faSortDesc, + data: { cmd: ACTIONS.SORTDSC, data: s.field }, + }); + } else if (s.dir === SORT.DSC) { + actions.push({ + label: 'Clear Sort', + icon: faSort, + data: { cmd: ACTIONS.SORTDEL, data: s.field }, + }); + } + + result.push({ + label: label, + data: s.field, + actions: actions, + }); + } + + return result; + } } enum ACTIONS { REMOVE = 'del', UP = 'up', DOWN = 'down', + SORTDSC = 'dsc', + SORTASC = 'asc', + SORTDEL = 'nne', } type ActionData = { diff --git a/src/components/result/result.component.html b/src/components/result/result.component.html index 86c8134..9505214 100644 --- a/src/components/result/result.component.html +++ b/src/components/result/result.component.html @@ -1 +1,5 @@ - + diff --git a/src/components/result/result.component.ts b/src/components/result/result.component.ts index 05b1d0e..6210dea 100644 --- a/src/components/result/result.component.ts +++ b/src/components/result/result.component.ts @@ -4,9 +4,10 @@ import { TreeComponent } from '../tree/tree.component'; import { QueryService } from '../../services/query.service'; import { Query } from '../../models/query'; import { ExecuteService } from '../../services/execute.service'; -import { forkJoin } from 'rxjs'; -import { Header } from '../../models/header'; import { TableComponent } from '../table/table.component'; +import { Result } from '../../models/result'; +import { Header } from '../../models/header'; +import { Page } from '../../models/page'; @Component({ selector: 'fbi-result', @@ -16,34 +17,43 @@ import { TableComponent } from '../table/table.component'; styleUrl: './result.component.scss', }) export class ResultComponent { - headers: Header[] = []; - rows: Record[] = []; + data: Result = new Result({}); + + private query: Query = new Query({}); + private page: Page = new Page({}); constructor( - queryService: QueryService, + private queryService: QueryService, private executeService: ExecuteService ) { let last: string = ''; - queryService.Query.subscribe((query: Query) => { + queryService.Query$.subscribe((query: Query) => { if (query.isValid()) { const current = query.toString(); if (last !== current) { - this.load(query); + this.query = query; + this.page = new Page({}); + this.load(); last = current; } } }); } - private load(query: Query): void { - forkJoin({ - headers: this.executeService.headers(query), - data: this.executeService.data(query), - }).subscribe( - (result: { headers: Header[]; data: Record[] }) => { - this.headers = result.headers; - this.rows = result.data; - } - ); + private load(): void { + const query = new Query(this.query); + query.page = new Page(this.page); + this.executeService.data(query).subscribe((result: Result) => { + this.data = result; + }); + } + + onPage(page: Page): void { + this.page = new Page(page); + this.load(); + } + + onSort(header: Header): void { + this.queryService.sort(header.source); } } diff --git a/src/components/table/table.component.html b/src/components/table/table.component.html index fbc6a00..ef52e83 100644 --- a/src/components/table/table.component.html +++ b/src/components/table/table.component.html @@ -1,14 +1,59 @@ +@if (data.headers.length > 0) { - - - - - - - - + + + + @for (h of data.headers; track h) { + + } + + + + @for (row of data.data; track row; let i = $index) { + + + @for (h of data.headers; track h) { + + } + + } + + @if (data.page) { + + + + + + }
- {{ h.label }} -
{{ i }} - {{ row[h.source] }} -
+ {{ h.label }} + + @switch (h.sort) { @case (SORT.ASC) { + + } @case(SORT.DSC) { + + } @default { + + } } + +
+ @if (data.page) { + {{ (data.page.page - 1) * data.page.size + i + 1 }} + } @else { + {{ i + 1 }} + } + + {{ row[h.source] }} +
+ + Showing {{ (data.page.page - 1) * data.page.size + 1 }} to + {{ data.page.page * data.page.size }} of {{ data.total }} + + +
+} diff --git a/src/components/table/table.component.scss b/src/components/table/table.component.scss index e69de29..29f92e0 100644 --- a/src/components/table/table.component.scss +++ b/src/components/table/table.component.scss @@ -0,0 +1,61 @@ +table { + border-collapse: collapse; +} + +thead { + th { + border: 1px solid #ddd; + background-color: #eee; + text-align: left; + vertical-align: top; + margin: 2px; + padding: 0px 2px; + } + + th.highlight { + background-color: #cdf; + } + + .spacer { + border: none; + background-color: inherit; + } +} + +tbody { + td { + border: 1px solid #ddd; + text-align: right; + padding: 2px; + } + + td.highlight { + background-color: #def; + } + + td:hover { + background-color: #eff; + } +} + +tfoot { + td { + text-align: center; + } +} + +.left { + text-align: left; +} + +.unused { + color: #ddd; +} + +.button { + color: #777; +} + +.clickable { + cursor: pointer; +} diff --git a/src/components/table/table.component.ts b/src/components/table/table.component.ts index f351a8f..ef2755d 100644 --- a/src/components/table/table.component.ts +++ b/src/components/table/table.component.ts @@ -1,15 +1,41 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Header } from '../../models/header'; +import { + faSort, + faSortDown, + faSortUp, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { SORT } from '../../enums/sort'; +import { Result } from '../../models/result'; +import { Page } from '../../models/page'; @Component({ selector: 'fbi-table', standalone: true, - imports: [CommonModule], + imports: [CommonModule, FontAwesomeModule], templateUrl: './table.component.html', styleUrl: './table.component.scss', }) export class TableComponent { - @Input() headers!: Header[]; - @Input() rows!: Record[]; + @Input() data!: Result; + + @Output() page = new EventEmitter(); + @Output() sort = new EventEmitter
(); + + faSortDown = faSortDown; + faSortUp = faSortUp; + faSort = faSort; + SORT = SORT; + + onPage(event: MouseEvent): void { + event.stopPropagation(); + this.page.emit(); + } + + onSort(event: MouseEvent, header: Header): void { + event.stopPropagation(); + this.sort.emit(header); + } } diff --git a/src/enums/milestone.ts b/src/enums/milestone.ts index ac3d3a6..dcff374 100644 --- a/src/enums/milestone.ts +++ b/src/enums/milestone.ts @@ -2,7 +2,9 @@ export enum Milestone { Cohort = 'cohort', Year = 'year', County = 'mcode', + CountyLabel = 'county', School = 'lcode', + SchoolLabel = 'school', Grade = 'grade', ELACount = 'ecount', ELAMean = 'emean', diff --git a/src/enums/sort.ts b/src/enums/sort.ts new file mode 100644 index 0000000..0b15821 --- /dev/null +++ b/src/enums/sort.ts @@ -0,0 +1,5 @@ +export enum SORT { + NONE = 'none', + ASC = 'asc', + DSC = 'dsc', +} diff --git a/src/models/header.ts b/src/models/header.ts index 9ba23c2..f6b8b24 100644 --- a/src/models/header.ts +++ b/src/models/header.ts @@ -1,9 +1,13 @@ +import { SORT } from '../enums/sort'; + export class Header { label: string; source: string; + sort: SORT; constructor(data: Partial
) { this.label = data?.label ?? ''; this.source = data?.source ?? ''; + this.sort = data?.sort ?? SORT.NONE; } } diff --git a/src/models/page.ts b/src/models/page.ts index ee9ec06..93fc5b6 100644 --- a/src/models/page.ts +++ b/src/models/page.ts @@ -1,13 +1,13 @@ export class Page { - start: number; - limit: number; + page: number; + size: number; constructor(data: Partial) { - this.start = data?.start ?? 1; - this.limit = data?.limit ?? 20; + this.page = data?.page ?? 1; + this.size = data?.size ?? 20; } toString(): string { - return `(${this.start}:${this.limit})`; + return `${this.page}:${this.size}`; } } diff --git a/src/models/query.ts b/src/models/query.ts index 309bf94..e406a6c 100644 --- a/src/models/query.ts +++ b/src/models/query.ts @@ -1,14 +1,17 @@ import { Page } from './page'; +import { Sort } from './sort'; export class Query { fields: string[]; filter: string[]; - page: Page; + sort: Sort; + page?: Page; constructor(data: Partial) { this.fields = data?.fields ?? []; this.filter = data?.filter ?? []; - this.page = new Page(data?.page ?? {}); + this.sort = new Sort(data?.sort ?? {}); + if (data?.page) this.page = new Page(data?.page); } isValid(): boolean { @@ -16,8 +19,11 @@ export class Query { } toString(): string { - const fields = (this.fields ?? []).join(', '); - const filters = ''; - return `${fields}${filters}`; + return [ + this.fields.join(','), + '', + this.page?.toString(), + this.sort.toString(), + ].join(';'); } } diff --git a/src/models/result.ts b/src/models/result.ts new file mode 100644 index 0000000..94b3a1b --- /dev/null +++ b/src/models/result.ts @@ -0,0 +1,16 @@ +import { Header } from './header'; +import { Page } from './page'; + +export class Result { + headers: Header[]; + data: Record[]; + total: number; + page?: Page; + + constructor(data: Partial) { + this.headers = data?.headers ?? []; + this.data = data?.data ?? []; + this.total = data?.total ?? 0; + if (data?.page) this.page = new Page(data.page); + } +} diff --git a/src/models/sort.ts b/src/models/sort.ts new file mode 100644 index 0000000..ddb1e4d --- /dev/null +++ b/src/models/sort.ts @@ -0,0 +1,19 @@ +import { SORT } from '../enums/sort'; + +export class Sort { + field: string; + dir: SORT; + + constructor(data: Partial) { + this.field = data?.field ?? ''; + this.dir = data?.dir ?? SORT.NONE; + } + + isValid(): boolean { + return this.field !== '' && this.dir !== SORT.NONE; + } + + toString(): string { + return `${this.field}:${this.dir}`; + } +} diff --git a/src/services/data.service.ts b/src/services/data.service.ts index f915bfe..3a1920d 100644 --- a/src/services/data.service.ts +++ b/src/services/data.service.ts @@ -68,16 +68,29 @@ export class DataService { const data: Record[] = []; (results?.data ?? []).forEach((record: Record) => { - let code = fn(Csv.County_Code, record); + const code = fn(Csv.County_Code, record); // skip non-data rows on the csv if (isNaN(code)) return; + const county = fs(Csv.County_Code, record); + + const county_label = fs(Csv.County_Label, record); + const school_label = fs(Csv.School_Label, record); + if (county_label === '' || school_label === '') return; + + if (!this._counties.has(county)) { + this._counties.set(county, county_label); + } + const school = fs(Csv.School_Code, record); + if (!this._schools.has(school)) { + this._schools.set(school, school_label); + } const ms = {} as Record; ms[Milestone.Year] = year; ms[Milestone.Grade] = grade; ms[Milestone.Cohort] = cohort; - ms[Milestone.County] = fs(Csv.County_Code, record); - ms[Milestone.School] = fs(Csv.School_Code, record); + ms[Milestone.County] = this._counties.get(county); //fs(Csv.County_Code, record); + ms[Milestone.School] = this._schools.get(school); //fs(Csv.School_Code, record); if (year !== 2020) { ms[Milestone.ELACount] = fn(Csv.ELA_Count, record); ms[Milestone.ELAMean] = fn(Csv.ELA_Mean, record); @@ -106,12 +119,14 @@ export class DataService { } data.push(ms); - const county = fs(Csv.County_Code, record); - this._counties.set(county, fs(Csv.County_Label, record)); - this._schools.set( - `${county}-${fs(Csv.School_Code, record)}`, - fs(Csv.School_Label, record) - ); + // this._counties.set( + // fs(Csv.County_Code, record), + // fs(Csv.County_Label, record) + // ); + // this._schools.set( + // fs(Csv.School_Code, record), + // fs(Csv.School_Label, record) + // ); }); if (year !== 2020) { diff --git a/src/services/execute.service.ts b/src/services/execute.service.ts index d0c8df4..6327319 100644 --- a/src/services/execute.service.ts +++ b/src/services/execute.service.ts @@ -4,6 +4,8 @@ import { Query } from '../models/query'; import { Observable, of, map, take } from 'rxjs'; import { Header } from '../models/header'; import { MetaService } from './meta.service'; +import { SORT } from '../enums/sort'; +import { Result } from '../models/result'; @Injectable({ providedIn: 'root' }) export class ExecuteService { @@ -12,27 +14,36 @@ export class ExecuteService { private metaService: MetaService ) {} - headers(query: Query): Observable { - if (!query?.isValid()) return of([]); + headers(query: Query): Header[] { + if (!query?.isValid?.()) return []; - const fields = query.fields; - return this.metaService.Flat.pipe( - map((items: Header[]) => - items - .filter((item: Header) => fields.includes(item.source)) - .sort( - (a: Header, b: Header) => - fields.indexOf(a.source) - fields.indexOf(b.source) - ) - ), - take(1) - ); + const fields = [...query.fields]; + const sort = query.sort; + + return this.metaService + .Flat() + .filter((item: Header) => fields.includes(item.source)) + .sort( + (a: Header, b: Header) => + fields.indexOf(a.source) - fields.indexOf(b.source) + ) + .map((item: Header) => { + if (item.source === sort.field) { + item.sort = sort.dir; + } + return item; + }); } - data(query: Query): Observable[]> { - if (!query?.isValid()) return of([]); + data(query: Query): Observable { + if (!query?.isValid()) return of(new Result({})); + console.log(query.toString()); - const fields = query.fields; + const fields = [...query.fields]; + const page = query.page; + const sort = query.sort; + + const headers = this.headers(query); return this.dataService.Data$.pipe( // apply filter @@ -43,37 +54,47 @@ export class ExecuteService { return true; }); }), - // apply fields - map((data: Record[]) => - data.map((i: Record) => { + // apply fields, make unique + map((data: Record[]) => { + let unique = new Map>(); + data.forEach((i: Record) => { const r = {} as Record; fields.forEach((field: string) => (r[field] = i[field])); - return r; - }) - ), - // make the data unique and apply paging - map((data: Record[]) => { - let i = 0; - const result: Record[] = []; - const page = query.page; - - data.some((d: Record) => { - const idx = result.findIndex((r: Record) => - fields - .map((field: string) => d[field] === r[field]) - .reduce((acc, cur) => acc && cur, true) - ); - - // if this element has not been seen before, add it to the results we care about. - if (idx < 0) { - result.push(d); - ++i; - } - return i >= page.start + page.limit; + const key = fields.map((field: string) => i[field]).join(':'); + unique.set(key, r); }); - - return result.slice(page.start, page.start + page.limit); + return [...unique].map(([name, value]) => value); }), + // sort + map((data: Record[]) => { + if (!sort.isValid()) return data; + const field = sort.field; + + if (!fields.includes(field)) { + } else if (sort.dir === SORT.ASC) { + return data.sort((a: Record, b: Record) => + a[field] > b[field] ? 1 : -1 + ); + } else if (sort.dir === SORT.DSC) { + return data.sort((a: Record, b: Record) => + a[field] < b[field] ? 1 : -1 + ); + } + return data; + }), + // paging + map( + (data: Record[]) => + new Result({ + headers: headers, + data: page + ? data.slice((page.page - 1) * page.size, page.page * page.size) + : data, + page: page, + total: data.length, + }) + + ), // force it to close take(1) ); diff --git a/src/services/meta.service.ts b/src/services/meta.service.ts index c59894c..f653016 100644 --- a/src/services/meta.service.ts +++ b/src/services/meta.service.ts @@ -76,35 +76,54 @@ export class MetaService { { data: Milestone.Soc3, label: 'Advanced Learner %' }, ], }, - ]; - private subject = new ReplaySubject[]>(1); - readonly Data = this.subject.asObservable(); - readonly Flat = this.subject.asObservable().pipe( - map((data: Partial[]) => { - const recurse = (path: string, item: Partial): Header[] => { - const result = []; - const children = item?.children ?? []; - if (children.length > 0) { - children.forEach((child: Partial) => { - result.push(...recurse(`${path}${item.label}`, child)); - }); - } else { - result.push( - new Header({ source: item.data, label: `${path} - ${item.label}` }) - ); - } - return result; - }; + ] as Partial[]; - const result: Header[] = []; - data.forEach((item: Partial) => - result.push(...recurse('', item)) - ); - return result; - }) - ); + private subject = new ReplaySubject(1); + readonly Data$ = this.subject + .asObservable() + .pipe( + map((items: TreeNode[]) => + items.map((item: TreeNode) => new TreeNode(item)) + ) + ); + // readonly Flat$ = this.subject.asObservable().pipe( + // map((data: TreeNode[]) => { + // const result: Header[] = []; + // data.forEach((item: TreeNode) => result.push(...this.flatten('', item))); + // return result; + // }) + // ); + + // readonly Data = () => + // this.data.map((item: Partial) => new TreeNode(item)); + readonly Flat = () => { + const result: Header[] = []; + this.data.forEach((item: Partial) => + result.push(...this.flatten('', item)) + ); + return result; + }; constructor() { - this.subject.next(this.data as Partial[]); + this.subject.next( + this.data.map((item: Partial) => new TreeNode(item)) + ); + } + + private flatten(path: string, item: Partial): Header[] { + const result: Header[] = []; + const children = item?.children ?? []; + + if (children.length > 0) { + children.forEach((child: Partial) => + result.push(...this.flatten(`${path}${item.label}`, child)) + ); + } else { + result.push( + new Header({ source: item.data, label: `${path} - ${item.label}` }) + ); + } + + return result; } } diff --git a/src/services/query.service.ts b/src/services/query.service.ts index f138376..09d2ded 100644 --- a/src/services/query.service.ts +++ b/src/services/query.service.ts @@ -1,11 +1,13 @@ import { BehaviorSubject } from 'rxjs'; import { Query } from '../models/query'; import { Injectable } from '@angular/core'; +import { Sort } from '../models/sort'; +import { SORT } from '../enums/sort'; @Injectable({ providedIn: 'root' }) export class QueryService { private querySubject = new BehaviorSubject(new Query({})); - readonly Query = this.querySubject.asObservable(); + readonly Query$ = this.querySubject.asObservable(); constructor() {} @@ -21,4 +23,25 @@ export class QueryService { q.fields = q.fields.filter((f: string) => f !== field); this.querySubject.next(q); } + + sort(field: string): void { + const q = new Query(this.querySubject.value); + const s = q.sort; + if (s.field === field) { + switch (s.dir) { + case SORT.ASC: + q.sort = new Sort({ field: field, dir: SORT.DSC }); + break; + case SORT.DSC: + q.sort = new Sort({}); + break; + default: + q.sort = new Sort({ field: field, dir: SORT.ASC }); + break; + } + } else { + q.sort = new Sort({ field: field, dir: SORT.ASC }); + } + this.querySubject.next(q); + } }