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,
|
page,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => [
|
const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => {
|
||||||
`${item.pk}`,
|
const label = item.assignedBackchannelApplicationName
|
||||||
`${
|
? item.assignedBackchannelApplicationName
|
||||||
item.assignedBackchannelApplicationName
|
: item.assignedApplicationName;
|
||||||
? item.assignedBackchannelApplicationName
|
return [
|
||||||
: item.assignedApplicationName
|
`${item.pk}`,
|
||||||
} (${item.name})`,
|
html`<div class="selection-main">${label}</div>
|
||||||
];
|
<div class="selection-desc">${item.name}</div>`,
|
||||||
|
label,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
const provisionMaker = (results: ProviderData) => ({
|
const provisionMaker = (results: ProviderData) => ({
|
||||||
pagination: results.pagination,
|
pagination: results.pagination,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
} from "@goauthentik/elements/utils/eventEmitter";
|
} from "@goauthentik/elements/utils/eventEmitter";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
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 { customElement, property } from "lit/decorators.js";
|
||||||
import { createRef, ref } from "lit/directives/ref.js";
|
import { createRef, ref } from "lit/directives/ref.js";
|
||||||
import type { Ref } from "lit/directives/ref.js";
|
import type { Ref } from "lit/directives/ref.js";
|
||||||
|
@ -32,9 +32,14 @@ import {
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import type { BasePagination, DualSelectPair } from "./types";
|
import type { BasePagination, DualSelectPair } from "./types";
|
||||||
|
|
||||||
type PairValue = string | TemplateResult;
|
function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) {
|
||||||
type Pair = [string, PairValue];
|
const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2];
|
||||||
const alphaSort = ([_k1, v1]: Pair, [_k2, v2]: Pair) => (v1 < v2 ? -1 : v1 > v2 ? 1 : 0);
|
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];
|
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.
|
* @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")
|
@customElement("ak-dual-select")
|
||||||
export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) {
|
export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
|
@ -159,22 +169,21 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
if (this.availablePane.value!.moveable.length === 0) {
|
if (this.availablePane.value!.moveable.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const options = new Map(this.options);
|
this.selected = this.availablePane.value!.moveable.reduce(
|
||||||
const selected = new Map(this.selected);
|
(acc, key) => {
|
||||||
this.availablePane.value!.moveable.forEach((key) => {
|
const value = this.options.find(keyfinder(key));
|
||||||
const value = options.get(key);
|
return value && !acc.find(keyfinder(value[0])) ? [...acc, value] : acc;
|
||||||
if (value) {
|
},
|
||||||
selected.set(key, value);
|
[...this.selected],
|
||||||
}
|
);
|
||||||
});
|
// This is where the information gets... lossy. Dammit.
|
||||||
this.selected = Array.from(selected.entries()).sort();
|
|
||||||
this.availablePane.value!.clearMove();
|
this.availablePane.value!.clearMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
addOne(key: string) {
|
addOne(key: string) {
|
||||||
const requested = this.options.find(([k, _]) => k === key);
|
const requested = this.options.find(keyfinder(key));
|
||||||
if (requested) {
|
if (requested && !this.selected.find(keyfinder(requested[0]))) {
|
||||||
this.selected = Array.from(new Map([...this.selected, requested]).entries()).sort();
|
this.selected = [...this.selected, requested];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,8 +191,8 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
// updating the list of currently visible options;
|
// updating the list of currently visible options;
|
||||||
addAllVisible() {
|
addAllVisible() {
|
||||||
// Create a new array of all current options and selected, and de-dupe.
|
// Create a new array of all current options and selected, and de-dupe.
|
||||||
const selected = new Map([...this.options, ...this.selected]);
|
const selected = mapDualPairs([...this.options, ...this.selected]);
|
||||||
this.selected = Array.from(selected.entries()).sort();
|
this.selected = Array.from(selected.entries());
|
||||||
this.availablePane.value!.clearMove();
|
this.availablePane.value!.clearMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,18 +201,18 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const deselected = new Set(this.selectedPane.value!.moveable);
|
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();
|
this.selectedPane.value!.clearMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeOne(key: string) {
|
removeOne(key: string) {
|
||||||
this.selected = this.selected.filter(([k, _]) => k !== key);
|
this.selected = this.selected.filter(([k]) => k !== key);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllVisible() {
|
removeAllVisible() {
|
||||||
// Remove all the items from selected that are in the *currently visible* options list
|
// Remove all the items from selected that are in the *currently visible* options list
|
||||||
const options = new Set(this.options.map(([k, _]) => k));
|
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();
|
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">
|
||||||
<span class="pf-c-dual-list-selector__item-main">
|
<span class="pf-c-dual-list-selector__item-main">
|
||||||
<span class="pf-c-dual-list-selector__item-text"
|
<span class="pf-c-dual-list-selector__item-text"
|
||||||
>${label}${this.selected.has(key)
|
><span>${label}</span>${this.selected.has(key)
|
||||||
? html`<i class="fa fa-check"></i>`
|
? html`<span
|
||||||
|
class="pf-c-dual-list-selector__item-text-selected-indicator"
|
||||||
|
><i class="fa fa-check"></i
|
||||||
|
></span>`
|
||||||
: nothing}</span
|
: nothing}</span
|
||||||
></span
|
></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-c-dual-list-selector__list-item--m-disabled__item-toggle-icon--Color: var(
|
||||||
--pf-global--disabled-color--200
|
--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;
|
user-select: none;
|
||||||
flex-grow: 0;
|
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`
|
export const selectedPaneStyles = css`
|
||||||
|
@ -160,11 +167,22 @@ export const selectedPaneStyles = css`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const availablePaneStyles = 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 {
|
.pf-c-dual-list-selector__item-text i {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 0.5rem;
|
padding-left: 1rem;
|
||||||
font-weight: 200;
|
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);
|
font-size: var(--pf-global--FontSize--xs);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { TemplateResult } from "lit";
|
||||||
|
|
||||||
import { Pagination } from "@goauthentik/api";
|
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<
|
export type BasePagination = Pick<
|
||||||
Pagination,
|
Pagination,
|
||||||
|
|
Reference in New Issue