mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
web: Fix user list default paths. (#23062)
This commit is contained in:
@@ -98,9 +98,12 @@ export class UserListPage extends WithLicenseSummary(
|
||||
@property({ type: String })
|
||||
public order = "-last_login";
|
||||
|
||||
@property({ type: String, useDefault: true })
|
||||
@property({ type: String, attribute: "active-path", useDefault: true })
|
||||
public activePath: string = DefaultUIConfig.defaults.userPath;
|
||||
|
||||
@property({ type: String, attribute: "default-active-path", useDefault: true })
|
||||
public defaultActivePath: string = DefaultUIConfig.defaults.userPath;
|
||||
|
||||
@state()
|
||||
protected hideDeactivated = getURLParam<boolean>("hideDeactivated", false);
|
||||
|
||||
@@ -109,18 +112,39 @@ export class UserListPage extends WithLicenseSummary(
|
||||
|
||||
protected canImpersonate = false;
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
//#region Lifecycle
|
||||
|
||||
/**
|
||||
* Synchronizes `activePath` and `defaultActivePath` from three sources in priority order:
|
||||
*
|
||||
* 1. URL param (explicit navigation)
|
||||
* 2. Brand default user path (admin-configured override)
|
||||
* 3. Compiled-in `DefaultUIConfig` default (fallback)
|
||||
*
|
||||
* `activePath` is set to `""` (show all users) when neither a URL param nor a
|
||||
* brand-level override is present, avoiding silent list filtering.
|
||||
* `defaultActivePath` always resolves to a value via the fallback chain.
|
||||
*/
|
||||
protected synchronizeUserPaths(): void {
|
||||
this.canImpersonate = this.can(CapabilitiesEnum.CanImpersonate);
|
||||
|
||||
const initialDefaultUserPath = DefaultUIConfig.defaults.userPath;
|
||||
const brandDefaultUserPath = this.uiConfig.defaults.userPath;
|
||||
const defaultUserPath = brandDefaultUserPath || initialDefaultUserPath;
|
||||
const userPathParam = getURLParam<string>("path", "");
|
||||
|
||||
this.activePath = getURLParam<string>(
|
||||
"path",
|
||||
brandDefaultUserPath || initialDefaultUserPath,
|
||||
);
|
||||
const pathPresent =
|
||||
(userPathParam && userPathParam !== "") || defaultUserPath !== initialDefaultUserPath;
|
||||
|
||||
const resolvedUserPath = pathPresent ? userPathParam || defaultUserPath : "";
|
||||
|
||||
this.activePath = resolvedUserPath;
|
||||
this.defaultActivePath = userPathParam || defaultUserPath;
|
||||
}
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.synchronizeUserPaths();
|
||||
}
|
||||
|
||||
protected override async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
||||
@@ -138,6 +162,17 @@ export class UserListPage extends WithLicenseSummary(
|
||||
return users;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Event Listeners
|
||||
|
||||
protected treeViewRefreshListener = (ev: CustomEvent<{ path: string }>) => {
|
||||
this.activePath = ev.detail.path;
|
||||
this.defaultActivePath = ev.detail.path;
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
protected buildExportParams = async (): Promise<CoreUsersExportCreateRequest> => {
|
||||
return {
|
||||
...(await this.defaultEndpointConfig()),
|
||||
@@ -346,15 +381,15 @@ export class UserListPage extends WithLicenseSummary(
|
||||
}
|
||||
|
||||
protected renderObjectCreate(): SlottedTemplateResult {
|
||||
const { activePath } = this;
|
||||
const { defaultActivePath } = this;
|
||||
|
||||
return guard([activePath], () => {
|
||||
return guard([defaultActivePath], () => {
|
||||
return [
|
||||
html`<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
type="button"
|
||||
${modalInvoker(AKUserWizard, {
|
||||
defaultPath: activePath,
|
||||
defaultPath: defaultActivePath,
|
||||
})}
|
||||
aria-description=${msg("Open the new user wizard")}
|
||||
>
|
||||
@@ -383,10 +418,8 @@ export class UserListPage extends WithLicenseSummary(
|
||||
<ak-treeview
|
||||
label=${msg("User paths")}
|
||||
.items=${this.userPaths?.paths || []}
|
||||
activePath=${this.activePath}
|
||||
@ak-refresh=${(ev: CustomEvent<{ path: string }>) => {
|
||||
this.activePath = ev.detail.path;
|
||||
}}
|
||||
default-active-path=${this.activePath}
|
||||
@ak-refresh=${this.treeViewRefreshListener}
|
||||
></ak-treeview>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ export const DefaultUIConfig = {
|
||||
},
|
||||
locale: "",
|
||||
defaults: {
|
||||
userPath: "",
|
||||
userPath: "users",
|
||||
},
|
||||
} as const satisfies UIConfig;
|
||||
|
||||
|
||||
+117
-102
@@ -2,16 +2,19 @@ import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { setURLParams } from "#elements/router/RouteMatch";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { CSSResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFTreeView from "@patternfly/patternfly/components/TreeView/tree-view.css";
|
||||
|
||||
//#region Tree View Node
|
||||
|
||||
export interface TreeViewItem {
|
||||
id?: string;
|
||||
id: string | null;
|
||||
label: string;
|
||||
childItems: TreeViewItem[];
|
||||
parent?: TreeViewItem;
|
||||
@@ -21,33 +24,35 @@ export interface TreeViewItem {
|
||||
@customElement("ak-treeview-node")
|
||||
export class TreeViewNode extends AKElement {
|
||||
@property({ attribute: false })
|
||||
item?: TreeViewItem;
|
||||
public item: TreeViewItem | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
open = false;
|
||||
public open = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
host?: TreeView;
|
||||
public host: TreeView | null = null;
|
||||
|
||||
@property()
|
||||
activePath = "";
|
||||
@property({ type: String, attribute: "active-path" })
|
||||
public activePath = "";
|
||||
|
||||
@property()
|
||||
separator = "";
|
||||
@property({ type: String })
|
||||
public separator = "";
|
||||
|
||||
get openable(): boolean {
|
||||
public get openable(): boolean {
|
||||
return (this.item?.childItems || []).length > 0;
|
||||
}
|
||||
|
||||
get fullPath(): string {
|
||||
public get fullPath(): string {
|
||||
const pathItems = [];
|
||||
let item = this.item;
|
||||
|
||||
while (item) {
|
||||
if (item.id) {
|
||||
pathItems.push(item.id);
|
||||
}
|
||||
item = item.parent;
|
||||
item = item.parent || null;
|
||||
}
|
||||
|
||||
return pathItems.reverse().join(this.separator);
|
||||
}
|
||||
|
||||
@@ -55,7 +60,7 @@ export class TreeViewNode extends AKElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
protected override firstUpdated(): void {
|
||||
const pathSegments = this.activePath.split(this.separator);
|
||||
const level = this.item?.level || 0;
|
||||
// Ignore the last item as that shouldn't be expanded
|
||||
@@ -63,7 +68,7 @@ export class TreeViewNode extends AKElement {
|
||||
if (pathSegments[level] === this.item?.id) {
|
||||
this.open = true;
|
||||
}
|
||||
if (this.activePath === this.fullPath && this.host !== undefined) {
|
||||
if (this.activePath === this.fullPath && this.host) {
|
||||
this.host.activeNode = this;
|
||||
}
|
||||
}
|
||||
@@ -84,85 +89,87 @@ export class TreeViewNode extends AKElement {
|
||||
);
|
||||
};
|
||||
|
||||
render(): TemplateResult {
|
||||
protected override render(): SlottedTemplateResult {
|
||||
const shouldRenderChildren = (this.item?.childItems || []).length > 0 && this.open;
|
||||
const itemLabel = this.item?.label || msg("Unnamed");
|
||||
const current = this.host?.activeNode === this;
|
||||
|
||||
return html`
|
||||
<li
|
||||
class="pf-c-tree-view__list-item ${this.open ? "pf-m-expanded" : ""}"
|
||||
role="treeitem"
|
||||
aria-expanded=${ifPresent(this.openable, this.open ? "true" : "false")}
|
||||
aria-label=${itemLabel}
|
||||
aria-selected=${current ? "true" : "false"}
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="pf-c-tree-view__content">
|
||||
<div
|
||||
class="pf-c-tree-view__node ${current ? "pf-m-current" : ""}"
|
||||
@click=${this.#selectionListener}
|
||||
>
|
||||
<div class="pf-c-tree-view__node-container">
|
||||
${this.openable
|
||||
? html` <button
|
||||
type="button"
|
||||
aria-label=${ifPresent(
|
||||
this.openable,
|
||||
this.open
|
||||
? msg(str`Collapse "${itemLabel}"`)
|
||||
: msg(str`Expand "${itemLabel}"`),
|
||||
)}
|
||||
class="pf-c-tree-view__node-toggle"
|
||||
@click=${(e: Event) => {
|
||||
if (this.openable) {
|
||||
this.open = !this.open;
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-tree-view__node-toggle-icon">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>`
|
||||
: nothing}
|
||||
<span class="pf-c-tree-view__node-icon">
|
||||
<i
|
||||
class="fas ${this.open ? "fa-folder-open" : "fa-folder"}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label=${msg(str`Select "${itemLabel}"`)}
|
||||
@click=${this.#selectionListener}
|
||||
class="pf-c-tree-view__node-text"
|
||||
>
|
||||
${itemLabel}
|
||||
</button>
|
||||
</div>
|
||||
return html`<li
|
||||
class="pf-c-tree-view__list-item ${this.open ? "pf-m-expanded" : ""}"
|
||||
role="treeitem"
|
||||
aria-expanded=${ifPresent(this.openable, this.open ? "true" : "false")}
|
||||
aria-label=${itemLabel}
|
||||
aria-selected=${current ? "true" : "false"}
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="pf-c-tree-view__content">
|
||||
<div
|
||||
class="pf-c-tree-view__node ${current ? "pf-m-current" : ""}"
|
||||
@click=${this.#selectionListener}
|
||||
>
|
||||
<div class="pf-c-tree-view__node-container">
|
||||
${this.openable
|
||||
? html` <button
|
||||
type="button"
|
||||
aria-label=${ifPresent(
|
||||
this.openable,
|
||||
this.open
|
||||
? msg(str`Collapse "${itemLabel}"`)
|
||||
: msg(str`Expand "${itemLabel}"`),
|
||||
)}
|
||||
class="pf-c-tree-view__node-toggle"
|
||||
@click=${(e: Event) => {
|
||||
if (this.openable) {
|
||||
this.open = !this.open;
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-tree-view__node-toggle-icon">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>`
|
||||
: null}
|
||||
<span class="pf-c-tree-view__node-icon">
|
||||
<i
|
||||
class="fas ${this.open ? "fa-folder-open" : "fa-folder"}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label=${msg(str`Select "${itemLabel}"`)}
|
||||
@click=${this.#selectionListener}
|
||||
class="pf-c-tree-view__node-text"
|
||||
>
|
||||
${itemLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="pf-c-tree-view__list"
|
||||
?hidden=${!shouldRenderChildren}
|
||||
role="group"
|
||||
aria-label=${msg(str`Items of "${itemLabel}"`)}
|
||||
>
|
||||
${this.item?.childItems.map((item) => {
|
||||
return html`<ak-treeview-node
|
||||
.item=${item}
|
||||
activePath=${this.activePath}
|
||||
separator=${this.separator}
|
||||
.host=${this.host}
|
||||
></ak-treeview-node>`;
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
`;
|
||||
</div>
|
||||
<ul
|
||||
class="pf-c-tree-view__list"
|
||||
?hidden=${!shouldRenderChildren}
|
||||
role="group"
|
||||
aria-label=${msg(str`Items of "${itemLabel}"`)}
|
||||
>
|
||||
${this.item?.childItems.map((item) => {
|
||||
return html`<ak-treeview-node
|
||||
.item=${item}
|
||||
active-path=${this.activePath}
|
||||
separator=${this.separator}
|
||||
.host=${this.host}
|
||||
></ak-treeview-node>`;
|
||||
})}
|
||||
</ul>
|
||||
</li> `;
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Tree View
|
||||
|
||||
@customElement("ak-treeview")
|
||||
export class TreeView extends AKElement {
|
||||
static styles: CSSResult[] = [PFTreeView];
|
||||
@@ -171,61 +178,67 @@ export class TreeView extends AKElement {
|
||||
public label: string | null = null;
|
||||
|
||||
@property({ type: Array })
|
||||
items: string[] = [];
|
||||
public items: string[] = [];
|
||||
|
||||
@property()
|
||||
activePath = "";
|
||||
@property({ type: String, attribute: "default-active-path", useDefault: true })
|
||||
public defaultActivePath = "";
|
||||
|
||||
@state()
|
||||
activeNode?: TreeViewNode;
|
||||
@property({ attribute: false })
|
||||
public activeNode: TreeViewNode | null = null;
|
||||
|
||||
separator = "/";
|
||||
protected separator = "/";
|
||||
|
||||
public createNode(path: string[], parentItem: TreeViewItem, level: number): TreeViewItem {
|
||||
const id = path.shift() || null;
|
||||
const idx = parentItem.childItems.findIndex((item) => item.id === id);
|
||||
|
||||
createNode(path: string[], parentItem: TreeViewItem, level: number): TreeViewItem {
|
||||
const id = path.shift();
|
||||
const idx = parentItem.childItems.findIndex((e: TreeViewItem) => {
|
||||
return e.id === id;
|
||||
});
|
||||
if (idx < 0) {
|
||||
const item: TreeViewItem = {
|
||||
id: id,
|
||||
id,
|
||||
label: id || "",
|
||||
childItems: [],
|
||||
level: level,
|
||||
parent: parentItem,
|
||||
};
|
||||
|
||||
parentItem.childItems.push(item);
|
||||
if (path.length !== 0) {
|
||||
|
||||
if (path.length) {
|
||||
const child = this.createNode(path, item, level + 1);
|
||||
child.parent = item;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
return this.createNode(path, parentItem.childItems[idx], level + 1);
|
||||
}
|
||||
|
||||
parse(data: string[]): TreeViewItem {
|
||||
protected parse(data: string[]): TreeViewItem {
|
||||
const rootItem: TreeViewItem = {
|
||||
id: undefined,
|
||||
id: null,
|
||||
label: msg("Root"),
|
||||
childItems: [],
|
||||
level: -1,
|
||||
};
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const path: string = data[i];
|
||||
const split: string[] = path.split(this.separator);
|
||||
|
||||
this.createNode(split, rootItem, 0);
|
||||
}
|
||||
|
||||
return rootItem;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
protected override render(): SlottedTemplateResult {
|
||||
const rootItem = this.parse(this.items);
|
||||
|
||||
return html`<div class="pf-c-tree-view pf-m-guides">
|
||||
<ul class="pf-c-tree-view__list" role="tree" aria-label=${ifPresent(this.label)}>
|
||||
<ak-treeview-node
|
||||
.item=${rootItem}
|
||||
activePath=${this.activePath}
|
||||
active-path=${this.defaultActivePath}
|
||||
open
|
||||
separator=${this.separator}
|
||||
.host=${this}
|
||||
@@ -235,6 +248,8 @@ export class TreeView extends AKElement {
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-treeview": TreeView;
|
||||
|
||||
Reference in New Issue
Block a user