web: Fix issues surrounding wizard step behavior. (#12779)
This resolves a few stateful situations which may arise when opening and closing wizard pages.
This commit is contained in:
		| @ -52,6 +52,21 @@ export class PolicyWizard extends AKElement { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     selectListener = ({ detail }: CustomEvent<TypeCreate>) => { | ||||||
|  |         if (!this.wizard) return; | ||||||
|  |  | ||||||
|  |         const { component, modelName } = detail; | ||||||
|  |         const idx = this.wizard.steps.indexOf("initial") + 1; | ||||||
|  |  | ||||||
|  |         // Exclude all current steps starting with type-, | ||||||
|  |         // this happens when the user selects a type and then goes back | ||||||
|  |         this.wizard.steps = this.wizard.steps.filter((step) => !step.startsWith("type-")); | ||||||
|  |  | ||||||
|  |         this.wizard.steps.splice(idx, 0, `type-${component}-${modelName}`); | ||||||
|  |  | ||||||
|  |         this.wizard.isValid = true; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         return html` |         return html` | ||||||
|             <ak-wizard |             <ak-wizard | ||||||
| @ -62,23 +77,10 @@ export class PolicyWizard extends AKElement { | |||||||
|                 <ak-wizard-page-type-create |                 <ak-wizard-page-type-create | ||||||
|                     slot="initial" |                     slot="initial" | ||||||
|                     .types=${this.policyTypes} |                     .types=${this.policyTypes} | ||||||
|                     @select=${(ev: CustomEvent<TypeCreate>) => { |                     @select=${this.selectListener} | ||||||
|                         if (!this.wizard) return; |  | ||||||
|                         const idx = this.wizard.steps.indexOf("initial") + 1; |  | ||||||
|                         // Exclude all current steps starting with type-, |  | ||||||
|                         // this happens when the user selects a type and then goes back |  | ||||||
|                         this.wizard.steps = this.wizard.steps.filter( |  | ||||||
|                             (step) => !step.startsWith("type-"), |  | ||||||
|                         ); |  | ||||||
|                         this.wizard.steps.splice( |  | ||||||
|                             idx, |  | ||||||
|                             0, |  | ||||||
|                             `type-${ev.detail.component}-${ev.detail.modelName}`, |  | ||||||
|                         ); |  | ||||||
|                         this.wizard.isValid = true; |  | ||||||
|                     }} |  | ||||||
|                 > |                 > | ||||||
|                 </ak-wizard-page-type-create> |                 </ak-wizard-page-type-create> | ||||||
|  |  | ||||||
|                 ${this.policyTypes.map((type) => { |                 ${this.policyTypes.map((type) => { | ||||||
|                     return html` |                     return html` | ||||||
|                         <ak-wizard-page-form |                         <ak-wizard-page-form | ||||||
|  | |||||||
| @ -41,6 +41,7 @@ export class ActionWizardPage extends WizardPage { | |||||||
|  |  | ||||||
|     activeCallback = async (): Promise<void> => { |     activeCallback = async (): Promise<void> => { | ||||||
|         this.states = []; |         this.states = []; | ||||||
|  |  | ||||||
|         this.host.actions.map((act, idx) => { |         this.host.actions.map((act, idx) => { | ||||||
|             this.states.push({ |             this.states.push({ | ||||||
|                 action: act, |                 action: act, | ||||||
| @ -48,9 +49,12 @@ export class ActionWizardPage extends WizardPage { | |||||||
|                 idx: idx, |                 idx: idx, | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         this.host.canBack = false; |         this.host.canBack = false; | ||||||
|         this.host.canCancel = false; |         this.host.canCancel = false; | ||||||
|  |  | ||||||
|         await this.run(); |         await this.run(); | ||||||
|  |  | ||||||
|         // Ensure wizard is closable, even when run() failed |         // Ensure wizard is closable, even when run() failed | ||||||
|         this.host.isValid = true; |         this.host.isValid = true; | ||||||
|     }; |     }; | ||||||
| @ -59,15 +63,20 @@ export class ActionWizardPage extends WizardPage { | |||||||
|  |  | ||||||
|     async run(): Promise<void> { |     async run(): Promise<void> { | ||||||
|         this.currentStep = this.states[0]; |         this.currentStep = this.states[0]; | ||||||
|  |  | ||||||
|         await new Promise((r) => setTimeout(r, 500)); |         await new Promise((r) => setTimeout(r, 500)); | ||||||
|  |  | ||||||
|         for await (const bundle of this.states) { |         for await (const bundle of this.states) { | ||||||
|             this.currentStep = bundle; |             this.currentStep = bundle; | ||||||
|             this.currentStep.state = ActionState.running; |             this.currentStep.state = ActionState.running; | ||||||
|             this.requestUpdate(); |             this.requestUpdate(); | ||||||
|             try { |             try { | ||||||
|                 await bundle.action.run(); |                 await bundle.action.run(); | ||||||
|  |  | ||||||
|                 await new Promise((r) => setTimeout(r, 500)); |                 await new Promise((r) => setTimeout(r, 500)); | ||||||
|  |  | ||||||
|                 this.currentStep.state = ActionState.done; |                 this.currentStep.state = ActionState.done; | ||||||
|  |  | ||||||
|                 this.requestUpdate(); |                 this.requestUpdate(); | ||||||
|             } catch (exc) { |             } catch (exc) { | ||||||
|                 if (exc instanceof ResponseError) { |                 if (exc instanceof ResponseError) { | ||||||
| @ -75,12 +84,16 @@ export class ActionWizardPage extends WizardPage { | |||||||
|                 } else { |                 } else { | ||||||
|                     this.currentStep.action.subText = (exc as Error).toString(); |                     this.currentStep.action.subText = (exc as Error).toString(); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 this.currentStep.state = ActionState.failed; |                 this.currentStep.state = ActionState.failed; | ||||||
|                 this.requestUpdate(); |                 this.requestUpdate(); | ||||||
|  |  | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.host.isValid = true; |         this.host.isValid = true; | ||||||
|  |  | ||||||
|         this.dispatchEvent( |         this.dispatchEvent( | ||||||
|             new CustomEvent(EVENT_REFRESH, { |             new CustomEvent(EVENT_REFRESH, { | ||||||
|                 bubbles: true, |                 bubbles: true, | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | |||||||
| import { msg, str } from "@lit/localize"; | import { msg, str } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
|  | import { Ref, createRef, ref } from "lit/directives/ref.js"; | ||||||
|  |  | ||||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||||
| import PFForm from "@patternfly/patternfly/components/Form/form.css"; | import PFForm from "@patternfly/patternfly/components/Form/form.css"; | ||||||
| @ -21,6 +22,8 @@ export enum TypeCreateWizardPageLayouts { | |||||||
|  |  | ||||||
| @customElement("ak-wizard-page-type-create") | @customElement("ak-wizard-page-type-create") | ||||||
| export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { | export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { | ||||||
|  |     //#region Properties | ||||||
|  |  | ||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     types: TypeCreate[] = []; |     types: TypeCreate[] = []; | ||||||
|  |  | ||||||
| @ -30,6 +33,8 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { | |||||||
|     @property({ type: String }) |     @property({ type: String }) | ||||||
|     layout: TypeCreateWizardPageLayouts = TypeCreateWizardPageLayouts.list; |     layout: TypeCreateWizardPageLayouts = TypeCreateWizardPageLayouts.list; | ||||||
|  |  | ||||||
|  |     //#endregion | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
| @ -49,10 +54,25 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     sidebarLabel = () => msg("Select type"); |     //#region Refs | ||||||
|  |  | ||||||
|  |     formRef: Ref<HTMLFormElement> = createRef(); | ||||||
|  |  | ||||||
|  |     //#endregion | ||||||
|  |  | ||||||
|  |     public sidebarLabel = () => msg("Select type"); | ||||||
|  |  | ||||||
|  |     public reset = () => { | ||||||
|  |         super.reset(); | ||||||
|  |         this.selectedType = undefined; | ||||||
|  |         this.formRef.value?.reset(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     activeCallback = (): void => { | ||||||
|  |         const form = this.formRef.value; | ||||||
|  |  | ||||||
|  |         this.host.isValid = form?.checkValidity() ?? false; | ||||||
|  |  | ||||||
|     activeCallback: () => Promise<void> = async () => { |  | ||||||
|         this.host.isValid = false; |  | ||||||
|         if (this.selectedType) { |         if (this.selectedType) { | ||||||
|             this.selectDispatch(this.selectedType); |             this.selectDispatch(this.selectedType); | ||||||
|         } |         } | ||||||
| @ -92,9 +112,8 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { | |||||||
|                     data-ouid-component-type="ak-type-create-grid-card" |                     data-ouid-component-type="ak-type-create-grid-card" | ||||||
|                     data-ouid-component-name=${componentName} |                     data-ouid-component-name=${componentName} | ||||||
|                     @click=${() => { |                     @click=${() => { | ||||||
|                         if (requiresEnterprise) { |                         if (requiresEnterprise) return; | ||||||
|                             return; |  | ||||||
|                         } |  | ||||||
|                         this.selectDispatch(type); |                         this.selectDispatch(type); | ||||||
|                         this.selectedType = type; |                         this.selectedType = type; | ||||||
|                     }} |                     }} | ||||||
| @ -120,11 +139,13 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { | |||||||
|  |  | ||||||
|     renderList(): TemplateResult { |     renderList(): TemplateResult { | ||||||
|         return html`<form |         return html`<form | ||||||
|  |             ${ref(this.formRef)} | ||||||
|             class="pf-c-form pf-m-horizontal" |             class="pf-c-form pf-m-horizontal" | ||||||
|             data-ouid-component-type="ak-type-create-list" |             data-ouid-component-type="ak-type-create-list" | ||||||
|         > |         > | ||||||
|             ${this.types.map((type) => { |             ${this.types.map((type) => { | ||||||
|                 const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense; |                 const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense; | ||||||
|  |  | ||||||
|                 return html`<div |                 return html`<div | ||||||
|                     class="pf-c-radio" |                     class="pf-c-radio" | ||||||
|                     data-ouid-component-type="ak-type-create-list-card" |                     data-ouid-component-type="ak-type-create-list-card" | ||||||
| @ -160,6 +181,8 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) { | |||||||
|                 return this.renderGrid(); |                 return this.renderGrid(); | ||||||
|             case TypeCreateWizardPageLayouts.list: |             case TypeCreateWizardPageLayouts.list: | ||||||
|                 return this.renderList(); |                 return this.renderList(); | ||||||
|  |             default: | ||||||
|  |                 throw new Error(`Unknown layout: ${this.layout}`) as never; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,8 +5,9 @@ import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; | |||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; | ||||||
| import { property } from "@lit/reactive-element/decorators/property.js"; | import { property } from "@lit/reactive-element/decorators/property.js"; | ||||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | ||||||
| import { state } from "lit/decorators.js"; | import { state } from "lit/decorators.js"; | ||||||
|  | import { classMap } from "lit/directives/class-map.js"; | ||||||
|  |  | ||||||
| import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css"; | import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css"; | ||||||
|  |  | ||||||
| @ -20,21 +21,6 @@ export const ApplyActionsSlot = "apply-actions"; | |||||||
|  |  | ||||||
| @customElement("ak-wizard") | @customElement("ak-wizard") | ||||||
| export class Wizard extends ModalButton { | export class Wizard extends ModalButton { | ||||||
|     @property({ type: Boolean }) |  | ||||||
|     canCancel = true; |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |  | ||||||
|     canBack = true; |  | ||||||
|  |  | ||||||
|     @property() |  | ||||||
|     header?: string; |  | ||||||
|  |  | ||||||
|     @property() |  | ||||||
|     description?: string; |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |  | ||||||
|     isValid = false; |  | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return super.styles.concat( |         return super.styles.concat( | ||||||
|             PFWizard, |             PFWizard, | ||||||
| @ -46,65 +32,138 @@ export class Wizard extends ModalButton { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @state() |     //#region Properties | ||||||
|     _steps: string[] = []; |  | ||||||
|  |  | ||||||
|     get steps(): string[] { |     /** | ||||||
|         return this._steps; |      * Whether the wizard can be cancelled. | ||||||
|     } |      */ | ||||||
|  |     @property({ type: Boolean }) | ||||||
|  |     canCancel = true; | ||||||
|  |  | ||||||
|     set steps(steps: string[]) { |     /** | ||||||
|         const addApplyActionsSlot = this.steps.includes(ApplyActionsSlot); |      * Whether the wizard can go back to the previous step. | ||||||
|         this._steps = steps; |      */ | ||||||
|         if (addApplyActionsSlot) { |     @property({ type: Boolean }) | ||||||
|             this.steps.push(ApplyActionsSlot); |     canBack = true; | ||||||
|         } |  | ||||||
|         this.steps.forEach((step) => { |  | ||||||
|             const exists = this.querySelector(`[slot=${step}]`) !== null; |  | ||||||
|             if (!exists) { |  | ||||||
|                 const el = document.createElement(step); |  | ||||||
|                 el.slot = step; |  | ||||||
|                 el.dataset["wizardmanaged"] = "true"; |  | ||||||
|                 this.appendChild(el); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         this.requestUpdate(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     _initialSteps: string[] = []; |     /** | ||||||
|  |      * Header title of the wizard. | ||||||
|  |      */ | ||||||
|  |     @property() | ||||||
|  |     header?: string; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Description of the wizard. | ||||||
|  |      */ | ||||||
|  |     @property() | ||||||
|  |     description?: string; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Whether the wizard is valid and can proceed to the next step. | ||||||
|  |      */ | ||||||
|  |     @property({ type: Boolean }) | ||||||
|  |     isValid = false; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Actions to display at the end of the wizard. | ||||||
|  |      */ | ||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     actions: WizardAction[] = []; |     actions: WizardAction[] = []; | ||||||
|  |  | ||||||
|     @state() |  | ||||||
|     _currentStep?: WizardPage; |  | ||||||
|  |  | ||||||
|     set currentStep(value: WizardPage | undefined) { |  | ||||||
|         this._currentStep = value; |  | ||||||
|         if (this._currentStep) { |  | ||||||
|             this._currentStep.activeCallback(); |  | ||||||
|             this._currentStep.requestUpdate(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     get currentStep(): WizardPage | undefined { |  | ||||||
|         return this._currentStep; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     finalHandler: () => Promise<void> = () => { |     finalHandler = () => { | ||||||
|         return Promise.resolve(); |         return Promise.resolve(); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     @property({ attribute: false }) |     @property({ attribute: false }) | ||||||
|     state: { [key: string]: unknown } = {}; |     state: { [key: string]: unknown } = {}; | ||||||
|  |  | ||||||
|  |     //#endregion | ||||||
|  |  | ||||||
|  |     //#region State | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Memoized step tag names. | ||||||
|  |      */ | ||||||
|  |     @state() | ||||||
|  |     _steps: string[] = []; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Step tag names present in the wizard. | ||||||
|  |      */ | ||||||
|  |     get steps(): string[] { | ||||||
|  |         return this._steps; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     set steps(nextSteps: string[]) { | ||||||
|  |         const addApplyActionsSlot = this._steps.includes(ApplyActionsSlot); | ||||||
|  |  | ||||||
|  |         this._steps = nextSteps; | ||||||
|  |  | ||||||
|  |         if (addApplyActionsSlot) { | ||||||
|  |             this.steps.push(ApplyActionsSlot); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (const step of this._steps) { | ||||||
|  |             const existingStepElement = this.getStepElementByName(step); | ||||||
|  |  | ||||||
|  |             if (existingStepElement) continue; | ||||||
|  |  | ||||||
|  |             const stepElement = document.createElement(step); | ||||||
|  |  | ||||||
|  |             stepElement.slot = step; | ||||||
|  |             stepElement.dataset["wizardmanaged"] = "true"; | ||||||
|  |  | ||||||
|  |             this.appendChild(stepElement); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.requestUpdate(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Initial steps to reset to. | ||||||
|  |      */ | ||||||
|  |     _initialSteps: string[] = []; | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|  |     _activeStep?: WizardPage; | ||||||
|  |  | ||||||
|  |     set activeStepElement(nextActiveStepElement: WizardPage | undefined) { | ||||||
|  |         this._activeStep = nextActiveStepElement; | ||||||
|  |  | ||||||
|  |         if (!this._activeStep) return; | ||||||
|  |  | ||||||
|  |         this._activeStep.activeCallback(); | ||||||
|  |         this._activeStep.requestUpdate(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The active step element being displayed. | ||||||
|  |      */ | ||||||
|  |     get activeStepElement(): WizardPage | undefined { | ||||||
|  |         return this._activeStep; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getStepElementByIndex(stepIndex: number): WizardPage | null { | ||||||
|  |         const stepName = this._steps[stepIndex]; | ||||||
|  |  | ||||||
|  |         return this.querySelector<WizardPage>(`[slot=${stepName}]`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getStepElementByName(stepName: string): WizardPage | null { | ||||||
|  |         return this.querySelector<WizardPage>(`[slot=${stepName}]`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //#endregion | ||||||
|  |  | ||||||
|  |     //#region Lifecycle | ||||||
|  |  | ||||||
|     firstUpdated(): void { |     firstUpdated(): void { | ||||||
|         this._initialSteps = this._steps; |         this._initialSteps = this._steps; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Add action to the beginning of the list |      * Add action to the beginning of the list. | ||||||
|      */ |      */ | ||||||
|     addActionBefore(displayName: string, run: () => Promise<boolean>): void { |     addActionBefore(displayName: string, run: () => Promise<boolean>): void { | ||||||
|         this.actions.unshift({ |         this.actions.unshift({ | ||||||
| @ -114,7 +173,9 @@ export class Wizard extends ModalButton { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Add action at the end of the list |      * Add action at the end of the list. | ||||||
|  |      * | ||||||
|  |      * @todo: Is this used? | ||||||
|      */ |      */ | ||||||
|     addActionAfter(displayName: string, run: () => Promise<boolean>): void { |     addActionAfter(displayName: string, run: () => Promise<boolean>): void { | ||||||
|         this.actions.push({ |         this.actions.push({ | ||||||
| @ -123,17 +184,60 @@ export class Wizard extends ModalButton { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderModalInner(): TemplateResult { |     /** | ||||||
|         const firstPage = this.querySelector<WizardPage>(`[slot=${this.steps[0]}]`); |      * Reset the wizard to it's initial state. | ||||||
|         if (!this.currentStep && firstPage) { |      */ | ||||||
|             this.currentStep = firstPage; |     reset = () => { | ||||||
|  |         this.open = false; | ||||||
|  |  | ||||||
|  |         this.querySelectorAll("[data-wizardmanaged=true]").forEach((el) => { | ||||||
|  |             el.remove(); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         for (const step of this.steps) { | ||||||
|  |             const stepElement = this.getStepElementByName(step); | ||||||
|  |  | ||||||
|  |             stepElement?.reset?.(); | ||||||
|         } |         } | ||||||
|         const currentIndex = this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0; |  | ||||||
|         let lastPage = currentIndex === this.steps.length - 1; |         this.steps = this._initialSteps; | ||||||
|  |         this.actions = []; | ||||||
|  |         this.state = {}; | ||||||
|  |         this.activeStepElement = undefined; | ||||||
|  |         this.canBack = true; | ||||||
|  |         this.canCancel = true; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     //#endregion | ||||||
|  |  | ||||||
|  |     //#region Rendering | ||||||
|  |  | ||||||
|  |     renderModalInner(): TemplateResult { | ||||||
|  |         const firstPage = this.getStepElementByIndex(0); | ||||||
|  |  | ||||||
|  |         if (!this.activeStepElement && firstPage) { | ||||||
|  |             this.activeStepElement = firstPage; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const activeStepIndex = this.activeStepElement | ||||||
|  |             ? this.steps.indexOf(this.activeStepElement.slot) | ||||||
|  |             : 0; | ||||||
|  |  | ||||||
|  |         let lastPage = activeStepIndex === this.steps.length - 1; | ||||||
|  |  | ||||||
|         if (lastPage && !this.steps.includes("ak-wizard-page-action") && this.actions.length > 0) { |         if (lastPage && !this.steps.includes("ak-wizard-page-action") && this.actions.length > 0) { | ||||||
|             this.steps = this.steps.concat("ak-wizard-page-action"); |             this.steps = this.steps.concat("ak-wizard-page-action"); | ||||||
|             lastPage = currentIndex === this.steps.length - 1; |             lastPage = activeStepIndex === this.steps.length - 1; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         const navigateToPreviousStep = () => { | ||||||
|  |             const prevPage = this.getStepElementByIndex(activeStepIndex - 1); | ||||||
|  |  | ||||||
|  |             if (prevPage) { | ||||||
|  |                 this.activeStepElement = prevPage; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|         return html`<div class="pf-c-wizard"> |         return html`<div class="pf-c-wizard"> | ||||||
|             <div class="pf-c-wizard__header"> |             <div class="pf-c-wizard__header"> | ||||||
|                 ${this.canCancel |                 ${this.canCancel | ||||||
| @ -141,13 +245,11 @@ export class Wizard extends ModalButton { | |||||||
|                           class="pf-c-button pf-m-plain pf-c-wizard__close" |                           class="pf-c-button pf-m-plain pf-c-wizard__close" | ||||||
|                           type="button" |                           type="button" | ||||||
|                           aria-label="${msg("Close")}" |                           aria-label="${msg("Close")}" | ||||||
|                           @click=${() => { |                           @click=${this.reset} | ||||||
|                               this.reset(); |  | ||||||
|                           }} |  | ||||||
|                       > |                       > | ||||||
|                           <i class="fas fa-times" aria-hidden="true"></i> |                           <i class="fas fa-times" aria-hidden="true"></i> | ||||||
|                       </button>` |                       </button>` | ||||||
|                     : html``} |                     : nothing} | ||||||
|                 <h1 class="pf-c-title pf-m-3xl pf-c-wizard__title">${this.header}</h1> |                 <h1 class="pf-c-title pf-m-3xl pf-c-wizard__title">${this.header}</h1> | ||||||
|                 <p class="pf-c-wizard__description">${this.description}</p> |                 <p class="pf-c-wizard__description">${this.description}</p> | ||||||
|             </div> |             </div> | ||||||
| @ -156,28 +258,25 @@ export class Wizard extends ModalButton { | |||||||
|                     <nav class="pf-c-wizard__nav"> |                     <nav class="pf-c-wizard__nav"> | ||||||
|                         <ol class="pf-c-wizard__nav-list"> |                         <ol class="pf-c-wizard__nav-list"> | ||||||
|                             ${this.steps.map((step, idx) => { |                             ${this.steps.map((step, idx) => { | ||||||
|                                 const currentIdx = this.currentStep |                                 const stepEl = this.getStepElementByName(step); | ||||||
|                                     ? this.steps.indexOf(this.currentStep.slot) |  | ||||||
|                                     : 0; |                                 if (!stepEl) return html`<p>Unexpected missing step: ${step}</p>`; | ||||||
|  |  | ||||||
|  |                                 const sidebarLabel = stepEl.sidebarLabel(); | ||||||
|  |  | ||||||
|                                 return html` |                                 return html` | ||||||
|                                     <li class="pf-c-wizard__nav-item"> |                                     <li class="pf-c-wizard__nav-item"> | ||||||
|                                         <button |                                         <button | ||||||
|                                             class="pf-c-wizard__nav-link ${idx === currentIdx |                                             class=${classMap({ | ||||||
|                                                 ? "pf-m-current" |                                                 "pf-c-wizard__nav-link": true, | ||||||
|                                                 : ""}" |                                                 "pf-m-current": idx === activeStepIndex, | ||||||
|                                             ?disabled=${currentIdx < idx} |                                             })} | ||||||
|  |                                             ?disabled=${activeStepIndex < idx} | ||||||
|                                             @click=${() => { |                                             @click=${() => { | ||||||
|                                                 const stepEl = this.querySelector<WizardPage>( |                                                 this.activeStepElement = stepEl; | ||||||
|                                                     `[slot=${step}]`, |  | ||||||
|                                                 ); |  | ||||||
|                                                 if (stepEl) { |  | ||||||
|                                                     this.currentStep = stepEl; |  | ||||||
|                                                 } |  | ||||||
|                                             }} |                                             }} | ||||||
|                                         > |                                         > | ||||||
|                                             ${this.querySelector<WizardPage>( |                                             ${sidebarLabel} | ||||||
|                                                 `[slot=${step}]`, |  | ||||||
|                                             )?.sidebarLabel()} |  | ||||||
|                                         </button> |                                         </button> | ||||||
|                                     </li> |                                     </li> | ||||||
|                                 `; |                                 `; | ||||||
| @ -186,7 +285,7 @@ export class Wizard extends ModalButton { | |||||||
|                     </nav> |                     </nav> | ||||||
|                     <main class="pf-c-wizard__main"> |                     <main class="pf-c-wizard__main"> | ||||||
|                         <div class="pf-c-wizard__main-body"> |                         <div class="pf-c-wizard__main-body"> | ||||||
|                             <slot name=${this.currentStep?.slot || this.steps[0]}></slot> |                             <slot name=${this.activeStepElement?.slot || this.steps[0]}></slot> | ||||||
|                         </div> |                         </div> | ||||||
|                     </main> |                     </main> | ||||||
|                 </div> |                 </div> | ||||||
| @ -196,44 +295,38 @@ export class Wizard extends ModalButton { | |||||||
|                         type="submit" |                         type="submit" | ||||||
|                         ?disabled=${!this.isValid} |                         ?disabled=${!this.isValid} | ||||||
|                         @click=${async () => { |                         @click=${async () => { | ||||||
|                             const cb = await this.currentStep?.nextCallback(); |                             const completedStep = await this.activeStepElement?.nextCallback(); | ||||||
|                             if (!cb) { |                             if (!completedStep) return; | ||||||
|                                 return; |  | ||||||
|                             } |  | ||||||
|                             if (lastPage) { |                             if (lastPage) { | ||||||
|                                 await this.finalHandler(); |                                 await this.finalHandler(); | ||||||
|                                 this.reset(); |                                 this.reset(); | ||||||
|                             } else { |  | ||||||
|                                 const nextPage = this.querySelector<WizardPage>( |                                 return; | ||||||
|                                     `[slot=${this.steps[currentIndex + 1]}]`, |                             } | ||||||
|                                 ); |  | ||||||
|                                 if (nextPage) { |                             const nextPage = this.getStepElementByIndex(activeStepIndex + 1); | ||||||
|                                     this.currentStep = nextPage; |  | ||||||
|                                 } |                             if (nextPage) { | ||||||
|  |                                 this.activeStepElement = nextPage; | ||||||
|                             } |                             } | ||||||
|                         }} |                         }} | ||||||
|                     > |                     > | ||||||
|                         ${lastPage ? msg("Finish") : msg("Next")} |                         ${lastPage ? msg("Finish") : msg("Next")} | ||||||
|                     </button> |                     </button> | ||||||
|                     ${(this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0) > 0 && |                     ${(this.activeStepElement | ||||||
|                     this.canBack |                         ? this.steps.indexOf(this.activeStepElement.slot) | ||||||
|  |                         : 0) > 0 && this.canBack | ||||||
|                         ? html` |                         ? html` | ||||||
|                               <button |                               <button | ||||||
|                                   class="pf-c-button pf-m-secondary" |                                   class="pf-c-button pf-m-secondary" | ||||||
|                                   type="button" |                                   type="button" | ||||||
|                                   @click=${() => { |                                   @click=${navigateToPreviousStep} | ||||||
|                                       const prevPage = this.querySelector<WizardPage>( |  | ||||||
|                                           `[slot=${this.steps[currentIndex - 1]}]`, |  | ||||||
|                                       ); |  | ||||||
|                                       if (prevPage) { |  | ||||||
|                                           this.currentStep = prevPage; |  | ||||||
|                                       } |  | ||||||
|                                   }} |  | ||||||
|                               > |                               > | ||||||
|                                   ${msg("Back")} |                                   ${msg("Back")} | ||||||
|                               </button> |                               </button> | ||||||
|                           ` |                           ` | ||||||
|                         : html``} |                         : nothing} | ||||||
|                     ${this.canCancel |                     ${this.canCancel | ||||||
|                         ? html`<div class="pf-c-wizard__footer-cancel"> |                         ? html`<div class="pf-c-wizard__footer-cancel"> | ||||||
|                               <button |                               <button | ||||||
| @ -246,24 +339,13 @@ export class Wizard extends ModalButton { | |||||||
|                                   ${msg("Cancel")} |                                   ${msg("Cancel")} | ||||||
|                               </button> |                               </button> | ||||||
|                           </div>` |                           </div>` | ||||||
|                         : html``} |                         : nothing} | ||||||
|                 </footer> |                 </footer> | ||||||
|             </div> |             </div> | ||||||
|         </div>`; |         </div>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     reset(): void { |     //#endregion | ||||||
|         this.open = false; |  | ||||||
|         this.querySelectorAll("[data-wizardmanaged=true]").forEach((el) => { |  | ||||||
|             el.remove(); |  | ||||||
|         }); |  | ||||||
|         this.steps = this._initialSteps; |  | ||||||
|         this.actions = []; |  | ||||||
|         this.state = {}; |  | ||||||
|         this.currentStep = undefined; |  | ||||||
|         this.canBack = true; |  | ||||||
|         this.canCancel = true; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|  | |||||||
| @ -39,24 +39,24 @@ export class WizardFormPage extends WizardPage { | |||||||
|  |  | ||||||
|     inputCallback(): void { |     inputCallback(): void { | ||||||
|         const form = this.shadowRoot?.querySelector<HTMLFormElement>("form"); |         const form = this.shadowRoot?.querySelector<HTMLFormElement>("form"); | ||||||
|         if (!form) { |  | ||||||
|             return; |         if (!form) return; | ||||||
|         } |  | ||||||
|         const state = form.checkValidity(); |         const state = form.checkValidity(); | ||||||
|         this.host.isValid = state; |         this.host.isValid = state; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     nextCallback = async (): Promise<boolean> => { |     nextCallback = async (): Promise<boolean> => { | ||||||
|         const form = this.shadowRoot?.querySelector<WizardForm>("ak-wizard-form"); |         const form = this.shadowRoot?.querySelector<WizardForm>("ak-wizard-form"); | ||||||
|  |  | ||||||
|         if (!form) { |         if (!form) { | ||||||
|             console.warn("authentik/wizard: could not find form element"); |             console.warn("authentik/wizard: could not find form element"); | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const response = await form.submit(); |         const response = await form.submit(); | ||||||
|         if (response === undefined) { |  | ||||||
|             return false; |         return Boolean(response); | ||||||
|         } |  | ||||||
|         return response; |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     nextDataCallback: (data: KeyUnknown) => Promise<boolean> = async (): Promise<boolean> => { |     nextDataCallback: (data: KeyUnknown) => Promise<boolean> = async (): Promise<boolean> => { | ||||||
|  | |||||||
| @ -6,14 +6,32 @@ import { customElement, property } from "lit/decorators.js"; | |||||||
|  |  | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Callback for when the page is brought into view. | ||||||
|  |  */ | ||||||
|  | export type WizardPageActiveCallback = () => void | Promise<void>; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Callback for when the next button is pressed. | ||||||
|  |  * | ||||||
|  |  * @returns `true` if the wizard can proceed to the next page, `false` otherwise. | ||||||
|  |  */ | ||||||
|  | export type WizardPageNextCallback = () => boolean | Promise<boolean>; | ||||||
|  |  | ||||||
| @customElement("ak-wizard-page") | @customElement("ak-wizard-page") | ||||||
| export class WizardPage extends AKElement { | export class WizardPage extends AKElement { | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [PFBase]; |         return [PFBase]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The label to display in the sidebar for this page. | ||||||
|  |      * | ||||||
|  |      * Override this to provide a custom label. | ||||||
|  |      * @todo: Should this be a getter or static property? | ||||||
|  |      */ | ||||||
|     @property() |     @property() | ||||||
|     sidebarLabel: () => string = () => { |     sidebarLabel = (): string => { | ||||||
|         return "UNNAMED"; |         return "UNNAMED"; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @ -22,9 +40,18 @@ export class WizardPage extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Called when this is the page brought into view |      * Reset the page to its initial state. | ||||||
|  |      * | ||||||
|  |      * @abstract | ||||||
|      */ |      */ | ||||||
|     activeCallback: () => Promise<void> = async () => { |     public reset(): void | Promise<void> { | ||||||
|  |         console.debug(`authentik/wizard ${this.localName}: reset)`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Called when this is the page brought into view. | ||||||
|  |      */ | ||||||
|  |     activeCallback: WizardPageActiveCallback = () => { | ||||||
|         this.host.isValid = false; |         this.host.isValid = false; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @ -32,9 +59,11 @@ export class WizardPage extends AKElement { | |||||||
|      * Called when the `next` button on the wizard is pressed. For forms, results in the submission |      * Called when the `next` button on the wizard is pressed. For forms, results in the submission | ||||||
|      * of the current form to the back-end before being allowed to proceed to the next page. This is |      * of the current form to the back-end before being allowed to proceed to the next page. This is | ||||||
|      * sub-optimal if we want to collect multiple bits of data before finishing the whole course. |      * sub-optimal if we want to collect multiple bits of data before finishing the whole course. | ||||||
|  |      * | ||||||
|  |      * @returns `true` if the wizard can proceed to the next page, `false` otherwise. | ||||||
|      */ |      */ | ||||||
|     nextCallback: () => Promise<boolean> = async () => { |     nextCallback: WizardPageNextCallback = () => { | ||||||
|         return true; |         return Promise.resolve(true); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     requestUpdate( |     requestUpdate( | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Teffen Ellis
					Teffen Ellis