Compare commits
	
		
			29 Commits
		
	
	
		
			remove-ses
			...
			web/legibi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1bc8fa0a9d | |||
| 3ec0d30965 | |||
| 50d2f69332 | |||
| 7d972ec711 | |||
| 854427e463 | |||
| be349e2e14 | |||
| bd0e81b8ad | |||
| f6afb59515 | |||
| dddde09be5 | |||
| 6d7fc94698 | |||
| 1dcf9108ad | |||
| 7bb6a3dfe6 | |||
| 9cc440eee1 | |||
| fe9e4526ac | |||
| 20b66f850c | |||
| 67b327414b | |||
| 5b8d86b5a9 | |||
| 67aed3e318 | |||
| 9809b94030 | |||
| e7527c551b | |||
| 36b10b434a | |||
| 831797b871 | |||
| 5cc2c0f45f | |||
| 32442766f4 | |||
| 75790909a8 | |||
| e0d5df89ca | |||
| f25a9c624e | |||
| 914993a788 | |||
| 89dad07a66 | 
| @ -1,6 +1,5 @@ | ||||
| import { AkControlElement } from "@goauthentik/elements/AkControlElement.js"; | ||||
| import { debounce } from "@goauthentik/elements/utils/debounce"; | ||||
| import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { PropertyValues, html } from "lit"; | ||||
| @ -12,6 +11,11 @@ import type { Pagination } from "@goauthentik/api"; | ||||
|  | ||||
| import "./ak-dual-select"; | ||||
| import { AkDualSelect } from "./ak-dual-select"; | ||||
| import { | ||||
|     DualSelectChangeEvent, | ||||
|     DualSelectPaginatorNavEvent, | ||||
|     DualSelectSearchEvent, | ||||
| } from "./events"; | ||||
| import type { DataProvider, DualSelectPair } from "./types"; | ||||
|  | ||||
| /** | ||||
| @ -26,7 +30,7 @@ import type { DataProvider, DualSelectPair } from "./types"; | ||||
|  */ | ||||
|  | ||||
| @customElement("ak-dual-select-provider") | ||||
| export class AkDualSelectProvider extends CustomListenerElement(AkControlElement) { | ||||
| export class AkDualSelectProvider extends AkControlElement { | ||||
|     /** A function that takes a page and returns the DualSelectPair[] collection with which to update | ||||
|      * the "Available" pane. | ||||
|      * | ||||
| @ -86,9 +90,9 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement | ||||
|         this.onNav = this.onNav.bind(this); | ||||
|         this.onChange = this.onChange.bind(this); | ||||
|         this.onSearch = this.onSearch.bind(this); | ||||
|         this.addCustomListener("ak-pagination-nav-to", this.onNav); | ||||
|         this.addCustomListener("ak-dual-select-change", this.onChange); | ||||
|         this.addCustomListener("ak-dual-select-search", this.onSearch); | ||||
|         this.addEventListener(DualSelectPaginatorNavEvent.eventName, this.onNav); | ||||
|         this.addEventListener(DualSelectSearchEvent.eventName, this.onSearch); | ||||
|         this.addEventListener(DualSelectChangeEvent.eventName, this.onChange); | ||||
|     } | ||||
|  | ||||
|     willUpdate(changedProperties: PropertyValues<this>) { | ||||
| @ -122,26 +126,16 @@ export class AkDualSelectProvider extends CustomListenerElement(AkControlElement | ||||
|         this.isLoading = false; | ||||
|     } | ||||
|  | ||||
|     onNav(event: Event) { | ||||
|         if (!(event instanceof CustomEvent)) { | ||||
|             throw new Error(`Expecting a CustomEvent for navigation, received ${event} instead`); | ||||
|         } | ||||
|         this.fetch(event.detail); | ||||
|     onNav(event: DualSelectPaginatorNavEvent) { | ||||
|         this.fetch(event.page); | ||||
|     } | ||||
|  | ||||
|     onChange(event: Event) { | ||||
|         if (!(event instanceof CustomEvent)) { | ||||
|             throw new Error(`Expecting a CustomEvent for change, received ${event} instead`); | ||||
|         } | ||||
|         this.internalSelected = event.detail.value; | ||||
|         this.selected = this.internalSelected; | ||||
|     onChange(event: DualSelectChangeEvent) { | ||||
|         this.selected = this.internalSelected = event.selected; | ||||
|     } | ||||
|  | ||||
|     onSearch(event: Event) { | ||||
|         if (!(event instanceof CustomEvent)) { | ||||
|             throw new Error(`Expecting a CustomEvent for change, received ${event} instead`); | ||||
|         } | ||||
|         this.doSearch(event.detail); | ||||
|     onSearch(event: DualSelectSearchEvent) { | ||||
|         this.doSearch(event.search); | ||||
|     } | ||||
|  | ||||
|     doSearch(search: string) { | ||||
|  | ||||
| @ -1,8 +1,5 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { | ||||
|     CustomEmitterElement, | ||||
|     CustomListenerElement, | ||||
| } from "@goauthentik/elements/utils/eventEmitter"; | ||||
| import { match } from "ts-pattern"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { PropertyValues, html, nothing } from "lit"; | ||||
| @ -23,15 +20,13 @@ import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-p | ||||
| import "./components/ak-pagination"; | ||||
| import "./components/ak-search-bar"; | ||||
| import { | ||||
|     EVENT_ADD_ALL, | ||||
|     EVENT_ADD_ONE, | ||||
|     EVENT_ADD_SELECTED, | ||||
|     EVENT_DELETE_ALL, | ||||
|     EVENT_REMOVE_ALL, | ||||
|     EVENT_REMOVE_ONE, | ||||
|     EVENT_REMOVE_SELECTED, | ||||
| } from "./constants"; | ||||
| import type { BasePagination, DualSelectPair, SearchbarEvent } from "./types"; | ||||
|     DualSelectChangeEvent, | ||||
|     DualSelectMoveRequestEvent, | ||||
|     DualSelectPanelSearchEvent, | ||||
|     DualSelectSearchEvent, | ||||
|     DualSelectUpdateEvent, | ||||
| } from "./events"; | ||||
| import type { BasePagination, DualSelectPair } from "./types"; | ||||
|  | ||||
| function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) { | ||||
|     const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2]; | ||||
| @ -60,7 +55,7 @@ const keyfinder = | ||||
|         k === key; | ||||
|  | ||||
| @customElement("ak-dual-select") | ||||
| export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) { | ||||
| export class AkDualSelect extends AKElement { | ||||
|     static get styles() { | ||||
|         return styles; | ||||
|     } | ||||
| @ -96,21 +91,9 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE | ||||
|         super(); | ||||
|         this.handleMove = this.handleMove.bind(this); | ||||
|         this.handleSearch = this.handleSearch.bind(this); | ||||
|         [ | ||||
|             EVENT_ADD_ALL, | ||||
|             EVENT_ADD_SELECTED, | ||||
|             EVENT_DELETE_ALL, | ||||
|             EVENT_REMOVE_ALL, | ||||
|             EVENT_REMOVE_SELECTED, | ||||
|             EVENT_ADD_ONE, | ||||
|             EVENT_REMOVE_ONE, | ||||
|         ].forEach((eventName: string) => { | ||||
|             this.addCustomListener(eventName, (event: Event) => this.handleMove(eventName, event)); | ||||
|         }); | ||||
|         this.addCustomListener("ak-dual-select-move", () => { | ||||
|             this.requestUpdate(); | ||||
|         }); | ||||
|         this.addCustomListener("ak-search", this.handleSearch); | ||||
|         this.addEventListener(DualSelectMoveRequestEvent.eventName, this.handleMove); | ||||
|         this.addEventListener(DualSelectUpdateEvent.eventName, () => this.requestUpdate()); | ||||
|         this.addEventListener(DualSelectPanelSearchEvent.eventName, this.handleSearch); | ||||
|     } | ||||
|  | ||||
|     willUpdate(changedProperties: PropertyValues<this>) { | ||||
| @ -123,47 +106,17 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     handleMove(eventName: string, event: Event) { | ||||
|         if (!(event instanceof CustomEvent)) { | ||||
|             throw new Error(`Expected move event here, got ${eventName}`); | ||||
|         } | ||||
|  | ||||
|         switch (eventName) { | ||||
|             case EVENT_ADD_SELECTED: { | ||||
|                 this.addSelected(); | ||||
|                 break; | ||||
|             } | ||||
|             case EVENT_REMOVE_SELECTED: { | ||||
|                 this.removeSelected(); | ||||
|                 break; | ||||
|             } | ||||
|             case EVENT_ADD_ALL: { | ||||
|                 this.addAllVisible(); | ||||
|                 break; | ||||
|             } | ||||
|             case EVENT_REMOVE_ALL: { | ||||
|                 this.removeAllVisible(); | ||||
|                 break; | ||||
|             } | ||||
|             case EVENT_DELETE_ALL: { | ||||
|                 this.removeAll(); | ||||
|                 break; | ||||
|             } | ||||
|             case EVENT_ADD_ONE: { | ||||
|                 this.addOne(event.detail); | ||||
|                 break; | ||||
|             } | ||||
|             case EVENT_REMOVE_ONE: { | ||||
|                 this.removeOne(event.detail); | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             default: | ||||
|                 throw new Error( | ||||
|                     `AkDualSelect.handleMove received unknown event type: ${eventName}`, | ||||
|                 ); | ||||
|         } | ||||
|         this.dispatchCustomEvent("ak-dual-select-change", { value: this.value }); | ||||
|     handleMove(event: DualSelectMoveRequestEvent) { | ||||
|         match(event.move) | ||||
|             .with("add-all", () => this.addAllVisible()) | ||||
|             .with("add-one", () => this.addOne(event.key)) | ||||
|             .with("add-selected", () => this.addSelected()) | ||||
|             .with("delete-all", () => this.removeAll()) | ||||
|             .with("remove-all", () => this.removeAllVisible()) | ||||
|             .with("remove-one", () => this.removeOne(event.key)) | ||||
|             .with("remove-selected", () => this.removeSelected()) | ||||
|             .exhaustive(); | ||||
|         this.dispatchEvent(new DualSelectChangeEvent(this.value)); | ||||
|         event.stopPropagation(); | ||||
|     } | ||||
|  | ||||
| @ -182,7 +135,10 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE | ||||
|         this.availablePane.value!.clearMove(); | ||||
|     } | ||||
|  | ||||
|     addOne(key: string) { | ||||
|     addOne(key?: string) { | ||||
|         if (!key) { | ||||
|             return; | ||||
|         } | ||||
|         const requested = this.options.find(keyfinder(key)); | ||||
|         if (requested && !this.selected.find(keyfinder(requested[0]))) { | ||||
|             this.selected = [...this.selected, requested]; | ||||
| @ -207,7 +163,10 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE | ||||
|         this.selectedPane.value!.clearMove(); | ||||
|     } | ||||
|  | ||||
|     removeOne(key: string) { | ||||
|     removeOne(key?: string) { | ||||
|         if (!key) { | ||||
|             return; | ||||
|         } | ||||
|         this.selected = this.selected.filter(([k]) => k !== key); | ||||
|     } | ||||
|  | ||||
| @ -223,18 +182,18 @@ export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKE | ||||
|         this.selectedPane.value!.clearMove(); | ||||
|     } | ||||
|  | ||||
|     handleSearch(event: SearchbarEvent) { | ||||
|         switch (event.detail.source) { | ||||
|     handleSearch(event: DualSelectPanelSearchEvent) { | ||||
|         switch (event.source) { | ||||
|             case "ak-dual-list-available-search": | ||||
|                 return this.handleAvailableSearch(event.detail.value); | ||||
|                 return this.handleAvailableSearch(event.filterOn); | ||||
|             case "ak-dual-list-selected-search": | ||||
|                 return this.handleSelectedSearch(event.detail.value); | ||||
|                 return this.handleSelectedSearch(event.filterOn); | ||||
|         } | ||||
|         event.stopPropagation(); | ||||
|     } | ||||
|  | ||||
|     handleAvailableSearch(value: string) { | ||||
|         this.dispatchCustomEvent("ak-dual-select-search", value); | ||||
|         this.dispatchEvent(new DualSelectSearchEvent(value)); | ||||
|     } | ||||
|  | ||||
|     handleSelectedSearch(value: string) { | ||||
|  | ||||
| @ -1,26 +1,19 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||
| import { bound } from "@goauthentik/elements/decorators/bound"; | ||||
|  | ||||
| import { html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators.js"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { classMap } from "lit/directives/class-map.js"; | ||||
| import { map } from "lit/directives/map.js"; | ||||
|  | ||||
| import { availablePaneStyles, listStyles } from "./styles.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import { availablePaneStyles } from "./styles.css"; | ||||
|  | ||||
| import { EVENT_ADD_ONE } from "../constants"; | ||||
| import { | ||||
|     DualSelectMoveAvailableEvent, | ||||
|     DualSelectMoveRequestEvent, | ||||
|     DualSelectUpdateEvent, | ||||
| } from "../events"; | ||||
| import type { DualSelectPair } from "../types"; | ||||
|  | ||||
| const styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles]; | ||||
|  | ||||
| const hostAttributes = [ | ||||
|     ["aria-labelledby", "dual-list-selector-available-pane-status"], | ||||
|     ["aria-multiselectable", "true"], | ||||
|     ["role", "listbox"], | ||||
| ]; | ||||
| import { AkDualSelectAbstractPane } from "./ak-dual-select-pane"; | ||||
|  | ||||
| /** | ||||
|  * @element ak-dual-select-available-panel | ||||
| @ -40,9 +33,9 @@ const hostAttributes = [ | ||||
|  * | ||||
|  */ | ||||
| @customElement("ak-dual-select-available-pane") | ||||
| export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { | ||||
| export class AkDualSelectAvailablePane extends AkDualSelectAbstractPane { | ||||
|     static get styles() { | ||||
|         return styles; | ||||
|         return [...AkDualSelectAbstractPane.styles, availablePaneStyles]; | ||||
|     } | ||||
|  | ||||
|     /* The array of key/value pairs this pane is currently showing */ | ||||
| @ -56,68 +49,31 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) { | ||||
|     @property({ type: Object }) | ||||
|     readonly selected: Set<string> = new Set(); | ||||
|  | ||||
|     /* This is the only mutator for this object. It collects the list of objects the user has | ||||
|      * clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent | ||||
|      * orchestrator for the dual-select widget can and will access it to get the list of keys to be | ||||
|      * moved (removed) if the user so requests. | ||||
|      * | ||||
|      */ | ||||
|     @state() | ||||
|     public toMove: Set<string> = new Set(); | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.onClick = this.onClick.bind(this); | ||||
|         this.onMove = this.onMove.bind(this); | ||||
|     } | ||||
|  | ||||
|     connectedCallback() { | ||||
|         super.connectedCallback(); | ||||
|         hostAttributes.forEach(([attr, value]) => { | ||||
|             if (!this.hasAttribute(attr)) { | ||||
|                 this.setAttribute(attr, value); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     clearMove() { | ||||
|         this.toMove = new Set(); | ||||
|     } | ||||
|  | ||||
|     @bound | ||||
|     onClick(key: string) { | ||||
|         if (this.selected.has(key)) { | ||||
|             return; | ||||
|         } | ||||
|         if (this.toMove.has(key)) { | ||||
|             this.toMove.delete(key); | ||||
|         } else { | ||||
|             this.toMove.add(key); | ||||
|         } | ||||
|         this.dispatchCustomEvent( | ||||
|             "ak-dual-select-available-move-changed", | ||||
|             Array.from(this.toMove.values()).sort(), | ||||
|         ); | ||||
|         this.dispatchCustomEvent("ak-dual-select-move"); | ||||
|         this.move(key); | ||||
|         this.dispatchEvent(new DualSelectMoveAvailableEvent(this.moveable.sort())); | ||||
|         this.dispatchEvent(new DualSelectUpdateEvent()); | ||||
|         // Necessary because updating a map won't trigger a state change | ||||
|         this.requestUpdate(); | ||||
|     } | ||||
|  | ||||
|     @bound | ||||
|     onMove(key: string) { | ||||
|         this.toMove.delete(key); | ||||
|         this.dispatchCustomEvent(EVENT_ADD_ONE, key); | ||||
|         this.dispatchEvent(new DualSelectMoveRequestEvent("add-one", key)); | ||||
|         this.requestUpdate(); | ||||
|     } | ||||
|  | ||||
|     get moveable() { | ||||
|         return Array.from(this.toMove.values()); | ||||
|     } | ||||
|  | ||||
|     // DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and | ||||
|     // will not re-arrange or reconstruct the list automatically if the actual sources do not | ||||
|     // change; this allows the available pane to illustrate selected items with the checkmark | ||||
|     // without causing the list to scroll back up to the top. | ||||
|  | ||||
|     render() { | ||||
|     override render() { | ||||
|         return html` | ||||
|             <div class="pf-c-dual-list-selector__menu"> | ||||
|                 <ul class="pf-c-dual-list-selector__list"> | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { css, html, nothing } from "lit"; | ||||
| @ -8,13 +7,7 @@ import { customElement, property } from "lit/decorators.js"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { | ||||
|     EVENT_ADD_ALL, | ||||
|     EVENT_ADD_SELECTED, | ||||
|     EVENT_DELETE_ALL, | ||||
|     EVENT_REMOVE_ALL, | ||||
|     EVENT_REMOVE_SELECTED, | ||||
| } from "../constants"; | ||||
| import { DualSelectMoveRequestEvent, type MoveEventType } from "../events"; | ||||
|  | ||||
| const styles = [ | ||||
|     PFBase, | ||||
| @ -47,7 +40,7 @@ const styles = [ | ||||
|  */ | ||||
|  | ||||
| @customElement("ak-dual-select-controls") | ||||
| export class AkDualSelectControls extends CustomEmitterElement(AKElement) { | ||||
| export class AkDualSelectControls extends AKElement { | ||||
|     static get styles() { | ||||
|         return styles; | ||||
|     } | ||||
| @ -96,11 +89,11 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) { | ||||
|         this.onClick = this.onClick.bind(this); | ||||
|     } | ||||
|  | ||||
|     onClick(eventName: string) { | ||||
|         this.dispatchCustomEvent(eventName); | ||||
|     onClick(eventName: MoveEventType) { | ||||
|         this.dispatchEvent(new DualSelectMoveRequestEvent(eventName)); | ||||
|     } | ||||
|  | ||||
|     renderButton(label: string, event: string, active: boolean, direction: string) { | ||||
|     renderButton(label: string, event: MoveEventType, active: boolean, direction: string) { | ||||
|         return html` | ||||
|             <div class="pf-c-dual-list-selector__controls-item"> | ||||
|                 <button | ||||
| @ -121,23 +114,18 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) { | ||||
|     render() { | ||||
|         return html` | ||||
|             <div class="ak-dual-list-selector__controls"> | ||||
|                 ${this.renderButton( | ||||
|                     msg("Add"), | ||||
|                     EVENT_ADD_SELECTED, | ||||
|                     this.addActive, | ||||
|                     "fa-angle-right", | ||||
|                 )} | ||||
|                 ${this.renderButton(msg("Add"), "add-selected", this.addActive, "fa-angle-right")} | ||||
|                 ${this.selectAll | ||||
|                     ? html` | ||||
|                           ${this.renderButton( | ||||
|                               msg("Add All Available"), | ||||
|                               EVENT_ADD_ALL, | ||||
|                               "add-all", | ||||
|                               this.addAllActive, | ||||
|                               "fa-angle-double-right", | ||||
|                           )} | ||||
|                           ${this.renderButton( | ||||
|                               msg("Remove All Available"), | ||||
|                               EVENT_REMOVE_ALL, | ||||
|                               "remove-all", | ||||
|                               this.removeAllActive, | ||||
|                               "fa-angle-double-left", | ||||
|                           )} | ||||
| @ -145,14 +133,14 @@ export class AkDualSelectControls extends CustomEmitterElement(AKElement) { | ||||
|                     : nothing} | ||||
|                 ${this.renderButton( | ||||
|                     msg("Remove"), | ||||
|                     EVENT_REMOVE_SELECTED, | ||||
|                     "remove-selected", | ||||
|                     this.removeActive, | ||||
|                     "fa-angle-left", | ||||
|                 )} | ||||
|                 ${this.deleteAll | ||||
|                     ? html`${this.renderButton( | ||||
|                           msg("Remove All"), | ||||
|                           EVENT_DELETE_ALL, | ||||
|                           "delete-all", | ||||
|                           this.enableDeleteAll, | ||||
|                           "fa-times", | ||||
|                       )}` | ||||
|  | ||||
| @ -0,0 +1,75 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
|  | ||||
| import { TemplateResult } from "lit"; | ||||
| import { state } from "lit/decorators.js"; | ||||
|  | ||||
| import { listStyles } from "./styles.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| const styles = [PFBase, PFButton, PFDualListSelector, listStyles]; | ||||
|  | ||||
| const hostAttributes = [ | ||||
|     ["aria-labelledby", "dual-list-selector-selected-pane-status"], | ||||
|     ["aria-multiselectable", "true"], | ||||
|     ["role", "listbox"], | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|  * @element ak-dual-select-panel | ||||
|  * | ||||
|  * The "selected options" or "right" pane in a dual-list multi-select.  It receives from its parent | ||||
|  * a list of the selected options, and maintains an internal list of objects selected to move. | ||||
|  * | ||||
|  * @fires ak-dual-select-selected-move-changed - When the list of "to move" entries changed. | ||||
|  * Includes the current `toMove` content. | ||||
|  * | ||||
|  * @fires ak-dual-select-remove-one - Double-click with the element clicked on. | ||||
|  * | ||||
|  * It is not expected that the `ak-dual-select-selected-move-changed` will be used; instead, the | ||||
|  * attribute will be read by the parent when a control is clicked. | ||||
|  * | ||||
|  */ | ||||
| export abstract class AkDualSelectAbstractPane extends AKElement { | ||||
|     static get styles() { | ||||
|         return styles; | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      * This is the only mutator for this object. It collects the list of objects the user has | ||||
|      * clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent | ||||
|      * orchestrator for the dual-select widget can and will access it to get the list of keys to be | ||||
|      * moved (removed) if the user so requests. | ||||
|      * | ||||
|      */ | ||||
|     @state() | ||||
|     public toMove: Set<string> = new Set(); | ||||
|  | ||||
|     connectedCallback() { | ||||
|         super.connectedCallback(); | ||||
|         hostAttributes.forEach(([attr, value]) => { | ||||
|             if (!this.hasAttribute(attr)) { | ||||
|                 this.setAttribute(attr, value); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     clearMove() { | ||||
|         this.toMove = new Set(); | ||||
|     } | ||||
|  | ||||
|     move(key: string) { | ||||
|         if (this.toMove.has(key)) { | ||||
|             this.toMove.delete(key); | ||||
|         } else { | ||||
|             this.toMove.add(key); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     get moveable() { | ||||
|         return Array.from(this.toMove.values()); | ||||
|     } | ||||
|  | ||||
|     abstract render(): TemplateResult; | ||||
| } | ||||
| @ -1,26 +1,19 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||
| import { bound } from "@goauthentik/elements/decorators/bound"; | ||||
|  | ||||
| import { html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators.js"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { classMap } from "lit/directives/class-map.js"; | ||||
| import { map } from "lit/directives/map.js"; | ||||
|  | ||||
| import { listStyles, selectedPaneStyles } from "./styles.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import { selectedPaneStyles } from "./styles.css"; | ||||
|  | ||||
| import { EVENT_REMOVE_ONE } from "../constants"; | ||||
| import { | ||||
|     DualSelectMoveRequestEvent, | ||||
|     DualSelectMoveSelectedEvent, | ||||
|     DualSelectUpdateEvent, | ||||
| } from "../events"; | ||||
| import type { DualSelectPair } from "../types"; | ||||
|  | ||||
| const styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles]; | ||||
|  | ||||
| const hostAttributes = [ | ||||
|     ["aria-labelledby", "dual-list-selector-selected-pane-status"], | ||||
|     ["aria-multiselectable", "true"], | ||||
|     ["role", "listbox"], | ||||
| ]; | ||||
| import { AkDualSelectAbstractPane } from "./ak-dual-select-pane"; | ||||
|  | ||||
| /** | ||||
|  * @element ak-dual-select-available-panel | ||||
| @ -38,70 +31,32 @@ const hostAttributes = [ | ||||
|  * | ||||
|  */ | ||||
| @customElement("ak-dual-select-selected-pane") | ||||
| export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) { | ||||
| export class AkDualSelectSelectedPane extends AkDualSelectAbstractPane { | ||||
|     static get styles() { | ||||
|         return styles; | ||||
|         return [...AkDualSelectAbstractPane.styles, selectedPaneStyles]; | ||||
|     } | ||||
|  | ||||
|     /* The array of key/value pairs that are in the selected list.  ALL of them. */ | ||||
|     @property({ type: Array }) | ||||
|     readonly selected: DualSelectPair[] = []; | ||||
|  | ||||
|     /* | ||||
|      * This is the only mutator for this object. It collects the list of objects the user has | ||||
|      * clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent | ||||
|      * orchestrator for the dual-select widget can and will access it to get the list of keys to be | ||||
|      * moved (removed) if the user so requests. | ||||
|      * | ||||
|      */ | ||||
|     @state() | ||||
|     public toMove: Set<string> = new Set(); | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.onClick = this.onClick.bind(this); | ||||
|         this.onMove = this.onMove.bind(this); | ||||
|     } | ||||
|  | ||||
|     connectedCallback() { | ||||
|         super.connectedCallback(); | ||||
|         hostAttributes.forEach(([attr, value]) => { | ||||
|             if (!this.hasAttribute(attr)) { | ||||
|                 this.setAttribute(attr, value); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     clearMove() { | ||||
|         this.toMove = new Set(); | ||||
|     } | ||||
|  | ||||
|     @bound | ||||
|     onClick(key: string) { | ||||
|         if (this.toMove.has(key)) { | ||||
|             this.toMove.delete(key); | ||||
|         } else { | ||||
|             this.toMove.add(key); | ||||
|         } | ||||
|         this.dispatchCustomEvent( | ||||
|             "ak-dual-select-selected-move-changed", | ||||
|             Array.from(this.toMove.values()).sort(), | ||||
|         ); | ||||
|         this.dispatchCustomEvent("ak-dual-select-move"); | ||||
|         this.move(key); | ||||
|         this.dispatchEvent(new DualSelectMoveSelectedEvent(this.moveable.sort())); | ||||
|         this.dispatchEvent(new DualSelectUpdateEvent()); | ||||
|         // Necessary because updating a map won't trigger a state change | ||||
|         this.requestUpdate(); | ||||
|     } | ||||
|  | ||||
|     @bound | ||||
|     onMove(key: string) { | ||||
|         this.toMove.delete(key); | ||||
|         this.dispatchCustomEvent(EVENT_REMOVE_ONE, key); | ||||
|         this.dispatchEvent(new DualSelectMoveRequestEvent("remove-one", key)); | ||||
|         this.requestUpdate(); | ||||
|     } | ||||
|  | ||||
|     get moveable() { | ||||
|         return Array.from(this.toMove.values()); | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|     override render() { | ||||
|         return html` | ||||
|             <div class="pf-c-dual-list-selector__menu"> | ||||
|                 <ul class="pf-c-dual-list-selector__list"> | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { css, html, nothing } from "lit"; | ||||
| @ -9,6 +8,7 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import { DualSelectPaginatorNavEvent } from "../events"; | ||||
| import type { BasePagination } from "../types"; | ||||
|  | ||||
| const styles = [ | ||||
| @ -27,7 +27,7 @@ const styles = [ | ||||
| ]; | ||||
|  | ||||
| @customElement("ak-pagination") | ||||
| export class AkPagination extends CustomEmitterElement(AKElement) { | ||||
| export class AkPagination extends AKElement { | ||||
|     static get styles() { | ||||
|         return styles; | ||||
|     } | ||||
| @ -41,7 +41,7 @@ export class AkPagination extends CustomEmitterElement(AKElement) { | ||||
|     } | ||||
|  | ||||
|     onClick(nav: number | undefined) { | ||||
|         this.dispatchCustomEvent("ak-pagination-nav-to", nav ?? 0); | ||||
|         this.dispatchEvent(new DualSelectPaginatorNavEvent(nav ?? 0)); | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||
|  | ||||
| import { html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| @ -9,12 +8,12 @@ import type { Ref } from "lit/directives/ref.js"; | ||||
| import { globalVariables, searchStyles } from "./search.css"; | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| import type { SearchbarEvent } from "../types"; | ||||
| import { DualSelectPanelSearchEvent } from "../events"; | ||||
|  | ||||
| const styles = [PFBase, globalVariables, searchStyles]; | ||||
|  | ||||
| @customElement("ak-search-bar") | ||||
| export class AkSearchbar extends CustomEmitterElement(AKElement) { | ||||
| export class AkSearchbar extends AKElement { | ||||
|     static get styles() { | ||||
|         return styles; | ||||
|     } | ||||
| @ -40,10 +39,7 @@ export class AkSearchbar extends CustomEmitterElement(AKElement) { | ||||
|         if (this.input.value) { | ||||
|             this.value = this.input.value.value; | ||||
|         } | ||||
|         this.dispatchCustomEvent<SearchbarEvent>("ak-search", { | ||||
|             source: this.name, | ||||
|             value: this.value, | ||||
|         }); | ||||
|         this.dispatchEvent(new DualSelectPanelSearchEvent(this.name, this.value)); | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|  | ||||
							
								
								
									
										112
									
								
								web/src/elements/ak-dual-select/events.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								web/src/elements/ak-dual-select/events.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | ||||
| import { DualSelectPair } from "./types"; | ||||
|  | ||||
| // Handled by the Server layer provider | ||||
|  | ||||
| // Request to provide a different page of the paginated results in the "available" panel. | ||||
| export class DualSelectPaginatorNavEvent extends Event { | ||||
|     static readonly eventName = "ak-dual-select-paginator-nav"; | ||||
|     constructor(public page: number = 0) { | ||||
|         super(DualSelectPaginatorNavEvent.eventName, { bubbles: true, composed: true }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Request to provide a filtered collection for the "available" panel via a search string | ||||
| export class DualSelectSearchEvent extends Event { | ||||
|     static readonly eventName = "ak-dual-select-search"; | ||||
|     constructor(public search: string) { | ||||
|         super(DualSelectSearchEvent.eventName, { bubbles: true, composed: true }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Request to update the "selected" list in the provider | ||||
| export class DualSelectChangeEvent extends Event { | ||||
|     static readonly eventName = "ak-dual-select-change"; | ||||
|     constructor(public selected: DualSelectPair[]) { | ||||
|         super(DualSelectChangeEvent.eventName, { bubbles: true, composed: true }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Paginator and specific item events | ||||
|  | ||||
| export const moveEvents = [ | ||||
|     "add-all", | ||||
|     "add-one", | ||||
|     "add-selected", | ||||
|     "delete-all", | ||||
|     "remove-all", | ||||
|     "remove-one", | ||||
|     "remove-selected", | ||||
| ] as const; | ||||
|  | ||||
| export type MoveEventType = (typeof moveEvents)[number]; | ||||
|  | ||||
| // Request to add or remove all, some, or just one item from the "selected" panel | ||||
| export class DualSelectMoveRequestEvent extends Event { | ||||
|     static readonly eventName = "ak-dual-select-request-move"; | ||||
|     constructor( | ||||
|         public move: MoveEventType, | ||||
|         public key?: string, | ||||
|     ) { | ||||
|         super(DualSelectMoveRequestEvent.eventName, { bubbles: true, composed: true }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Update events | ||||
|  | ||||
| // Request to update the viewset | ||||
| export class DualSelectUpdateEvent extends Event { | ||||
|     static readonly eventName = "ak-dual-select-update"; | ||||
|     constructor() { | ||||
|         super(DualSelectUpdateEvent.eventName, { bubbles: true, composed: true }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| interface DualSelectMoveChangedEvent { | ||||
|     keys: string[]; | ||||
| } | ||||
|  | ||||
| // Request to update the list of "marked for move" items in the "available" panel | ||||
| export class DualSelectMoveAvailableEvent extends Event implements DualSelectMoveChangedEvent { | ||||
|     static readonly eventName = "ak-dual-select-move-available"; | ||||
|     constructor(public keys: string[]) { | ||||
|         super(DualSelectMoveAvailableEvent.eventName, { bubbles: true, composed: true }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Request to update the list of "marked for move" items in the "selected" panel | ||||
| export class DualSelectMoveSelectedEvent extends Event implements DualSelectMoveChangedEvent { | ||||
|     static readonly eventName = "ak-dual-select-move-selected"; | ||||
|     constructor(public keys: string[]) { | ||||
|         super(DualSelectMoveSelectedEvent.eventName, { bubbles: true, composed: true }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Request to update either panel with a Filter | ||||
| export class DualSelectPanelSearchEvent extends Event { | ||||
|     static readonly eventName = "ak-dual-select-panel-search"; | ||||
|     constructor( | ||||
|         public source: string, | ||||
|         public filterOn: string, | ||||
|     ) { | ||||
|         super(DualSelectPanelSearchEvent.eventName, { bubbles: true, composed: true }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|     interface HTMLElementEventMap { | ||||
|         [DualSelectUpdateEvent.eventName]: DualSelectUpdateEvent; | ||||
|         [DualSelectMoveAvailableEvent.eventName]: DualSelectMoveAvailableEvent; | ||||
|         [DualSelectMoveSelectedEvent.eventName]: DualSelectMoveSelectedEvent; | ||||
|         [DualSelectMoveRequestEvent.eventName]: DualSelectMoveRequestEvent; | ||||
|         [DualSelectPaginatorNavEvent.eventName]: DualSelectPaginatorNavEvent; | ||||
|         [DualSelectSearchEvent.eventName]: DualSelectSearchEvent; | ||||
|         [DualSelectChangeEvent.eventName]: DualSelectChangeEvent; | ||||
|         [DualSelectPanelSearchEvent.eventName]: DualSelectPanelSearchEvent; | ||||
|     } | ||||
|  | ||||
|     interface WindowEventMap { | ||||
|         [DualSelectMoveRequestEvent.eventName]: DualSelectMoveRequestEvent; | ||||
|         [DualSelectPaginatorNavEvent.eventName]: DualSelectPaginatorNavEvent; | ||||
|         [DualSelectMoveSelectedEvent.eventName]: DualSelectMoveSelectedEvent; | ||||
|     } | ||||
| } | ||||
| @ -6,6 +6,7 @@ import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import "../components/ak-dual-select-available-pane"; | ||||
| import { AkDualSelectAvailablePane } from "../components/ak-dual-select-available-pane"; | ||||
| import { DualSelectMoveSelectedEvent } from "../events"; | ||||
| import "./sb-host-provider"; | ||||
|  | ||||
| const metadata: Meta<AkDualSelectAvailablePane> = { | ||||
| @ -53,15 +54,15 @@ const container = (testItem: TemplateResult) => | ||||
|     </div>`; | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| const handleMoveChanged = (result: any) => { | ||||
| const handleMoveChanged = (result: DualSelectMoveSelectedEvent) => { | ||||
|     const target = document.querySelector("#action-button-message-pad"); | ||||
|     target!.innerHTML = ""; | ||||
|     result.detail.forEach((key: string) => { | ||||
|     result.keys.forEach((key: string) => { | ||||
|         target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!); | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| window.addEventListener("ak-dual-select-available-move-changed", handleMoveChanged); | ||||
| window.addEventListener(DualSelectMoveSelectedEvent.eventName, handleMoveChanged); | ||||
|  | ||||
| type Story = StoryObj; | ||||
|  | ||||
|  | ||||
| @ -5,6 +5,7 @@ import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import "../components/ak-dual-select-controls"; | ||||
| import { AkDualSelectControls } from "../components/ak-dual-select-controls"; | ||||
| import { DualSelectMoveRequestEvent } from "../events"; | ||||
|  | ||||
| const metadata: Meta<AkDualSelectControls> = { | ||||
|     title: "Elements / Dual Select / Control Panel", | ||||
| @ -59,10 +60,9 @@ const displayMessage = (result: any) => { | ||||
|     target!.appendChild(doc.firstChild!); | ||||
| }; | ||||
|  | ||||
| window.addEventListener("ak-dual-select-add", () => displayMessage("add")); | ||||
| window.addEventListener("ak-dual-select-remove", () => displayMessage("remove")); | ||||
| window.addEventListener("ak-dual-select-add-all", () => displayMessage("add all")); | ||||
| window.addEventListener("ak-dual-select-remove-all", () => displayMessage("remove all")); | ||||
| window.addEventListener(DualSelectMoveRequestEvent.eventName, (ev: DualSelectMoveRequestEvent) => | ||||
|     displayMessage(ev.move.toString()), | ||||
| ); | ||||
|  | ||||
| type Story = StoryObj; | ||||
|  | ||||
|  | ||||
| @ -9,6 +9,7 @@ import { Pagination } from "@goauthentik/api"; | ||||
|  | ||||
| import "../ak-dual-select"; | ||||
| import { AkDualSelect } from "../ak-dual-select"; | ||||
| import { DualSelectPaginatorNavEvent } from "../events"; | ||||
| import type { DualSelectPair } from "../types"; | ||||
|  | ||||
| const goodForYouRaw = ` | ||||
| @ -83,11 +84,11 @@ export class AkSbFruity extends LitElement { | ||||
|             totalPages: Math.ceil(this.options.length / this.pageLength), | ||||
|         }; | ||||
|         this.onNavigation = this.onNavigation.bind(this); | ||||
|         this.addEventListener("ak-pagination-nav-to", this.onNavigation); | ||||
|         this.addEventListener(DualSelectPaginatorNavEvent.eventName, this.onNavigation); | ||||
|     } | ||||
|  | ||||
|     onNavigation(evt: Event) { | ||||
|         const current: number = (evt as CustomEvent).detail; | ||||
|     onNavigation(evt: DualSelectPaginatorNavEvent) { | ||||
|         const current = evt.page; | ||||
|         const index = current - 1; | ||||
|         if (index * this.pageLength > this.options.length) { | ||||
|             console.warn( | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import "../components/ak-dual-select-selected-pane"; | ||||
| import { AkDualSelectSelectedPane } from "../components/ak-dual-select-selected-pane"; | ||||
| import { DualSelectMoveSelectedEvent } from "../events"; | ||||
| import "./sb-host-provider"; | ||||
|  | ||||
| const metadata: Meta<AkDualSelectSelectedPane> = { | ||||
| @ -50,15 +51,15 @@ const container = (testItem: TemplateResult) => | ||||
|     </div>`; | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| const handleMoveChanged = (result: any) => { | ||||
| const handleMoveChanged = (result: DualSelectMoveSelectedEvent) => { | ||||
|     const target = document.querySelector("#action-button-message-pad"); | ||||
|     target!.innerHTML = ""; | ||||
|     result.detail.forEach((key: string) => { | ||||
|     result.keys.forEach((key: string) => { | ||||
|         target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!); | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| window.addEventListener("ak-dual-select-selected-move-changed", handleMoveChanged); | ||||
| window.addEventListener(DualSelectMoveSelectedEvent.eventName, handleMoveChanged); | ||||
|  | ||||
| type Story = StoryObj; | ||||
|  | ||||
|  | ||||
| @ -5,6 +5,7 @@ import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import "../components/ak-pagination"; | ||||
| import { AkPagination } from "../components/ak-pagination"; | ||||
| import { DualSelectPaginatorNavEvent } from "../events"; | ||||
|  | ||||
| const metadata: Meta<AkPagination> = { | ||||
|     title: "Elements / Dual Select / Pagination Control", | ||||
| @ -43,18 +44,18 @@ const container = (testItem: TemplateResult) => | ||||
|     </div>`; | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| const handleMoveChanged = (result: any) => { | ||||
| const handleMoveChanged = (result: DualSelectPaginatorNavEvent) => { | ||||
|     console.debug(result); | ||||
|     const target = document.querySelector("#action-button-message-pad"); | ||||
|     target!.append( | ||||
|         new DOMParser().parseFromString( | ||||
|             `<li>Request to move to page ${result.detail}</li>`, | ||||
|             `<li>Request to move to page ${result.page}</li>`, | ||||
|             "text/xml", | ||||
|         ).firstChild!, | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| window.addEventListener("ak-pagination-nav-to", handleMoveChanged); | ||||
| window.addEventListener(DualSelectPaginatorNavEvent.eventName, handleMoveChanged); | ||||
|  | ||||
| type Story = StoryObj; | ||||
|  | ||||
|  | ||||
| @ -29,10 +29,3 @@ export type DataProvision = { | ||||
| }; | ||||
|  | ||||
| export type DataProvider = (page: number, search?: string) => Promise<DataProvision>; | ||||
|  | ||||
| export interface SearchbarEvent extends CustomEvent { | ||||
|     detail: { | ||||
|         source: string; | ||||
|         value: string; | ||||
|     }; | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	