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.
This commit is contained in:
parent
95ec1a6bb2
commit
0a0afbe08d
|
@ -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`<div class="selection-main">${label}</div>
|
||||
<div class="selection-desc">${item.name}</div>`,
|
||||
label,
|
||||
];
|
||||
};
|
||||
|
||||
const provisionMaker = (results: ProviderData) => ({
|
||||
pagination: results.pagination,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -138,8 +138,11 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
|
|||
<span class="pf-c-dual-list-selector__item">
|
||||
<span class="pf-c-dual-list-selector__item-main">
|
||||
<span class="pf-c-dual-list-selector__item-text"
|
||||
>${label}${this.selected.has(key)
|
||||
? html`<i class="fa fa-check"></i>`
|
||||
><span>${label}</span>${this.selected.has(key)
|
||||
? html`<span
|
||||
class="pf-c-dual-list-selector__item-text-selected-indicator"
|
||||
><i class="fa fa-check"></i
|
||||
></span>`
|
||||
: nothing}</span
|
||||
></span
|
||||
></span
|
||||
|
|
|
@ -86,6 +86,8 @@ export const globalVariables = css`
|
|||
--pf-c-dual-list-selector__list-item--m-disabled__item-toggle-icon--Color: var(
|
||||
--pf-global--disabled-color--200
|
||||
);
|
||||
--pf-c-dual-list-selector--selection-desc--FontSize: var(--pf-global--FontSize--xs);
|
||||
--pf-c-dual-list-selector--selection-desc--Color: var(--pf-global--Color--dark-200);
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -151,6 +153,11 @@ export const listStyles = css`
|
|||
user-select: none;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.pf-c-dual-list-selector__item-text .selection-desc {
|
||||
font-size: var(--pf-c-dual-list-selector--selection-desc--FontSize);
|
||||
color: var(--pf-c-dual-list-selector--selection-desc--Color);
|
||||
}
|
||||
`;
|
||||
|
||||
export const selectedPaneStyles = css`
|
||||
|
@ -160,11 +167,22 @@ export const selectedPaneStyles = css`
|
|||
`;
|
||||
|
||||
export const availablePaneStyles = css`
|
||||
.pf-c-dual-list-selector__item-text {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.pf-c-dual-list-selector__item-text .pf-c-dual-list-selector__item-text-selected-indicator {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.pf-c-dual-list-selector__item-text i {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
font-weight: 200;
|
||||
color: var(--pf-global--palette--black-500);
|
||||
color: var(--pf-c-dual-list-selector--selection-desc--Color);
|
||||
font-size: var(--pf-global--FontSize--xs);
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -2,7 +2,9 @@ import { TemplateResult } from "lit";
|
|||
|
||||
import { Pagination } from "@goauthentik/api";
|
||||
|
||||
export type DualSelectPair = [string, string | TemplateResult];
|
||||
// Key, Label (string or TemplateResult), (optional) string to sort by. If the sort string is
|
||||
// missing, it will use the label, which doesn't always work for TemplateResults).
|
||||
export type DualSelectPair = [string, string | TemplateResult, string?];
|
||||
|
||||
export type BasePagination = Pick<
|
||||
Pagination,
|
||||
|
|
Reference in New Issue