web: application wizard refactor & completion
This commit refactors the various components of the Wizard and ApplicationWizard, creating a much more maintainable and satisfying Wizard experience for both developers (i.e, *me* and *Jens* so far), and for the customer. The Wizard base has been refactored into three components: **AkWizardController** The `AkWizardController` provides the event listenters for the wizard; it hooks them up, recevies the events, and forwards them to the wizard. It unwraps the event objects and forwards the relevant messages contained in the events. It knows of three event categories: - Navigation requests (move to a different step) - Update requests (the current step has updated the business content) - Close requests (close or cancel the wizard). **ak-wizard-frame** The `ak-wizard-frame` is the ModalButton interface. It provides the Header, Breadcrumbs (nee` "navigation block"), Buttons, and a DIV into which the main content is rendered. **AkWizard** `AkWizard` is an *incomplete* implementation of the wizard. It's meant to be inherited by a child class, which will implement the rest. It extends `AKElement`. It provides the basic content needed, such as steps, currentStep (as an index), an accessor for the step itself, an accessor for the frame, and the interface to the `AkWizardController`. **ApplicationWizard** The `ApplicationWizard` itself has been refactored to accommodate these changes. It inherits from `AkWizard` and provides the business logic for what to do when a form updates, some custom logic for preventing moving through the wizard when the forms are incomplete, and a persistence layer for filling out different providers in the same session. It's simplified a *lot*. The types specified for `AkWizard` are pretty nifty, I think. I could wish the types being passed via the custom events were more robust, but [strongly typed custom events](https://github.com/lit/lit-element/issues/808) turn out to be quite the pain in the, er, neck. As it is, the `precommit` pass did very good at preventing the worst disasters. The steps themselves were re-written as objects so that they could take advantage of their `valid` and `disabled` states and provide more meaningful buttons and labels. I think it's a solid compromise, and it moved a lot of display logic out of the core `handleUpdate()` business method. The tests, such as they are, are passing.
This commit is contained in:
parent
998615dbcc
commit
fbb6fb0a8e
|
@ -7,8 +7,8 @@ import { query } from "@lit/reactive-element/decorators.js";
|
|||
|
||||
import { styles as AwadStyles } from "./BasePanel.css";
|
||||
|
||||
import { applicationWizardContext } from "./ak-application-wizard-context-name";
|
||||
import type { WizardState, WizardStateUpdate } from "./types";
|
||||
import { applicationWizardContext } from "./ContextIdentity";
|
||||
import type { ApplicationWizardState, ApplicationWizardStateUpdate } from "./types";
|
||||
|
||||
export class ApplicationWizardPageBase
|
||||
extends CustomEmitterElement(AKElement)
|
||||
|
@ -24,11 +24,11 @@ export class ApplicationWizardPageBase
|
|||
rendered = false;
|
||||
|
||||
@consume({ context: applicationWizardContext })
|
||||
public wizard!: WizardState;
|
||||
public wizard!: ApplicationWizardState;
|
||||
|
||||
// This used to be more complex; now it just establishes the event name.
|
||||
dispatchWizardUpdate(update: WizardStateUpdate) {
|
||||
this.dispatchCustomEvent("ak-application-wizard-update", update);
|
||||
dispatchWizardUpdate(update: ApplicationWizardStateUpdate) {
|
||||
this.dispatchCustomEvent("ak-wizard-update", update);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createContext } from "@lit-labs/context";
|
||||
|
||||
import { WizardState } from "./types";
|
||||
import { ApplicationWizardState } from "./types";
|
||||
|
||||
export const applicationWizardContext = createContext<WizardState>(
|
||||
export const applicationWizardContext = createContext<ApplicationWizardState>(
|
||||
Symbol("ak-application-wizard-state-context"),
|
||||
);
|
||||
|
||||
export default applicationWizardContext;
|
|
@ -1,23 +1,19 @@
|
|||
import { type AkWizardMain } from "@goauthentik/app/components/ak-wizard-main/ak-wizard-main";
|
||||
import { merge } from "@goauthentik/common/merge";
|
||||
import "@goauthentik/components/ak-wizard-main";
|
||||
import { CloseWizard } from "@goauthentik/components/ak-wizard-main/commonWizardButtons";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { ContextProvider, ContextRoot } from "@lit-labs/context";
|
||||
import { ContextProvider } from "@lit-labs/context";
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { type Ref, createRef, ref } from "lit/directives/ref.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import applicationWizardContext from "./ak-application-wizard-context-name";
|
||||
import applicationWizardContext from "./ContextIdentity";
|
||||
import { newSteps } from "./steps";
|
||||
import { OneOfProvider, WizardState, WizardStateUpdate } from "./types";
|
||||
import {
|
||||
ApplicationStep,
|
||||
ApplicationWizardState,
|
||||
ApplicationWizardStateUpdate,
|
||||
OneOfProvider,
|
||||
} from "./types";
|
||||
|
||||
const freshWizardState = () => ({
|
||||
providerModel: "",
|
||||
|
@ -26,55 +22,41 @@ const freshWizardState = () => ({
|
|||
});
|
||||
|
||||
@customElement("ak-application-wizard")
|
||||
export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFButton, PFRadio];
|
||||
export class ApplicationWizard extends CustomListenerElement(
|
||||
AkWizard<ApplicationWizardStateUpdate, ApplicationStep>,
|
||||
) {
|
||||
constructor() {
|
||||
super(msg("Create"), msg("New application"), msg("Create a new application"));
|
||||
this.steps = newSteps();
|
||||
}
|
||||
|
||||
@state()
|
||||
wizardState: WizardState = freshWizardState();
|
||||
|
||||
/**
|
||||
* Providing a context at the root element
|
||||
* We're going to be managing the content of the forms by percolating all of the data up to this
|
||||
* class, which will ultimately transmit all of it to the server as a transaction. The
|
||||
* WizardFramework doesn't know anything about the nature of the data itself; it just forwards
|
||||
* valid updates to us. So here we maintain a state object *and* update it so all child
|
||||
* components can access the wizard state.
|
||||
*
|
||||
*/
|
||||
@state()
|
||||
wizardState: ApplicationWizardState = freshWizardState();
|
||||
|
||||
wizardStateProvider = new ContextProvider(this, {
|
||||
context: applicationWizardContext,
|
||||
initialValue: this.wizardState,
|
||||
});
|
||||
|
||||
@state()
|
||||
steps = newSteps();
|
||||
|
||||
@property()
|
||||
prompt = msg("Create");
|
||||
|
||||
/**
|
||||
* One of our steps has multiple display variants, one for each type of service provider. We
|
||||
* want to *preserve* a customer's decisions about different providers; never make someone "go
|
||||
* back and type it all back in," even if it's probably rare that someone will chose one
|
||||
* provider, realize it's the wrong one, and go back to chose a different one, *and then go
|
||||
* back*. Nonetheless, strive to *never* lose customer input.
|
||||
*
|
||||
*/
|
||||
providerCache: Map<string, OneOfProvider> = new Map();
|
||||
|
||||
wizardRef: Ref<AkWizardMain> = createRef();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.handleUpdate = this.handleUpdate.bind(this);
|
||||
this.handleClosed = this.handleClosed.bind(this);
|
||||
}
|
||||
|
||||
get step() {
|
||||
return this.wizardRef.value?.currentStep ?? -1;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
new ContextRoot().attach(this.parentElement!);
|
||||
this.addCustomListener("ak-application-wizard-update", this.handleUpdate);
|
||||
this.addCustomListener("ak-wizard-closed", this.handleClosed);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeCustomListener("ak-application-wizard-update", this.handleUpdate);
|
||||
this.removeCustomListener("ak-wizard-closed", this.handleClosed);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
maybeProviderSwap(providerModel: string | undefined): boolean {
|
||||
if (
|
||||
providerModel === undefined ||
|
||||
|
@ -98,51 +80,46 @@ export class ApplicationWizard extends CustomListenerElement(AKElement) {
|
|||
}
|
||||
|
||||
// And this is where all the special cases go...
|
||||
handleUpdate(event: CustomEvent<WizardStateUpdate>) {
|
||||
if (event.detail.status === "submitted") {
|
||||
const submitStep = this.steps.find(({ id }) => id === "submit");
|
||||
if (!submitStep) {
|
||||
throw new Error("Could not find submit step?");
|
||||
}
|
||||
submitStep.buttons = [CloseWizard];
|
||||
this.steps = [...this.steps];
|
||||
handleUpdate(detail: ApplicationWizardStateUpdate) {
|
||||
if (detail.status === "submitted") {
|
||||
this.step.valid = true;
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
const update = event.detail.update;
|
||||
this.step.valid = this.step.valid || detail.status === "valid";
|
||||
|
||||
const update = detail.update;
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.maybeProviderSwap(update.providerModel)) {
|
||||
this.steps = [...this.steps];
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
if (event.detail.status === "valid" && this.steps[this.step + 1]) {
|
||||
this.steps[this.step + 1].disabled = false;
|
||||
this.steps = [...this.steps];
|
||||
}
|
||||
|
||||
this.wizardState = merge(this.wizardState, update) as WizardState;
|
||||
this.wizardState = merge(this.wizardState, update) as ApplicationWizardState;
|
||||
this.wizardStateProvider.setValue(this.wizardState);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
handleClosed() {
|
||||
close() {
|
||||
this.steps = newSteps();
|
||||
this.currentStep = 0;
|
||||
this.wizardState = freshWizardState();
|
||||
this.providerCache = new Map();
|
||||
this.wizardStateProvider.setValue(this.wizardState);
|
||||
this.frame.value!.open = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ak-wizard-main
|
||||
${ref(this.wizardRef)}
|
||||
.steps=${this.steps}
|
||||
header=${msg("New application")}
|
||||
description=${msg("Create a new application.")}
|
||||
prompt=${this.prompt}
|
||||
>
|
||||
</ak-wizard-main>
|
||||
`;
|
||||
handleNav(stepId: number | undefined) {
|
||||
if (stepId === undefined || this.steps[stepId] === undefined) {
|
||||
throw new Error(`Attempt to navigate to undefined step: ${stepId}`);
|
||||
}
|
||||
if (stepId > this.currentStep && !this.step.valid) {
|
||||
return;
|
||||
}
|
||||
this.currentStep = stepId;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { WizardStep } from "@goauthentik/components/ak-wizard-main";
|
||||
import {
|
||||
BackStep,
|
||||
CancelWizard,
|
||||
CloseWizard,
|
||||
DisabledNextStep,
|
||||
NextStep,
|
||||
SubmitStep,
|
||||
} from "@goauthentik/components/ak-wizard-main/commonWizardButtons";
|
||||
|
@ -12,47 +13,70 @@ import "./application/ak-application-wizard-application-details";
|
|||
import "./auth-method-choice/ak-application-wizard-authentication-method-choice";
|
||||
import "./commit/ak-application-wizard-commit-application";
|
||||
import "./methods/ak-application-wizard-authentication-method";
|
||||
import { ApplicationStep as ApplicationStepType } from "./types";
|
||||
|
||||
type NamedStep = WizardStep & {
|
||||
id: string;
|
||||
valid: boolean;
|
||||
};
|
||||
class ApplicationStep implements ApplicationStepType {
|
||||
id = "application";
|
||||
label = "Application Details";
|
||||
disabled = false;
|
||||
valid = false;
|
||||
get buttons() {
|
||||
return [this.valid ? NextStep : DisabledNextStep, CancelWizard];
|
||||
}
|
||||
render() {
|
||||
return html`<ak-application-wizard-application-details></ak-application-wizard-application-details>`;
|
||||
}
|
||||
}
|
||||
|
||||
export const newSteps = (): NamedStep[] => [
|
||||
{
|
||||
id: "application",
|
||||
label: "Application Details",
|
||||
render: () =>
|
||||
html`<ak-application-wizard-application-details></ak-application-wizard-application-details>`,
|
||||
disabled: false,
|
||||
valid: false,
|
||||
buttons: [NextStep, CancelWizard],
|
||||
},
|
||||
{
|
||||
id: "provider-method",
|
||||
label: "Authentication Method",
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-method-choice></ak-application-wizard-authentication-method-choice>`,
|
||||
disabled: false,
|
||||
valid: false,
|
||||
buttons: [NextStep, BackStep, CancelWizard],
|
||||
},
|
||||
{
|
||||
id: "provider-details",
|
||||
label: "Authentication Details",
|
||||
render: () =>
|
||||
html`<ak-application-wizard-authentication-method></ak-application-wizard-authentication-method>`,
|
||||
disabled: true,
|
||||
valid: false,
|
||||
buttons: [SubmitStep, BackStep, CancelWizard],
|
||||
},
|
||||
{
|
||||
id: "submit",
|
||||
label: "Submit New Application",
|
||||
render: () =>
|
||||
html`<ak-application-wizard-commit-application></ak-application-wizard-commit-application>`,
|
||||
disabled: true,
|
||||
valid: false,
|
||||
buttons: [BackStep, CancelWizard],
|
||||
},
|
||||
class ProviderMethodStep implements ApplicationStepType {
|
||||
id = "provider-method";
|
||||
label = "Authentication Method";
|
||||
disabled = false;
|
||||
valid = false;
|
||||
|
||||
get buttons() {
|
||||
return [BackStep, this.valid ? NextStep : DisabledNextStep, CancelWizard];
|
||||
}
|
||||
|
||||
render() {
|
||||
// prettier-ignore
|
||||
return html`<ak-application-wizard-authentication-method-choice
|
||||
></ak-application-wizard-authentication-method-choice> `;
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderStepDetails implements ApplicationStepType {
|
||||
id = "provider-details";
|
||||
label = "Authentication Details";
|
||||
disabled = true;
|
||||
valid = false;
|
||||
get buttons() {
|
||||
return [BackStep, this.valid ? SubmitStep : DisabledNextStep, CancelWizard];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-application-wizard-authentication-method></ak-application-wizard-authentication-method>`;
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitApplicationStep implements ApplicationStepType {
|
||||
id = "submit";
|
||||
label = "Submit New Application";
|
||||
disabled = true;
|
||||
valid = false;
|
||||
|
||||
get buttons() {
|
||||
return this.valid ? [CloseWizard] : [BackStep, CancelWizard];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-application-wizard-commit-application></ak-application-wizard-commit-application>`;
|
||||
}
|
||||
}
|
||||
|
||||
export const newSteps = (): ApplicationStep[] => [
|
||||
new ApplicationStep(),
|
||||
new ProviderMethodStep(),
|
||||
new ProviderStepDetails(),
|
||||
new SubmitApplicationStep(),
|
||||
];
|
||||
|
|
|
@ -3,14 +3,14 @@ import { customElement } from "@lit/reactive-element/decorators/custom-element.j
|
|||
import { state } from "@lit/reactive-element/decorators/state.js";
|
||||
import { LitElement, html } from "lit";
|
||||
|
||||
import applicationWizardContext from "../ak-application-wizard-context-name";
|
||||
import type { WizardState } from "../types";
|
||||
import applicationWizardContext from "../ContextIdentity";
|
||||
import type { ApplicationWizardState } from "../types";
|
||||
|
||||
@customElement("ak-application-context-display-for-test")
|
||||
export class ApplicationContextDisplayForTest extends LitElement {
|
||||
@consume({ context: applicationWizardContext, subscribe: true })
|
||||
@state()
|
||||
private wizard!: WizardState;
|
||||
private wizard!: ApplicationWizardState;
|
||||
|
||||
render() {
|
||||
return html`<div><pre>${JSON.stringify(this.wizard, null, 2)}</pre></div>`;
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { type WizardStep } from "@goauthentik/components/ak-wizard-main/types";
|
||||
|
||||
import {
|
||||
ApplicationRequest,
|
||||
LDAPProviderRequest,
|
||||
OAuth2ProviderRequest,
|
||||
ProvidersSamlImportMetadataCreateRequest,
|
||||
ProxyProviderRequest,
|
||||
RadiusProviderRequest,
|
||||
SAMLProviderRequest,
|
||||
SCIMProviderRequest,
|
||||
type ApplicationRequest,
|
||||
type LDAPProviderRequest,
|
||||
type OAuth2ProviderRequest,
|
||||
type ProvidersSamlImportMetadataCreateRequest,
|
||||
type ProxyProviderRequest,
|
||||
type RadiusProviderRequest,
|
||||
type SAMLProviderRequest,
|
||||
type SCIMProviderRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export type OneOfProvider =
|
||||
|
@ -18,7 +20,7 @@ export type OneOfProvider =
|
|||
| Partial<OAuth2ProviderRequest>
|
||||
| Partial<LDAPProviderRequest>;
|
||||
|
||||
export interface WizardState {
|
||||
export interface ApplicationWizardState {
|
||||
providerModel: string;
|
||||
app: Partial<ApplicationRequest>;
|
||||
provider: OneOfProvider;
|
||||
|
@ -26,7 +28,12 @@ export interface WizardState {
|
|||
|
||||
type StatusType = "invalid" | "valid" | "submitted" | "failed";
|
||||
|
||||
export type WizardStateUpdate = {
|
||||
update?: Partial<WizardState>;
|
||||
export type ApplicationWizardStateUpdate = {
|
||||
update?: Partial<ApplicationWizardState>;
|
||||
status?: StatusType;
|
||||
};
|
||||
|
||||
export type ApplicationStep = WizardStep & {
|
||||
id: string;
|
||||
valid: boolean;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import "@goauthentik/app/components/ak-wizard-main/ak-wizard-frame";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { ReactiveControllerHost, html } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { Ref, createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { AkWizardController } from "./AkWizardController";
|
||||
import { AkWizardFrame } from "./ak-wizard-frame";
|
||||
import { type WizardStep, type WizardStepLabel } from "./types";
|
||||
|
||||
/**
|
||||
* Abstract parent class for wizards. This Class activates the Controller, provides the default
|
||||
* renderer and handleNav() functions, and organizes the various texts used to describe a Modal
|
||||
* Wizard's interaction: its prompt, header, and description.
|
||||
*/
|
||||
|
||||
export class AkWizard<D, Step extends WizardStep = WizardStep>
|
||||
extends AKElement
|
||||
implements ReactiveControllerHost
|
||||
{
|
||||
// prettier-ignore
|
||||
static get styles() { return [PFBase, PFButton]; }
|
||||
|
||||
@state()
|
||||
steps: Step[] = [];
|
||||
|
||||
@state()
|
||||
currentStep = 0;
|
||||
|
||||
/**
|
||||
* A reference to the frame. Since the frame implements and inherits from ModalButton,
|
||||
* you will need either a reference to or query to the frame in order to call
|
||||
* `.close()` on it.
|
||||
*/
|
||||
frame: Ref<AkWizardFrame> = createRef();
|
||||
|
||||
get step() {
|
||||
return this.steps[this.currentStep];
|
||||
}
|
||||
|
||||
prompt = msg("Create");
|
||||
|
||||
header: string;
|
||||
|
||||
description?: string;
|
||||
|
||||
wizard: AkWizardController<D>;
|
||||
|
||||
constructor(prompt: string, header: string, description?: string) {
|
||||
super();
|
||||
this.header = header;
|
||||
this.prompt = prompt;
|
||||
this.description = description;
|
||||
this.wizard = new AkWizardController(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the labels used by the frame's Breadcrumbs display.
|
||||
*/
|
||||
get stepLabels(): WizardStepLabel[] {
|
||||
let disabled = false;
|
||||
return this.steps.map((step, index) => {
|
||||
disabled = disabled || step.disabled;
|
||||
return {
|
||||
label: step.label,
|
||||
active: index === this.currentStep,
|
||||
index,
|
||||
disabled,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* You should still consider overriding this if you need to consider details like "Is the step
|
||||
* requested valid?"
|
||||
*/
|
||||
handleNav(stepId: number | undefined) {
|
||||
if (stepId === undefined || this.steps[stepId] === undefined) {
|
||||
throw new Error(`Attempt to navigate to undefined step: ${stepId}`);
|
||||
}
|
||||
this.currentStep = stepId;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
close() {
|
||||
throw new Error("This function must be overridden in the child class.");
|
||||
}
|
||||
|
||||
/**
|
||||
* This is where all the business logic and special cases go. The Wizard Controller intercepts
|
||||
* updates tagged `ak-wizard-update` and forwards the event content here. Business logic about
|
||||
* "is the current step valid?" and "should the Next button be made enabled" are controlled
|
||||
* here. (Any step implementing WizardStep can do it anyhow it pleases, putting "is the current
|
||||
* form valid" and so forth into the step object itself.)
|
||||
*/
|
||||
handleUpdate(_detail: D) {
|
||||
throw new Error("This function must be overridden in the child class.");
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ak-wizard-frame
|
||||
${ref(this.frame)}
|
||||
header=${this.header}
|
||||
description=${ifDefined(this.description)}
|
||||
prompt=${this.prompt}
|
||||
.buttons=${this.step.buttons}
|
||||
.stepLabels=${this.stepLabels}
|
||||
.form=${this.step.render.bind(this.step)}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${this.prompt}</button>
|
||||
</ak-wizard-frame>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import { type ReactiveController } from "lit";
|
||||
|
||||
import { type AkWizard, type WizardNavCommand } from "./types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isCustomEvent = (v: any): v is CustomEvent =>
|
||||
v instanceof CustomEvent && "detail" in v;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isNavEvent = (v: any): v is CustomEvent<WizardNavCommand> =>
|
||||
isCustomEvent(v) && "command" in v.detail;
|
||||
|
||||
/**
|
||||
* AkWizardController
|
||||
*
|
||||
* A ReactiveController that plugs into any wizard and provides a somewhat more convenient API for
|
||||
* interacting with that wizard. It expects three different events from the wizard frame, each of
|
||||
* which has a corresponding method that then forwards the necessary information to the host:
|
||||
*
|
||||
* - nav: A request to navigate to different step. Calls the host's `handleNav()` with the requested
|
||||
step number.
|
||||
* - update: A request to update the content of the current step. Forwarded to the host's
|
||||
* `handleUpdate()` method.
|
||||
* - close: A request to end the wizard interaction. Forwarded to the host's `close()` method.
|
||||
*
|
||||
*/
|
||||
|
||||
export class AkWizardController<Data> implements ReactiveController {
|
||||
private host: AkWizard<Data>;
|
||||
|
||||
constructor(host: AkWizard<Data>) {
|
||||
this.host = host;
|
||||
this.handleNavRequest = this.handleNavRequest.bind(this);
|
||||
this.handleUpdateRequest = this.handleUpdateRequest.bind(this);
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
get maxStep() {
|
||||
return this.host.steps.length - 1;
|
||||
}
|
||||
|
||||
get nextStep() {
|
||||
return this.host.currentStep < this.maxStep ? this.host.currentStep + 1 : undefined;
|
||||
}
|
||||
|
||||
get backStep() {
|
||||
return this.host.currentStep > 0 ? this.host.currentStep - 1 : undefined;
|
||||
}
|
||||
|
||||
get step() {
|
||||
return this.host.steps[this.host.currentStep];
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.addEventListener("ak-wizard-nav", this.handleNavRequest);
|
||||
this.host.addEventListener("ak-wizard-update", this.handleUpdateRequest);
|
||||
this.host.addEventListener("ak-wizard-closed", this.handleCloseRequest);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.host.removeEventListener("ak-wizard-nav", this.handleNavRequest);
|
||||
this.host.removeEventListener("ak-wizard-update", this.handleUpdateRequest);
|
||||
this.host.removeEventListener("ak-wizard-closed", this.handleCloseRequest);
|
||||
}
|
||||
|
||||
handleNavRequest(event: Event) {
|
||||
if (!isNavEvent(event)) {
|
||||
throw new Error(`Unexpected event received by nav handler: ${event}`);
|
||||
}
|
||||
|
||||
if (event.detail.command === "close") {
|
||||
this.host.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const navigate = (): number | undefined => {
|
||||
switch (event.detail.command) {
|
||||
case "next":
|
||||
return this.nextStep;
|
||||
case "back":
|
||||
return this.backStep;
|
||||
case "goto":
|
||||
return event.detail.step;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unrecognized command passed to ak-wizard-controller:handleNavRequest: ${event.detail.command}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
this.host.handleNav(navigate());
|
||||
}
|
||||
|
||||
handleUpdateRequest(event: Event) {
|
||||
if (!isCustomEvent(event)) {
|
||||
throw new Error(`Unexpected event received by nav handler: ${event}`);
|
||||
}
|
||||
this.host.handleUpdate(event.detail);
|
||||
}
|
||||
|
||||
handleCloseRequest() {
|
||||
this.host.close();
|
||||
}
|
||||
}
|
|
@ -3,13 +3,13 @@ import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
|
|||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, property, query } from "@lit/reactive-element/decorators.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { map } from "lit/directives/map.js";
|
||||
|
||||
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||
|
||||
import { type WizardButton, type WizardStep } from "./types";
|
||||
import { type WizardButton, WizardStepLabel } from "./types";
|
||||
|
||||
/**
|
||||
* AKWizardFrame is the main container for displaying Wizard pages.
|
||||
|
@ -22,10 +22,9 @@ import { type WizardButton, type WizardStep } from "./types";
|
|||
*
|
||||
* @element ak-wizard-frame
|
||||
*
|
||||
* @fires ak-wizard-nav - Tell the orchestrator what page the user wishes to move to. This is the
|
||||
* only event that causes this wizard to change its appearance.
|
||||
* @slot - Where the form itself should go
|
||||
*
|
||||
* NOTE: The event name is configurable as an attribute.
|
||||
* @fires ak-wizard-nav - Tell the orchestrator what page the user wishes to move to.
|
||||
*
|
||||
*/
|
||||
|
||||
|
@ -35,47 +34,58 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
|||
return [...super.styles, PFWizard];
|
||||
}
|
||||
|
||||
/* Prop-drilled. Do not alter. */
|
||||
/**
|
||||
* The text for the title of the wizard
|
||||
*/
|
||||
@property()
|
||||
header?: string;
|
||||
|
||||
/**
|
||||
* The text for a descriptive subtitle for the wizard
|
||||
*/
|
||||
@property()
|
||||
description?: string;
|
||||
|
||||
@property()
|
||||
eventName: string = "ak-wizard-nav";
|
||||
|
||||
/**
|
||||
* The labels for all current steps, including their availability
|
||||
*/
|
||||
@property({ attribute: false, type: Array })
|
||||
steps!: WizardStep[];
|
||||
stepLabels!: WizardStepLabel[];
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
currentStep!: number;
|
||||
/**
|
||||
* What buttons to Show
|
||||
*/
|
||||
@property({ attribute: false, type: Array })
|
||||
buttons: WizardButton[] = [];
|
||||
|
||||
/**
|
||||
* Show the [Cancel] icon and offer the [Cancel] button
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "can-cancel" })
|
||||
canCancel = false;
|
||||
|
||||
/**
|
||||
* The form renderer, passed as a function
|
||||
*/
|
||||
@property({ type: Object })
|
||||
form!: () => TemplateResult;
|
||||
|
||||
@query("#main-content *:first-child")
|
||||
content!: HTMLElement;
|
||||
|
||||
@property({ type: Boolean })
|
||||
canCancel!: boolean;
|
||||
|
||||
get step() {
|
||||
const step = this.steps[this.currentStep];
|
||||
if (!step) {
|
||||
throw new Error(`Request for step that does not exist: ${this.currentStep}`);
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.renderButtons = this.renderButtons.bind(this);
|
||||
}
|
||||
|
||||
renderModalInner() {
|
||||
// prettier-ignore
|
||||
return html`<div class="pf-c-wizard">
|
||||
${this.renderHeader()}
|
||||
<div class="pf-c-wizard__outer-wrap">
|
||||
<div class="pf-c-wizard__inner-wrap">
|
||||
${this.renderNavigation()} ${this.renderMainSection()}
|
||||
${this.renderNavigation()}
|
||||
${this.renderMainSection()}
|
||||
</div>
|
||||
${this.renderFooter()}
|
||||
</div>
|
||||
|
@ -95,38 +105,38 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
|||
class="pf-c-button pf-m-plain pf-c-wizard__close"
|
||||
type="button"
|
||||
aria-label="${msg("Close")}"
|
||||
@click=${() => this.dispatchCustomEvent(this.eventName, { command: "close" })}
|
||||
@click=${() => this.dispatchCustomEvent("ak-wizard-nav", { command: "close" })}
|
||||
>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
renderNavigation() {
|
||||
let disabled = false;
|
||||
|
||||
return html`<nav class="pf-c-wizard__nav">
|
||||
<ol class="pf-c-wizard__nav-list">
|
||||
${this.steps.map((step, idx) => {
|
||||
disabled = disabled || this.step.disabled;
|
||||
return this.renderNavigationStep(step, disabled, idx);
|
||||
${this.stepLabels.map((step) => {
|
||||
return this.renderNavigationStep(step);
|
||||
})}
|
||||
</ol>
|
||||
</nav>`;
|
||||
}
|
||||
|
||||
renderNavigationStep(step: WizardStep, disabled: boolean, idx: number) {
|
||||
renderNavigationStep(step: WizardStepLabel) {
|
||||
const buttonClasses = {
|
||||
"pf-c-wizard__nav-link": true,
|
||||
"pf-m-current": idx === this.currentStep,
|
||||
"pf-m-current": step.active,
|
||||
};
|
||||
|
||||
return html`
|
||||
<li class="pf-c-wizard__nav-item">
|
||||
<button
|
||||
class=${classMap(buttonClasses)}
|
||||
?disabled=${disabled}
|
||||
?disabled=${step.disabled}
|
||||
@click=${() =>
|
||||
this.dispatchCustomEvent(this.eventName, { command: "goto", step: idx })}
|
||||
this.dispatchCustomEvent("ak-wizard-nav", {
|
||||
command: "goto",
|
||||
step: step.index,
|
||||
})}
|
||||
>
|
||||
${step.label}
|
||||
</button>
|
||||
|
@ -138,24 +148,22 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
|||
// independent context.
|
||||
renderMainSection() {
|
||||
return html`<main class="pf-c-wizard__main">
|
||||
<div id="main-content" class="pf-c-wizard__main-body">${this.step.render()}</div>
|
||||
<div id="main-content" class="pf-c-wizard__main-body">${this.form()}</div>
|
||||
</main>`;
|
||||
}
|
||||
|
||||
renderFooter() {
|
||||
return html`
|
||||
<footer class="pf-c-wizard__footer">
|
||||
${map(this.step.buttons, this.renderButtons)}
|
||||
</footer>
|
||||
<footer class="pf-c-wizard__footer">${map(this.buttons, this.renderButtons)}</footer>
|
||||
`;
|
||||
}
|
||||
|
||||
renderButtons([label, command]: WizardButton) {
|
||||
switch (command) {
|
||||
switch (command.command) {
|
||||
case "next":
|
||||
return this.renderButton(label, "pf-m-primary", command);
|
||||
return this.renderButton(label, "pf-m-primary", command.command);
|
||||
case "back":
|
||||
return this.renderButton(label, "pf-m-secondary", command);
|
||||
return this.renderButton(label, "pf-m-secondary", command.command);
|
||||
case "close":
|
||||
return this.renderLink(label, "pf-m-link");
|
||||
default:
|
||||
|
@ -169,7 +177,7 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
|||
class=${classMap(buttonClasses)}
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.dispatchCustomEvent(this.eventName, { command });
|
||||
this.dispatchCustomEvent("ak-wizard-nav", { command });
|
||||
}}
|
||||
>
|
||||
${label}
|
||||
|
@ -182,7 +190,7 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
|||
<button
|
||||
class=${classMap(buttonClasses)}
|
||||
type="button"
|
||||
@click=${() => this.dispatchCustomEvent(this.eventName, { command: "close" })}
|
||||
@click=${() => this.dispatchCustomEvent("ak-wizard-nav", { command: "close" })}
|
||||
>
|
||||
${label}
|
||||
</button>
|
||||
|
|
|
@ -1,201 +0,0 @@
|
|||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import {
|
||||
CustomEmitterElement,
|
||||
CustomListenerElement,
|
||||
} from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
import { html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import "./ak-wizard-frame";
|
||||
import { AkWizardFrame } from "./ak-wizard-frame";
|
||||
import type { WizardPanel, WizardStep } from "./types";
|
||||
|
||||
// Not just a check that it has a validator, but a check that satisfies Typescript that we're using
|
||||
// it correctly; anything within the hasValidator conditional block will know it's dealing with
|
||||
// a fully operational WizardPanel.
|
||||
//
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const hasValidator = (v: any): v is Required<Pick<WizardPanel, "validator">> =>
|
||||
"validator" in v && typeof v.validator === "function";
|
||||
|
||||
/**
|
||||
* AKWizardMain
|
||||
*
|
||||
* @element ak-wizard-main
|
||||
*
|
||||
* This is the controller for a multi-form wizard. It provides an interface for describing a pop-up
|
||||
* (modal) wizard, the contents of which are independent of the navigation. This controller only
|
||||
* handles the navigation.
|
||||
*
|
||||
* Each step (see the `types.ts` file) provides label, a "currently valid" boolean, a "disabled"
|
||||
* boolean, a function that returns the HTML of the object to be rendered, a `disabled` flag
|
||||
* indicating
|
||||
|
||||
Its tasks are:
|
||||
* - keep the collection of steps
|
||||
* - maintain the open/close status of the modal
|
||||
* - listens for navigation events
|
||||
* - if a navigation event is valid, switch to the panel requested
|
||||
*
|
||||
*
|
||||
|
||||
*/
|
||||
|
||||
@customElement("ak-wizard-main")
|
||||
export class AkWizardMain extends CustomEmitterElement(CustomListenerElement(AKElement)) {
|
||||
static get styles() {
|
||||
return [PFBase, PFButton, PFRadio];
|
||||
}
|
||||
|
||||
@property()
|
||||
eventName: string = "ak-wizard-nav";
|
||||
|
||||
/**
|
||||
* The steps of the Wizard.
|
||||
*
|
||||
* @attribute
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
steps: WizardStep[] = [];
|
||||
|
||||
/**
|
||||
* The current step of the wizard.
|
||||
*
|
||||
* @attribute
|
||||
*/
|
||||
@state()
|
||||
currentStep: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.handleNavigation = this.handleNavigation.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* The text of the modal button
|
||||
*
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String })
|
||||
prompt = "Show Wizard";
|
||||
|
||||
/**
|
||||
* The text of the header on the wizard, upper bar.
|
||||
*
|
||||
* @attribute
|
||||
*/
|
||||
@property()
|
||||
header!: string;
|
||||
|
||||
/**
|
||||
* The text of the description under the header.
|
||||
*
|
||||
* @attribute
|
||||
*/
|
||||
@property()
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Whether or not to show the "cancel" button in the wizard.
|
||||
*
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
canCancel!: boolean;
|
||||
|
||||
@query("ak-wizard-frame")
|
||||
frame!: AkWizardFrame;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addCustomListener(this.eventName, this.handleNavigation);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeCustomListener(this.eventName, this.handleNavigation);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
get maxStep() {
|
||||
return this.steps.length - 1;
|
||||
}
|
||||
|
||||
get nextStep() {
|
||||
return this.currentStep < this.maxStep ? this.currentStep + 1 : undefined;
|
||||
}
|
||||
|
||||
get backStep() {
|
||||
return this.currentStep > 0 ? this.currentStep - 1 : undefined;
|
||||
}
|
||||
|
||||
get step() {
|
||||
return this.steps[this.currentStep];
|
||||
}
|
||||
|
||||
handleNavigation(event: CustomEvent<{ command: string; step?: number }>) {
|
||||
const command = event.detail.command;
|
||||
switch (command) {
|
||||
case "back": {
|
||||
if (this.backStep !== undefined && this.steps[this.backStep]) {
|
||||
this.currentStep = this.backStep;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "goto": {
|
||||
if (
|
||||
typeof event.detail.step === "number" &&
|
||||
event.detail.step >= 0 &&
|
||||
event.detail.step <= this.maxStep
|
||||
)
|
||||
this.currentStep = event.detail.step;
|
||||
return;
|
||||
}
|
||||
case "next": {
|
||||
if (
|
||||
this.nextStep &&
|
||||
this.steps[this.nextStep] &&
|
||||
!this.steps[this.nextStep].disabled &&
|
||||
this.validated
|
||||
) {
|
||||
this.currentStep = this.nextStep;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "close": {
|
||||
this.currentStep = 0;
|
||||
this.frame.open = false;
|
||||
this.dispatchCustomEvent("ak-wizard-closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get validated() {
|
||||
if (hasValidator(this.frame.content)) {
|
||||
return this.frame.content.validator();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ak-wizard-frame
|
||||
?canCancel=${this.canCancel}
|
||||
header=${this.header}
|
||||
description=${ifDefined(this.description)}
|
||||
eventName=${this.eventName}
|
||||
.steps=${this.steps}
|
||||
.currentStep=${this.currentStep}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${this.prompt}</button>
|
||||
</ak-wizard-frame>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkWizardMain;
|
|
@ -1,9 +0,0 @@
|
|||
import { createContext } from "@lit-labs/context";
|
||||
|
||||
import { WizardStep } from "./types";
|
||||
|
||||
export const akWizardCurrentStepContextName = createContext<WizardStep>(
|
||||
Symbol("ak-wizard-current-step"),
|
||||
);
|
||||
|
||||
export default akWizardCurrentStepContextName;
|
|
@ -1,7 +0,0 @@
|
|||
import { createContext } from "@lit-labs/context";
|
||||
|
||||
import { WizardStep } from "./types";
|
||||
|
||||
export const akWizardStepsContextName = createContext<WizardStep[]>(Symbol("ak-wizard-steps"));
|
||||
|
||||
export default akWizardStepsContextName;
|
|
@ -2,12 +2,14 @@ import { msg } from "@lit/localize";
|
|||
|
||||
import { WizardButton } from "./types";
|
||||
|
||||
export const NextStep: WizardButton = [msg("Next"), "next"];
|
||||
export const NextStep: WizardButton = [msg("Next"), { command: "next" }];
|
||||
|
||||
export const BackStep: WizardButton = [msg("Back"), "back"];
|
||||
export const BackStep: WizardButton = [msg("Back"), { command: "back" }];
|
||||
|
||||
export const SubmitStep: WizardButton = [msg("Submit"), "next"];
|
||||
export const SubmitStep: WizardButton = [msg("Submit"), { command: "next" }];
|
||||
|
||||
export const CancelWizard: WizardButton = [msg("Cancel"), "close"];
|
||||
export const CancelWizard: WizardButton = [msg("Cancel"), { command: "close" }];
|
||||
|
||||
export const CloseWizard: WizardButton = [msg("Close"), "close"];
|
||||
export const CloseWizard: WizardButton = [msg("Close"), { command: "close" }];
|
||||
|
||||
export const DisabledNextStep: WizardButton = [msg("Next"), { command: "next" }, true];
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
import "./ak-wizard-main";
|
||||
import type { WizardStep } from "./types";
|
||||
|
||||
export { WizardStep };
|
|
@ -3,8 +3,8 @@ import { Meta } from "@storybook/web-components";
|
|||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import AkWizard from "../ak-wizard-frame";
|
||||
import "../ak-wizard-main";
|
||||
import AkWizard from "../ak-wizard-main";
|
||||
import { BackStep, CancelWizard, CloseWizard, NextStep } from "../commonWizardButtons";
|
||||
import type { WizardStep } from "../types";
|
||||
|
||||
|
|
|
@ -1,11 +1,61 @@
|
|||
import { TemplateResult } from "lit";
|
||||
import { type LitElement, type ReactiveControllerHost, type TemplateResult } from "lit";
|
||||
|
||||
export type WizardNavCommand = "next" | "back" | "close" | ["goto", number];
|
||||
/** These are the navigation commands that the frame will send up to the controller. In the
|
||||
* accompanying file, `./commonWizardButtons.ts`, you'll find a variety of Next, Back, Close,
|
||||
* Cancel, and Submit buttons that can be used to send these, but these commands are also
|
||||
* used by the breadcrumbs to hop around the wizard (if the wizard client so chooses to allow),
|
||||
*/
|
||||
|
||||
// The label of the button, the command the button should execute, and if the button
|
||||
// should be marked "disabled."
|
||||
export type WizardNavCommand =
|
||||
| { command: "next" }
|
||||
| { command: "back" }
|
||||
| { command: "close" }
|
||||
| { command: "goto"; step: number };
|
||||
|
||||
/**
|
||||
* The pattern for buttons being passed to the wizard. See `./commonWizardButtons.ts` for
|
||||
* example implementations. The details are: Label, Command, and Disabled.
|
||||
*/
|
||||
export type WizardButton = [string, WizardNavCommand, boolean?];
|
||||
|
||||
/**
|
||||
* Objects of this type are produced by the Controller, and are used in the Breadcrumbs to
|
||||
* indicate the name of the step, whether or not it is the current step ("active"), and
|
||||
* whether or not it is disabled. It is up to WizardClients to ensure that a step is
|
||||
* not both "active" and "disabled".
|
||||
*/
|
||||
|
||||
export type WizardStepLabel = {
|
||||
label: string;
|
||||
index: number;
|
||||
active: boolean;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
type LitControllerHost = ReactiveControllerHost & LitElement;
|
||||
|
||||
export interface AkWizard<D> extends LitControllerHost {
|
||||
// Every wizard must provide a list of the steps to show. This list can change, but if it does,
|
||||
// note that the *first* page must never change, and it's the responsibility of the developer to
|
||||
// ensure that if the list changes that the currentStep points to the right place.
|
||||
steps: WizardStep[];
|
||||
|
||||
// The index of the current step;
|
||||
currentStep: number;
|
||||
|
||||
// An accessor to the current step;
|
||||
step: WizardStep;
|
||||
|
||||
// Handle pressing the "close," "cancel," or "done" buttons.
|
||||
close: () => void;
|
||||
|
||||
// When a navigation event such as "next," "back," or "go to" (from the breadcrumbs) occurs.
|
||||
handleNav: (_1: number | undefined) => void;
|
||||
|
||||
// When a notification that the data on the live form has changed.
|
||||
handleUpdate: (_1: D) => void;
|
||||
}
|
||||
|
||||
export interface WizardStep {
|
||||
// The name of the step, as shown in the navigation.
|
||||
label: string;
|
||||
|
@ -14,19 +64,10 @@ export interface WizardStep {
|
|||
// such.
|
||||
render: () => TemplateResult;
|
||||
|
||||
// A collection of buttons, in render order, that are to be shown in the button bar. The
|
||||
// semantics of the buttons are simple: 'next' will navigate to currentStep + 1, 'back' will
|
||||
// navigate to currentStep - 1, 'close' will close the window, and ['goto', number] will
|
||||
// navigate to a specific step in order.
|
||||
//
|
||||
// It is possible for the controlling component that wraps ak-wizard-main to supply a modified
|
||||
// collection of steps at any time, thus altering the behavior of future steps, or providing a
|
||||
// tree-like structure to the wizard.
|
||||
//
|
||||
// Note that if you change the steps radically (inserting some in front of the currentStep,
|
||||
// which is something you should never, ever do... never, ever make the customer go backward to
|
||||
// solve a problem that was your responsibility. "Going back" to fix their own mistakes is, of
|
||||
// course, their responsibility) you may have to set the currentStep as well.
|
||||
// A collection of buttons, in render order, that are to be shown in the button bar. If you can,
|
||||
// always lead with the [Back] button and ensure it's in the same place every time. The
|
||||
// controller's current behavior is to call the host's `handleNav()` command with the index of
|
||||
// the requested step, or undefined if the command is nonsensical.
|
||||
buttons: WizardButton[];
|
||||
|
||||
// If this step is "disabled," the prior step's next button will be disabled.
|
||||
|
|
Reference in New Issue