web: migrate dropdowns to wizards (#2633)
* web/admin: add basic wizards for providers
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* web: add dark mode for wizard
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* web/admin: migrate policies to wizard
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* start source
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* policies: sanitze_dict when returning log messages during tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* Revert "web/admin: migrate policies to wizard"
This reverts commit d8b7f62d3e.
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
# Conflicts:
# web/src/locales/zh-Hans.po
# web/src/locales/zh-Hant.po
# web/src/locales/zh_TW.po
* web: rewrite wizard to be element based
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* further cleanup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* update sources
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* web: migrate property mappings
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* migrate stages
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* migrate misc dropdowns
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
* migrate outpost integrations
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
@ -40,6 +40,7 @@ export class ProxyForm extends Form<unknown> {
|
||||
elementName = this.typeMap[this.type];
|
||||
}
|
||||
this.innerElement = document.createElement(elementName) as Form<unknown>;
|
||||
this.innerElement.viewportCheck = this.viewportCheck;
|
||||
for (const k in this.args) {
|
||||
this.innerElement.setAttribute(k, this.args[k] as string);
|
||||
(this.innerElement as unknown as Record<string, unknown>)[k] = this.args[k];
|
||||
|
||||
33
web/src/elements/wizard/FormWizardPage.ts
Normal file
33
web/src/elements/wizard/FormWizardPage.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { Form } from "../forms/Form";
|
||||
import { WizardPage } from "./WizardPage";
|
||||
|
||||
@customElement("ak-wizard-page-form")
|
||||
export class FormWizardPage extends WizardPage {
|
||||
_isValid = true;
|
||||
|
||||
isValid(): boolean {
|
||||
return this._isValid;
|
||||
}
|
||||
|
||||
nextCallback = async () => {
|
||||
const form = this.querySelector<Form<unknown>>("*");
|
||||
if (!form) {
|
||||
return Promise.reject(t`No form found`);
|
||||
}
|
||||
const formPromise = form.submit(new Event("submit"));
|
||||
if (!formPromise) {
|
||||
return Promise.reject(t`Form didn't return a promise for submitting`);
|
||||
}
|
||||
return formPromise
|
||||
.then(() => {
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
return false;
|
||||
});
|
||||
};
|
||||
}
|
||||
174
web/src/elements/wizard/Wizard.ts
Normal file
174
web/src/elements/wizard/Wizard.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { property } from "@lit/reactive-element/decorators/property.js";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
|
||||
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
|
||||
|
||||
import { ModalButton } from "../buttons/ModalButton";
|
||||
import { WizardPage } from "./WizardPage";
|
||||
|
||||
@customElement("ak-wizard")
|
||||
export class Wizard extends ModalButton {
|
||||
@property()
|
||||
header?: string;
|
||||
|
||||
@property()
|
||||
description?: string;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFWizard);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
steps: string[] = [];
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
setSteps(...steps: string[]): void {
|
||||
this.steps = steps;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
finalHandler?: () => Promise<void>;
|
||||
|
||||
renderModalInner(): TemplateResult {
|
||||
const firstPage = this.querySelector<WizardPage>(`[slot=${this.steps[0]}]`);
|
||||
if (!this.currentStep && firstPage) {
|
||||
this.currentStep = firstPage;
|
||||
}
|
||||
this.currentStep?.requestUpdate();
|
||||
const currentIndex = this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0;
|
||||
return html`<div class="pf-c-wizard">
|
||||
<div class="pf-c-wizard__header">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain pf-c-wizard__close"
|
||||
type="button"
|
||||
aria-label="${t`Close`}"
|
||||
>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
<h1 class="pf-c-title pf-m-3xl pf-c-wizard__title">${this.header}</h1>
|
||||
<p class="pf-c-wizard__description">${this.description}</p>
|
||||
</div>
|
||||
<div class="pf-c-wizard__outer-wrap">
|
||||
<div class="pf-c-wizard__inner-wrap">
|
||||
<nav class="pf-c-wizard__nav">
|
||||
<ol class="pf-c-wizard__nav-list">
|
||||
${this.steps.map((step, idx) => {
|
||||
const currentIdx = this.currentStep
|
||||
? this.steps.indexOf(this.currentStep.slot)
|
||||
: 0;
|
||||
return html`
|
||||
<li class="pf-c-wizard__nav-item">
|
||||
<button
|
||||
class="pf-c-wizard__nav-link ${idx === currentIdx
|
||||
? "pf-m-current"
|
||||
: ""}"
|
||||
?disabled=${currentIdx < idx}
|
||||
@click=${() => {
|
||||
const stepEl = this.querySelector<WizardPage>(
|
||||
`[slot=${step}]`,
|
||||
);
|
||||
if (stepEl) {
|
||||
this.currentStep = stepEl;
|
||||
}
|
||||
}}
|
||||
>
|
||||
${this.querySelector<WizardPage>(
|
||||
`[slot=${step}]`,
|
||||
)?.sidebarLabel()}
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
<main class="pf-c-wizard__main">
|
||||
<div class="pf-c-wizard__main-body">
|
||||
<slot name=${this.currentStep?.slot || this.steps[0]}></slot>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer class="pf-c-wizard__footer">
|
||||
<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
type="submit"
|
||||
?disabled=${!this._currentStep?.isValid()}
|
||||
@click=${async () => {
|
||||
const cb = await this.currentStep?.nextCallback();
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
if (currentIndex === this.steps.length - 1) {
|
||||
if (this.finalHandler) {
|
||||
await this.finalHandler();
|
||||
}
|
||||
this.open = false;
|
||||
} else {
|
||||
const nextPage = this.querySelector<WizardPage>(
|
||||
`[slot=${this.steps[currentIndex + 1]}]`,
|
||||
);
|
||||
if (nextPage) {
|
||||
this.currentStep = nextPage;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
${currentIndex === this.steps.length - 1 ? t`Finish` : t`Next`}
|
||||
</button>
|
||||
${(this.currentStep ? this.steps.indexOf(this.currentStep.slot) : 0) > 0
|
||||
? html`
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
const prevPage = this.querySelector<WizardPage>(
|
||||
`[slot=${this.steps[currentIndex - 1]}]`,
|
||||
);
|
||||
if (prevPage) {
|
||||
this.currentStep = prevPage;
|
||||
}
|
||||
}}
|
||||
>
|
||||
${t`Back`}
|
||||
</button>
|
||||
`
|
||||
: html``}
|
||||
<div class="pf-c-wizard__footer-cancel">
|
||||
<button
|
||||
class="pf-c-button pf-m-link"
|
||||
type="button"
|
||||
@click=${() => {
|
||||
this.open = false;
|
||||
const firstPage = this.querySelector<WizardPage>(
|
||||
`[slot=${this.steps[0]}]`,
|
||||
);
|
||||
if (firstPage) {
|
||||
this.currentStep = firstPage;
|
||||
}
|
||||
}}
|
||||
>
|
||||
${t`Cancel`}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
46
web/src/elements/wizard/WizardPage.ts
Normal file
46
web/src/elements/wizard/WizardPage.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { LitElement, PropertyDeclaration, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { Wizard } from "./Wizard";
|
||||
|
||||
@customElement("ak-wizard-page")
|
||||
export class WizardPage extends LitElement {
|
||||
@property()
|
||||
sidebarLabel: () => string = () => {
|
||||
return "UNNAMED";
|
||||
};
|
||||
|
||||
isValid(): boolean {
|
||||
return this._isValid;
|
||||
}
|
||||
|
||||
get host(): Wizard {
|
||||
return this.parentElement as Wizard;
|
||||
}
|
||||
|
||||
_isValid = false;
|
||||
|
||||
activeCallback: () => Promise<void> = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
nextCallback: () => Promise<boolean> = async () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
requestUpdate(
|
||||
name?: PropertyKey,
|
||||
oldValue?: unknown,
|
||||
options?: PropertyDeclaration<unknown, unknown>,
|
||||
): void {
|
||||
this.querySelectorAll("*").forEach((el) => {
|
||||
if ("requestUpdate" in el) {
|
||||
(el as LitElement).requestUpdate();
|
||||
}
|
||||
});
|
||||
return super.requestUpdate(name, oldValue, options);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user