From 0a0afbe08d2e061cae87c5ef191457171940009e Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Wed, 3 Jan 2024 13:28:19 -0800 Subject: [PATCH] web: multi-select Modified the display so that if it's a template we display it correctly opposite the text, and provide classes that can be used in the display to differentiate between the main label and the descriptive label. Added a sort key, so the select can sort the right-hand pane correctly. Fixed the `this.selected` setters to use Arrays instead of maps. Theoretically, this is terribly inefficient, as it makes it theoretically O(n^2) rather than O(1), but in practice even if both lists were 10,000 elements long a modern desktop could perform the entire scan in 150ms or so. --- web/src/admin/outposts/OutpostForm.ts | 19 ++++--- .../elements/ak-dual-select/ak-dual-select.ts | 51 +++++++++++-------- .../ak-dual-select-available-pane.ts | 7 ++- .../ak-dual-select/components/styles.css.ts | 22 +++++++- web/src/elements/ak-dual-select/types.ts | 4 +- 5 files changed, 69 insertions(+), 34 deletions(-) diff --git a/web/src/admin/outposts/OutpostForm.ts b/web/src/admin/outposts/OutpostForm.ts index 8e18818e7..93c2167bb 100644 --- a/web/src/admin/outposts/OutpostForm.ts +++ b/web/src/admin/outposts/OutpostForm.ts @@ -47,14 +47,17 @@ const providerListArgs = (page: number) => ({ page, }); -const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => [ - `${item.pk}`, - `${ - item.assignedBackchannelApplicationName - ? item.assignedBackchannelApplicationName - : item.assignedApplicationName - } (${item.name})`, -]; +const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => { + const label = item.assignedBackchannelApplicationName + ? item.assignedBackchannelApplicationName + : item.assignedApplicationName; + return [ + `${item.pk}`, + html`
${label}
+
${item.name}
`, + label, + ]; +}; const provisionMaker = (results: ProviderData) => ({ pagination: results.pagination, diff --git a/web/src/elements/ak-dual-select/ak-dual-select.ts b/web/src/elements/ak-dual-select/ak-dual-select.ts index 0fa9cee31..d42fb1bf7 100644 --- a/web/src/elements/ak-dual-select/ak-dual-select.ts +++ b/web/src/elements/ak-dual-select/ak-dual-select.ts @@ -5,7 +5,7 @@ import { } from "@goauthentik/elements/utils/eventEmitter"; import { msg, str } from "@lit/localize"; -import { PropertyValues, TemplateResult, html, nothing } from "lit"; +import { PropertyValues, html, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; import { createRef, ref } from "lit/directives/ref.js"; import type { Ref } from "lit/directives/ref.js"; @@ -32,9 +32,14 @@ import { } from "./constants"; import type { BasePagination, DualSelectPair } from "./types"; -type PairValue = string | TemplateResult; -type Pair = [string, PairValue]; -const alphaSort = ([_k1, v1]: Pair, [_k2, v2]: Pair) => (v1 < v2 ? -1 : v1 > v2 ? 1 : 0); +function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) { + const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2]; + return l < r ? -1 : l > r ? 1 : 0; +} + +function mapDualPairs(pairs: DualSelectPair[]) { + return new Map(pairs.map(([k, v, _]) => [k, v])); +} const styles = [PFBase, PFButton, globalVariables, mainStyles]; @@ -48,6 +53,11 @@ const styles = [PFBase, PFButton, globalVariables, mainStyles]; * @fires ak-dual-select-change - A custom change event with the current `selected` list. */ +const keyfinder = + (key: string) => + ([k]: DualSelectPair) => + k === key; + @customElement("ak-dual-select") export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) { static get styles() { @@ -159,22 +169,21 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE if (this.availablePane.value!.moveable.length === 0) { return; } - const options = new Map(this.options); - const selected = new Map(this.selected); - this.availablePane.value!.moveable.forEach((key) => { - const value = options.get(key); - if (value) { - selected.set(key, value); - } - }); - this.selected = Array.from(selected.entries()).sort(); + this.selected = this.availablePane.value!.moveable.reduce( + (acc, key) => { + const value = this.options.find(keyfinder(key)); + return value && !acc.find(keyfinder(value[0])) ? [...acc, value] : acc; + }, + [...this.selected], + ); + // This is where the information gets... lossy. Dammit. this.availablePane.value!.clearMove(); } addOne(key: string) { - const requested = this.options.find(([k, _]) => k === key); - if (requested) { - this.selected = Array.from(new Map([...this.selected, requested]).entries()).sort(); + const requested = this.options.find(keyfinder(key)); + if (requested && !this.selected.find(keyfinder(requested[0]))) { + this.selected = [...this.selected, requested]; } } @@ -182,8 +191,8 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE // updating the list of currently visible options; addAllVisible() { // Create a new array of all current options and selected, and de-dupe. - const selected = new Map([...this.options, ...this.selected]); - this.selected = Array.from(selected.entries()).sort(); + const selected = mapDualPairs([...this.options, ...this.selected]); + this.selected = Array.from(selected.entries()); this.availablePane.value!.clearMove(); } @@ -192,18 +201,18 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE return; } const deselected = new Set(this.selectedPane.value!.moveable); - this.selected = this.selected.filter(([key, _]) => !deselected.has(key)); + this.selected = this.selected.filter(([key]) => !deselected.has(key)); this.selectedPane.value!.clearMove(); } removeOne(key: string) { - this.selected = this.selected.filter(([k, _]) => k !== key); + this.selected = this.selected.filter(([k]) => k !== key); } removeAllVisible() { // Remove all the items from selected that are in the *currently visible* options list const options = new Set(this.options.map(([k, _]) => k)); - this.selected = this.selected.filter(([k, _]) => !options.has(k)); + this.selected = this.selected.filter(([k]) => !options.has(k)); this.selectedPane.value!.clearMove(); } diff --git a/web/src/elements/ak-dual-select/components/ak-dual-select-available-pane.ts b/web/src/elements/ak-dual-select/components/ak-dual-select-available-pane.ts index e7380ca98..06f724ea5 100644 --- a/web/src/elements/ak-dual-select/components/ak-dual-select-available-pane.ts +++ b/web/src/elements/ak-dual-select/components/ak-dual-select-available-pane.ts @@ -138,8 +138,11 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { ${label}${this.selected.has(key) - ? html`` + >${label}${this.selected.has(key) + ? html`` : nothing}