web/maintenance: remove writeOnly hacks from Form and HorizontalFormElement (#14649)

* 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/standards: use attribute naming scheme for attributes

## What

Renamed the 'inputHint' attribute to 'input-hint', because it is an attribute, not a property.
Properties are camelCased, but attributes are kebab-cased.

Updated all instances where this appears with the usual magic:

```
$ perl -pi.bak -e 's/inputHint="code"/input-hint="code"/' $(rg -l 'inputHint="code"')
```

This fix is in preparation for both the Patternfly 5 project and the Schema-Driven Forms project.

* web/maintenance: remove `writeOnly` hacks from Form and HorizontalFormElement

## What

The `writeOnly` hack substituted an obscuring, read-only field for secret keys and passwords that an
admin should never be able to see/read, only *write*, but allowed the user to click on and replace
the key or password. The hack performed this substitution within `HorizontalFormElement` and
dispersed a flag throughout the code to enforce it. Another hack within `Form` directed the API to
not update / write changes to that field if the field had never been activated.

This commit replaces the `writeOnly` hack with a pair of purpose-built components,
`ak-private-text-input` and `ak-private-textarea-input`, that perform the exact same functionality
but without having to involve the HorizontalFormElement, which really should just be layout and
generic functionality.  It also replaces all the `writeOnly` hackery in Form with a simple
`doNotProcess` flag, which extends and genericizes this capability to any and all input fields.

The only major protocol change is that `?writeOnly` was a *positive* flag; you controlled it by
saying `this.instance !== undefined`; `?revealed` is a *positive* flog; you reveal the working input
field when `this.instance === undefined`.

It is not necessary to specify the monospace, autocomplete, and spell-check features; those are
enabled or disabled automatically when the `input-hint="code"` flag is passed.

## Why

Removing special cases from processing code is an important step toward the Authentik Elements NPM
package, as well as the Schema-Driven Forms update.

## Note

This is actually a very significant change; this is important functionality that I have hand-tested
quite a bit, but could wish for automated testing that also checks the database back-end to ensure
the fixes made write the keys and passwords as required. Checking the back-end directly is important
since these fields are never re-sent to the front-end after being saved!

Things like `placeholder`, `required`, and getting the `name`, `label` or `help` are all issues very
subject to Last-Line Effect, so give this the hairiest eyeball you've got, please.

* Found a few small things, like a missing import that might have broken something.

* web/admin: Update `private-text` field to pass new linting requirement.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes
This commit is contained in:
Ken Sternberg
2025-06-06 15:11:49 -07:00
committed by GitHub
parent 6b530ff764
commit 9a03bdeaf1
16 changed files with 223 additions and 181 deletions

View File

@ -1,4 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-private-textarea-input.js";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -45,37 +46,24 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
<ak-private-textarea-input
label=${msg("Certificate")}
name="certificateData"
?writeOnly=${this.instance !== undefined}
?required=${true}
>
<textarea
autocomplete="off"
spellcheck="false"
class="pf-c-form-control pf-m-monospace"
placeholder="-----BEGIN CERTIFICATE-----"
required
></textarea>
<p class="pf-c-form__helper-text">${msg("PEM-encoded Certificate data.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
name="keyData"
?writeOnly=${this.instance !== undefined}
input-hint="code"
placeholder="-----BEGIN CERTIFICATE-----"
required
?revealed=${this.instance === undefined}
help=${msg("PEM-encoded Certificate data.")}
></ak-private-textarea-input>
<ak-private-textarea-input
label=${msg("Private Key")}
>
<textarea
autocomplete="off"
class="pf-c-form-control pf-m-monospace"
spellcheck="false"
></textarea>
<p class="pf-c-form__helper-text">
${msg(
"Optional Private Key. If this is set, you can use this keypair for encryption.",
)}
</p>
</ak-form-element-horizontal>`;
name="keyData"
input-hint="code"
?revealed=${this.instance === undefined}
help=${msg(
"Optional Private Key. If this is set, you can use this keypair for encryption.",
)}
></ak-private-textarea-input>`;
}
}

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
import "@goauthentik/components/ak-private-textarea-input.js";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -61,17 +62,13 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
value="${ifDefined(this.installID)}"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
<ak-private-textarea-input
name="key"
?writeOnly=${this.instance !== undefined}
?revealed=${this.instance === undefined}
label=${msg("License key")}
input-hint="code"
>
<textarea
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
></textarea>
</ak-form-element-horizontal>`;
</ak-private-textarea-input>`;
}
}

View File

@ -7,6 +7,8 @@ import {
UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-private-text-input.js";
import "@goauthentik/components/ak-private-textarea-input.js";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
@ -246,30 +248,22 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
value=${ifDefined(this.instance?.syncPrincipal)}
help=${msg("Principal used to authenticate to the KDC for syncing.")}
></ak-text-input>
<ak-form-element-horizontal
<ak-private-text-input
name="syncPassword"
label=${msg("Sync password")}
?writeOnly=${this.instance !== undefined}
>
<input type="text" value="" class="pf-c-form-control" />
<p class="pf-c-form__helper-text">
${msg(
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?revealed=${this.instance === undefined}
help=${msg(
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
)}
></ak-private-text-input>
<ak-private-textarea-input
name="syncKeytab"
label=${msg("Sync keytab")}
?writeOnly=${this.instance !== undefined}
>
<textarea class="pf-c-form-control"></textarea>
<p class="pf-c-form__helper-text">
${msg(
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
)}
</p>
</ak-form-element-horizontal>
?revealed=${this.instance === undefined}
help=${msg(
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
)}
></ak-private-textarea-input>
<ak-text-input
name="syncCcache"
label=${msg("Sync credentials cache")}
@ -291,18 +285,14 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
)}
></ak-text-input>
<ak-form-element-horizontal
<ak-private-textarea-input
name="spnegoKeytab"
label=${msg("SPNEGO keytab")}
?writeOnly=${this.instance !== undefined}
>
<textarea class="pf-c-form-control"></textarea>
<p class="pf-c-form__helper-text">
${msg(
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
)}
</p>
</ak-form-element-horizontal>
?revealed=${this.instance === undefined}
help=${msg(
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
)}
></ak-private-textarea-input>
<ak-text-input
name="spnegoCcache"
label=${msg("SPNEGO credentials cache")}

View File

@ -2,6 +2,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
import { placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-private-text-input.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -259,13 +260,11 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
<ak-private-text-input
label=${msg("Bind Password")}
?writeOnly=${this.instance !== undefined}
name="bindPassword"
>
<input type="text" value="" class="pf-c-form-control" />
</ak-form-element-horizontal>
?revealed=${this.instance === undefined}
></ak-private-text-input>
<ak-form-element-horizontal
label=${msg("Base DN")}
?required=${true}

View File

@ -7,6 +7,7 @@ import {
UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-private-textarea-input.js";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
@ -439,19 +440,14 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
/>
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
<ak-private-textarea-input
label=${msg("Consumer secret")}
?required=${true}
?writeOnly=${this.instance !== undefined}
name="consumerSecret"
>
<textarea
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
></textarea>
<p class="pf-c-form__helper-text">${msg("Also known as Client Secret.")}</p>
</ak-form-element-horizontal>
input-hint="code"
help=${msg("Also known as Client Secret.")}
required
?revealed=${this.instance === undefined}
></ak-private-textarea-input>
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
<input
type="text"

View File

@ -1,6 +1,7 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-private-text-input.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -98,21 +99,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Secret key")}
?required=${true}
?writeOnly=${this.instance !== undefined}
<ak-private-text-input
name="clientSecret"
>
<input
type="text"
value=""
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
required
/>
</ak-form-element-horizontal>
label=${msg("Secret key")}
input-hint="code"
required
?revealed=${this.instance === undefined}
></ak-private-text-input>
</div>
</ak-form-group>
<ak-form-group>
@ -136,19 +129,12 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
spellcheck="false"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Secret key")}
?writeOnly=${this.instance !== undefined}
<ak-private-text-input
name="adminSecretKey"
>
<input
type="text"
value=""
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
/>
</ak-form-element-horizontal>
label=${msg("Secret key")}
input-hint="code"
?revealed=${this.instance === undefined}
></ak-private-text-input>
</div>
</ak-form-group>
<ak-form-group .expanded=${true}>

View File

@ -1,6 +1,7 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-private-text-input.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -75,13 +76,13 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("SMTP Password")}
?writeOnly=${this.instance !== undefined}
<ak-private-text-input
name="password"
>
<input type="text" value="" class="pf-c-form-control" />
</ak-form-element-horizontal>
label=${msg("SMTP Password")}
?revealed=${this.instance === undefined}
></ak-private-text-input>
<ak-form-element-horizontal name="useTls">
<label class="pf-c-switch">
<input

View File

@ -1,6 +1,7 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-private-text-input.js";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -68,26 +69,18 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Private Key")}
?required=${true}
?writeOnly=${this.instance !== undefined}
<ak-private-text-input
name="privateKey"
>
<input
type="text"
value=""
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.",
)}
</p>
</ak-form-element-horizontal>
label=${msg("Private Key")}
input-hint="code"
required
?revealed=${this.instance === undefined}
help=${msg(
"Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.",
)}
></ak-private-text-input>
<ak-switch-input
name="interactive"
label=${msg("Interactive")}

View File

@ -1,5 +1,6 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-private-text-input.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/utils/TimeDeltaHelp";
@ -72,13 +73,11 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
<ak-private-text-input
label=${msg("SMTP Password")}
?writeOnly=${this.instance !== undefined}
name="password"
>
<input type="text" value="" class="pf-c-form-control" />
</ak-form-element-horizontal>
?revealed=${this.instance === undefined}
></ak-private-text-input>
<ak-form-element-horizontal name="useTls">
<label class="pf-c-switch">
<input

View File

@ -21,13 +21,13 @@ export class HorizontalLightComponent<T> extends AKElement {
@property({ type: String, reflect: true })
name!: string;
@property({ type: String })
@property({ type: String, reflect: true })
label = "";
@property({ type: Boolean, reflect: true })
required = false;
@property({ type: String })
@property({ type: String, reflect: true })
help = "";
@property({ type: Object })

View File

@ -0,0 +1,73 @@
import { ifNotEmpty } from "@goauthentik/elements/utils/ifNotEmpty.js";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { HorizontalLightComponent } from "./HorizontalLightComponent";
@customElement("ak-private-text-input")
export class AkPrivateTextInput extends HorizontalLightComponent<string> {
@property({ type: String, reflect: true })
public value = "";
@property({ type: Boolean, reflect: true })
public revealed = false;
@property({ type: String })
public placeholder = "";
#onReveal() {
this.revealed = true;
}
#renderPrivateInput() {
return html`<div class="pf-c-form__horizontal-group" @click=${() => this.#onReveal()}>
<input
class="pf-c-form-control"
type="password"
disabled
data-form-ignore="true"
value="**************"
/>
<input type="text" value="${ifDefined(this.value)}" ?required=${this.required} hidden />
<p class="pf-c-form__helper-text" aria-live="polite">${msg("Click to change value")}</p>
</div>`;
}
protected renderVisibleInput() {
const code = this.inputHint === "code";
const setValue = (ev: InputEvent) => {
this.value = (ev.target as HTMLInputElement).value;
};
const classes = {
"pf-c-form-control": true,
"pf-m-monospace": code,
};
return html` <input
type="text"
@input=${setValue}
value=${ifDefined(this.value)}
class="${classMap(classes)}"
placeholder=${ifNotEmpty(this.placeholder)}
autocomplete=${ifDefined(code ? "off" : undefined)}
spellcheck=${ifDefined(code ? "false" : undefined)}
?required=${this.required}
/>`;
}
public override renderControl() {
return this.revealed ? this.renderVisibleInput() : this.#renderPrivateInput();
}
}
export default AkPrivateTextInput;
declare global {
interface HTMLElementTagNameMap {
"ak-private-text-input": AkPrivateTextInput;
}
}

View File

@ -0,0 +1,43 @@
import { ifNotEmpty } from "@goauthentik/elements/utils/ifNotEmpty.js";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { AkPrivateTextInput } from "./ak-private-text-input.js";
@customElement("ak-private-textarea-input")
export class AkPrivateTextAreaInput extends AkPrivateTextInput {
protected override renderVisibleInput() {
const code = this.inputHint === "code";
const setValue = (ev: InputEvent) => {
this.value = (ev.target as HTMLInputElement).value;
};
const classes = {
"pf-c-form-control": true,
"pf-m-monospace": code,
};
// Prevent the leading spaces added by Prettier's whitespace algo
// prettier-ignore
return html`<textarea
@input=${setValue}
class="${classMap(classes)}"
?required=${this.required}
name=${this.name}
placeholder=${ifNotEmpty(this.placeholder)}
autocomplete=${ifDefined(code ? "off" : undefined)}
spellcheck=${ifDefined(code ? "false" : undefined)}
>${this.value !== undefined ? this.value : ""}</textarea
> `;
}
}
export default AkPrivateTextAreaInput;
declare global {
interface HTMLElementTagNameMap {
"ak-private-textarea-input": AkPrivateTextAreaInput;
}
}

View File

@ -1,20 +1,28 @@
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { HorizontalLightComponent } from "./HorizontalLightComponent";
@customElement("ak-textarea-input")
export class AkTextareaInput extends HorizontalLightComponent<string> {
@property({ type: String, reflect: true })
value = "";
public value = "";
renderControl() {
public override renderControl() {
const code = this.inputHint === "code";
const setValue = (ev: InputEvent) => {
this.value = (ev.target as HTMLInputElement).value;
};
// Prevent the leading spaces added by Prettier's whitespace algo
// prettier-ignore
return html`<textarea
@input=${setValue}
class="pf-c-form-control"
?required=${this.required}
name=${this.name}
autocomplete=${ifDefined(code ? "off" : undefined)}
spellcheck=${ifDefined(code ? "false" : undefined)}
>${this.value !== undefined ? this.value : ""}</textarea
> `;
}

View File

@ -33,6 +33,8 @@ type HTMLNamedElement = Pick<HTMLInputElement, "name">;
export type AkControlElement<T = string | string[]> = HTMLInputElement & { json: () => T };
const doNotProcess = <T extends HTMLElement>(element: T) => element.dataset.formIgnore === "true";
/**
* Recursively assign `value` into `json` while interpreting the dot-path of `element.name`
*/
@ -74,7 +76,7 @@ export function serializeForm<T extends KeyUnknown>(
}
const inputElement = element.querySelector<AkControlElement>("[name]");
if (element.hidden || !inputElement || (element.writeOnly && !element.writeOnlyActivated)) {
if (element.hidden || !inputElement || doNotProcess(inputElement)) {
return;
}

View File

@ -29,11 +29,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
* 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.
* 4. There is some very specific use-case around the `writeOnly` boolean; this seems to be a case
* where the field isn't available for the user to view unless they explicitly request to be able
* to see the content; otherwise, a dead password field is shown. There are 10 uses of this
* feature.
*
*/
const isAkControl = (el: unknown): boolean =>
@ -79,12 +74,6 @@ export class HorizontalFormElement extends AKElement {
@property({ type: Boolean })
required = false;
@property({ type: Boolean })
writeOnly = false;
@property({ type: Boolean })
writeOnlyActivated = false;
@property({ attribute: false })
errorMessages: string[] | string[][] = [];
@ -130,7 +119,6 @@ export class HorizontalFormElement extends AKElement {
this.querySelectorAll("*").forEach((input) => {
if (isAkControl(input) && !input.getAttribute("name")) {
input.setAttribute("name", this.name);
// This is fine; writeOnly won't apply to anything built this way.
return;
}
@ -139,17 +127,6 @@ export class HorizontalFormElement extends AKElement {
} else {
return;
}
if (this.writeOnly && !this.writeOnlyActivated) {
const i = input as HTMLInputElement;
i.setAttribute("hidden", "true");
const handler = () => {
i.removeAttribute("hidden");
this.writeOnlyActivated = true;
i.parentElement?.removeEventListener("click", handler);
};
i.parentElement?.addEventListener("click", handler);
}
});
}
@ -165,23 +142,8 @@ export class HorizontalFormElement extends AKElement {
</label>
</div>
<div class="pf-c-form__group-control">
${this.writeOnly && !this.writeOnlyActivated
? html`<div class="pf-c-form__horizontal-group">
<input
class="pf-c-form-control"
type="password"
disabled
value="**************"
/>
</div>`
: html``}
<slot class="pf-c-form__horizontal-group"></slot>
<div class="pf-c-form__horizontal-group">
${this.writeOnly
? html`<p class="pf-c-form__helper-text" aria-live="polite">
${msg("Click to change value")}
</p>`
: html``}
${this.errorMessages.map((message) => {
if (message instanceof Object) {
return html`${Object.entries(message).map(([field, errMsg]) => {

View File

@ -0,0 +1,5 @@
import { ifDefined } from "lit/directives/if-defined.js";
// A variant of `ifDefined` that also doesn't do anything if the string is empty.
export const ifNotEmpty = <T>(value: T) =>
ifDefined(value === "" ? undefined : (value ?? undefined));