Compare commits
29 Commits
openapi-ge
...
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