Files
authentik/web/src/elements/forms/HorizontalFormElement.ts
Ken Sternberg bc4b07d57b web/admin: remove all special cases of slug handling, replace with a "smart slug" component (#14983)
* web: Add InvalidationFlow to Radius Provider dialogues

## What

- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
  - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
    to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.

* This (temporary) change is needed to prevent the unit tests from failing.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes

* Revert "This (temporary) change is needed to prevent the unit tests from failing."

This reverts commit dddde09be5.

* web/components: Remove all special cases of slug handling, replace with a "smart slug" component

This commit removes all special handling for the `slug` attribute in our text. A variant of the text
input control that can handle formatting-as-slugs has replaced all the slugificiation code; simply
drop it onto a page and tell it the (must be unique) selector from which to get the data to be
slugified. It only looks up one tier of the DOM so be careful that both the text input and its slug
accessory occupy the same DOM context.

## Details

### The Component

Now that we know a (lot) more about Lit, this component has been slightly updated to meet our
current standards.

- web/src/components/ak-slug-input.ts

Changes made:

- The "listen for the source object" has been moved to the `firstUpdated`, so that it no longer has
  to wait for the end of a render.
 - The `dirtyFlag` handler now uses the `@input` syntax.
- Updated the slug formatter to permit trailing dashes.
- Uses the `@bound` decorator, eliminating the need to do binding in the constructor (and so
  eliminating the constructor completely).

### Component uses:

The following components were revised to use `ak-slug-input` instead of a plain text input with the
slug-handling added by our forms manager.

- applications/ApplicationForm.ts
- flows/FlowForm.ts
- sources/kerberos/KerberosSourceForm.ts
- sources/ldap/LDAPSourceForm.ts
- sources/oauth/OAuthSourceForm.ts
- sources/plex/PlexSourceForm.ts
- sources/saml/SAMLSourceForm.ts
- sources/scim/SCIMSourceForm.ts

### Remove the redundant special slug handling code

- web/src/elements/forms/Form.ts
- web/src/elements/forms/HorizontalFormElement.ts

### A special case among special cases

- web/src/admin/stages/invitation/InvitationForm.ts

This form is our one case where we have a slug input field with no corresponding text source. Adding
a simple event handler to validate the value whenever it changed and write back a "clean" slug was
the most straightforward solution. I added a help line; it seemed "surprising" to ask someone for a
name and not follow the same rules as "names" everywhere else in our UI without explanation.

* After writing the commit message, I realized some of the comments I made MUST be added to the component.

* The `source` attribute needed its own comment to indicate that a `query()` compatible selector is expected.

* Added public/private/protected/# indicators to all fields.  Trying to balance between getting it 'right' and leaving an opening for harmonizing style-sharing and state-sharing between (text / textarea), slug, password and (visible / hidden / secret).

* Removed the ids as requested; the default "look for this" matches the original behavior without requiring it be hard-coded and unchangable.
2025-06-16 09:04:00 -07:00

163 lines
5.3 KiB
TypeScript

import { AKElement } from "@goauthentik/elements/Base";
import { FormGroup } from "@goauthentik/elements/forms/FormGroup";
import { formatSlug } from "@goauthentik/elements/router/utils.js";
import { msg, str } from "@lit/localize";
import { CSSResult, css } from "lit";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
/**
*
* Horizontal Form Element Container.
*
* This element provides the interface between elements of our forms and the
* form itself.
* @custom-element ak-form-element-horizontal
*/
/* TODO
* 1. Replace the "probe upward for a parent object to event" with an event handler on the parent
* group.
* 2. Updated() has a lot of that slug code again. Really, all you want is for the slug input object
* to update itself if its content seems to have been tracking some other key element.
* 3. Updated() pushes the `name` field down to the children, as if that were necessary; why isn't
* it being written on-demand when the child is written? Because it's slotted... despite there
* being very few unique uses.
*/
const isAkControl = (el: unknown): boolean =>
el instanceof HTMLElement &&
"dataset" in el &&
el.dataset instanceof DOMStringMap &&
"akControl" in el.dataset;
const nameables = new Set([
"input",
"textarea",
"select",
"ak-codemirror",
"ak-chip-group",
"ak-search-select",
"ak-radio",
]);
@customElement("ak-form-element-horizontal")
export class HorizontalFormElement extends AKElement {
static get styles(): CSSResult[] {
return [
PFBase,
PFForm,
PFFormControl,
css`
.pf-c-form__group {
display: grid;
grid-template-columns:
var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth)
var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth);
}
.pf-c-form__group-label {
padding-top: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
}
`,
];
}
@property()
label = "";
@property({ type: Boolean })
required = false;
@property({ attribute: false })
errorMessages: string[] | string[][] = [];
_invalid = false;
/* If this property changes, we want to make sure the parent control is "opened" so
* that users can see the change.[1]
*/
@property({ type: Boolean })
set invalid(v: boolean) {
this._invalid = v;
// check if we're in a form group, and expand that form group
const parent = this.parentElement?.parentElement;
if (parent && "expanded" in parent) {
(parent as FormGroup).expanded = true;
}
}
get invalid(): boolean {
return this._invalid;
}
@property()
name = "";
firstUpdated(): void {
this.updated();
}
updated(): void {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
input.focus();
});
this.querySelectorAll("*").forEach((input) => {
if (isAkControl(input) && !input.getAttribute("name")) {
input.setAttribute("name", this.name);
return;
}
if (nameables.has(input.tagName.toLowerCase())) {
input.setAttribute("name", this.name);
} else {
return;
}
});
}
render(): TemplateResult {
this.updated();
return html`<div class="pf-c-form__group">
<div class="pf-c-form__group-label">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${this.label}</span>
${this.required
? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>`
: html``}
</label>
</div>
<div class="pf-c-form__group-control">
<slot class="pf-c-form__horizontal-group"></slot>
<div class="pf-c-form__horizontal-group">
${this.errorMessages.map((message) => {
if (message instanceof Object) {
return html`${Object.entries(message).map(([field, errMsg]) => {
return html`<p
class="pf-c-form__helper-text pf-m-error"
aria-live="polite"
>
${msg(str`${field}: ${errMsg}`)}
</p>`;
})}`;
}
return html`<p class="pf-c-form__helper-text pf-m-error" aria-live="polite">
${message}
</p>`;
})}
</div>
</div>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-form-element-horizontal": HorizontalFormElement;
}
}