From 3f8b57896fce9ca4c84721f3880f543e9c3f3ef9 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Thu, 6 Aug 2026 15:14:31 -0400 Subject: [PATCH 1/6] web/admin: add next actions card on the user page Adds a card on the user overview page (shown with a valid enterprise license) to view, add, and remove the flows a user must complete on their next login, instead of editing the attribute as YAML. The flow picker excludes authentication and invalidation flows, matching the API validation. Documentation updated to make the card the primary path. --- web/src/admin/users/UserNextActionsCard.css | 21 ++ web/src/admin/users/UserNextActionsCard.ts | 195 ++++++++++++++++++ web/src/admin/users/UserOverviewTab.ts | 6 + .../docs/users-sources/user/next_actions.mdx | 16 +- 4 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 web/src/admin/users/UserNextActionsCard.css create mode 100644 web/src/admin/users/UserNextActionsCard.ts diff --git a/web/src/admin/users/UserNextActionsCard.css b/web/src/admin/users/UserNextActionsCard.css new file mode 100644 index 000000000000..ff3c76a8a1e4 --- /dev/null +++ b/web/src/admin/users/UserNextActionsCard.css @@ -0,0 +1,21 @@ +.ak-next-action { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--pf-global--spacer--sm); +} + +.ak-next-actions-empty { + color: var(--pf-global--Color--200); + margin: 0; +} + +.pf-c-card__footer { + display: flex; + align-items: flex-start; + gap: var(--pf-global--spacer--sm); +} + +.pf-c-card__footer ak-search-select { + flex-grow: 1; +} diff --git a/web/src/admin/users/UserNextActionsCard.ts b/web/src/admin/users/UserNextActionsCard.ts new file mode 100644 index 000000000000..3d2ad32b7c7c --- /dev/null +++ b/web/src/admin/users/UserNextActionsCard.ts @@ -0,0 +1,195 @@ +import "#elements/forms/SearchSelect/index"; + +import { aki } from "#common/api/client"; +import { AKRefreshEvent } from "#common/events"; + +import { AKElement } from "#elements/Base"; +import type { SearchSelectBase } from "#elements/forms/SearchSelect/SearchSelect"; +import { showAPIErrorMessage } from "#elements/messages/MessageContainer"; + +import { RenderFlowOption } from "#admin/flows/utils"; +import Styles from "#admin/users/UserNextActionsCard.css"; + +import { CoreApi, Flow, FlowDesignationEnum, FlowsApi, User } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFContent from "@patternfly/patternfly/components/Content/content.css"; + +export const USER_ATTRIBUTE_NEXT_ACTIONS = "goauthentik.io/user/next-actions"; + +const disallowedDesignations: FlowDesignationEnum[] = [ + FlowDesignationEnum.Authentication, + FlowDesignationEnum.Invalidation, +]; + +/** + * Card on the user overview page to manage the flows a user must complete + * on their next login. + */ +@customElement("ak-user-next-actions-card") +export class UserNextActionsCard extends AKElement { + #api = aki(CoreApi); + + @property({ attribute: false }) + public user?: User; + + @state() + protected selectedFlow: Flow | null = null; + + static styles = [PFCard, PFContent, PFButton, Styles]; + + protected get actions(): string[] { + const value = this.user?.attributes?.[USER_ATTRIBUTE_NEXT_ACTIONS]; + + if (typeof value === "string") { + return [value]; + } + + if (Array.isArray(value)) { + return value.filter((entry): entry is string => typeof entry === "string"); + } + + return []; + } + + protected fetchFlows = (query?: string): Promise => + aki(FlowsApi) + .flowsInstancesList({ + ordering: "slug", + ...(query ? { search: query } : {}), + }) + .then((flows) => + flows.results.filter( + (flow) => + !flow.designation || !disallowedDesignations.includes(flow.designation), + ), + ); + + #save(actions: string[]): Promise { + if (!this.user) { + return Promise.resolve(false); + } + + const attributes = { ...this.user.attributes }; + + if (actions.length) { + attributes[USER_ATTRIBUTE_NEXT_ACTIONS] = actions; + } else { + delete attributes[USER_ATTRIBUTE_NEXT_ACTIONS]; + } + + return this.#api + .coreUsersPartialUpdate({ + id: this.user.pk, + patchedUserRequest: { attributes }, + }) + .then(() => { + this.dispatchEvent(new AKRefreshEvent()); + return true; + }) + .catch((error) => { + showAPIErrorMessage(error); + return false; + }); + } + + protected addSelected = () => { + const slug = this.selectedFlow?.slug; + + if (!slug || this.actions.includes(slug)) { + return; + } + + this.#save([...this.actions, slug]).then((saved) => { + if (!saved) { + return; + } + + const search = + this.renderRoot.querySelector>("ak-search-select"); + + if (search) { + search.selectedObject = null; + } + + this.selectedFlow = null; + }); + }; + + protected removeAction = (slug: string) => { + this.#save(this.actions.filter((entry) => entry !== slug)); + }; + + protected override render() { + if (!this.user) { + return nothing; + } + + const actions = this.actions; + + return html` +
+ ${msg("Next actions on login", { id: "user-next-actions.card.title" })} +
+
+ ${actions.length + ? actions.map( + (slug) => html` +
+ ${slug} + +
+ `, + ) + : html`

+ ${msg("No actions pending.", { + id: "user-next-actions.empty.description", + })} +

`} +
+ + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-user-next-actions-card": UserNextActionsCard; + } +} diff --git a/web/src/admin/users/UserOverviewTab.ts b/web/src/admin/users/UserOverviewTab.ts index fdbf07e85a6a..27d4ec62553e 100644 --- a/web/src/admin/users/UserOverviewTab.ts +++ b/web/src/admin/users/UserOverviewTab.ts @@ -1,5 +1,6 @@ import "#admin/users/UserChart"; import "#admin/users/UserInfoCard"; +import "#admin/users/UserNextActionsCard"; import "#admin/users/UserNotesCard"; import "#components/ak-object-attributes-card"; import "#admin/events/ObjectChangelog"; @@ -66,6 +67,11 @@ export class UserOverviewTab extends AKElement { .objectAttributes=${this.user.attributes} > + ${this.hasEnterpriseLicense + ? html`
+ +
` + : nothing}
${msg("Changelog")}
**Users** and click the **Edit** icon of the user. -3. Add the flow slugs to the user's **Attributes** field: +2. Navigate to **Directory** > **Users** and click the name of the user. +3. In the **Next actions on login** card, select a flow and click **Add**. Pending actions are listed in the same card and can be removed there. - ```yaml - goauthentik.io/user/next-actions: - - default-password-change - ``` +Because the actions are stored in the `goauthentik.io/user/next-actions` attribute, they can also be set by editing the user's **Attributes** field directly, through the user API, a [blueprint](../../customize/blueprints/index.mdx), or from within a flow, for example with a [User Write stage](../../add-secure-apps/flows-stages/stages/user_write/index.md) in an enrollment flow: -4. Click **Update**. - -The attribute can also be set through the user API, a [blueprint](../../customize/blueprints/index.mdx), or from within a flow, for example with a [User Write stage](../../add-secure-apps/flows-stages/stages/user_write/index.md) in an enrollment flow. +```yaml +goauthentik.io/user/next-actions: + - default-password-change +``` From 513650fa6bdf6f176933450145220d3f4985f2b2 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Thu, 6 Aug 2026 15:37:18 -0400 Subject: [PATCH 2/6] web/admin: polish next actions card, label next action events Pending actions render as removable chips instead of plain rows, and the new next action events show readable labels in the event log. --- web/src/admin/users/UserNextActionsCard.css | 7 ----- web/src/admin/users/UserNextActionsCard.ts | 29 ++++++++++----------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/web/src/admin/users/UserNextActionsCard.css b/web/src/admin/users/UserNextActionsCard.css index ff3c76a8a1e4..c29535be6303 100644 --- a/web/src/admin/users/UserNextActionsCard.css +++ b/web/src/admin/users/UserNextActionsCard.css @@ -1,10 +1,3 @@ -.ak-next-action { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--pf-global--spacer--sm); -} - .ak-next-actions-empty { color: var(--pf-global--Color--200); margin: 0; diff --git a/web/src/admin/users/UserNextActionsCard.ts b/web/src/admin/users/UserNextActionsCard.ts index 3d2ad32b7c7c..c54df4527298 100644 --- a/web/src/admin/users/UserNextActionsCard.ts +++ b/web/src/admin/users/UserNextActionsCard.ts @@ -1,3 +1,5 @@ +import "#elements/chips/Chip"; +import "#elements/chips/ChipGroup"; import "#elements/forms/SearchSelect/index"; import { aki } from "#common/api/client"; @@ -138,22 +140,19 @@ export class UserNextActionsCard extends AKElement {
${actions.length - ? actions.map( - (slug) => html` -
- ${slug} - -
- `, - ) + ${slug} + + `, + )} + ` : html`

${msg("No actions pending.", { id: "user-next-actions.empty.description", From 439021eeab27e7d87d52997fbacec954ee522e33 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Thu, 6 Aug 2026 15:48:09 -0400 Subject: [PATCH 3/6] web/admin: make next actions card full width --- web/src/admin/users/UserNextActionsCard.css | 1 + web/src/admin/users/UserOverviewTab.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/admin/users/UserNextActionsCard.css b/web/src/admin/users/UserNextActionsCard.css index c29535be6303..5cf0ae8cc2a3 100644 --- a/web/src/admin/users/UserNextActionsCard.css +++ b/web/src/admin/users/UserNextActionsCard.css @@ -11,4 +11,5 @@ .pf-c-card__footer ak-search-select { flex-grow: 1; + max-width: 32rem; } diff --git a/web/src/admin/users/UserOverviewTab.ts b/web/src/admin/users/UserOverviewTab.ts index 27d4ec62553e..cc88177fc9ba 100644 --- a/web/src/admin/users/UserOverviewTab.ts +++ b/web/src/admin/users/UserOverviewTab.ts @@ -68,7 +68,7 @@ export class UserOverviewTab extends AKElement { >

${this.hasEnterpriseLicense - ? html`
+ ? html`
` : nothing} From b0066917688372d538cb4f4e524ac005f1f7a9e9 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Thu, 6 Aug 2026 15:53:49 -0400 Subject: [PATCH 4/6] web/admin: list next actions with truncation instead of chips --- web/src/admin/users/UserNextActionsCard.css | 14 ++++++++ web/src/admin/users/UserNextActionsCard.ts | 36 ++++++++++++++------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/web/src/admin/users/UserNextActionsCard.css b/web/src/admin/users/UserNextActionsCard.css index 5cf0ae8cc2a3..99273cbd0df8 100644 --- a/web/src/admin/users/UserNextActionsCard.css +++ b/web/src/admin/users/UserNextActionsCard.css @@ -1,3 +1,17 @@ +.ak-next-action-slug { + align-self: center; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; +} + +.pf-c-data-list__item-action { + display: flex; + align-items: center; +} + .ak-next-actions-empty { color: var(--pf-global--Color--200); margin: 0; diff --git a/web/src/admin/users/UserNextActionsCard.ts b/web/src/admin/users/UserNextActionsCard.ts index c54df4527298..ca0b2ec824d2 100644 --- a/web/src/admin/users/UserNextActionsCard.ts +++ b/web/src/admin/users/UserNextActionsCard.ts @@ -1,5 +1,3 @@ -import "#elements/chips/Chip"; -import "#elements/chips/ChipGroup"; import "#elements/forms/SearchSelect/index"; import { aki } from "#common/api/client"; @@ -21,6 +19,7 @@ import { customElement, property, state } from "lit/decorators.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css"; +import PFDataList from "@patternfly/patternfly/components/DataList/data-list.css"; export const USER_ATTRIBUTE_NEXT_ACTIONS = "goauthentik.io/user/next-actions"; @@ -43,7 +42,7 @@ export class UserNextActionsCard extends AKElement { @state() protected selectedFlow: Flow | null = null; - static styles = [PFCard, PFContent, PFButton, Styles]; + static styles = [PFCard, PFContent, PFButton, PFDataList, Styles]; protected get actions(): string[] { const value = this.user?.attributes?.[USER_ATTRIBUTE_NEXT_ACTIONS]; @@ -140,19 +139,32 @@ export class UserNextActionsCard extends AKElement {
${actions.length - ? html` + ? html`
    ${actions.map( (slug) => html` - this.removeAction(slug)} - > - ${slug} - +
  • +
    +
    + ${slug} +
    +
    + +
    +
    +
  • `, )} - ` +
` : html`

${msg("No actions pending.", { id: "user-next-actions.empty.description", From 2dc9441822e9499b162879deaa6e2ff2edcf8a20 Mon Sep 17 00:00:00 2001 From: Dominic R Date: Thu, 6 Aug 2026 15:59:28 -0400 Subject: [PATCH 5/6] web/admin: use a standard table for next actions --- web/src/admin/users/UserNextActionsCard.css | 29 --- web/src/admin/users/UserNextActionsCard.ts | 206 ------------------- web/src/admin/users/UserNextActionsList.ts | 209 ++++++++++++++++++++ web/src/admin/users/UserOverviewTab.ts | 7 +- 4 files changed, 214 insertions(+), 237 deletions(-) delete mode 100644 web/src/admin/users/UserNextActionsCard.css delete mode 100644 web/src/admin/users/UserNextActionsCard.ts create mode 100644 web/src/admin/users/UserNextActionsList.ts diff --git a/web/src/admin/users/UserNextActionsCard.css b/web/src/admin/users/UserNextActionsCard.css deleted file mode 100644 index 99273cbd0df8..000000000000 --- a/web/src/admin/users/UserNextActionsCard.css +++ /dev/null @@ -1,29 +0,0 @@ -.ak-next-action-slug { - align-self: center; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: block; -} - -.pf-c-data-list__item-action { - display: flex; - align-items: center; -} - -.ak-next-actions-empty { - color: var(--pf-global--Color--200); - margin: 0; -} - -.pf-c-card__footer { - display: flex; - align-items: flex-start; - gap: var(--pf-global--spacer--sm); -} - -.pf-c-card__footer ak-search-select { - flex-grow: 1; - max-width: 32rem; -} diff --git a/web/src/admin/users/UserNextActionsCard.ts b/web/src/admin/users/UserNextActionsCard.ts deleted file mode 100644 index ca0b2ec824d2..000000000000 --- a/web/src/admin/users/UserNextActionsCard.ts +++ /dev/null @@ -1,206 +0,0 @@ -import "#elements/forms/SearchSelect/index"; - -import { aki } from "#common/api/client"; -import { AKRefreshEvent } from "#common/events"; - -import { AKElement } from "#elements/Base"; -import type { SearchSelectBase } from "#elements/forms/SearchSelect/SearchSelect"; -import { showAPIErrorMessage } from "#elements/messages/MessageContainer"; - -import { RenderFlowOption } from "#admin/flows/utils"; -import Styles from "#admin/users/UserNextActionsCard.css"; - -import { CoreApi, Flow, FlowDesignationEnum, FlowsApi, User } from "@goauthentik/api"; - -import { msg } from "@lit/localize"; -import { html, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; - -import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFCard from "@patternfly/patternfly/components/Card/card.css"; -import PFContent from "@patternfly/patternfly/components/Content/content.css"; -import PFDataList from "@patternfly/patternfly/components/DataList/data-list.css"; - -export const USER_ATTRIBUTE_NEXT_ACTIONS = "goauthentik.io/user/next-actions"; - -const disallowedDesignations: FlowDesignationEnum[] = [ - FlowDesignationEnum.Authentication, - FlowDesignationEnum.Invalidation, -]; - -/** - * Card on the user overview page to manage the flows a user must complete - * on their next login. - */ -@customElement("ak-user-next-actions-card") -export class UserNextActionsCard extends AKElement { - #api = aki(CoreApi); - - @property({ attribute: false }) - public user?: User; - - @state() - protected selectedFlow: Flow | null = null; - - static styles = [PFCard, PFContent, PFButton, PFDataList, Styles]; - - protected get actions(): string[] { - const value = this.user?.attributes?.[USER_ATTRIBUTE_NEXT_ACTIONS]; - - if (typeof value === "string") { - return [value]; - } - - if (Array.isArray(value)) { - return value.filter((entry): entry is string => typeof entry === "string"); - } - - return []; - } - - protected fetchFlows = (query?: string): Promise => - aki(FlowsApi) - .flowsInstancesList({ - ordering: "slug", - ...(query ? { search: query } : {}), - }) - .then((flows) => - flows.results.filter( - (flow) => - !flow.designation || !disallowedDesignations.includes(flow.designation), - ), - ); - - #save(actions: string[]): Promise { - if (!this.user) { - return Promise.resolve(false); - } - - const attributes = { ...this.user.attributes }; - - if (actions.length) { - attributes[USER_ATTRIBUTE_NEXT_ACTIONS] = actions; - } else { - delete attributes[USER_ATTRIBUTE_NEXT_ACTIONS]; - } - - return this.#api - .coreUsersPartialUpdate({ - id: this.user.pk, - patchedUserRequest: { attributes }, - }) - .then(() => { - this.dispatchEvent(new AKRefreshEvent()); - return true; - }) - .catch((error) => { - showAPIErrorMessage(error); - return false; - }); - } - - protected addSelected = () => { - const slug = this.selectedFlow?.slug; - - if (!slug || this.actions.includes(slug)) { - return; - } - - this.#save([...this.actions, slug]).then((saved) => { - if (!saved) { - return; - } - - const search = - this.renderRoot.querySelector>("ak-search-select"); - - if (search) { - search.selectedObject = null; - } - - this.selectedFlow = null; - }); - }; - - protected removeAction = (slug: string) => { - this.#save(this.actions.filter((entry) => entry !== slug)); - }; - - protected override render() { - if (!this.user) { - return nothing; - } - - const actions = this.actions; - - return html` -

- ${msg("Next actions on login", { id: "user-next-actions.card.title" })} -
-
- ${actions.length - ? html`
    - ${actions.map( - (slug) => html` -
  • -
    -
    - ${slug} -
    -
    - -
    -
    -
  • - `, - )} -
` - : html`

- ${msg("No actions pending.", { - id: "user-next-actions.empty.description", - })} -

`} -
- - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ak-user-next-actions-card": UserNextActionsCard; - } -} diff --git a/web/src/admin/users/UserNextActionsList.ts b/web/src/admin/users/UserNextActionsList.ts new file mode 100644 index 000000000000..73e1e20159d0 --- /dev/null +++ b/web/src/admin/users/UserNextActionsList.ts @@ -0,0 +1,209 @@ +import "#elements/forms/DeleteBulkForm"; +import "#elements/forms/SearchSelect/index"; + +import { aki } from "#common/api/client"; +import { createPaginatedResponse } from "#common/api/responses"; +import { AKRefreshEvent } from "#common/events"; + +import type { SearchSelectBase } from "#elements/forms/SearchSelect/SearchSelect"; +import { showAPIErrorMessage } from "#elements/messages/MessageContainer"; +import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table"; +import { SlottedTemplateResult } from "#elements/types"; + +import { RenderFlowOption } from "#admin/flows/utils"; + +import { CoreApi, Flow, FlowDesignationEnum, FlowsApi, User } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { css, CSSResult, html, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +export const USER_ATTRIBUTE_NEXT_ACTIONS = "goauthentik.io/user/next-actions"; + +const disallowedDesignations: FlowDesignationEnum[] = [ + FlowDesignationEnum.Authentication, + FlowDesignationEnum.Invalidation, +]; + +interface NextActionRow { + name: string; + slug: string; + flow: Flow | null; +} + +function toSlugs(value: unknown): string[] { + if (typeof value === "string") { + return [value]; + } + + if (Array.isArray(value)) { + return value.filter((entry): entry is string => typeof entry === "string"); + } + + return []; +} + +/** + * Table on the user overview page listing the flows a user must complete on + * their next login. + */ +@customElement("ak-user-next-actions-list") +export class UserNextActionsList extends Table { + public static override verboseName = msg("Next action"); + public static override verboseNamePlural = msg("Next actions"); + + public static override styles: CSSResult[] = [ + ...super.styles, + css` + ak-search-select { + display: inline-block; + min-width: 24rem; + max-width: 32rem; + flex-grow: 1; + } + `, + ]; + + #api = aki(CoreApi); + + @property({ attribute: false }) + public user?: User; + + @state() + protected selectedFlow: Flow | null = null; + + protected override async apiEndpoint(): Promise> { + const slugs = toSlugs(this.user?.attributes?.[USER_ATTRIBUTE_NEXT_ACTIONS]); + const flows = slugs.length + ? (await aki(FlowsApi).flowsInstancesList({ ordering: "slug" })).results + : []; + + return createPaginatedResponse( + slugs.map((slug) => ({ + name: slug, + slug, + flow: flows.find((flow) => flow.slug === slug) ?? null, + })), + ); + } + + protected override columns: TableColumn[] = [[msg("Flow")], [msg("Slug")], [msg("Actions")]]; + + protected override updated(changed: PropertyValues) { + super.updated(changed); + + if (changed.has("user") && this.user) { + this.fetch(); + } + } + + async #patch(mutate: (actions: string[]) => string[]): Promise { + if (!this.user) { + return; + } + + // Re-fetch the user so consecutive changes don't work on stale attributes + const user = await this.#api.coreUsersRetrieve({ id: this.user.pk }); + const actions = mutate(toSlugs(user.attributes?.[USER_ATTRIBUTE_NEXT_ACTIONS])); + const attributes = { ...user.attributes }; + + if (actions.length) { + attributes[USER_ATTRIBUTE_NEXT_ACTIONS] = actions; + } else { + delete attributes[USER_ATTRIBUTE_NEXT_ACTIONS]; + } + + await this.#api.coreUsersPartialUpdate({ + id: user.pk, + patchedUserRequest: { attributes }, + }); + this.dispatchEvent(new AKRefreshEvent()); + } + + protected addSelected = () => { + const slug = this.selectedFlow?.slug; + + if (!slug) { + return; + } + + this.#patch((actions) => (actions.includes(slug) ? actions : [...actions, slug])) + .then(() => { + const search = + this.renderRoot.querySelector>("ak-search-select"); + + if (search) { + search.selectedObject = null; + } + + this.selectedFlow = null; + }) + .catch(showAPIErrorMessage); + }; + + protected fetchFlows = (query?: string): Promise => + aki(FlowsApi) + .flowsInstancesList({ + ordering: "slug", + ...(query ? { search: query } : {}), + }) + .then((flows) => + flows.results.filter( + (flow) => + !flow.designation || !disallowedDesignations.includes(flow.designation), + ), + ); + + protected override renderToolbar(): SlottedTemplateResult { + return html` + html`${flow.slug}`} + .value=${(flow: Flow | null) => String(flow?.pk ?? "")} + placeholder=${msg("Select a flow...", { + id: "user-next-actions.select.placeholder", + })} + blankable + @ak-change=${(event: CustomEvent<{ value: Flow | null }>) => { + event.stopPropagation(); + this.selectedFlow = event.detail.value; + }} + > + + + ${super.renderToolbar()} + `; + } + + protected override row(item: NextActionRow): SlottedTemplateResult[] { + return [ + html`${item.flow?.name ?? item.slug}`, + html`${item.slug}`, + html` + this.#patch((actions) => actions.filter((entry) => entry !== item.slug))} + > + + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-user-next-actions-list": UserNextActionsList; + } +} diff --git a/web/src/admin/users/UserOverviewTab.ts b/web/src/admin/users/UserOverviewTab.ts index cc88177fc9ba..9f9830d35df4 100644 --- a/web/src/admin/users/UserOverviewTab.ts +++ b/web/src/admin/users/UserOverviewTab.ts @@ -1,6 +1,6 @@ import "#admin/users/UserChart"; import "#admin/users/UserInfoCard"; -import "#admin/users/UserNextActionsCard"; +import "#admin/users/UserNextActionsList"; import "#admin/users/UserNotesCard"; import "#components/ak-object-attributes-card"; import "#admin/events/ObjectChangelog"; @@ -69,7 +69,10 @@ export class UserOverviewTab extends AKElement {
${this.hasEnterpriseLicense ? html`
- +
+ ${msg("Next actions on login", { id: "user-next-actions.card.title" })} +
+
` : nothing}
From a56ee369743f80065af7f52130bb86b54dd76fdd Mon Sep 17 00:00:00 2001 From: Dominic R Date: Thu, 6 Aug 2026 16:02:48 -0400 Subject: [PATCH 6/6] web/admin: fix delete modal label for next actions --- web/src/admin/users/UserNextActionsList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/admin/users/UserNextActionsList.ts b/web/src/admin/users/UserNextActionsList.ts index 73e1e20159d0..3bdaba013b25 100644 --- a/web/src/admin/users/UserNextActionsList.ts +++ b/web/src/admin/users/UserNextActionsList.ts @@ -187,7 +187,7 @@ export class UserNextActionsList extends Table { html`${item.flow?.name ?? item.slug}`, html`${item.slug}`, html` this.#patch((actions) => actions.filter((entry) => entry !== item.slug))}