web: Fix user list default paths. (#23062)

This commit is contained in:
Teffen Ellis
2026-06-16 15:57:08 +02:00
committed by GitHub
parent 4104af4a45
commit 40caedfbd0
3 changed files with 165 additions and 117 deletions
+47 -14
View File
@@ -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>
+1 -1
View File
@@ -93,7 +93,7 @@ export const DefaultUIConfig = {
},
locale: "",
defaults: {
userPath: "",
userPath: "users",
},
} as const satisfies UIConfig;
+117 -102
View File
@@ -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;