Added sorting, started on paging
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 { 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 { 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user