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

View File

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

View File

@ -7,6 +7,8 @@ import {
UserMatchingModeToLabel, UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils"; } from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; 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-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input"; import "@goauthentik/components/ak-textarea-input";
@ -246,30 +248,22 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
value=${ifDefined(this.instance?.syncPrincipal)} value=${ifDefined(this.instance?.syncPrincipal)}
help=${msg("Principal used to authenticate to the KDC for syncing.")} help=${msg("Principal used to authenticate to the KDC for syncing.")}
></ak-text-input> ></ak-text-input>
<ak-form-element-horizontal <ak-private-text-input
name="syncPassword" name="syncPassword"
label=${msg("Sync password")} label=${msg("Sync password")}
?writeOnly=${this.instance !== undefined} ?revealed=${this.instance === undefined}
> help=${msg(
<input type="text" value="" class="pf-c-form-control" /> "Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
<p class="pf-c-form__helper-text"> )}
${msg( ></ak-private-text-input>
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.", <ak-private-textarea-input
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
name="syncKeytab" name="syncKeytab"
label=${msg("Sync keytab")} label=${msg("Sync keytab")}
?writeOnly=${this.instance !== undefined} ?revealed=${this.instance === undefined}
> help=${msg(
<textarea class="pf-c-form-control"></textarea> "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 class="pf-c-form__helper-text"> )}
${msg( ></ak-private-textarea-input>
"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>
<ak-text-input <ak-text-input
name="syncCcache" name="syncCcache"
label=${msg("Sync credentials cache")} 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", "Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
)} )}
></ak-text-input> ></ak-text-input>
<ak-form-element-horizontal <ak-private-textarea-input
name="spnegoKeytab" name="spnegoKeytab"
label=${msg("SPNEGO keytab")} label=${msg("SPNEGO keytab")}
?writeOnly=${this.instance !== undefined} ?revealed=${this.instance === undefined}
> help=${msg(
<textarea class="pf-c-form-control"></textarea> "Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
<p class="pf-c-form__helper-text"> )}
${msg( ></ak-private-textarea-input>
"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>
<ak-text-input <ak-text-input
name="spnegoCcache" name="spnegoCcache"
label=${msg("SPNEGO credentials cache")} 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 { placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm"; import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; 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/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -259,13 +260,11 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
class="pf-c-form-control" class="pf-c-form-control"
/> />
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-private-text-input
label=${msg("Bind Password")} label=${msg("Bind Password")}
?writeOnly=${this.instance !== undefined}
name="bindPassword" name="bindPassword"
> ?revealed=${this.instance === undefined}
<input type="text" value="" class="pf-c-form-control" /> ></ak-private-text-input>
</ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Base DN")} label=${msg("Base DN")}
?required=${true} ?required=${true}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,13 +21,13 @@ export class HorizontalLightComponent<T> extends AKElement {
@property({ type: String, reflect: true }) @property({ type: String, reflect: true })
name!: string; name!: string;
@property({ type: String }) @property({ type: String, reflect: true })
label = ""; label = "";
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
required = false; required = false;
@property({ type: String }) @property({ type: String, reflect: true })
help = ""; help = "";
@property({ type: Object }) @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 { html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { HorizontalLightComponent } from "./HorizontalLightComponent"; import { HorizontalLightComponent } from "./HorizontalLightComponent";
@customElement("ak-textarea-input") @customElement("ak-textarea-input")
export class AkTextareaInput extends HorizontalLightComponent<string> { export class AkTextareaInput extends HorizontalLightComponent<string> {
@property({ type: String, reflect: true }) @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 // Prevent the leading spaces added by Prettier's whitespace algo
// prettier-ignore // prettier-ignore
return html`<textarea return html`<textarea
@input=${setValue}
class="pf-c-form-control" class="pf-c-form-control"
?required=${this.required} ?required=${this.required}
name=${this.name} name=${this.name}
autocomplete=${ifDefined(code ? "off" : undefined)}
spellcheck=${ifDefined(code ? "false" : undefined)}
>${this.value !== undefined ? this.value : ""}</textarea >${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 }; 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` * 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]"); const inputElement = element.querySelector<AkControlElement>("[name]");
if (element.hidden || !inputElement || (element.writeOnly && !element.writeOnlyActivated)) { if (element.hidden || !inputElement || doNotProcess(inputElement)) {
return; 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 * 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 * it being written on-demand when the child is written? Because it's slotted... despite there
* being very few unique uses. * 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 => const isAkControl = (el: unknown): boolean =>
@ -79,12 +74,6 @@ export class HorizontalFormElement extends AKElement {
@property({ type: Boolean }) @property({ type: Boolean })
required = false; required = false;
@property({ type: Boolean })
writeOnly = false;
@property({ type: Boolean })
writeOnlyActivated = false;
@property({ attribute: false }) @property({ attribute: false })
errorMessages: string[] | string[][] = []; errorMessages: string[] | string[][] = [];
@ -130,7 +119,6 @@ export class HorizontalFormElement extends AKElement {
this.querySelectorAll("*").forEach((input) => { this.querySelectorAll("*").forEach((input) => {
if (isAkControl(input) && !input.getAttribute("name")) { if (isAkControl(input) && !input.getAttribute("name")) {
input.setAttribute("name", this.name); input.setAttribute("name", this.name);
// This is fine; writeOnly won't apply to anything built this way.
return; return;
} }
@ -139,17 +127,6 @@ export class HorizontalFormElement extends AKElement {
} else { } else {
return; 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> </label>
</div> </div>
<div class="pf-c-form__group-control"> <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> <slot class="pf-c-form__horizontal-group"></slot>
<div class="pf-c-form__horizontal-group"> <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) => { ${this.errorMessages.map((message) => {
if (message instanceof Object) { if (message instanceof Object) {
return html`${Object.entries(message).map(([field, errMsg]) => { 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));