`;
}
diff --git a/web/src/components/ak-wizard-main/ak-wizard-main.ts b/web/src/components/ak-wizard-main/ak-wizard-main.ts
index 1189882cc..8443799a2 100644
--- a/web/src/components/ak-wizard-main/ak-wizard-main.ts
+++ b/web/src/components/ak-wizard-main/ak-wizard-main.ts
@@ -26,11 +26,22 @@ const hasValidator = (v: any): v is Required> =>
*
* @element ak-wizard-main
*
- * This is the entry point for the wizard. Its tasks are:
+ * 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")
@@ -56,7 +67,7 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
* @attribute
*/
@state()
- currentStep!: WizardStep;
+ currentStep: number = 0;
constructor() {
super();
@@ -71,14 +82,6 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
@property({ type: String })
prompt = "Show Wizard";
- /**
- * Mostly a control on the ModalButton that summons the wizard component.
- *
- * @attribute
- */
- @property({ type: Boolean, reflect: true })
- open = false;
-
/**
* The text of the header on the wizard, upper bar.
*
@@ -95,18 +98,17 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
@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;
- // Guarantee that if the current step was not passed in by the client, that we know
- // and set to the first step.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- willUpdate(_changedProperties: Map) {
- if (this.currentStep === undefined) {
- this.currentStep = this.steps[0];
- }
- }
-
connectedCallback() {
super.connectedCallback();
this.addCustomListener(this.eventName, this.handleNavigation);
@@ -117,30 +119,55 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
super.disconnectedCallback();
}
- // Note that we always scan for the valid next step and throw an error if we can't find it.
- // There should never be a question that the currentStep is a *valid* step.
- //
- // TODO: Put a phase in there so that the current step can validate the contents asynchronously
- // before setting the currentStep. Especially since setting the currentStep triggers a second
- // asynchronous event-- scheduling a re-render of everything interested in the currentStep
- // object.
- handleNavigation(event: CustomEvent<{ step: string; action: string }>) {
- const requestedStep = event.detail.step;
- if (!requestedStep) {
- throw new Error("Request for next step when no next step is available");
- }
- const step = this.steps.find(({ id }) => id === requestedStep);
- if (!step) {
- throw new Error("Request for next step when no next step is available.");
- }
- if (event.detail.action === "next" && !this.validated()) {
- return false;
- }
- this.currentStep = step;
- return true;
+ get maxStep() {
+ return this.steps.length - 1;
}
- validated() {
+ get nextStep() {
+ return this.currentStep < this.maxStep ? this.currentStep + 1 : undefined;
+ }
+
+ get backStep() {
+ return this.currentStep > 0 ? this.currentStep - 1 : undefined;
+ }
+
+ handleNavigation(event: CustomEvent<{ command: string; step?: number }>) {
+ const command = event.detail.command;
+ console.log(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.frame.open = this.open;
+ }
+ }
+ }
+
+ get validated() {
if (hasValidator(this.frame.content)) {
return this.frame.content.validator();
}
@@ -150,7 +177,7 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
render() {
return html`
Show Wizard
diff --git a/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts b/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts
index e35f9de5e..73c1ecfd9 100644
--- a/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts
+++ b/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts
@@ -1,6 +1,6 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta } from "@storybook/web-components";
-
+import { NextStep, BackStep, CancelWizard, CloseWizard } from "../commonWizardButtons";
import { TemplateResult, html } from "lit";
import "../ak-wizard-main";
@@ -37,27 +37,21 @@ const container = (testItem: TemplateResult) =>
const dummySteps: WizardStep[] = [
{
- id: "0",
label: "Test Step1",
renderer: () => html`
`,
disabled: false,
- valid: true,
- nextButtonLabel: undefined,
- backButtonLabel: "Back",
+ buttons: [BackStep, CloseWizard],
},
];
export const OnePageWizard = () => {
return container(
- html` `,
+ html` `,
);
};
diff --git a/web/src/components/ak-wizard-main/types.ts b/web/src/components/ak-wizard-main/types.ts
index 2c629f1b2..41830be08 100644
--- a/web/src/components/ak-wizard-main/types.ts
+++ b/web/src/components/ak-wizard-main/types.ts
@@ -1,13 +1,38 @@
import { TemplateResult } from "lit";
+export type WizardNavCommand = "next" | "back" | "close" | ["goto", number];
+
+
+// The label of the button, the command the button should execute, and if the button
+// should be marked "disabled."
+export type WizardButton = [string, WizardNavCommand, boolean?];
+
+
export interface WizardStep {
- id: string;
- label: string;
- valid: boolean;
+ // The name of the step, as shown in the navigation.
+ label: string;
+
+ // A function which returns the html for rendering the actual content of the step, its form and
+ // such.
renderer: () => 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.
+ buttons: WizardButton[];
+
+ // If this step is "disabled," the prior step's next button will be disabled.
disabled: boolean;
- nextButtonLabel?: string;
- backButtonLabel?: string;
}
export interface WizardPanel extends HTMLElement {