blob: dad802a061c04b5ceb3bba68f9b8e5cfa6e14318 [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '@polymer/iron-input/iron-input';
import '../gr-button/gr-button';
import '../gr-icon/gr-icon';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {page} from '../../../utils/page-wrapper-utils';
import {fireEvent} from '../../../utils/event-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
declare global {
interface HTMLElementTagNameMap {
'gr-list-view': GrListView;
}
}
@customElement('gr-list-view')
export class GrListView extends LitElement {
@property({type: Boolean})
createNew?: boolean;
@property({type: Array})
items?: unknown[];
@property({type: Number})
itemsPerPage = 25;
@property({type: String})
filter?: string;
@property({type: Number})
offset = 0;
@property({type: Boolean})
loading?: boolean;
@property({type: String})
path?: string;
private reloadTask?: DelayedTask;
override disconnectedCallback() {
this.reloadTask?.cancel();
super.disconnectedCallback();
}
static override get styles() {
return [
sharedStyles,
css`
#filter {
max-width: 25em;
}
#filter:focus {
outline: none;
}
#topContainer {
align-items: center;
display: flex;
height: 3rem;
justify-content: space-between;
margin: 0 var(--spacing-l);
}
#createNewContainer:not(.show) {
display: none;
}
a {
color: var(--primary-text-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
nav {
align-items: center;
display: flex;
height: 3rem;
justify-content: flex-end;
margin-right: 20px;
color: var(--deemphasized-text-color);
}
gr-icon {
font-size: 1.85rem;
margin-left: 16px;
}
`,
];
}
override render() {
return html`
<div id="topContainer">
<div class="filterContainer">
<label>Filter:</label>
<iron-input
.bindValue=${this.filter}
@bind-value-changed=${this.handleFilterBindValueChanged}
>
<input type="text" id="filter" />
</iron-input>
</div>
<div id="createNewContainer" class=${this.createNew ? 'show' : ''}>
<gr-button
id="createNew"
primary
link
@click=${() => this.createNewItem()}
>
Create New
</gr-button>
</div>
</div>
<slot></slot>
<nav>
Page ${this.computePage()}
<a
id="prevArrow"
href=${this.computeNavLink(-1)}
?hidden=${this.loading || this.offset === 0}
>
<gr-icon icon="chevron_left"></gr-icon>
</a>
<a
id="nextArrow"
href=${this.computeNavLink(1)}
?hidden=${this.hideNextArrow()}
>
<gr-icon icon="chevron_right"></gr-icon>
</a>
</nav>
`;
}
override willUpdate(changedProperties: PropertyValues) {
// We have to do this for the tests.
if (changedProperties.has('filter')) {
this.filterChanged(
this.filter,
changedProperties.get('filter') as string
);
}
}
private filterChanged(newFilter?: string, oldFilter?: string) {
// newFilter can be empty string and then !newFilter === true
if (!newFilter && !oldFilter) {
return;
}
this.debounceReload(newFilter);
}
// private but used in test
debounceReload(filter?: string) {
this.reloadTask = debounce(
this.reloadTask,
() => {
if (!this.isConnected || !this.path) return;
if (filter) {
// TODO: Use navigation service instead of `page.show()` directly.
page.show(`${this.path}/q/filter:${encodeURL(filter, false)}`);
return;
}
// TODO: Use navigation service instead of `page.show()` directly.
page.show(this.path);
},
REQUEST_DEBOUNCE_INTERVAL_MS
);
}
private createNewItem() {
fireEvent(this, 'create-clicked');
}
// private but used in test
computeNavLink(direction: number) {
// Offset could be a string when passed from the router.
const offset = +(this.offset || 0);
const newOffset = Math.max(0, offset + this.itemsPerPage * direction);
let href = getBaseUrl() + (this.path ?? '');
if (this.filter) {
href += '/q/filter:' + encodeURL(this.filter, false);
}
if (newOffset > 0) {
href += `,${newOffset}`;
}
return href;
}
// private but used in test
hideNextArrow() {
if (this.loading || !this.items?.length) return true;
const lastPage = this.items.length < this.itemsPerPage + 1;
return lastPage;
}
// TODO: fix offset (including itemsPerPage)
// to either support a decimal or make it go to the nearest
// whole number (e.g 3).
// private but used in test
computePage() {
return this.offset / this.itemsPerPage + 1;
}
private handleFilterBindValueChanged(e: BindValueChangeEvent) {
this.filter = e.detail.value;
}
}