Added sorting, started on paging

This commit is contained in:
2024-09-10 21:58:24 -04:00
parent 1e3dc2938b
commit 7a525930db
18 changed files with 558 additions and 177 deletions

View File

@@ -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<TreeNode>[]; query: Query }) => {
const inuse = d.query.fields;
const expanded = this.getExpanded(this.node);

View File

@@ -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<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 {
REMOVE = 'del',
UP = 'up',
DOWN = 'down',
SORTDSC = 'dsc',
SORTASC = 'asc',
SORTDEL = 'nne',
}
type ActionData = {

View File

@@ -1 +1,5 @@
<fbi-table [headers]="headers" [rows]="rows"></fbi-table>
<fbi-table
[data]="data"
(page)="onPage($event)"
(sort)="onSort($event)"
></fbi-table>

View File

@@ -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<string, any>[] = [];
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<string, any>[] }) => {
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);
}
}

View File

@@ -1,14 +1,59 @@
@if (data.headers.length > 0) {
<table>
<tr>
<th></th>
<th *ngFor="let h of headers">
{{ h.label }}
</th>
</tr>
<tr *ngFor="let row of rows; index as i">
<td>{{ i }}</td>
<td *ngFor="let h of headers">
{{ row[h.source] }}
</td>
</tr>
<thead>
<tr>
<th class="spacer"></th>
@for (h of data.headers; track h) {
<th [title]="h.label">
{{ h.label }}
<span
class="clickable"
[class.button]="h.sort !== SORT.NONE"
[class.unused]="h.sort === SORT.NONE"
(click)="onSort($event, h)"
>
@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>
}

View File

@@ -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;
}

View File

@@ -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<string, any>[];
@Input() data!: Result;
@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);
}
}