web: Table parity (#427)
* core: fix application API always being sorted by name * web: add sorting to tables * web: add search to TablePage * core: add search to applications API * core: add MetaNameSerializer * *: fix signature for non-modal serializers * providers/*: implement MetaNameSerializer * web: implement full app list page, use as default in sidebar * web: fix linting errors * admin: remove old application list * web: fix default sorting for application list * web: fix spacing for search element in toolbar
This commit is contained in:
@ -6,10 +6,72 @@ import { COMMON_STYLES } from "../../common/styles";
|
||||
import "./TablePagination";
|
||||
import "../EmptyState";
|
||||
|
||||
|
||||
export class TableColumn {
|
||||
|
||||
title: string;
|
||||
orderBy?: string;
|
||||
|
||||
onClick?: () => void;
|
||||
|
||||
constructor(title: string, orderBy?: string) {
|
||||
this.title = title;
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
headerClickHandler(table: Table<unknown>): void {
|
||||
if (!this.orderBy) {
|
||||
return;
|
||||
}
|
||||
if (table.order === this.orderBy) {
|
||||
table.order = `-${this.orderBy}`;
|
||||
} else {
|
||||
table.order = this.orderBy;
|
||||
}
|
||||
table.fetch();
|
||||
}
|
||||
|
||||
private getSortIndicator(table: Table<unknown>): string {
|
||||
switch (table.order) {
|
||||
case this.orderBy:
|
||||
return "fa-long-arrow-alt-down";
|
||||
case `-${this.orderBy}`:
|
||||
return "fa-long-arrow-alt-up";
|
||||
default:
|
||||
return "fa-arrows-alt-v";
|
||||
}
|
||||
}
|
||||
|
||||
renderSortable(table: Table<unknown>): TemplateResult {
|
||||
return html`
|
||||
<button class="pf-c-table__button" @click=${() => this.headerClickHandler(table)}>
|
||||
<div class="pf-c-table__button-content">
|
||||
<span class="pf-c-table__text">${gettext(this.title)}</span>
|
||||
<span class="pf-c-table__sort-indicator">
|
||||
<i class="fas ${this.getSortIndicator(table)}"></i>
|
||||
</span>
|
||||
</div>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
render(table: Table<unknown>): TemplateResult {
|
||||
return html`<th
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
class="
|
||||
${this.orderBy ? "pf-c-table__sort " : " "}
|
||||
${(table.order === this.orderBy || table.order === `-${this.orderBy}`) ? "pf-m-selected " : ""}
|
||||
">
|
||||
${this.orderBy ? this.renderSortable(table) : html`${gettext(this.title)}`}
|
||||
</th>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class Table<T> extends LitElement {
|
||||
abstract apiEndpoint(page: number): Promise<PBResponse<T>>;
|
||||
abstract columns(): Array<string>;
|
||||
abstract row(item: T): Array<TemplateResult>;
|
||||
abstract columns(): TableColumn[];
|
||||
abstract row(item: T): TemplateResult[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
renderExpanded(item: T): TemplateResult {
|
||||
@ -25,6 +87,12 @@ export abstract class Table<T> extends LitElement {
|
||||
@property({type: Number})
|
||||
page = 1;
|
||||
|
||||
@property({type: String})
|
||||
order?: string;
|
||||
|
||||
@property({type: String})
|
||||
search?: string;
|
||||
|
||||
@property({type: Boolean})
|
||||
expandable = false;
|
||||
|
||||
@ -43,6 +111,7 @@ export abstract class Table<T> extends LitElement {
|
||||
}
|
||||
|
||||
public fetch(): void {
|
||||
this.data = undefined;
|
||||
this.apiEndpoint(this.page).then((r) => {
|
||||
this.data = r;
|
||||
this.page = r.pagination.current;
|
||||
@ -123,12 +192,17 @@ export abstract class Table<T> extends LitElement {
|
||||
</button>`;
|
||||
}
|
||||
|
||||
renderSearch(): TemplateResult {
|
||||
return html``;
|
||||
}
|
||||
|
||||
renderTable(): TemplateResult {
|
||||
if (!this.data) {
|
||||
this.fetch();
|
||||
}
|
||||
return html`<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
${this.renderSearch()}
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
${this.renderToolbar()}
|
||||
</div>
|
||||
@ -143,7 +217,7 @@ export abstract class Table<T> extends LitElement {
|
||||
<thead>
|
||||
<tr role="row">
|
||||
${this.expandable ? html`<td role="cell">` : html``}
|
||||
${this.columns().map((col) => html`<th role="columnheader" scope="col">${gettext(col)}</th>`)}
|
||||
${this.columns().map((col) => col.render(this))}
|
||||
</tr>
|
||||
</thead>
|
||||
${this.data ? this.renderRows() : this.renderLoading()}
|
||||
|
||||
@ -1,19 +1,34 @@
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { Table } from "./Table";
|
||||
import "./TableSearch";
|
||||
|
||||
export abstract class TablePage<T> extends Table<T> {
|
||||
abstract pageTitle(): string;
|
||||
abstract pageDescription(): string;
|
||||
abstract pageDescription(): string | undefined;
|
||||
abstract pageIcon(): string;
|
||||
abstract searchEnabled(): boolean;
|
||||
|
||||
renderSearch(): TemplateResult {
|
||||
if (!this.searchEnabled()) {
|
||||
return super.renderSearch();
|
||||
}
|
||||
return html`<ak-table-search value=${ifDefined(this.search)} .onSearch=${(value: string) => {
|
||||
this.search = value;
|
||||
this.fetch();
|
||||
}}>
|
||||
</ak-table-search>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const description = this.pageDescription();
|
||||
return html`<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="${this.pageIcon()}"></i>
|
||||
${this.pageTitle()}
|
||||
</h1>
|
||||
<p>${this.pageDescription()}</p>
|
||||
${description ? html`<p>${description}</p>` : html``}
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
|
||||
41
web/src/elements/table/TableSearch.ts
Normal file
41
web/src/elements/table/TableSearch.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { COMMON_STYLES } from "../../common/styles";
|
||||
|
||||
@customElement("ak-table-search")
|
||||
export class TableSearch extends LitElement {
|
||||
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
@property()
|
||||
onSearch?: (value: string) => void;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return COMMON_STYLES;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-toolbar__group pf-m-filter-group">
|
||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||
<form class="pf-c-input-group" method="GET" @submit=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.onSearch) return;
|
||||
const el = this.shadowRoot?.querySelector<HTMLInputElement>("input[type=search]");
|
||||
if (!el) return;
|
||||
if (el.value === "") return;
|
||||
this.onSearch(el?.value);
|
||||
}}>
|
||||
<input class="pf-c-form-control" name="search" type="search" placeholder="Search..." value="${ifDefined(this.value)}" @search=${() => {
|
||||
if (!this.onSearch) return;
|
||||
this.onSearch("");
|
||||
}}>
|
||||
<button class="pf-c-button pf-m-control" type="submit">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user