diff --git a/web/package.json b/web/package.json index 811e221ca..cecb3d40d 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "lint:spelling": "codespell -D - -D ../.github/codespell-dictionary.txt -I ../.github/codespell-words.txt -S './src/locales/**' ./src -s", "lit-analyse": "lit-analyzer src", "precommit": "run-s tsc lit-analyse lint:precommit lint:spelling prettier", + "prequick": "run-s tsc:execute lit-analyse lint:precommit lint:spelling", "prettier-check": "prettier --check .", "prettier": "prettier --write .", "pseudolocalize:build-extract-script": "cd scripts && tsc --esModuleInterop --module es2020 --moduleResolution 'node' pseudolocalize.ts && mv pseudolocalize.js pseudolocalize.mjs", diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index 6175b5296..0b78c4dfb 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -13,6 +13,7 @@ import "@goauthentik/elements/table/TableSearch"; import { msg } from "@lit/localize"; import { CSSResult, TemplateResult, css, html } from "lit"; import { property, state } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; @@ -41,11 +42,7 @@ export class TableColumn { if (!this.orderBy) { return; } - if (table.order === this.orderBy) { - table.order = `-${this.orderBy}`; - } else { - table.order = this.orderBy; - } + table.order = table.order === this.orderBy ? `-${this.orderBy}` : this.orderBy; table.fetch(); } @@ -75,16 +72,12 @@ export class TableColumn { } render(table: Table): TemplateResult { - return html` + const classes = { + "pf-c-table__sort": !!this.orderBy, + "pf-m-selected": table.order === this.orderBy || table.order === `-${this.orderBy}`, + }; + + return html` ${this.orderBy ? this.renderSortable(table) : html`${this.title}`} `; } @@ -230,7 +223,7 @@ export abstract class Table extends AKElement { return html`
- +
`; @@ -241,11 +234,10 @@ export abstract class Table extends AKElement {
- ${inner - ? inner - : html`
${this.renderObjectCreate()}
-
`} + ${inner ?? + html`
${this.renderObjectCreate()}
+
`}
@@ -257,14 +249,13 @@ export abstract class Table extends AKElement { } renderError(): TemplateResult { - if (!this.error) { - return html``; - } - return html` - ${this.error instanceof ResponseError - ? html`
${this.error.message}
` - : html`
${this.error.detail}
`} -
`; + return this.error + ? html` + ${this.error instanceof ResponseError + ? html`
${this.error.message}
` + : html`
${this.error.detail}
`} +
` + : html``; } private renderRows(): TemplateResult[] | undefined { @@ -294,104 +285,89 @@ export abstract class Table extends AKElement { private renderRowGroup(items: T[]): TemplateResult[] { return items.map((item) => { const itemSelectHandler = (ev: InputEvent | PointerEvent) => { - let checked = false; const target = ev.target as HTMLElement; - if (ev.type === "input") { - checked = (target as HTMLInputElement).checked; - } else if (ev instanceof PointerEvent) { - if (target.classList.contains("ignore-click")) { - return; - } - checked = this.selectedElements.indexOf(item) === -1; - } - if (checked) { - // Prevent double-adding the element to selected items - if (this.selectedElements.indexOf(item) !== -1) { - return; - } - // Add item to selected - this.selectedElements.push(item); - } else { - // Get index of item and remove if selected - const index = this.selectedElements.indexOf(item); - if (index <= -1) return; - this.selectedElements.splice(index, 1); - } - this.requestUpdate(); - // Unset select-all if selectedElements is empty - const selectAllCheckbox = - this.shadowRoot?.querySelector("[name=select-all]"); - if (!selectAllCheckbox) { + if (ev instanceof PointerEvent && target.classList.contains("ignore-click")) { return; } - if (this.selectedElements.length < 1) { - selectAllCheckbox.checked = false; - this.requestUpdate(); + + const selected = this.selectedElements.includes(item); + const checked = + ev instanceof PointerEvent ? !selected : (target as HTMLInputElement).checked; + + if ((checked && selected) || !(checked || selected)) { + return; } + + this.selectedElements = this.selectedElements.filter((i) => i !== item); + if (checked) { + this.selectedElements.push(item); + } + + const selectAllCheckbox = + this.shadowRoot?.querySelector("[name=select-all]"); + if (selectAllCheckbox && this.selectedElements.length < 1) { + selectAllCheckbox.checked = false; + } + + this.requestUpdate(); }; - return html` + + const renderCheckbox = () => + html` + + `; + + const handleExpansion = (ev: Event) => { + ev.stopPropagation(); + const expanded = this.expandedElements.includes(item); + this.expandedElements = this.expandedElements.filter((i) => i !== item); + if (!expanded) { + this.expandedElements.push(item); + } + this.requestUpdate(); + }; + + const expandedClass = { + "pf-m-expanded": this.expandedElements.includes(item), + }; + + const renderExpansion = () => { + return html` + + `; + }; + + return html` - ${this.checkbox - ? html` - - ` - : html``} - ${this.expandable - ? html` - - ` - : html``} + ${this.checkbox ? renderCheckbox() : html``} + ${this.expandable ? renderExpansion() : html``} ${this.row(item).map((col) => { return html`${col}`; })} - + - ${this.expandedElements.indexOf(item) > -1 ? this.renderExpanded(item) : html``} + ${this.expandedElements.includes(item) ? this.renderExpanded(item) : html``} `; }); @@ -418,28 +394,24 @@ export abstract class Table extends AKElement { } renderSearch(): TemplateResult { - if (!this.searchEnabled()) { - return html``; - } - return html`
- { - this.search = value; - this.fetch(); - updateURLParams({ - search: value, - }); - }} - > - -
`; - } + const runSearch = (value: string) => { + this.search = value; + updateURLParams({ + search: value, + }); + this.fetch(); + }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - renderSelectedChip(item: T): TemplateResult { - return html``; + return !this.searchEnabled() + ? html`` + : html`
+ + +
`; } renderToolbarContainer(): TemplateResult { @@ -449,18 +421,7 @@ export abstract class Table extends AKElement {
${this.renderToolbar()}
${this.renderToolbarAfter()}
${this.renderToolbarSelected()}
- ${this.paginated - ? html` { - this.page = page; - updateURLParams({ tablePage: page }); - this.fetch(); - }} - > - ` - : html``} + ${this.paginated ? this.renderTablePagination() : html``} `; } @@ -469,57 +430,87 @@ export abstract class Table extends AKElement { this.fetch(); } + /* The checkbox on the table header row that allows the user to "activate all on this page," + * "deactivate all on this page" with a single click. + */ + renderAllOnThisPageCheckbox(): TemplateResult { + const checked = + this.selectedElements.length === this.data?.results.length && + this.selectedElements.length > 0; + + const onInput = (ev: InputEvent) => { + this.selectedElements = (ev.target as HTMLInputElement).checked + ? this.data?.results.slice(0) || [] + : []; + }; + + return html` + + `; + } + + /* For very large tables where the user is selecting a limited number of entries, we provide a + * chip-based subtable at the top that shows the list of selected entries. Long text result in + * ellipsized chips, which is sub-optimal. + */ + renderSelectedChip(_item: T): TemplateResult { + // Override this for chip-based displays + return html``; + } + + get needChipGroup() { + return this.checkbox && this.checkboxChip; + } + + renderChipGroup(): TemplateResult { + return html` + ${this.selectedElements.map((el) => { + return html`${this.renderSelectedChip(el)}`; + })} + `; + } + + /* A simple pagination display, shown at both the top and bottom of the page. */ + renderTablePagination(): TemplateResult { + const handler = (page: number) => { + updateURLParams({ tablePage: page }); + this.page = page; + this.fetch(); + }; + + return html` + + + `; + } + renderTable(): TemplateResult { - return html` ${this.checkbox && this.checkboxChip - ? html` - ${this.selectedElements.map((el) => { - return html`${this.renderSelectedChip(el)}`; - })} - ` - : html``} + const renderBottomPagination = () => + html`
${this.renderTablePagination()}
`; + + return html` ${this.needChipGroup ? this.renderChipGroup() : html``} ${this.renderToolbarContainer()} - ${this.checkbox - ? html`` - : html``} + ${this.checkbox ? this.renderAllOnThisPageCheckbox() : html``} ${this.expandable ? html`` : html``} ${this.columns().map((col) => col.render(this))} ${this.renderRows()}
- 0} - @input=${(ev: InputEvent) => { - if ((ev.target as HTMLInputElement).checked) { - this.selectedElements = - this.data?.results.slice(0) || []; - } else { - this.selectedElements = []; - } - }} - /> -
- ${this.paginated - ? html`
- { - this.page = page; - this.fetch(); - }} - > - -
` - : html``}`; + ${this.paginated ? renderBottomPagination() : html``}`; } render(): TemplateResult {