web/admin: revise wizard form handling (#7331)
* web: break circular dependency between AKElement & Interface. This commit changes the way the root node of the web application shell is discovered by child components, such that the base class shared by both no longer results in a circular dependency between the two models. I've run this in isolation and have seen no failures of discovery; the identity token exists as soon as the Interface is constructed and is found by every item on the page. * web: fix broken typescript references This built... and then it didn't? Anyway, the current fix is to provide type information the AkInterface for the data that consumers require. * web: extract the form processing from the form submission process Our forms have a lot of customized value handling, and the function `serializeForm` takes our input structures and creates a JSON object ready for submission across the wire for the various models provided by the API. That function was embedded in the `ak-form` object, but it has no actual dependencies on the state of that object; aside from identifying the input elements, which is done at the very start of processing, this large block of code stands alone. Separating out the "processing the form" from "identifying the form" allows us to customize our form handling and preserve form information on the client for transactional purposes such as our wizard. w * web: multi-select, but there's a styling issue. * web: provide a closed control for multi-select This commit creates a new control, using the ak-form-element-horizontal as a *CLOSED* object, for our multi-select. This control right now is limited to what we expect to be using in the wizard, but that doesn't mean it can't be smarter in the future. * web: hung up by a silly spelling error * web: update the form-handling method With the `serializeForm` method extracted, it's much easier to examine and parse every *form* with every keystroke, preserving them against the changes that happen as the customer navigates the Wizard. With that in place, it became straightforward to retrofit the "handle changes to the application, to the provider, and to the providerType" into the three pages of the wizard, and to provide *all* of the form elements in a base class such that no specialized handling needs to happen to any of the child pages. Fixed an ugly typo in the oauth2 provider, as well. * web: wizard should work with multi-select and should reflect default values (Note: This commit is predicated on both the "Extract serializeForm function from Form.ts" and "Provide a controlled multi-select input control" PRs.) The initial attempt at the wizard was woefully naive in its implementation, missing some critical details along the way. This revision starts off with one stronger assumption: trust that Jens knows what he's doing, and knew what he was building when he wrote the initial `Form` handler. The problem with the `Form` handler, and the reason I avoided it, was simply that it does too many things, especially in its ModelForm variant: it receives a model from the back-end, renders a (hand-written) form for that model, allows the user to interact with that model, and facilitates saving it to the back-end again, complete with on-page notifications of success or failure. The Wizard could not use all of that. It needs to gather the information for *two* models (an Application and a Provider, plus the ProviderType) and has a new and specialized end-point for a transaction that allows the committing or roll back of both models to happen simultaneously, predicated on success or failure respectively. With "Extract `serializeForm` completed, it was possible to repurpose the forms that already existed, stripping them down to just their input components, and eventing the entire thing in a single event loop of "events flow up, data flows down." In this case, the *entire form* is serialized on a per-event basis and pushed up the to the orchestration layer, which saves them off. Writing a parent `BasePanel` class that has accessors for `formValues` and `valid` means that the state of every page is accessible with a simple query. This simplified the `BaseProviderPanel` class to just specialize the `dispatchUpdate` method to send the wizard update with the new provider information filled out. Because the *form* is being treated as the source of truth about the state of a `Partial<Application>` or `Partial<*Provider>` object, the defaults are now being captured as expected. Likewise, this simplified the `providerCache` layer which preserves customer input in the event that the customer starts filling out the wrong provider to a simple conditional clause in the orchestrator. The Wizard has much fewer smarts because it doesn't (and probably never did) need them. Along with the above changes, the following has also been done: For SAML and SCIM, the providerMappings now works. They weren't being managed as `state` objects, so they weren't receiving updates when the update event retrieved the information from the back-end. In order to make clear what's happening, I have extracted the loops from the original definition and built them as named objects: `propertyMappings`, `pmUserValues`, `pmGroupValues` and so on, which I then pass into the new multi-select component. I fixed a really embarrassing typo in Oauth2's "advanced settings" block. I have extracted the CoreGroup search-select into a custom component. I deleted the `merge` function. That was a faulty experiment with non-deterministic outcomes, and I was never happy with it. I'm glad its gone. I've added a title header to each of the providers, so the user can be sure that they're looking at the right provider type when they start filling out the form. I've created a new token, `data-ak-control`, with which we can mark all objects that we can treat as Authentik value-producing components, the form value of which is available through a `json()` method. I've added this bit of intelligence to the `serializeForm` function, short-circuiting the complex processing and putting the "this is the shape of the value we expect from this input" *onto the input itself*. Which is where it belongs. * web: add error handling to wizard. * web: improve error handling in light components Rather than reproduce the error handling across all of the LightComponents, I've made a parent class that takes the common fields to distribute between the ak-form-element-horizontal and the input object itself. This made it much easier to properly display errors in freeform input fields in the wizard, as well as working with the routine error handling in Form.ts * Added the radio control to the list of LightComponents. * Fix bug where event was recorded twice. * Fixed merge bug (?) that somehow deleted the Authorization Select block in OAuth2. * web: prettier had opinions * web: added error handling and display * web: bump @lit-labs/context from 0.4.1 to 0.5.1 in /web Bumps [@lit-labs/context](https://github.com/lit/lit/tree/HEAD/packages/labs/context) from 0.4.1 to 0.5.1. - [Release notes](https://github.com/lit/lit/releases) - [Changelog](https://github.com/lit/lit/blob/main/packages/labs/context/CHANGELOG.md) - [Commits](https://github.com/lit/lit/commits/@lit-labs/context@0.5.1/packages/labs/context) --- updated-dependencies: - dependency-name: "@lit-labs/context" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * web: updated wizard to run with latest package.json configuration Apparently, there were stale dependencies in package-lock.json that were conflicting with the requests in our package.json. By running `npm update`, I was able to resolve the conflict. I have also removed the default names from the context names collection; they weren't doing any good, and they permit frictionless renaming of dependencies, which is never a good idea. * web: schlepping on the errors messages During testing, I realized I was unhappy with the error messages. They're not very helpful. By adding links to navigate back to the place where the error occurred, and providing better context for what the error could have been, I hope to help the use correct their errors. * make package the same as main Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
72
web/src/components/HorizontalLightComponent.ts
Normal file
72
web/src/components/HorizontalLightComponent.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
type HelpType = TemplateResult | typeof nothing;
|
||||
|
||||
export class HorizontalLightComponent extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Object })
|
||||
bighelp?: TemplateResult | TemplateResult[];
|
||||
|
||||
@property({ type: Boolean })
|
||||
hidden = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
invalid = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
errorMessages: string[] = [];
|
||||
|
||||
renderControl() {
|
||||
throw new Error("Must be implemented in a subclass");
|
||||
}
|
||||
|
||||
renderHelp(): HelpType[] {
|
||||
const bigHelp: HelpType[] = Array.isArray(this.bighelp)
|
||||
? this.bighelp
|
||||
: [this.bighelp ?? nothing];
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,
|
||||
...bigHelp,
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
// prettier-ignore
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
.errorMessages=${this.errorMessages}
|
||||
?invalid=${this.invalid}
|
||||
>
|
||||
${this.renderControl()}
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
}
|
||||
150
web/src/components/ak-multi-select.ts
Normal file
150
web/src/components/ak-multi-select.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import "@goauthentik/app/elements/forms/HorizontalFormElement";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { map } from "lit/directives/map.js";
|
||||
import { Ref, createRef, ref } from "lit/directives/ref.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";
|
||||
|
||||
type Pair = [string, string];
|
||||
|
||||
const selectStyles = css`
|
||||
select[multiple] {
|
||||
min-height: 15rem;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Horizontal layout control with a multi-select.
|
||||
*
|
||||
* @part select - The select itself, to override the height specified above.
|
||||
*/
|
||||
@customElement("ak-multi-select")
|
||||
export class AkMultiSelect extends AKElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.dataset.akControl = "true";
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [PFBase, PFForm, PFFormControl, selectStyles];
|
||||
}
|
||||
|
||||
/**
|
||||
* The [name] attribute, which is also distributed to the layout manager and the input control.
|
||||
*/
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
/**
|
||||
* The text label to display on the control
|
||||
*/
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
/**
|
||||
* The values to be displayed in the select. The format is [Value, Label], where the label is
|
||||
* what will be displayed.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
options: Pair[] = [];
|
||||
|
||||
/**
|
||||
* If true, at least one object must be selected
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
/**
|
||||
* Supporting a simple help string
|
||||
*/
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
/**
|
||||
* For more complex help instructions, provide a template result.
|
||||
*/
|
||||
@property({ type: Object })
|
||||
bighelp!: TemplateResult | TemplateResult[];
|
||||
|
||||
/**
|
||||
* An array of strings representing the objects currently selected.
|
||||
*/
|
||||
@property({ type: Array })
|
||||
values: string[] = [];
|
||||
|
||||
/**
|
||||
* Helper accessor for older code
|
||||
*/
|
||||
get value() {
|
||||
return this.values;
|
||||
}
|
||||
|
||||
/**
|
||||
* One of two criteria (the other being the data-ak-control flag) that specifies this as a
|
||||
* control that produces values of specific interest to our REST API. This is our modern
|
||||
* accessor name.
|
||||
*/
|
||||
json() {
|
||||
return this.values;
|
||||
}
|
||||
|
||||
renderHelp() {
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
}
|
||||
|
||||
handleChange(ev: Event) {
|
||||
if (ev.type === "change") {
|
||||
this.values = Array.from(this.selectRef.value!.querySelectorAll("option"))
|
||||
.filter((option) => option.selected)
|
||||
.map((option) => option.value);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("ak-select", {
|
||||
detail: this.values,
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
selectRef: Ref<HTMLSelectElement> = createRef();
|
||||
|
||||
render() {
|
||||
return html` <div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<select
|
||||
part="select"
|
||||
class="pf-c-form-control"
|
||||
name=${ifDefined(this.name)}
|
||||
multiple
|
||||
${ref(this.selectRef)}
|
||||
@change=${this.handleChange}
|
||||
>
|
||||
${map(
|
||||
this.options,
|
||||
([value, label]) =>
|
||||
html`<option value=${value} ?selected=${this.values.includes(value)}>
|
||||
${label}
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkMultiSelect;
|
||||
@ -1,51 +1,21 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
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-number-input")
|
||||
export class AkNumberInput extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
export class AkNumberInput extends HorizontalLightComponent {
|
||||
@property({ type: Number, reflect: true })
|
||||
value = 0;
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
renderControl() {
|
||||
return html`<input
|
||||
type="number"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
/>
|
||||
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
</ak-form-element-horizontal> `;
|
||||
/>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,35 +1,13 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { RadioOption } from "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
||||
|
||||
@customElement("ak-radio-input")
|
||||
export class AkRadioInput<T> extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
export class AkRadioInput<T> extends HorizontalLightComponent {
|
||||
@property({ type: Object })
|
||||
value!: T;
|
||||
|
||||
@ -37,24 +15,25 @@ export class AkRadioInput<T> extends AKElement {
|
||||
options: RadioOption<T>[] = [];
|
||||
|
||||
handleInput(ev: CustomEvent) {
|
||||
this.value = ev.detail.value;
|
||||
if ("detail" in ev) {
|
||||
this.value = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<ak-radio
|
||||
renderHelp() {
|
||||
// This is weird, but Typescript says it's necessary?
|
||||
return [nothing as typeof nothing];
|
||||
}
|
||||
|
||||
renderControl() {
|
||||
return html`<ak-radio
|
||||
.options=${this.options}
|
||||
.value=${this.value}
|
||||
@input=${this.handleInput}
|
||||
></ak-radio>
|
||||
${this.help.trim()
|
||||
? html`<p class="pf-c-form__helper-radio">${this.help}</p>`
|
||||
: nothing}
|
||||
</ak-form-element-horizontal> `;
|
||||
: nothing}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,44 +1,16 @@
|
||||
import { convertToSlug } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
||||
|
||||
@customElement("ak-slug-input")
|
||||
export class AkSlugInput extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
export class AkSlugInput extends HorizontalLightComponent {
|
||||
@property({ type: String, reflect: true })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
hidden = false;
|
||||
|
||||
@property({ type: Object })
|
||||
bighelp!: TemplateResult | TemplateResult[];
|
||||
|
||||
@property({ type: String })
|
||||
source = "";
|
||||
|
||||
@ -59,13 +31,6 @@ export class AkSlugInput extends AKElement {
|
||||
this.input.addEventListener("input", this.handleTouch);
|
||||
}
|
||||
|
||||
renderHelp() {
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
}
|
||||
|
||||
// Do not stop propagation of this event; it must be sent up the tree so that a parent
|
||||
// component, such as a custom forms manager, may receive it.
|
||||
handleTouch(ev: Event) {
|
||||
@ -150,21 +115,13 @@ export class AkSlugInput extends AKElement {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
renderControl() {
|
||||
return html`<input
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
/>
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
/>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,65 +1,21 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
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-text-input")
|
||||
export class AkTextInput extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
export class AkTextInput extends HorizontalLightComponent {
|
||||
@property({ type: String, reflect: true })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
hidden = false;
|
||||
|
||||
@property({ type: Object })
|
||||
bighelp!: TemplateResult | TemplateResult[];
|
||||
|
||||
renderHelp() {
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
renderControl() {
|
||||
return html` <input
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
/>
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
/>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,57 +1,22 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
||||
|
||||
@customElement("ak-textarea-input")
|
||||
export class AkTextareaInput extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: String })
|
||||
export class AkTextareaInput extends HorizontalLightComponent {
|
||||
@property({ type: String, reflect: true })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Object })
|
||||
bighelp!: TemplateResult | TemplateResult[];
|
||||
|
||||
renderHelp() {
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-textarea">${this.help}</p>` : nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
renderControl() {
|
||||
// Prevent the leading spaces added by Prettier's whitespace algo
|
||||
// prettier-ignore
|
||||
return html`<textarea
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<textarea class="pf-c-form-control" ?required=${this.required} name=${this.name}>
|
||||
${this.value !== undefined ? this.value : ""}</textarea
|
||||
>
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
>${this.value !== undefined ? this.value : ""}</textarea
|
||||
> `;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
79
web/src/components/stories/ak-multi-select.stories.ts
Normal file
79
web/src/components/stories/ak-multi-select.stories.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html, render } from "lit";
|
||||
|
||||
import "../ak-multi-select";
|
||||
import AkMultiSelect from "../ak-multi-select";
|
||||
|
||||
const metadata: Meta<AkMultiSelect> = {
|
||||
title: "Components / MultiSelect",
|
||||
component: "ak-multi-select",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized value control for multi-select displays",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #fff; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<div id="message-pad" style="margin-top: 1em"></div>
|
||||
</div>`;
|
||||
|
||||
const testOptions = [
|
||||
["funky", "Option One: Funky"],
|
||||
["strange", "Option Two: Strange"],
|
||||
["weird", "Option Three: Weird"],
|
||||
];
|
||||
|
||||
export const RadioInput = () => {
|
||||
const result = "";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
const messagePad = document.getElementById("message-pad");
|
||||
const component: AkMultiSelect | null = document.querySelector(
|
||||
'ak-multi-select[name="ak-test-multi-select"]',
|
||||
);
|
||||
|
||||
const results = html`
|
||||
<p>Results from event:</p>
|
||||
<ul style="list-style-type: disc">
|
||||
${ev.target.value.map((v: string) => html`<li>${v}</li>`)}
|
||||
</ul>
|
||||
<p>Results from component:</p>
|
||||
<ul style="list-style-type: disc">
|
||||
${component!.json().map((v: string) => html`<li>${v}</li>`)}
|
||||
</ul>
|
||||
`;
|
||||
|
||||
render(results, messagePad!);
|
||||
};
|
||||
|
||||
return container(
|
||||
html`<ak-multi-select
|
||||
@ak-select=${displayChange}
|
||||
label="Test Radio Button"
|
||||
name="ak-test-multi-select"
|
||||
help="This is where you would read the help messages"
|
||||
.options=${testOptions}
|
||||
></ak-multi-select>
|
||||
<div>${result}</div>`,
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user