web: Revised navigation
After working with the navigation for awhile, I realized that it's a poor map; what I really wanted was a controller/view pair, where events flow up to the controller and then messages on "what to draw" flow down to the view. It work quite well, and the wizard frame is smaller and smarter for it. I've also moved the WDIO-driven tests into the 'tests' folder, because it (a) makes more sense to put them there, and (b) it prevents any confusion about who's in charge of node_modules.
This commit is contained in:
parent
c8512c3116
commit
3b4530fb7f
|
@ -0,0 +1,112 @@
|
||||||
|
reports/
|
||||||
|
|
||||||
|
# Created by https://www.gitignore.io/api/node
|
||||||
|
# Edit at https://www.gitignore.io/?templates=node
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Uncomment the public line if your project uses Gatsby
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# https://create-react-app.dev/docs/using-the-public-folder/#docsNav
|
||||||
|
# public
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# End of https://www.gitignore.io/api/node
|
||||||
|
api/**
|
||||||
|
storybook-static/
|
|
@ -4,8 +4,6 @@ help: ## Print out this help message.
|
||||||
sort -nr | head -1) && \
|
sort -nr | head -1) && \
|
||||||
perl -ne "m/^((\w|-)*):.*##\s*(.*)/ && print(sprintf(\"%s: %s\t%s\n\", \$$1, \" \"x($$M-length(\$$1)), \$$3))" Makefile
|
perl -ne "m/^((\w|-)*):.*##\s*(.*)/ && print(sprintf(\"%s: %s\t%s\n\", \$$1, \" \"x($$M-length(\$$1)), \$$3))" Makefile
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Set env NODE_ENV to 'development' to run this against the dev server"
|
|
||||||
@echo ""
|
|
||||||
|
|
||||||
.PHONY: update-local-chromedriver
|
.PHONY: update-local-chromedriver
|
||||||
update-local-chromedriver: ## Update the chrome driver to match the local chrome version, restoring package.json
|
update-local-chromedriver: ## Update the chrome driver to match the local chrome version, restoring package.json
|
|
@ -1 +0,0 @@
|
||||||
reports/
|
|
|
@ -103,7 +103,6 @@ export class ApplicationWizardCommitApplication extends BasePanel {
|
||||||
Promise.allSettled([network, timeout]).then(([network_resolution]) => {
|
Promise.allSettled([network, timeout]).then(([network_resolution]) => {
|
||||||
if (network_resolution.status === "rejected") {
|
if (network_resolution.status === "rejected") {
|
||||||
this.commitState = errorState;
|
this.commitState = errorState;
|
||||||
console.log(network_resolution.reason);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { WizardStep } from "@goauthentik/components/ak-wizard-main";
|
import { WizardStep } from "@goauthentik/components/ak-wizard-main";
|
||||||
|
import {
|
||||||
|
BackStep,
|
||||||
|
CancelWizard,
|
||||||
|
CloseWizard,
|
||||||
|
NextStep,
|
||||||
|
SubmitStep,
|
||||||
|
} from "@goauthentik/components/ak-wizard-main/commonWizardButtons";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
|
@ -15,7 +22,7 @@ export const steps: WizardStep[] = [
|
||||||
renderer: () =>
|
renderer: () =>
|
||||||
html`<ak-application-wizard-application-details></ak-application-wizard-application-details>`,
|
html`<ak-application-wizard-application-details></ak-application-wizard-application-details>`,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
nextButtonLabel: msg("Next"),
|
buttons: [NextStep, CancelWizard],
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -24,8 +31,7 @@ export const steps: WizardStep[] = [
|
||||||
renderer: () =>
|
renderer: () =>
|
||||||
html`<ak-application-wizard-authentication-method-choice></ak-application-wizard-authentication-method-choice>`,
|
html`<ak-application-wizard-authentication-method-choice></ak-application-wizard-authentication-method-choice>`,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
nextButtonLabel: msg("Next"),
|
buttons: [NextStep, BackStep, CancelWizard],
|
||||||
backButtonLabel: msg("Back"),
|
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -34,8 +40,7 @@ export const steps: WizardStep[] = [
|
||||||
renderer: () =>
|
renderer: () =>
|
||||||
html`<ak-application-wizard-authentication-method></ak-application-wizard-authentication-method>`,
|
html`<ak-application-wizard-authentication-method></ak-application-wizard-authentication-method>`,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
nextButtonLabel: msg("Next"),
|
buttons: [SubmitStep, BackStep, CancelWizard],
|
||||||
backButtonLabel: msg("Back"),
|
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -44,8 +49,7 @@ export const steps: WizardStep[] = [
|
||||||
renderer: () =>
|
renderer: () =>
|
||||||
html`<ak-application-wizard-commit-application></ak-application-wizard-commit-application>`,
|
html`<ak-application-wizard-commit-application></ak-application-wizard-commit-application>`,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
nextButtonLabel: msg("Submit"),
|
buttons: [BackStep, CancelWizard],
|
||||||
backButtonLabel: msg("Back"),
|
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,15 +5,16 @@ import { msg } from "@lit/localize";
|
||||||
import { customElement, property, query } from "@lit/reactive-element/decorators.js";
|
import { customElement, property, query } from "@lit/reactive-element/decorators.js";
|
||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import { classMap } from "lit/directives/class-map.js";
|
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 PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||||
|
|
||||||
import type { WizardStep } from "./types";
|
import { type WizardButton, type WizardStep } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AKWizardFrame is the main container for displaying Wizard pages.
|
* AKWizardFrame is the main container for displaying Wizard pages.
|
||||||
*
|
*
|
||||||
* AKWizardFrame is one component of a total Wizard development environment. It provides the header,
|
* AKWizardFrame is one component of a Wizard development environment. It provides the header,
|
||||||
* titled navigation sidebar, and bottom row button bar. It takes its cues about what to render from
|
* titled navigation sidebar, and bottom row button bar. It takes its cues about what to render from
|
||||||
* two data structure, `this.steps: WizardStep[]`, which lists all the current steps *in order* and
|
* two data structure, `this.steps: WizardStep[]`, which lists all the current steps *in order* and
|
||||||
* doesn't care otherwise about their structure, and `this.currentStep: WizardStep` which must be a
|
* doesn't care otherwise about their structure, and `this.currentStep: WizardStep` which must be a
|
||||||
|
@ -34,9 +35,7 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||||
return [...super.styles, PFWizard];
|
return [...super.styles, PFWizard];
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({ type: Boolean })
|
/* Prop-drilled. Do not alter. */
|
||||||
canCancel = true;
|
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
header?: string;
|
header?: string;
|
||||||
|
|
||||||
|
@ -49,28 +48,26 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false, type: Array })
|
||||||
steps!: WizardStep[];
|
steps!: WizardStep[];
|
||||||
|
|
||||||
@property({ attribute: false, type: Object })
|
@property({ attribute: false, type: Number })
|
||||||
currentStep!: WizardStep;
|
currentStep!: number;
|
||||||
|
|
||||||
@query("#main-content *:first-child")
|
@query("#main-content *:first-child")
|
||||||
content!: HTMLElement;
|
content!: HTMLElement;
|
||||||
|
|
||||||
reset() {
|
@property({ type: Boolean })
|
||||||
this.open = false;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxStep() {
|
constructor() {
|
||||||
return this.steps.length - 1;
|
super();
|
||||||
}
|
this.renderButtons = this.renderButtons.bind(this);
|
||||||
|
|
||||||
get nextStep() {
|
|
||||||
const idx = this.steps.findIndex((step) => step === this.currentStep);
|
|
||||||
return idx < this.maxStep ? this.steps[idx + 1] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
get backStep() {
|
|
||||||
const idx = this.steps.findIndex((step) => step === this.currentStep);
|
|
||||||
return idx > 0 ? this.steps[idx - 1] : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderModalInner() {
|
renderModalInner() {
|
||||||
|
@ -100,7 +97,7 @@ export class AkWizardFrame extends CustomEmitterElement(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=${() => this.reset()}
|
@click=${() => this.dispatchCustomEvent(this.eventName, { command: "close" })}
|
||||||
>
|
>
|
||||||
<i class="fas fa-times" aria-hidden="true"></i>
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
</button>`;
|
</button>`;
|
||||||
|
@ -109,15 +106,15 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||||
renderNavigation() {
|
renderNavigation() {
|
||||||
return html`<nav class="pf-c-wizard__nav">
|
return html`<nav class="pf-c-wizard__nav">
|
||||||
<ol class="pf-c-wizard__nav-list">
|
<ol class="pf-c-wizard__nav-list">
|
||||||
${this.steps.map((step) => this.renderNavigationStep(step))}
|
${this.steps.map((step, idx) => this.renderNavigationStep(step, idx))}
|
||||||
</ol>
|
</ol>
|
||||||
</nav>`;
|
</nav>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNavigationStep(step: WizardStep) {
|
renderNavigationStep(step: WizardStep, idx: number) {
|
||||||
const buttonClasses = {
|
const buttonClasses = {
|
||||||
"pf-c-wizard__nav-link": true,
|
"pf-c-wizard__nav-link": true,
|
||||||
"pf-m-current": step.id === this.currentStep.id,
|
"pf-m-current": idx === this.currentStep,
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
@ -125,7 +122,8 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||||
<button
|
<button
|
||||||
class=${classMap(buttonClasses)}
|
class=${classMap(buttonClasses)}
|
||||||
?disabled=${step.disabled}
|
?disabled=${step.disabled}
|
||||||
@click=${() => this.dispatchCustomEvent(this.eventName, { step: step.id })}
|
@click=${() =>
|
||||||
|
this.dispatchCustomEvent(this.eventName, { command: "goto", step: idx })}
|
||||||
>
|
>
|
||||||
${step.label}
|
${step.label}
|
||||||
</button>
|
</button>
|
||||||
|
@ -137,48 +135,53 @@ export class AkWizardFrame extends CustomEmitterElement(ModalButton) {
|
||||||
// independent context.
|
// independent context.
|
||||||
renderMainSection() {
|
renderMainSection() {
|
||||||
return html`<main class="pf-c-wizard__main">
|
return html`<main class="pf-c-wizard__main">
|
||||||
<div id="main-content" class="pf-c-wizard__main-body">
|
<div id="main-content" class="pf-c-wizard__main-body">${this.step.renderer()}</div>
|
||||||
${this.currentStep.renderer()}
|
|
||||||
</div>
|
|
||||||
</main>`;
|
</main>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooter() {
|
renderFooter() {
|
||||||
return html`
|
return html`
|
||||||
<footer class="pf-c-wizard__footer">
|
<footer class="pf-c-wizard__footer">
|
||||||
${this.nextStep ? this.renderFooterNextButton(this.nextStep) : nothing}
|
${map(this.step.buttons, this.renderButtons)}
|
||||||
${this.backStep ? this.renderFooterBackButton(this.backStep) : nothing}
|
|
||||||
${this.canCancel ? this.renderFooterCancelButton() : nothing}
|
|
||||||
</footer>
|
</footer>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooterNextButton(nextStep: WizardStep) {
|
renderButtons([label, command]: WizardButton) {
|
||||||
|
switch (command) {
|
||||||
|
case "next":
|
||||||
|
return this.renderButton(label, "pf-m-primary", command);
|
||||||
|
case "back":
|
||||||
|
return this.renderButton(label, "pf-m-secondary", command);
|
||||||
|
case "close":
|
||||||
|
return this.renderLink(label, "pf-m-link");
|
||||||
|
default:
|
||||||
|
throw new Error(`Button type not understood: ${command} for ${label}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderButton(label: string, classname: string, command: string) {
|
||||||
|
const buttonClasses = { "pf-c-button": true, [classname]: true };
|
||||||
return html`<button
|
return html`<button
|
||||||
class="pf-c-button pf-m-primary"
|
class=${classMap(buttonClasses)}
|
||||||
type="submit"
|
type="button"
|
||||||
@click=${() =>
|
@click=${() => {
|
||||||
this.dispatchCustomEvent(this.eventName, { step: nextStep.id, action: "next" })}
|
this.dispatchCustomEvent(this.eventName, { command });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
${this.currentStep.nextButtonLabel}
|
${label}
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFooterBackButton(backStep: WizardStep) {
|
renderLink(label: string, classname: string) {
|
||||||
return html`<button
|
const buttonClasses = { "pf-c-button": true, [classname]: true };
|
||||||
class="pf-c-button pf-m-secondary"
|
|
||||||
type="button"
|
|
||||||
@click=${() =>
|
|
||||||
this.dispatchCustomEvent(this.eventName, { step: backStep.id, action: "back" })}
|
|
||||||
>
|
|
||||||
${this.currentStep.backButtonLabel}
|
|
||||||
</button> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFooterCancelButton() {
|
|
||||||
return html`<div class="pf-c-wizard__footer-cancel">
|
return html`<div class="pf-c-wizard__footer-cancel">
|
||||||
<button class="pf-c-button pf-m-link" type="button" @click=${() => this.reset()}>
|
<button
|
||||||
${msg("Cancel")}
|
class=${classMap(buttonClasses)}
|
||||||
|
type="button"
|
||||||
|
@click=${() => this.dispatchCustomEvent(this.eventName, { command: "close" })}
|
||||||
|
>
|
||||||
|
${label}
|
||||||
</button>
|
</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,22 @@ const hasValidator = (v: any): v is Required<Pick<WizardPanel, "validator">> =>
|
||||||
*
|
*
|
||||||
* @element ak-wizard-main
|
* @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
|
* - keep the collection of steps
|
||||||
* - maintain the open/close status of the modal
|
* - maintain the open/close status of the modal
|
||||||
* - listens for navigation events
|
* - listens for navigation events
|
||||||
* - if a navigation event is valid, switch to the panel requested
|
* - if a navigation event is valid, switch to the panel requested
|
||||||
|
*
|
||||||
|
*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@customElement("ak-wizard-main")
|
@customElement("ak-wizard-main")
|
||||||
|
@ -56,7 +67,7 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
* @attribute
|
* @attribute
|
||||||
*/
|
*/
|
||||||
@state()
|
@state()
|
||||||
currentStep!: WizardStep;
|
currentStep: number = 0;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -71,14 +82,6 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
prompt = "Show Wizard";
|
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.
|
* The text of the header on the wizard, upper bar.
|
||||||
*
|
*
|
||||||
|
@ -95,18 +98,17 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
@property()
|
@property()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to show the "cancel" button in the wizard.
|
||||||
|
*
|
||||||
|
* @attribute
|
||||||
|
*/
|
||||||
|
@property({ type: Boolean })
|
||||||
|
canCancel!: boolean;
|
||||||
|
|
||||||
@query("ak-wizard-frame")
|
@query("ak-wizard-frame")
|
||||||
frame!: AkWizardFrame;
|
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<string, any>) {
|
|
||||||
if (this.currentStep === undefined) {
|
|
||||||
this.currentStep = this.steps[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.addCustomListener(this.eventName, this.handleNavigation);
|
this.addCustomListener(this.eventName, this.handleNavigation);
|
||||||
|
@ -117,30 +119,55 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that we always scan for the valid next step and throw an error if we can't find it.
|
get maxStep() {
|
||||||
// There should never be a question that the currentStep is a *valid* step.
|
return this.steps.length - 1;
|
||||||
//
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
if (hasValidator(this.frame.content)) {
|
||||||
return this.frame.content.validator();
|
return this.frame.content.validator();
|
||||||
}
|
}
|
||||||
|
@ -150,7 +177,7 @@ export class AkWizardMain extends CustomListenerElement(AKElement) {
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<ak-wizard-frame
|
<ak-wizard-frame
|
||||||
?open=${this.open}
|
?canCancel=${this.canCancel}
|
||||||
header=${this.header}
|
header=${this.header}
|
||||||
description=${ifDefined(this.description)}
|
description=${ifDefined(this.description)}
|
||||||
eventName=${this.eventName}
|
eventName=${this.eventName}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { WizardButton } from "./types";
|
||||||
|
|
||||||
|
export const NextStep: WizardButton = [msg("Next"), "next"];
|
||||||
|
|
||||||
|
export const BackStep: WizardButton = [msg("Back"), "back"];
|
||||||
|
|
||||||
|
export const SubmitStep: WizardButton = [msg("Submit"), "next"];
|
||||||
|
|
||||||
|
export const CancelWizard: WizardButton = [msg("Cancel"), "close"];
|
||||||
|
|
||||||
|
export const CloseWizard: WizardButton = [msg("Close"), "close"];
|
||||||
|
|
|
@ -36,7 +36,8 @@ export class AkDemoWizard extends AKElement {
|
||||||
<ak-wizard-context .steps=${this.steps}>
|
<ak-wizard-context .steps=${this.steps}>
|
||||||
<ak-wizard-frame
|
<ak-wizard-frame
|
||||||
?open=${this.open}
|
?open=${this.open}
|
||||||
header=${this.header}
|
header=${this.header}
|
||||||
|
canCancel
|
||||||
description=${ifDefined(this.description)}
|
description=${ifDefined(this.description)}
|
||||||
>
|
>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-primary">Show Wizard</button>
|
<button slot="trigger" class="pf-c-button pf-m-primary">Show Wizard</button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import "@goauthentik/elements/messages/MessageContainer";
|
import "@goauthentik/elements/messages/MessageContainer";
|
||||||
import { Meta } from "@storybook/web-components";
|
import { Meta } from "@storybook/web-components";
|
||||||
|
import { NextStep, BackStep, CancelWizard, CloseWizard } from "../commonWizardButtons";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
|
|
||||||
import "../ak-wizard-main";
|
import "../ak-wizard-main";
|
||||||
|
@ -37,27 +37,21 @@ const container = (testItem: TemplateResult) =>
|
||||||
|
|
||||||
const dummySteps: WizardStep[] = [
|
const dummySteps: WizardStep[] = [
|
||||||
{
|
{
|
||||||
id: "0",
|
|
||||||
label: "Test Step1",
|
label: "Test Step1",
|
||||||
renderer: () => html`<h2>This space intentionally left blank today</h2>`,
|
renderer: () => html`<h2>This space intentionally left blank today</h2>`,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
valid: true,
|
buttons: [NextStep, CancelWizard],
|
||||||
nextButtonLabel: "Next",
|
|
||||||
backButtonLabel: undefined,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "1",
|
|
||||||
label: "Test Step 2",
|
label: "Test Step 2",
|
||||||
renderer: () => html`<h2>This space also intentionally left blank</h2>`,
|
renderer: () => html`<h2>This space also intentionally left blank</h2>`,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
valid: true,
|
buttons: [BackStep, CloseWizard],
|
||||||
nextButtonLabel: undefined,
|
|
||||||
backButtonLabel: "Back",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const OnePageWizard = () => {
|
export const OnePageWizard = () => {
|
||||||
return container(
|
return container(
|
||||||
html` <ak-wizard-main .steps=${dummySteps} prompt="Start the show!"></ak-wizard-main>`,
|
html` <ak-wizard-main .steps=${dummySteps} canCancel header="The Grand Illusion" prompt="Start the show!"></ak-wizard-main>`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,38 @@
|
||||||
import { TemplateResult } from "lit";
|
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 {
|
export interface WizardStep {
|
||||||
id: string;
|
// The name of the step, as shown in the navigation.
|
||||||
label: string;
|
label: string;
|
||||||
valid: boolean;
|
|
||||||
|
// A function which returns the html for rendering the actual content of the step, its form and
|
||||||
|
// such.
|
||||||
renderer: () => TemplateResult;
|
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;
|
disabled: boolean;
|
||||||
nextButtonLabel?: string;
|
|
||||||
backButtonLabel?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WizardPanel extends HTMLElement {
|
export interface WizardPanel extends HTMLElement {
|
||||||
|
|
Reference in New Issue