Added sorting, started on paging
This commit is contained in:
@@ -20,8 +20,8 @@ export class MetadataComponent {
|
|||||||
|
|
||||||
constructor(metaService: MetaService, private queryService: QueryService) {
|
constructor(metaService: MetaService, private queryService: QueryService) {
|
||||||
combineLatest({
|
combineLatest({
|
||||||
meta: metaService.Data,
|
meta: metaService.Data$,
|
||||||
query: queryService.Query,
|
query: queryService.Query$,
|
||||||
}).subscribe((d: { meta: Partial<TreeNode>[]; query: Query }) => {
|
}).subscribe((d: { meta: Partial<TreeNode>[]; query: Query }) => {
|
||||||
const inuse = d.query.fields;
|
const inuse = d.query.fields;
|
||||||
const expanded = this.getExpanded(this.node);
|
const expanded = this.getExpanded(this.node);
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ import {
|
|||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
faRemove,
|
faRemove,
|
||||||
|
faSort,
|
||||||
|
faSortAsc,
|
||||||
|
faSortDesc,
|
||||||
|
faSortDown,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { ExecuteService } from '../../services/execute.service';
|
import { ExecuteService } from '../../services/execute.service';
|
||||||
import { Header } from '../../models/header';
|
import { Header } from '../../models/header';
|
||||||
|
import { SORT } from '../../enums/sort';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fbi-query',
|
selector: 'fbi-query',
|
||||||
@@ -26,54 +31,34 @@ export class QueryComponent {
|
|||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
executeService: ExecuteService
|
executeService: ExecuteService
|
||||||
) {
|
) {
|
||||||
queryService.Query.subscribe((query: Query) => {
|
queryService.Query$.subscribe((query: Query) => {
|
||||||
executeService.headers(query).subscribe((headers: Header[]) => {
|
const headers = executeService.headers(query);
|
||||||
this.node = new TreeNode({
|
const fields = this.getFields(query, headers);
|
||||||
hidden: true,
|
const filters = this.getFilters(query, headers);
|
||||||
expanded: true,
|
const sort = this.getSorts(query, headers);
|
||||||
children: [
|
this.node = new TreeNode({
|
||||||
{
|
hidden: true,
|
||||||
label: 'Fields',
|
expanded: true,
|
||||||
leaf: false,
|
children: [
|
||||||
expanded: true,
|
{
|
||||||
children: (query?.fields ?? []).map(
|
label: 'Fields',
|
||||||
(field: string, index: number, array: string[]) => {
|
leaf: false,
|
||||||
const actions = [];
|
expanded: true,
|
||||||
if (index !== 0) {
|
children: fields,
|
||||||
actions.push({
|
},
|
||||||
label: 'Move Up',
|
{
|
||||||
icon: faArrowUp,
|
label: 'Filters',
|
||||||
data: { cmd: ACTIONS.UP, data: index - 1 },
|
expanded: true,
|
||||||
});
|
leaf: false,
|
||||||
}
|
children: filters,
|
||||||
if (index !== array.length - 1) {
|
},
|
||||||
actions.push({
|
{
|
||||||
label: 'Move Down',
|
label: 'Sort',
|
||||||
icon: faArrowDown,
|
expanded: true,
|
||||||
data: { cmd: ACTIONS.DOWN, data: index + 1 },
|
leaf: false,
|
||||||
});
|
children: sort,
|
||||||
}
|
},
|
||||||
actions.push({
|
] as TreeNode[],
|
||||||
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[],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -85,19 +70,139 @@ export class QueryComponent {
|
|||||||
this.queryService.remove(event.node.data);
|
this.queryService.remove(event.node.data);
|
||||||
break;
|
break;
|
||||||
case ACTIONS.UP:
|
case ACTIONS.UP:
|
||||||
this.queryService.add(event.node.data, data.data);
|
|
||||||
break;
|
|
||||||
case ACTIONS.DOWN:
|
case ACTIONS.DOWN:
|
||||||
this.queryService.add(event.node.data, data.data);
|
this.queryService.add(event.node.data, data.data);
|
||||||
break;
|
break;
|
||||||
|
case ACTIONS.SORTASC:
|
||||||
|
case ACTIONS.SORTDSC:
|
||||||
|
case ACTIONS.SORTDEL:
|
||||||
|
this.queryService.sort(data.data);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getFields(query: Query, headers: Header[]): Partial<TreeNode>[] {
|
||||||
|
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<TreeNode>[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSorts(query: Query, headers: Header[]): Partial<TreeNode>[] {
|
||||||
|
const result: Partial<TreeNode>[] = [];
|
||||||
|
|
||||||
|
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 {
|
enum ACTIONS {
|
||||||
REMOVE = 'del',
|
REMOVE = 'del',
|
||||||
UP = 'up',
|
UP = 'up',
|
||||||
DOWN = 'down',
|
DOWN = 'down',
|
||||||
|
SORTDSC = 'dsc',
|
||||||
|
SORTASC = 'asc',
|
||||||
|
SORTDEL = 'nne',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionData = {
|
type ActionData = {
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
<fbi-table [headers]="headers" [rows]="rows"></fbi-table>
|
<fbi-table
|
||||||
|
[data]="data"
|
||||||
|
(page)="onPage($event)"
|
||||||
|
(sort)="onSort($event)"
|
||||||
|
></fbi-table>
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { TreeComponent } from '../tree/tree.component';
|
|||||||
import { QueryService } from '../../services/query.service';
|
import { QueryService } from '../../services/query.service';
|
||||||
import { Query } from '../../models/query';
|
import { Query } from '../../models/query';
|
||||||
import { ExecuteService } from '../../services/execute.service';
|
import { ExecuteService } from '../../services/execute.service';
|
||||||
import { forkJoin } from 'rxjs';
|
|
||||||
import { Header } from '../../models/header';
|
|
||||||
import { TableComponent } from '../table/table.component';
|
import { TableComponent } from '../table/table.component';
|
||||||
|
import { Result } from '../../models/result';
|
||||||
|
import { Header } from '../../models/header';
|
||||||
|
import { Page } from '../../models/page';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fbi-result',
|
selector: 'fbi-result',
|
||||||
@@ -16,34 +17,43 @@ import { TableComponent } from '../table/table.component';
|
|||||||
styleUrl: './result.component.scss',
|
styleUrl: './result.component.scss',
|
||||||
})
|
})
|
||||||
export class ResultComponent {
|
export class ResultComponent {
|
||||||
headers: Header[] = [];
|
data: Result = new Result({});
|
||||||
rows: Record<string, any>[] = [];
|
|
||||||
|
private query: Query = new Query({});
|
||||||
|
private page: Page = new Page({});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private executeService: ExecuteService
|
private executeService: ExecuteService
|
||||||
) {
|
) {
|
||||||
let last: string = '';
|
let last: string = '';
|
||||||
queryService.Query.subscribe((query: Query) => {
|
queryService.Query$.subscribe((query: Query) => {
|
||||||
if (query.isValid()) {
|
if (query.isValid()) {
|
||||||
const current = query.toString();
|
const current = query.toString();
|
||||||
if (last !== current) {
|
if (last !== current) {
|
||||||
this.load(query);
|
this.query = query;
|
||||||
|
this.page = new Page({});
|
||||||
|
this.load();
|
||||||
last = current;
|
last = current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private load(query: Query): void {
|
private load(): void {
|
||||||
forkJoin({
|
const query = new Query(this.query);
|
||||||
headers: this.executeService.headers(query),
|
query.page = new Page(this.page);
|
||||||
data: this.executeService.data(query),
|
this.executeService.data(query).subscribe((result: Result) => {
|
||||||
}).subscribe(
|
this.data = result;
|
||||||
(result: { headers: Header[]; data: Record<string, any>[] }) => {
|
});
|
||||||
this.headers = result.headers;
|
}
|
||||||
this.rows = result.data;
|
|
||||||
}
|
onPage(page: Page): void {
|
||||||
);
|
this.page = new Page(page);
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSort(header: Header): void {
|
||||||
|
this.queryService.sort(header.source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,59 @@
|
|||||||
|
@if (data.headers.length > 0) {
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<thead>
|
||||||
<th></th>
|
<tr>
|
||||||
<th *ngFor="let h of headers">
|
<th class="spacer"></th>
|
||||||
{{ h.label }}
|
@for (h of data.headers; track h) {
|
||||||
</th>
|
<th [title]="h.label">
|
||||||
</tr>
|
{{ h.label }}
|
||||||
<tr *ngFor="let row of rows; index as i">
|
<span
|
||||||
<td>{{ i }}</td>
|
class="clickable"
|
||||||
<td *ngFor="let h of headers">
|
[class.button]="h.sort !== SORT.NONE"
|
||||||
{{ row[h.source] }}
|
[class.unused]="h.sort === SORT.NONE"
|
||||||
</td>
|
(click)="onSort($event, h)"
|
||||||
</tr>
|
>
|
||||||
|
@switch (h.sort) { @case (SORT.ASC) {
|
||||||
|
<fa-icon [icon]="faSortUp"></fa-icon>
|
||||||
|
} @case(SORT.DSC) {
|
||||||
|
<fa-icon [icon]="faSortDown"></fa-icon>
|
||||||
|
} @default {
|
||||||
|
<fa-icon [icon]="faSort"></fa-icon>
|
||||||
|
} }
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (row of data.data; track row; let i = $index) {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
@if (data.page) {
|
||||||
|
{{ (data.page.page - 1) * data.page.size + i + 1 }}
|
||||||
|
} @else {
|
||||||
|
{{ i + 1 }}
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
@for (h of data.headers; track h) {
|
||||||
|
<td>
|
||||||
|
{{ row[h.source] }}
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
@if (data.page) {
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td [attr.colspan]="data.headers.length + 1">
|
||||||
|
<span>
|
||||||
|
Showing {{ (data.page.page - 1) * data.page.size + 1 }} to
|
||||||
|
{{ data.page.page * data.page.size }} of {{ data.total }}
|
||||||
|
</span>
|
||||||
|
<span> </span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
}
|
||||||
</table>
|
</table>
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { 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({
|
@Component({
|
||||||
selector: 'fbi-table',
|
selector: 'fbi-table',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, FontAwesomeModule],
|
||||||
templateUrl: './table.component.html',
|
templateUrl: './table.component.html',
|
||||||
styleUrl: './table.component.scss',
|
styleUrl: './table.component.scss',
|
||||||
})
|
})
|
||||||
export class TableComponent {
|
export class TableComponent {
|
||||||
@Input() headers!: Header[];
|
@Input() data!: Result;
|
||||||
@Input() rows!: Record<string, any>[];
|
|
||||||
|
@Output() page = new EventEmitter<Page>();
|
||||||
|
@Output() sort = new EventEmitter<Header>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ export enum Milestone {
|
|||||||
Cohort = 'cohort',
|
Cohort = 'cohort',
|
||||||
Year = 'year',
|
Year = 'year',
|
||||||
County = 'mcode',
|
County = 'mcode',
|
||||||
|
CountyLabel = 'county',
|
||||||
School = 'lcode',
|
School = 'lcode',
|
||||||
|
SchoolLabel = 'school',
|
||||||
Grade = 'grade',
|
Grade = 'grade',
|
||||||
ELACount = 'ecount',
|
ELACount = 'ecount',
|
||||||
ELAMean = 'emean',
|
ELAMean = 'emean',
|
||||||
|
|||||||
5
src/enums/sort.ts
Normal file
5
src/enums/sort.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum SORT {
|
||||||
|
NONE = 'none',
|
||||||
|
ASC = 'asc',
|
||||||
|
DSC = 'dsc',
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import { SORT } from '../enums/sort';
|
||||||
|
|
||||||
export class Header {
|
export class Header {
|
||||||
label: string;
|
label: string;
|
||||||
source: string;
|
source: string;
|
||||||
|
sort: SORT;
|
||||||
|
|
||||||
constructor(data: Partial<Header>) {
|
constructor(data: Partial<Header>) {
|
||||||
this.label = data?.label ?? '';
|
this.label = data?.label ?? '';
|
||||||
this.source = data?.source ?? '';
|
this.source = data?.source ?? '';
|
||||||
|
this.sort = data?.sort ?? SORT.NONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
export class Page {
|
export class Page {
|
||||||
start: number;
|
page: number;
|
||||||
limit: number;
|
size: number;
|
||||||
|
|
||||||
constructor(data: Partial<Page>) {
|
constructor(data: Partial<Page>) {
|
||||||
this.start = data?.start ?? 1;
|
this.page = data?.page ?? 1;
|
||||||
this.limit = data?.limit ?? 20;
|
this.size = data?.size ?? 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
return `(${this.start}:${this.limit})`;
|
return `${this.page}:${this.size}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
|
import { Sort } from './sort';
|
||||||
|
|
||||||
export class Query {
|
export class Query {
|
||||||
fields: string[];
|
fields: string[];
|
||||||
filter: string[];
|
filter: string[];
|
||||||
page: Page;
|
sort: Sort;
|
||||||
|
page?: Page;
|
||||||
|
|
||||||
constructor(data: Partial<Query>) {
|
constructor(data: Partial<Query>) {
|
||||||
this.fields = data?.fields ?? [];
|
this.fields = data?.fields ?? [];
|
||||||
this.filter = data?.filter ?? [];
|
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 {
|
isValid(): boolean {
|
||||||
@@ -16,8 +19,11 @@ export class Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
const fields = (this.fields ?? []).join(', ');
|
return [
|
||||||
const filters = '';
|
this.fields.join(','),
|
||||||
return `${fields}${filters}`;
|
'',
|
||||||
|
this.page?.toString(),
|
||||||
|
this.sort.toString(),
|
||||||
|
].join(';');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/models/result.ts
Normal file
16
src/models/result.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Header } from './header';
|
||||||
|
import { Page } from './page';
|
||||||
|
|
||||||
|
export class Result {
|
||||||
|
headers: Header[];
|
||||||
|
data: Record<string, any>[];
|
||||||
|
total: number;
|
||||||
|
page?: Page;
|
||||||
|
|
||||||
|
constructor(data: Partial<Result>) {
|
||||||
|
this.headers = data?.headers ?? [];
|
||||||
|
this.data = data?.data ?? [];
|
||||||
|
this.total = data?.total ?? 0;
|
||||||
|
if (data?.page) this.page = new Page(data.page);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/models/sort.ts
Normal file
19
src/models/sort.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { SORT } from '../enums/sort';
|
||||||
|
|
||||||
|
export class Sort {
|
||||||
|
field: string;
|
||||||
|
dir: SORT;
|
||||||
|
|
||||||
|
constructor(data: Partial<Sort>) {
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,16 +68,29 @@ export class DataService {
|
|||||||
const data: Record<string, any>[] = [];
|
const data: Record<string, any>[] = [];
|
||||||
|
|
||||||
(results?.data ?? []).forEach((record: Record<string, any>) => {
|
(results?.data ?? []).forEach((record: Record<string, any>) => {
|
||||||
let code = fn(Csv.County_Code, record);
|
const code = fn(Csv.County_Code, record);
|
||||||
// skip non-data rows on the csv
|
// skip non-data rows on the csv
|
||||||
if (isNaN(code)) return;
|
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<string, any>;
|
const ms = {} as Record<string, any>;
|
||||||
ms[Milestone.Year] = year;
|
ms[Milestone.Year] = year;
|
||||||
ms[Milestone.Grade] = grade;
|
ms[Milestone.Grade] = grade;
|
||||||
ms[Milestone.Cohort] = cohort;
|
ms[Milestone.Cohort] = cohort;
|
||||||
ms[Milestone.County] = fs(Csv.County_Code, record);
|
ms[Milestone.County] = this._counties.get(county); //fs(Csv.County_Code, record);
|
||||||
ms[Milestone.School] = fs(Csv.School_Code, record);
|
ms[Milestone.School] = this._schools.get(school); //fs(Csv.School_Code, record);
|
||||||
if (year !== 2020) {
|
if (year !== 2020) {
|
||||||
ms[Milestone.ELACount] = fn(Csv.ELA_Count, record);
|
ms[Milestone.ELACount] = fn(Csv.ELA_Count, record);
|
||||||
ms[Milestone.ELAMean] = fn(Csv.ELA_Mean, record);
|
ms[Milestone.ELAMean] = fn(Csv.ELA_Mean, record);
|
||||||
@@ -106,12 +119,14 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
data.push(ms);
|
data.push(ms);
|
||||||
|
|
||||||
const county = fs(Csv.County_Code, record);
|
// this._counties.set(
|
||||||
this._counties.set(county, fs(Csv.County_Label, record));
|
// fs(Csv.County_Code, record),
|
||||||
this._schools.set(
|
// fs(Csv.County_Label, record)
|
||||||
`${county}-${fs(Csv.School_Code, record)}`,
|
// );
|
||||||
fs(Csv.School_Label, record)
|
// this._schools.set(
|
||||||
);
|
// fs(Csv.School_Code, record),
|
||||||
|
// fs(Csv.School_Label, record)
|
||||||
|
// );
|
||||||
});
|
});
|
||||||
|
|
||||||
if (year !== 2020) {
|
if (year !== 2020) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Query } from '../models/query';
|
|||||||
import { Observable, of, map, take } from 'rxjs';
|
import { Observable, of, map, take } from 'rxjs';
|
||||||
import { Header } from '../models/header';
|
import { Header } from '../models/header';
|
||||||
import { MetaService } from './meta.service';
|
import { MetaService } from './meta.service';
|
||||||
|
import { SORT } from '../enums/sort';
|
||||||
|
import { Result } from '../models/result';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ExecuteService {
|
export class ExecuteService {
|
||||||
@@ -12,27 +14,36 @@ export class ExecuteService {
|
|||||||
private metaService: MetaService
|
private metaService: MetaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
headers(query: Query): Observable<Header[]> {
|
headers(query: Query): Header[] {
|
||||||
if (!query?.isValid()) return of([]);
|
if (!query?.isValid?.()) return [];
|
||||||
|
|
||||||
const fields = query.fields;
|
const fields = [...query.fields];
|
||||||
return this.metaService.Flat.pipe(
|
const sort = query.sort;
|
||||||
map((items: Header[]) =>
|
|
||||||
items
|
return this.metaService
|
||||||
.filter((item: Header) => fields.includes(item.source))
|
.Flat()
|
||||||
.sort(
|
.filter((item: Header) => fields.includes(item.source))
|
||||||
(a: Header, b: Header) =>
|
.sort(
|
||||||
fields.indexOf(a.source) - fields.indexOf(b.source)
|
(a: Header, b: Header) =>
|
||||||
)
|
fields.indexOf(a.source) - fields.indexOf(b.source)
|
||||||
),
|
)
|
||||||
take(1)
|
.map((item: Header) => {
|
||||||
);
|
if (item.source === sort.field) {
|
||||||
|
item.sort = sort.dir;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
data(query: Query): Observable<Record<string, any>[]> {
|
data(query: Query): Observable<Result> {
|
||||||
if (!query?.isValid()) return of([]);
|
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(
|
return this.dataService.Data$.pipe(
|
||||||
// apply filter
|
// apply filter
|
||||||
@@ -43,37 +54,47 @@ export class ExecuteService {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
// apply fields
|
// apply fields, make unique
|
||||||
map((data: Record<string, any>[]) =>
|
map((data: Record<string, any>[]) => {
|
||||||
data.map((i: Record<string, any>) => {
|
let unique = new Map<string, Record<string, any>>();
|
||||||
|
data.forEach((i: Record<string, any>) => {
|
||||||
const r = {} as Record<string, any>;
|
const r = {} as Record<string, any>;
|
||||||
fields.forEach((field: string) => (r[field] = i[field]));
|
fields.forEach((field: string) => (r[field] = i[field]));
|
||||||
return r;
|
const key = fields.map((field: string) => i[field]).join(':');
|
||||||
})
|
unique.set(key, r);
|
||||||
),
|
|
||||||
// make the data unique and apply paging
|
|
||||||
map((data: Record<string, any>[]) => {
|
|
||||||
let i = 0;
|
|
||||||
const result: Record<string, any>[] = [];
|
|
||||||
const page = query.page;
|
|
||||||
|
|
||||||
data.some((d: Record<string, any>) => {
|
|
||||||
const idx = result.findIndex((r: Record<string, any>) =>
|
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
return [...unique].map(([name, value]) => value);
|
||||||
return result.slice(page.start, page.start + page.limit);
|
|
||||||
}),
|
}),
|
||||||
|
// sort
|
||||||
|
map((data: Record<string, any>[]) => {
|
||||||
|
if (!sort.isValid()) return data;
|
||||||
|
const field = sort.field;
|
||||||
|
|
||||||
|
if (!fields.includes(field)) {
|
||||||
|
} else if (sort.dir === SORT.ASC) {
|
||||||
|
return data.sort((a: Record<string, any>, b: Record<string, any>) =>
|
||||||
|
a[field] > b[field] ? 1 : -1
|
||||||
|
);
|
||||||
|
} else if (sort.dir === SORT.DSC) {
|
||||||
|
return data.sort((a: Record<string, any>, b: Record<string, any>) =>
|
||||||
|
a[field] < b[field] ? 1 : -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
// paging
|
||||||
|
map(
|
||||||
|
(data: Record<string, any>[]) =>
|
||||||
|
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
|
// force it to close
|
||||||
take(1)
|
take(1)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -76,35 +76,54 @@ export class MetaService {
|
|||||||
{ data: Milestone.Soc3, label: 'Advanced Learner %' },
|
{ data: Milestone.Soc3, label: 'Advanced Learner %' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
] as Partial<TreeNode>[];
|
||||||
private subject = new ReplaySubject<Partial<TreeNode>[]>(1);
|
|
||||||
readonly Data = this.subject.asObservable();
|
|
||||||
readonly Flat = this.subject.asObservable().pipe(
|
|
||||||
map((data: Partial<TreeNode>[]) => {
|
|
||||||
const recurse = (path: string, item: Partial<TreeNode>): Header[] => {
|
|
||||||
const result = [];
|
|
||||||
const children = item?.children ?? [];
|
|
||||||
if (children.length > 0) {
|
|
||||||
children.forEach((child: Partial<TreeNode>) => {
|
|
||||||
result.push(...recurse(`${path}${item.label}`, child));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.push(
|
|
||||||
new Header({ source: item.data, label: `${path} - ${item.label}` })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const result: Header[] = [];
|
private subject = new ReplaySubject<TreeNode[]>(1);
|
||||||
data.forEach((item: Partial<TreeNode>) =>
|
readonly Data$ = this.subject
|
||||||
result.push(...recurse('', item))
|
.asObservable()
|
||||||
);
|
.pipe(
|
||||||
return result;
|
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<TreeNode>) => new TreeNode(item));
|
||||||
|
readonly Flat = () => {
|
||||||
|
const result: Header[] = [];
|
||||||
|
this.data.forEach((item: Partial<TreeNode>) =>
|
||||||
|
result.push(...this.flatten('', item))
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.subject.next(this.data as Partial<TreeNode>[]);
|
this.subject.next(
|
||||||
|
this.data.map((item: Partial<TreeNode>) => new TreeNode(item))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private flatten(path: string, item: Partial<TreeNode>): Header[] {
|
||||||
|
const result: Header[] = [];
|
||||||
|
const children = item?.children ?? [];
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
children.forEach((child: Partial<TreeNode>) =>
|
||||||
|
result.push(...this.flatten(`${path}${item.label}`, child))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result.push(
|
||||||
|
new Header({ source: item.data, label: `${path} - ${item.label}` })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { Query } from '../models/query';
|
import { Query } from '../models/query';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Sort } from '../models/sort';
|
||||||
|
import { SORT } from '../enums/sort';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class QueryService {
|
export class QueryService {
|
||||||
private querySubject = new BehaviorSubject<Query>(new Query({}));
|
private querySubject = new BehaviorSubject<Query>(new Query({}));
|
||||||
readonly Query = this.querySubject.asObservable();
|
readonly Query$ = this.querySubject.asObservable();
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
@@ -21,4 +23,25 @@ export class QueryService {
|
|||||||
q.fields = q.fields.filter((f: string) => f !== field);
|
q.fields = q.fields.filter((f: string) => f !== field);
|
||||||
this.querySubject.next(q);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user