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) {
|
|
||||||
this.currentStep = nextPage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextPage = this.getStepElementByIndex(activeStepIndex + 1);
|
||||||
|
|
||||||
|
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