mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
9 Commits
fix-redund
...
wizard-cle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a80ead4ba9 | ||
|
|
1c4c0f3d45 | ||
|
|
b4fa1b260f | ||
|
|
eb78a55776 | ||
|
|
feda970a39 | ||
|
|
528a5c2e61 | ||
|
|
044a0620af | ||
|
|
d8e8442b68 | ||
|
|
a63e6946e9 |
@@ -51,7 +51,7 @@ export class FooterLinkInput extends AkControlElement<FooterLink> {
|
||||
) as unknown as FooterLink;
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
get valid() {
|
||||
const href = this.json()?.href ?? "";
|
||||
return hasLegalScheme(href) && URL.canParse(href);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const metadata: Meta<FooterLinkInput> = {
|
||||
return;
|
||||
}
|
||||
const target = event.target as FooterLinkInput;
|
||||
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.isValid ? "Yes" : "No"}`;
|
||||
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.valid ? "Yes" : "No"}`;
|
||||
});
|
||||
}, 250);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("ak-admin-settings-footer-link", () => {
|
||||
it("should render an empty control", async () => {
|
||||
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(false);
|
||||
await expect(await link.getProperty("valid")).toStrictEqual(false);
|
||||
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" });
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("ak-admin-settings-footer-link", () => {
|
||||
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await link.$('input[name="name"]').setValue("foo");
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(false);
|
||||
await expect(await link.getProperty("valid")).toStrictEqual(false);
|
||||
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" });
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("ak-admin-settings-footer-link", () => {
|
||||
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await link.$('input[name="href"]').setValue("https://foo.com");
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("valid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("toJson")).toEqual({
|
||||
name: "",
|
||||
href: "https://foo.com",
|
||||
@@ -48,7 +48,7 @@ describe("ak-admin-settings-footer-link", () => {
|
||||
const link = await $("ak-admin-settings-footer-link");
|
||||
await link.$('input[name="name"]').setValue("foo");
|
||||
await link.$('input[name="href"]').setValue("https://foo.com");
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("valid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("toJson")).toEqual({
|
||||
name: "foo",
|
||||
href: "https://foo.com",
|
||||
@@ -64,6 +64,6 @@ describe("ak-admin-settings-footer-link", () => {
|
||||
name: "foo",
|
||||
href: "never://foo.com",
|
||||
});
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(false);
|
||||
await expect(await link.getProperty("valid")).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,21 +20,23 @@ import { ValidationError } from "@goauthentik/api";
|
||||
import { msg } from "@lit/localize";
|
||||
import { property, query } from "lit/decorators.js";
|
||||
|
||||
export class ApplicationWizardStep<T = Record<string, unknown>> extends WizardStep {
|
||||
export abstract class ApplicationWizardStep<T = Record<string, unknown>> extends WizardStep {
|
||||
static styles = [...WizardStep.styles, ...styles];
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
wizard!: ApplicationWizardState;
|
||||
public wizard!: ApplicationWizardState;
|
||||
|
||||
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
|
||||
// these fields and provide them to all the child classes.
|
||||
wizardTitle = msg("New application");
|
||||
wizardDescription = msg("Create a new application and configure a provider for it.");
|
||||
canCancel = true;
|
||||
protected override wizardTitle = msg("New application");
|
||||
protected override wizardDescription = msg(
|
||||
"Create a new application and configure a provider for it.",
|
||||
);
|
||||
protected override cancelable = true;
|
||||
|
||||
// This should be overridden in the children for more precise targeting.
|
||||
@query("form")
|
||||
form!: HTMLFormElement;
|
||||
protected form!: HTMLFormElement;
|
||||
|
||||
get formValues(): T {
|
||||
return serializeForm<T>([
|
||||
|
||||
@@ -24,89 +24,92 @@ import { html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
|
||||
|
||||
const trimMany = (o: Record<string, unknown>, vs: string[]) =>
|
||||
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isStr = (v: any): v is string => typeof v === "string";
|
||||
|
||||
@customElement("ak-application-wizard-application-step")
|
||||
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
label = msg("Application");
|
||||
export class ApplicationWizardApplicationStep extends ApplicationWizardStep<
|
||||
Partial<ApplicationRequest>
|
||||
> {
|
||||
public override label = msg("Application");
|
||||
|
||||
@state()
|
||||
errors = new Map<string, string>();
|
||||
protected errors = new Map<string, string>();
|
||||
|
||||
@query("form#applicationform")
|
||||
form!: HTMLFormElement;
|
||||
protected form!: HTMLFormElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// This is the first step. Ensure it is always enabled.
|
||||
this.enabled = true;
|
||||
}
|
||||
// This is the first step. Ensure it is always enabled.
|
||||
public override enabled = false;
|
||||
|
||||
errorMessages(name: string) {
|
||||
return this.errors.has(name)
|
||||
? [this.errors.get(name)]
|
||||
: (this.wizard.errors?.app?.[name] ??
|
||||
this.wizard.errors?.app?.[camelToSnake(name)] ??
|
||||
[]);
|
||||
protected errorMessages(name: string) {
|
||||
const message = this.errors.get(name);
|
||||
|
||||
if (message) {
|
||||
return [message];
|
||||
}
|
||||
|
||||
const appErrors = this.wizard.errors?.app;
|
||||
|
||||
if (!appErrors || typeof appErrors !== "object") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return appErrors[name] ?? appErrors[camelToSnake(name)] ?? [];
|
||||
}
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
|
||||
}
|
||||
|
||||
get valid() {
|
||||
public reportValidity() {
|
||||
this.errors = new Map();
|
||||
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
|
||||
|
||||
if (values.name === "") {
|
||||
const name = this.formValues.name?.trim();
|
||||
const slug = this.formValues.slug?.trim();
|
||||
const metaLaunchUrl = this.formValues.metaLaunchUrl?.trim();
|
||||
|
||||
if (!name) {
|
||||
this.errors.set("name", msg("An application name is required"));
|
||||
}
|
||||
if (
|
||||
!(
|
||||
isStr(values.metaLaunchUrl) &&
|
||||
(values.metaLaunchUrl === "" || URL.canParse(values.metaLaunchUrl))
|
||||
)
|
||||
) {
|
||||
|
||||
if (!metaLaunchUrl || !URL.canParse(metaLaunchUrl)) {
|
||||
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
|
||||
}
|
||||
if (!(isStr(values.slug) && values.slug !== "" && isSlug(values.slug))) {
|
||||
|
||||
if (!slug || !isSlug(slug)) {
|
||||
this.errors.set("slug", msg("Not a valid slug"));
|
||||
}
|
||||
|
||||
return this.errors.size === 0;
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
if (button.kind === "next") {
|
||||
if (!this.valid) {
|
||||
this.handleEnabling({
|
||||
disabled: ["provider-choice", "provider", "bindings", "submit"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
|
||||
protected override dispatchButtonEvent(button: NavigableButton) {
|
||||
if (button.kind !== "next") {
|
||||
return super.dispatchButtonEvent(button);
|
||||
}
|
||||
|
||||
let payload: ApplicationWizardStateUpdate = {
|
||||
app: this.formValues,
|
||||
errors: this.removeErrors("app"),
|
||||
};
|
||||
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
|
||||
payload = {
|
||||
...payload,
|
||||
provider: { name: `Provider for ${app.name}` },
|
||||
};
|
||||
}
|
||||
this.handleUpdate(payload, button.destination, {
|
||||
enable: "provider-choice",
|
||||
if (!this.reportValidity()) {
|
||||
this.handleEnabling({
|
||||
disabled: ["provider-choice", "provider", "bindings", "submit"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
super.handleButton(button);
|
||||
|
||||
const app = this.formValues;
|
||||
|
||||
let payload: ApplicationWizardStateUpdate = {
|
||||
app: this.formValues,
|
||||
errors: this.removeErrors("app"),
|
||||
};
|
||||
|
||||
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
|
||||
payload = {
|
||||
...payload,
|
||||
provider: { name: `Provider for ${app.name}` },
|
||||
};
|
||||
}
|
||||
|
||||
this.handleUpdate(payload, button.destination, {
|
||||
enable: "provider-choice",
|
||||
});
|
||||
}
|
||||
|
||||
renderForm(app: Partial<ApplicationRequest>, errors: ValidationRecord) {
|
||||
|
||||
@@ -44,18 +44,18 @@ const PASS_FAIL = [
|
||||
|
||||
@customElement("ak-application-wizard-edit-binding-step")
|
||||
export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
|
||||
label = msg("Edit Binding");
|
||||
public label = msg("Edit Binding");
|
||||
|
||||
hide = true;
|
||||
public override hide = true;
|
||||
|
||||
@query("form#bindingform")
|
||||
form!: HTMLFormElement;
|
||||
protected form!: HTMLFormElement;
|
||||
|
||||
@query(".policy-search-select")
|
||||
searchSelect!: SearchSelectBase<Policy> | SearchSelectBase<Group> | SearchSelectBase<User>;
|
||||
|
||||
@state()
|
||||
policyGroupUser: target = target.policy;
|
||||
protected policyGroupUser: target = target.policy;
|
||||
|
||||
instanceId = -1;
|
||||
|
||||
@@ -69,7 +69,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
|
||||
];
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
protected override dispatchButtonEvent(button: NavigableButton) {
|
||||
if (button.kind === "next") {
|
||||
if (!this.form.checkValidity()) {
|
||||
return;
|
||||
@@ -93,7 +93,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
|
||||
this.handleUpdate({ bindings }, "bindings");
|
||||
return;
|
||||
}
|
||||
super.handleButton(button);
|
||||
super.dispatchButtonEvent(button);
|
||||
}
|
||||
|
||||
// The search select configurations for the three different types of fetches that we care about,
|
||||
|
||||
@@ -24,10 +24,10 @@ import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-application-wizard-provider-choice-step")
|
||||
export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) {
|
||||
label = msg("Choose a Provider");
|
||||
public override label = msg("Choose a Provider");
|
||||
|
||||
@state()
|
||||
failureMessage = "";
|
||||
protected failureMessage = "";
|
||||
|
||||
@consume({ context: applicationWizardProvidersContext, subscribe: true })
|
||||
public providerModelsList!: LocalTypeCreate[];
|
||||
@@ -40,18 +40,20 @@ export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(Appl
|
||||
];
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
protected override dispatchButtonEvent(button: NavigableButton) {
|
||||
this.failureMessage = "";
|
||||
if (button.kind === "next") {
|
||||
if (!this.wizard.providerModel) {
|
||||
this.failureMessage = msg("Please choose a provider type before proceeding.");
|
||||
this.handleEnabling({ disabled: ["provider", "bindings", "submit"] });
|
||||
return;
|
||||
}
|
||||
this.handleUpdate(undefined, button.destination, { enable: "provider" });
|
||||
|
||||
if (button.kind !== "next") {
|
||||
return super.dispatchButtonEvent(button);
|
||||
}
|
||||
|
||||
if (!this.wizard.providerModel) {
|
||||
this.failureMessage = msg("Please choose a provider type before proceeding.");
|
||||
this.handleEnabling({ disabled: ["provider", "bindings", "submit"] });
|
||||
return;
|
||||
}
|
||||
super.handleButton(button);
|
||||
|
||||
this.handleUpdate(undefined, button.destination, { enable: "provider" });
|
||||
}
|
||||
|
||||
@bound
|
||||
@@ -62,31 +64,33 @@ export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(Appl
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
if (this.providerModelsList.length === 0) {
|
||||
return html`<ak-empty-state default-label></ak-empty-state>`;
|
||||
}
|
||||
|
||||
const selectedTypes = this.providerModelsList.filter(
|
||||
(t) => t.modelName === this.wizard.providerModel,
|
||||
);
|
||||
|
||||
return this.providerModelsList.length > 0
|
||||
? html` <ak-wizard-title>${msg("Choose a Provider Type")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-wizard-page-type-create
|
||||
.types=${this.providerModelsList}
|
||||
name="selectProviderType"
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
||||
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
||||
this.handleUpdate(
|
||||
{
|
||||
...this.wizard,
|
||||
providerModel: ev.detail.modelName,
|
||||
},
|
||||
undefined,
|
||||
{ enable: "provider" },
|
||||
);
|
||||
}}
|
||||
></ak-wizard-page-type-create>
|
||||
</form>`
|
||||
: html`<ak-empty-state default-label></ak-empty-state>`;
|
||||
return html` <ak-wizard-title>${msg("Choose a Provider Type")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-wizard-page-type-create
|
||||
.types=${this.providerModelsList}
|
||||
name="selectProviderType"
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
||||
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
||||
this.handleUpdate(
|
||||
{
|
||||
...this.wizard,
|
||||
providerModel: ev.detail.modelName,
|
||||
},
|
||||
undefined,
|
||||
{ enable: "provider" },
|
||||
);
|
||||
}}
|
||||
></ak-wizard-page-type-create>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,27 +43,28 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep {
|
||||
return this.element.formValues;
|
||||
}
|
||||
|
||||
override handleButton(button: NavigableButton) {
|
||||
if (button.kind === "next") {
|
||||
if (!this.valid) {
|
||||
this.handleEnabling({
|
||||
disabled: ["bindings", "submit"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
provider: {
|
||||
...this.formValues,
|
||||
mode: this.wizard.proxyMode,
|
||||
},
|
||||
errors: this.removeErrors("provider"),
|
||||
};
|
||||
this.handleUpdate(payload, button.destination, {
|
||||
enable: ["bindings", "submit"],
|
||||
protected override dispatchButtonEvent(button: NavigableButton) {
|
||||
if (button.kind !== "next") {
|
||||
return super.dispatchButtonEvent(button);
|
||||
}
|
||||
|
||||
if (!this.valid) {
|
||||
this.handleEnabling({
|
||||
disabled: ["bindings", "submit"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
super.handleButton(button);
|
||||
|
||||
const payload = {
|
||||
provider: {
|
||||
...this.formValues,
|
||||
mode: this.wizard.proxyMode,
|
||||
},
|
||||
errors: this.removeErrors("provider"),
|
||||
};
|
||||
this.handleUpdate(payload, button.destination, {
|
||||
enable: ["bindings", "submit"],
|
||||
});
|
||||
}
|
||||
|
||||
get buttons(): WizardButton[] {
|
||||
|
||||
@@ -177,16 +177,16 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
|
||||
});
|
||||
}
|
||||
|
||||
override handleButton(button: WizardButton) {
|
||||
match([button.kind, this.state])
|
||||
protected override dispatchButtonEvent(button: WizardButton) {
|
||||
return match([button.kind, this.state])
|
||||
.with([P.union("back", "cancel"), P._], () => {
|
||||
super.handleButton(button);
|
||||
return super.dispatchButtonEvent(button);
|
||||
})
|
||||
.with(["close", "submitted"], () => {
|
||||
super.handleButton(button);
|
||||
return super.dispatchButtonEvent(button);
|
||||
})
|
||||
.with(["next", "reviewing"], () => {
|
||||
this.send();
|
||||
return this.send();
|
||||
})
|
||||
.with([P._, "running"], () => {
|
||||
throw new Error("No buttons should be showing when running submit phase");
|
||||
|
||||
@@ -19,13 +19,13 @@ import { property, query } from "lit/decorators.js";
|
||||
export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKElement {
|
||||
static styles: CSSResult[] = [...AwadStyles];
|
||||
|
||||
label = "";
|
||||
public label = "";
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
wizard!: ApplicationWizardState;
|
||||
public wizard!: ApplicationWizardState;
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
errors: Record<string | number | symbol, string> = {};
|
||||
public errors: Record<string, string> = {};
|
||||
|
||||
@query("form#providerform")
|
||||
form!: HTMLFormElement;
|
||||
@@ -50,7 +50,7 @@ export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKEl
|
||||
[]);
|
||||
}
|
||||
|
||||
isValid(name: keyof T) {
|
||||
isValid(name: Extract<keyof T, string>) {
|
||||
return !(
|
||||
(this.wizard.errors?.provider?.[name as string] ?? []).length > 0 ||
|
||||
this.errors?.[name] !== undefined
|
||||
|
||||
@@ -55,7 +55,7 @@ export class ServiceConnectionWizard extends AKElement {
|
||||
"initial",
|
||||
`type-${ev.detail.component}-${ev.detail.modelName}`,
|
||||
];
|
||||
this.wizard.isValid = true;
|
||||
this.wizard.valid = true;
|
||||
}}
|
||||
>
|
||||
</ak-wizard-page-type-create>
|
||||
@@ -63,7 +63,7 @@ export class ServiceConnectionWizard extends AKElement {
|
||||
return html`
|
||||
<ak-wizard-page-form
|
||||
slot=${`type-${type.component}-${type.modelName}`}
|
||||
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
||||
.sidebarLabel=${msg(str`Create ${type.name}`)}
|
||||
>
|
||||
<ak-proxy-form type=${type.component}></ak-proxy-form>
|
||||
</ak-wizard-page-form>
|
||||
|
||||
@@ -66,7 +66,7 @@ export class PolicyWizard extends AKElement {
|
||||
|
||||
this.wizard.steps.splice(idx, 0, `type-${component}-${modelName}`);
|
||||
|
||||
this.wizard.isValid = true;
|
||||
this.wizard.valid = true;
|
||||
};
|
||||
|
||||
render(): TemplateResult {
|
||||
@@ -87,7 +87,7 @@ export class PolicyWizard extends AKElement {
|
||||
return html`
|
||||
<ak-wizard-page-form
|
||||
slot=${`type-${type.component}-${type.modelName}`}
|
||||
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
||||
.sidebarLabel=${msg(str`Create ${type.name}`)}
|
||||
>
|
||||
<ak-proxy-form type=${type.component}></ak-proxy-form>
|
||||
</ak-wizard-page-form>
|
||||
@@ -96,7 +96,7 @@ export class PolicyWizard extends AKElement {
|
||||
${this.showBindingPage
|
||||
? html`<ak-wizard-page-form
|
||||
slot="create-binding"
|
||||
.sidebarLabel=${() => msg("Create Binding")}
|
||||
.sidebarLabel=${msg("Create Binding")}
|
||||
.activePageCallback=${async (context: FormWizardPage) => {
|
||||
const createSlot = context.host.steps[1];
|
||||
const bindingForm =
|
||||
|
||||
@@ -65,7 +65,7 @@ export class PropertyMappingWizard extends AKElement {
|
||||
"initial",
|
||||
`type-${ev.detail.component}-${ev.detail.modelName}`,
|
||||
];
|
||||
this.wizard.isValid = true;
|
||||
this.wizard.valid = true;
|
||||
}}
|
||||
>
|
||||
</ak-wizard-page-type-create>
|
||||
@@ -73,7 +73,7 @@ export class PropertyMappingWizard extends AKElement {
|
||||
return html`
|
||||
<ak-wizard-page-form
|
||||
slot=${`type-${type.component}-${type.modelName}`}
|
||||
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
||||
.sidebarLabel=${msg(str`Create ${type.name}`)}
|
||||
>
|
||||
<ak-proxy-form type=${type.component}></ak-proxy-form>
|
||||
</ak-wizard-page-form>
|
||||
|
||||
@@ -30,20 +30,18 @@ export class ProviderWizard extends AKElement {
|
||||
static styles: CSSResult[] = [PFBase, PFButton];
|
||||
|
||||
@property()
|
||||
createText = msg("Create");
|
||||
public createText = msg("Create");
|
||||
|
||||
@property({ attribute: false })
|
||||
providerTypes: TypeCreate[] = [];
|
||||
public providerTypes: TypeCreate[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
finalHandler: () => Promise<void> = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
public finalHandler?: () => Promise<void>;
|
||||
|
||||
@query("ak-wizard")
|
||||
wizard?: Wizard;
|
||||
protected wizard?: Wizard;
|
||||
|
||||
connectedCallback() {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
|
||||
this.providerTypes = providerTypes;
|
||||
@@ -56,9 +54,7 @@ export class ProviderWizard extends AKElement {
|
||||
.steps=${["initial"]}
|
||||
header=${msg("New provider")}
|
||||
description=${msg("Create a new provider.")}
|
||||
.finalHandler=${() => {
|
||||
return this.finalHandler();
|
||||
}}
|
||||
.finalHandler=${() => this.finalHandler?.()}
|
||||
>
|
||||
<ak-wizard-page-type-create
|
||||
name="selectProviderType"
|
||||
@@ -68,7 +64,7 @@ export class ProviderWizard extends AKElement {
|
||||
@select=${(ev: CustomEvent<TypeCreate>) => {
|
||||
if (!this.wizard) return;
|
||||
this.wizard.steps = ["initial", `type-${ev.detail.component}`];
|
||||
this.wizard.isValid = true;
|
||||
this.wizard.valid = true;
|
||||
}}
|
||||
>
|
||||
</ak-wizard-page-type-create>
|
||||
@@ -76,7 +72,7 @@ export class ProviderWizard extends AKElement {
|
||||
return html`
|
||||
<ak-wizard-page-form
|
||||
slot=${`type-${type.component}`}
|
||||
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
||||
.sidebarLabel=${msg(str`Create ${type.name}`)}
|
||||
>
|
||||
<ak-proxy-form type=${type.component}></ak-proxy-form>
|
||||
</ak-wizard-page-form>
|
||||
|
||||
@@ -48,7 +48,7 @@ export class OAuth2ProviderRedirectURI extends AkControlElement<RedirectURI> {
|
||||
) as unknown as RedirectURI;
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
get valid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export class SourceWizard extends AKElement {
|
||||
"initial",
|
||||
`type-${ev.detail.component}-${ev.detail.modelName}`,
|
||||
];
|
||||
this.wizard.isValid = true;
|
||||
this.wizard.valid = true;
|
||||
}}
|
||||
>
|
||||
</ak-wizard-page-type-create>
|
||||
@@ -65,7 +65,7 @@ export class SourceWizard extends AKElement {
|
||||
return html`
|
||||
<ak-wizard-page-form
|
||||
slot=${`type-${type.component}-${type.modelName}`}
|
||||
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
||||
.sidebarLabel=${msg(str`Create ${type.name}`)}
|
||||
>
|
||||
<ak-proxy-form
|
||||
.args=${{
|
||||
|
||||
@@ -93,7 +93,7 @@ export class StageWizard extends AKElement {
|
||||
0,
|
||||
`type-${ev.detail.component}-${ev.detail.modelName}`,
|
||||
);
|
||||
this.wizard.isValid = true;
|
||||
this.wizard.valid = true;
|
||||
}}
|
||||
>
|
||||
</ak-wizard-page-type-create>
|
||||
@@ -101,7 +101,7 @@ export class StageWizard extends AKElement {
|
||||
return html`
|
||||
<ak-wizard-page-form
|
||||
slot=${`type-${type.component}-${type.modelName}`}
|
||||
.sidebarLabel=${() => msg(str`Create ${type.name}`)}
|
||||
.sidebarLabel=${msg(str`Create ${type.name}`)}
|
||||
>
|
||||
<ak-proxy-form type=${type.component}></ak-proxy-form>
|
||||
</ak-wizard-page-form>
|
||||
@@ -110,7 +110,7 @@ export class StageWizard extends AKElement {
|
||||
${this.showBindingPage
|
||||
? html`<ak-wizard-page-form
|
||||
slot="create-binding"
|
||||
.sidebarLabel=${() => msg("Create Binding")}
|
||||
.sidebarLabel=${msg("Create Binding")}
|
||||
.activePageCallback=${async (context: FormWizardPage) => {
|
||||
const createSlot = context.host.steps[1];
|
||||
const bindingForm =
|
||||
|
||||
@@ -77,6 +77,41 @@ html > form > input {
|
||||
|
||||
.pf-c-form {
|
||||
--pf-c-form__group--m-action--MarginTop: var(--pf-global--spacer--form-element);
|
||||
|
||||
&.ak-m-radio-list {
|
||||
--pf-c-form--GridGap: 0.5rem;
|
||||
|
||||
.pf-c-radio {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-radio {
|
||||
--pf-c-radio__label--FontWeight: 500;
|
||||
--pf-c-radio--GridGap: var(--pf-global--spacer--xs) var(--pf-global--spacer--lg);
|
||||
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--pf-c-card--m-rounded--BorderRadius);
|
||||
|
||||
transition: background-color linear 100ms;
|
||||
|
||||
outline: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--radio-background-color--hover,
|
||||
var(--pf-global--BackgroundColor--light-200)
|
||||
);
|
||||
}
|
||||
|
||||
&:has(input:checked) {
|
||||
background-color: var(
|
||||
--radio-background-color--selected,
|
||||
var(--pf-global--BackgroundColor--light-200)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* #region Icons */
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
--ak-global--Color--100: var(--ak-dark-foreground) !important;
|
||||
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
|
||||
--pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important;
|
||||
--radio-background-color--hover: rgba(3, 3, 3, 0.1);
|
||||
--radio-background-color--selected: rgba(3, 3, 3, 0.3);
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { wizardStepContext } from "./WizardContexts.js";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
@@ -41,6 +42,17 @@ const BUTTON_KIND_TO_LABEL: Record<ButtonKind, string> = {
|
||||
close: msg("Close"),
|
||||
};
|
||||
|
||||
function buttonLabel(button: WizardButton) {
|
||||
return button.label ?? BUTTON_KIND_TO_LABEL[button.kind];
|
||||
}
|
||||
|
||||
function buttonClasses(button: WizardButton) {
|
||||
return {
|
||||
"pf-c-button": true,
|
||||
[BUTTON_KIND_TO_CLASS[button.kind]]: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @class WizardStep
|
||||
*
|
||||
@@ -60,7 +72,7 @@ const BUTTON_KIND_TO_LABEL: Record<ButtonKind, string> = {
|
||||
* @fires WizardCloseEvent - request parent container (Wizard) to close the wizard
|
||||
*/
|
||||
|
||||
export class WizardStep extends AKElement {
|
||||
export abstract class WizardStep extends AKElement {
|
||||
// These additions are necessary because we don't want to inherit *all* of the modal box
|
||||
// modifiers, just the ones related to managing the height of the display box.
|
||||
static styles = [
|
||||
@@ -79,39 +91,43 @@ export class WizardStep extends AKElement {
|
||||
`,
|
||||
];
|
||||
|
||||
//#region Properties
|
||||
|
||||
@property({ type: Boolean, attribute: true, reflect: true })
|
||||
enabled = false;
|
||||
public enabled = false;
|
||||
|
||||
/**
|
||||
* The name. Should match the slot. Reflected if not present.
|
||||
*/
|
||||
@property({ type: String, attribute: true, reflect: true })
|
||||
name?: string;
|
||||
public name?: string;
|
||||
|
||||
//#endregion
|
||||
|
||||
@consume({ context: wizardStepContext, subscribe: true })
|
||||
wizardStepState: WizardStepState = { currentStep: undefined, stepLabels: [] };
|
||||
protected wizardStepState: WizardStepState = { currentStep: undefined, stepLabels: [] };
|
||||
|
||||
/**
|
||||
* What appears in the titlebar of the Wizard. Usually, but not necessarily, the same for all
|
||||
* steps. Recommendation: Set this, the description, and `canCancel` in a subclass, and stop
|
||||
* steps. Recommendation: Set this, the description, and `cancelable` in a subclass, and stop
|
||||
* worrying about them.
|
||||
*/
|
||||
wizardTitle = "--unset--";
|
||||
protected wizardTitle = "--unset--";
|
||||
|
||||
/**
|
||||
* The text for a descriptive subtitle for the wizard
|
||||
*/
|
||||
wizardDescription?: string;
|
||||
protected wizardDescription?: string;
|
||||
|
||||
/**
|
||||
* Show the [Cancel] icon and offer the [Cancel] button
|
||||
*/
|
||||
canCancel = false;
|
||||
protected cancelable = false;
|
||||
|
||||
/**
|
||||
* The ID of the current step.
|
||||
*/
|
||||
id = "";
|
||||
public id = "";
|
||||
|
||||
/**
|
||||
*The label of the current step. Displayed in the navigation bar.
|
||||
@@ -121,7 +137,7 @@ export class WizardStep extends AKElement {
|
||||
/**
|
||||
* If true, this step's label will not be shown in the navigation bar
|
||||
*/
|
||||
hide = false;
|
||||
public hide = false;
|
||||
|
||||
// ___ _ _ _ _ ___ ___
|
||||
// | _ \_ _| |__| (_)__ /_\ | _ \_ _|
|
||||
@@ -137,13 +153,11 @@ export class WizardStep extends AKElement {
|
||||
}
|
||||
|
||||
// Override this to provide the form.
|
||||
public renderMain() {
|
||||
throw new Error("This must be overridden in client classes");
|
||||
}
|
||||
public abstract renderMain(): SlottedTemplateResult;
|
||||
|
||||
// Override this to intercept 'next' and 'back' events, perform validation, and include enabling
|
||||
// before allowing navigation to continue.
|
||||
public handleButton(button: WizardButton, details?: NavigationEventInit) {
|
||||
protected dispatchButtonEvent(button: WizardButton, details?: NavigationEventInit) {
|
||||
if (["close", "cancel"].includes(button.kind)) {
|
||||
this.dispatchEvent(new WizardCloseEvent());
|
||||
return;
|
||||
@@ -163,25 +177,32 @@ export class WizardStep extends AKElement {
|
||||
|
||||
// END Public API
|
||||
|
||||
connectedCallback() {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.name) {
|
||||
const name = this.getAttribute("slot");
|
||||
|
||||
if (!name) {
|
||||
throw new Error("Steps must have a unique slot attribute.");
|
||||
throw new TypeError("Steps must have a unique slot attribute.");
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@bound
|
||||
onWizardNavigationEvent(ev: Event, button: WizardButton) {
|
||||
ev.stopPropagation();
|
||||
//#endregion
|
||||
|
||||
//#region Event Listeners
|
||||
|
||||
#wizardNavigationListener = (event: Event, button: WizardButton) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!isNavigable(button)) {
|
||||
throw new Error("Non-navigable button sent to handleNavigationEvent");
|
||||
}
|
||||
this.handleButton(button);
|
||||
}
|
||||
|
||||
this.dispatchButtonEvent(button);
|
||||
};
|
||||
|
||||
@bound
|
||||
onWizardCloseEvent(ev: Event) {
|
||||
@@ -189,46 +210,39 @@ export class WizardStep extends AKElement {
|
||||
this.dispatchEvent(new WizardCloseEvent());
|
||||
}
|
||||
|
||||
getButtonLabel(button: WizardButton) {
|
||||
return button.label ?? BUTTON_KIND_TO_LABEL[button.kind];
|
||||
}
|
||||
//#endregion
|
||||
|
||||
getButtonClasses(button: WizardButton) {
|
||||
return {
|
||||
"pf-c-button": true,
|
||||
[BUTTON_KIND_TO_CLASS[button.kind]]: true,
|
||||
};
|
||||
}
|
||||
//#region Render
|
||||
|
||||
@bound
|
||||
renderCloseButton(button: WizardButton) {
|
||||
return html`<div class="pf-c-wizard__footer-cancel">
|
||||
<button
|
||||
class=${classMap(this.getButtonClasses(button))}
|
||||
class=${classMap(buttonClasses(button))}
|
||||
type="button"
|
||||
@click=${this.onWizardCloseEvent}
|
||||
>
|
||||
${this.getButtonLabel(button)}
|
||||
${buttonLabel(button)}
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@bound
|
||||
renderDisabledButton(button: WizardButton) {
|
||||
return html`<button class=${classMap(this.getButtonClasses(button))} type="button" disabled>
|
||||
${this.getButtonLabel(button)}
|
||||
return html`<button class=${classMap(buttonClasses(button))} type="button" disabled>
|
||||
${buttonLabel(button)}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
@bound
|
||||
renderNavigableButton(button: WizardButton) {
|
||||
return html`<button
|
||||
class=${classMap(this.getButtonClasses(button))}
|
||||
class=${classMap(buttonClasses(button))}
|
||||
type="button"
|
||||
@click=${(ev: Event) => this.onWizardNavigationEvent(ev, button)}
|
||||
@click=${(ev: Event) => this.#wizardNavigationListener(ev, button)}
|
||||
data-ouid-button-kind="wizard-${button.kind}"
|
||||
>
|
||||
${this.getButtonLabel(button)}
|
||||
${buttonLabel(button)}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
@@ -277,46 +291,42 @@ export class WizardStep extends AKElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.wizardStepState.currentStep === this.getAttribute("slot")
|
||||
? html` <div class="pf-c-modal-box ak-wizard-box">
|
||||
<div class="pf-c-wizard">
|
||||
<div class="pf-c-wizard__header" data-ouid-component-id="wizard-header">
|
||||
${this.canCancel ? this.renderHeaderCancelIcon() : nothing}
|
||||
<h1 class="pf-c-title pf-m-3xl pf-c-wizard__title">
|
||||
${this.wizardTitle}
|
||||
</h1>
|
||||
<p class="pf-c-wizard__description">${this.wizardDescription}</p>
|
||||
</div>
|
||||
if (this.wizardStepState.currentStep !== this.getAttribute("slot")) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
<div class="pf-c-wizard__outer-wrap">
|
||||
<div class="pf-c-wizard__inner-wrap">
|
||||
<nav class="pf-c-wizard__nav" data-ouid-component-id="wizard-navbar">
|
||||
<ol class="pf-c-wizard__nav-list">
|
||||
${map(
|
||||
this.wizardStepState.stepLabels,
|
||||
this.renderSidebarStep,
|
||||
)}
|
||||
</ol>
|
||||
</nav>
|
||||
<main class="pf-c-wizard__main">
|
||||
<div
|
||||
id="main-content"
|
||||
class="pf-c-wizard__main-body"
|
||||
data-ouid-component-id="wizard-body"
|
||||
>
|
||||
${this.renderMain()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer
|
||||
class="pf-c-wizard__footer"
|
||||
data-ouid-component-id="wizard-footer"
|
||||
>
|
||||
${this.buttons.map(this.renderButton)}
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: nothing;
|
||||
return html` <div class="pf-c-modal-box ak-wizard-box">
|
||||
<div class="pf-c-wizard">
|
||||
<div class="pf-c-wizard__header" data-ouid-component-id="wizard-header">
|
||||
${this.cancelable ? this.renderHeaderCancelIcon() : nothing}
|
||||
<h1 class="pf-c-title pf-m-3xl pf-c-wizard__title">${this.wizardTitle}</h1>
|
||||
<p class="pf-c-wizard__description">${this.wizardDescription}</p>
|
||||
</div>
|
||||
|
||||
<div class="pf-c-wizard__outer-wrap">
|
||||
<div class="pf-c-wizard__inner-wrap">
|
||||
<nav class="pf-c-wizard__nav" data-ouid-component-id="wizard-navbar">
|
||||
<ol class="pf-c-wizard__nav-list">
|
||||
${map(this.wizardStepState.stepLabels, this.renderSidebarStep)}
|
||||
</ol>
|
||||
</nav>
|
||||
<main class="pf-c-wizard__main">
|
||||
<div
|
||||
id="main-content"
|
||||
class="pf-c-wizard__main-body"
|
||||
data-ouid-component-id="wizard-body"
|
||||
>
|
||||
${this.renderMain()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer class="pf-c-wizard__footer" data-ouid-component-id="wizard-footer">
|
||||
${this.buttons.map(this.renderButton)}
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class AkControlElement<T = string | string[]> extends AKElement {
|
||||
return this.json();
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
get valid(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,15 +96,16 @@ export class ArrayInput<T> extends AkControlElement<T[]> implements IArrayInput<
|
||||
return this.items;
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
get valid() {
|
||||
if (!this.validate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oneIsValid = (g: HTMLDivElement) =>
|
||||
g.querySelector<HTMLInputElement & AkControlElement<T>>("[name]")?.isValid ?? true;
|
||||
const allAreValid = Array.from(this.inputGroups ?? []).every(oneIsValid);
|
||||
return allAreValid && (this.validator ? this.validator(this.items) : true);
|
||||
const valid = Array.from(this.inputGroups || []).every((inputGroup) => {
|
||||
return inputGroup.querySelector<AkControlElement>("[name]")?.valid ?? true;
|
||||
});
|
||||
|
||||
return valid && (this.validator?.(this.items) ?? true);
|
||||
}
|
||||
|
||||
itemsFromDom(): T[] {
|
||||
|
||||
@@ -20,7 +20,7 @@ import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
type BulkDeleteMetadata = { key: string; value: string }[];
|
||||
|
||||
@customElement("ak-delete-objects-table")
|
||||
export class DeleteObjectsTable<T> extends Table<T> {
|
||||
export class DeleteObjectsTable<T extends object = object> extends Table<T> {
|
||||
paginated = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -117,7 +117,7 @@ export class DeleteObjectsTable<T> extends Table<T> {
|
||||
}
|
||||
|
||||
@customElement("ak-forms-delete-bulk")
|
||||
export class DeleteBulkForm<T> extends ModalButton {
|
||||
export class DeleteBulkForm<T = object> extends ModalButton {
|
||||
@property({ attribute: false })
|
||||
objects: T[] = [];
|
||||
|
||||
@@ -246,7 +246,7 @@ export class DeleteBulkForm<T> extends ModalButton {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-delete-objects-table": DeleteObjectsTable<unknown>;
|
||||
"ak-forms-delete-bulk": DeleteBulkForm<unknown>;
|
||||
"ak-delete-objects-table": DeleteObjectsTable;
|
||||
"ak-forms-delete-bulk": DeleteBulkForm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { camelToSnake } from "#common/utils";
|
||||
import { isControlElement } from "#elements/AkControlElement";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { PreventFormSubmit } from "#elements/forms/helpers";
|
||||
import { HorizontalFormElement } from "#elements/forms/HorizontalFormElement";
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { createFileMap, isNamedElement, NamedElement } from "#elements/utils/inputs";
|
||||
@@ -170,9 +169,9 @@ export function serializeForm<T = Record<string, unknown>>(elements: Iterable<AK
|
||||
*
|
||||
*/
|
||||
export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
abstract send(data: T): Promise<unknown>;
|
||||
public abstract send(data: T): Promise<unknown>;
|
||||
|
||||
viewportCheck = true;
|
||||
public viewportCheck = true;
|
||||
|
||||
//#region Properties
|
||||
|
||||
@@ -298,16 +297,16 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
if (instanceOfValidationError(parsedError)) {
|
||||
// assign all input-related errors to their elements
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
const elements = this.shadowRoot?.querySelectorAll(
|
||||
"ak-form-element-horizontal",
|
||||
);
|
||||
|
||||
elements.forEach((element) => {
|
||||
for (const element of elements || []) {
|
||||
element.requestUpdate();
|
||||
|
||||
const elementName = element.name;
|
||||
if (!elementName) return;
|
||||
|
||||
if (!elementName) continue;
|
||||
|
||||
const snakeProperty = camelToSnake(elementName);
|
||||
|
||||
@@ -318,19 +317,13 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
element.errorMessages = [];
|
||||
element.invalid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (parsedError.nonFieldErrors) {
|
||||
this.nonFieldErrors = parsedError.nonFieldErrors;
|
||||
}
|
||||
|
||||
errorMessage = msg("Invalid update request.");
|
||||
|
||||
// Only change the message when we have `detail`.
|
||||
// Everything else is handled in the form.
|
||||
if ("detail" in parsedError) {
|
||||
errorMessage = parsedError.detail;
|
||||
}
|
||||
errorMessage = pluckErrorDetail(parsedError, msg("Invalid update request."));
|
||||
}
|
||||
|
||||
showMessage({
|
||||
|
||||
@@ -42,10 +42,6 @@ export class Radio<T> extends CustomEmitterElement(AKElement) {
|
||||
var(--pf-c-form--m-horizontal__group-label--md--PaddingTop) * 1.3
|
||||
);
|
||||
}
|
||||
.pf-c-radio label,
|
||||
.pf-c-radio span {
|
||||
user-select: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const metadata: Meta<IArrayInput<unknown>> = {
|
||||
return;
|
||||
}
|
||||
const target = event.target as FooterLinkInput;
|
||||
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.isValid ? "Yes" : "No"}`;
|
||||
messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.valid ? "Yes" : "No"}`;
|
||||
});
|
||||
}, 250);
|
||||
|
||||
|
||||
@@ -100,12 +100,16 @@ export class TableColumn {
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Table<T> extends WithLicenseSummary(AKElement) implements TableLike {
|
||||
export abstract class Table<T extends object>
|
||||
extends WithLicenseSummary(AKElement)
|
||||
implements TableLike
|
||||
{
|
||||
abstract apiEndpoint(): Promise<PaginatedResponse<T>>;
|
||||
abstract columns(): TableColumn[];
|
||||
abstract row(item: T): SlottedTemplateResult[];
|
||||
|
||||
private isLoading = false;
|
||||
@state()
|
||||
protected loading = false;
|
||||
|
||||
#pageParam = `${this.tagName.toLowerCase()}-page`;
|
||||
#searchParam = `${this.tagName.toLowerCase()}-search`;
|
||||
@@ -126,10 +130,10 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
data?: PaginatedResponse<T>;
|
||||
public data?: PaginatedResponse<T>;
|
||||
|
||||
@property({ type: Number })
|
||||
page = getURLParam(this.#pageParam, 1);
|
||||
public page = getURLParam(this.#pageParam, 1);
|
||||
|
||||
/**
|
||||
* Set if your `selectedElements` use of the selection box is to enable bulk-delete,
|
||||
@@ -138,43 +142,43 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
* @prop
|
||||
*/
|
||||
@property({ attribute: "clear-on-refresh", type: Boolean, reflect: true })
|
||||
clearOnRefresh = false;
|
||||
public clearOnRefresh = false;
|
||||
|
||||
@property({ type: String })
|
||||
order?: string;
|
||||
public order?: string;
|
||||
|
||||
@property({ type: String })
|
||||
search: string = "";
|
||||
public search: string = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
checkbox = false;
|
||||
public checkbox = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
clickable = false;
|
||||
public clickable = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
clickHandler: (item: T) => void = () => {};
|
||||
public clickHandler: (item: T) => void = () => {};
|
||||
|
||||
@property({ type: Boolean })
|
||||
radioSelect = false;
|
||||
public radioSelect = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
checkboxChip = false;
|
||||
public checkboxChip = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
selectedElements: T[] = [];
|
||||
public selectedElements: T[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
paginated = true;
|
||||
public paginated = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
expandable = false;
|
||||
public expandable = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
expandedElements: T[] = [];
|
||||
public expandedElements: T[] = [];
|
||||
|
||||
@state()
|
||||
error?: APIError;
|
||||
protected error: APIError | null = null;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
@@ -213,16 +217,25 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
`,
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(EVENT_REFRESH, async () => {
|
||||
await this.fetch();
|
||||
});
|
||||
//#region Lifecycle
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(EVENT_REFRESH, () => this.fetch());
|
||||
|
||||
if (this.searchEnabled()) {
|
||||
this.search = getURLParam(this.#searchParam, "");
|
||||
}
|
||||
}
|
||||
|
||||
public override firstUpdated(): void {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region API
|
||||
|
||||
async defaultEndpointConfig() {
|
||||
return {
|
||||
ordering: this.order,
|
||||
@@ -239,14 +252,14 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
}
|
||||
|
||||
public async fetch(): Promise<void> {
|
||||
if (this.isLoading) return;
|
||||
if (this.loading) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.loading = true;
|
||||
|
||||
return this.apiEndpoint()
|
||||
.then((data) => {
|
||||
this.data = data;
|
||||
this.error = undefined;
|
||||
this.error = null;
|
||||
|
||||
this.page = this.data.pagination.current;
|
||||
const newExpanded: T[] = [];
|
||||
@@ -289,11 +302,14 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
this.error = await parseAPIResponseError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
this.requestUpdate();
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Render
|
||||
|
||||
private renderLoading(): TemplateResult {
|
||||
return html`<tr role="row">
|
||||
<td role="cell" colspan="25">
|
||||
@@ -337,7 +353,7 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
if (this.error) {
|
||||
return [this.renderEmpty(this.renderError())];
|
||||
}
|
||||
if (!this.data || this.isLoading) {
|
||||
if (!this.data || this.loading) {
|
||||
return [this.renderLoading()];
|
||||
}
|
||||
if (this.data.pagination.count === 0) {
|
||||
@@ -521,10 +537,6 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
</div>`;
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
/* The checkbox on the table header row that allows the user to "activate all on this page,"
|
||||
* "deactivate all on this page" with a single click.
|
||||
*/
|
||||
@@ -610,4 +622,6 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
render(): TemplateResult {
|
||||
return this.renderTable();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -17,23 +17,24 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
|
||||
import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";
|
||||
|
||||
export abstract class TableModal<T> extends Table<T> {
|
||||
export abstract class TableModal<T extends object> extends Table<T> {
|
||||
@property()
|
||||
size: PFSize = PFSize.Large;
|
||||
|
||||
@property({ type: Boolean })
|
||||
set open(value: boolean) {
|
||||
this._open = value;
|
||||
this.#open = value;
|
||||
|
||||
if (value) {
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
get open(): boolean {
|
||||
return this._open;
|
||||
return this.#open;
|
||||
}
|
||||
|
||||
_open = false;
|
||||
#open = false;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
...super.styles,
|
||||
@@ -47,9 +48,8 @@ export abstract class TableModal<T> extends Table<T> {
|
||||
];
|
||||
|
||||
public async fetch(): Promise<void> {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
if (!this.open) return;
|
||||
|
||||
return super.fetch();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFSidebar from "@patternfly/patternfly/components/Sidebar/sidebar.css";
|
||||
|
||||
export abstract class TablePage<T> extends Table<T> {
|
||||
export abstract class TablePage<T extends object> extends Table<T> {
|
||||
abstract pageTitle(): string;
|
||||
abstract pageDescription(): string | undefined;
|
||||
abstract pageIcon(): string;
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("ak-array-input", () => {
|
||||
await component();
|
||||
const link = await $("ak-array-input");
|
||||
await browser.pause(500);
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("valid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("toJson")).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ describe("ak-array-input", () => {
|
||||
await component(sampleItems);
|
||||
const link = await $("ak-array-input");
|
||||
await browser.pause(500);
|
||||
await expect(await link.getProperty("isValid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("valid")).toStrictEqual(true);
|
||||
await expect(await link.getProperty("toJson")).toEqual(sampleItems);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ConstructorWithMixin, createMixin, LitElementConstructor } from "#elements/types";
|
||||
import {
|
||||
AbstractLitElementConstructor,
|
||||
ConstructorWithMixin,
|
||||
createMixin,
|
||||
LitElementConstructor,
|
||||
} from "#elements/types";
|
||||
import { CustomEventDetail, isCustomEvent } from "#elements/utils/customEvents";
|
||||
|
||||
export interface CustomEventEmitterMixin<EventType extends string = string> {
|
||||
@@ -11,7 +16,7 @@ export interface CustomEventEmitterMixin<EventType extends string = string> {
|
||||
|
||||
export function CustomEmitterElement<
|
||||
EventType extends string = string,
|
||||
T extends LitElementConstructor = LitElementConstructor,
|
||||
T extends LitElementConstructor | AbstractLitElementConstructor = LitElementConstructor,
|
||||
>(SuperClass: T) {
|
||||
abstract class CustomEventEmmiter
|
||||
extends SuperClass
|
||||
@@ -31,7 +36,7 @@ export function CustomEmitterElement<
|
||||
normalizedDetail = detail;
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
(this as InstanceType<T>).dispatchEvent(
|
||||
new CustomEvent(eventType, {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
|
||||
@@ -15,6 +15,12 @@ export function isNamedElement(element: Element): element is NamedElement {
|
||||
return "name" in element.attributes;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"[name]": NamedElement<HTMLElement>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a map of files provided by input elements within the given iterable.
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { pluckErrorDetail } from "#common/errors/network";
|
||||
|
||||
import { WizardAction } from "#elements/wizard/Wizard";
|
||||
import { WizardPage } from "#elements/wizard/WizardPage";
|
||||
|
||||
import { ResponseError } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
@@ -33,65 +32,58 @@ export class ActionWizardPage extends WizardPage {
|
||||
static styles: CSSResult[] = [PFBase, PFBullseye, PFEmptyState, PFTitle, PFProgressStepper];
|
||||
|
||||
@property({ attribute: false })
|
||||
states: ActionStateBundle[] = [];
|
||||
public states: ActionStateBundle[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
currentStep?: ActionStateBundle;
|
||||
public currentStep?: ActionStateBundle;
|
||||
|
||||
activeCallback = async (): Promise<void> => {
|
||||
this.states = [];
|
||||
public override nextCallback = null;
|
||||
|
||||
this.host.actions.map((act, idx) => {
|
||||
this.states.push({
|
||||
action: act,
|
||||
state: ActionState.pending,
|
||||
idx: idx,
|
||||
});
|
||||
public override activeCallback = (): Promise<void> => {
|
||||
this.states = this.host.actions.map((action, idx) => ({
|
||||
action,
|
||||
state: ActionState.pending,
|
||||
idx: idx,
|
||||
}));
|
||||
|
||||
this.host.previousNavigation = false;
|
||||
this.host.cancelable = false;
|
||||
|
||||
return this.#run().finally(() => {
|
||||
// Ensure wizard is closable, even when run() failed
|
||||
this.host.valid = true;
|
||||
});
|
||||
|
||||
this.host.canBack = false;
|
||||
this.host.canCancel = false;
|
||||
|
||||
await this.run();
|
||||
|
||||
// Ensure wizard is closable, even when run() failed
|
||||
this.host.isValid = true;
|
||||
};
|
||||
|
||||
sidebarLabel = () => msg("Apply changes");
|
||||
public override sidebarLabel = msg("Apply changes");
|
||||
|
||||
async run(): Promise<void> {
|
||||
async #run(): Promise<void> {
|
||||
this.currentStep = this.states[0];
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
await new Promise((r) => requestAnimationFrame(r));
|
||||
|
||||
for await (const bundle of this.states) {
|
||||
this.currentStep = bundle;
|
||||
this.currentStep.state = ActionState.running;
|
||||
|
||||
this.requestUpdate();
|
||||
|
||||
try {
|
||||
await bundle.action.run();
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
await new Promise((r) => requestAnimationFrame(r));
|
||||
|
||||
this.currentStep.state = ActionState.done;
|
||||
|
||||
this.requestUpdate();
|
||||
} catch (exc) {
|
||||
if (exc instanceof ResponseError) {
|
||||
this.currentStep.action.subText = await exc.response.text();
|
||||
} else {
|
||||
this.currentStep.action.subText = (exc as Error).toString();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.currentStep.action.subText = pluckErrorDetail(error);
|
||||
this.currentStep.state = ActionState.failed;
|
||||
this.requestUpdate();
|
||||
|
||||
return;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
this.host.isValid = true;
|
||||
this.host.valid = true;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_REFRESH, {
|
||||
|
||||
@@ -10,32 +10,37 @@ import { customElement } from "lit/decorators.js";
|
||||
*/
|
||||
@customElement("ak-wizard-page-form")
|
||||
export class FormWizardPage extends WizardPage {
|
||||
activePageCallback: (context: FormWizardPage) => Promise<void> = async () => {
|
||||
return Promise.resolve();
|
||||
public activePageCallback?: (context: FormWizardPage) => Promise<void>;
|
||||
|
||||
public override activeCallback = async () => {
|
||||
this.host.valid = true;
|
||||
this.activePageCallback?.(this);
|
||||
};
|
||||
|
||||
activeCallback = async () => {
|
||||
this.host.isValid = true;
|
||||
this.activePageCallback(this);
|
||||
};
|
||||
|
||||
nextCallback = async (): Promise<boolean> => {
|
||||
const form = this.querySelector<Form<unknown>>("*");
|
||||
public override nextCallback = (): Promise<boolean> => {
|
||||
const form = this.querySelector("*");
|
||||
|
||||
if (!form) {
|
||||
return Promise.reject(msg("No form found"));
|
||||
throw new TypeError(msg("No child elements found in wizard page"));
|
||||
}
|
||||
|
||||
const formPromise = form.submit(new SubmitEvent("submit"));
|
||||
|
||||
if (!formPromise) {
|
||||
return Promise.reject(msg("Form didn't return a promise for submitting"));
|
||||
if (!(form instanceof Form || form instanceof HTMLFormElement)) {
|
||||
console.warn("authentik/wizard: form inside the form slot is not a Form", form);
|
||||
throw new TypeError(msg("Wizard page doesn't contain a form"));
|
||||
}
|
||||
|
||||
return formPromise
|
||||
const validity = form.reportValidity();
|
||||
|
||||
if (!validity) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const submitResult = form.submit(new SubmitEvent("submit"));
|
||||
|
||||
return Promise.resolve(submitResult)
|
||||
.then((data) => {
|
||||
this.host.state[this.slot] = data;
|
||||
this.host.canBack = false;
|
||||
this.host.previousNavigation = false;
|
||||
|
||||
return true;
|
||||
})
|
||||
|
||||
@@ -26,16 +26,18 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
||||
//#region Properties
|
||||
|
||||
@property({ attribute: false })
|
||||
types: TypeCreate[] = [];
|
||||
public types: TypeCreate[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
selectedType?: TypeCreate;
|
||||
public selectedType: TypeCreate | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
layout: TypeCreateWizardPageLayouts = TypeCreateWizardPageLayouts.list;
|
||||
public layout: TypeCreateWizardPageLayouts = TypeCreateWizardPageLayouts.list;
|
||||
|
||||
//#endregion
|
||||
|
||||
public override nextCallback = null;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFForm,
|
||||
@@ -55,22 +57,21 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
||||
|
||||
//#region Refs
|
||||
|
||||
formRef: Ref<HTMLFormElement> = createRef();
|
||||
#formRef: Ref<HTMLFormElement> = createRef();
|
||||
|
||||
//#endregion
|
||||
|
||||
public sidebarLabel = () => msg("Select type");
|
||||
public override sidebarLabel = msg("Select type");
|
||||
|
||||
public reset = () => {
|
||||
super.reset();
|
||||
this.selectedType = undefined;
|
||||
this.formRef.value?.reset();
|
||||
public reset = (): void => {
|
||||
this.selectedType = null;
|
||||
this.#formRef.value?.reset();
|
||||
};
|
||||
|
||||
activeCallback = (): void => {
|
||||
const form = this.formRef.value;
|
||||
const form = this.#formRef.value;
|
||||
|
||||
this.host.isValid = form?.checkValidity() ?? false;
|
||||
this.host.valid = form?.checkValidity() ?? false;
|
||||
|
||||
if (this.selectedType) {
|
||||
this.selectDispatch(this.selectedType);
|
||||
@@ -138,7 +139,7 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
||||
|
||||
renderList(): TemplateResult {
|
||||
return html`<form
|
||||
${ref(this.formRef)}
|
||||
${ref(this.#formRef)}
|
||||
class="pf-c-form pf-m-horizontal"
|
||||
data-ouid-component-type="ak-type-create-list"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import "#elements/wizard/ActionWizardPage";
|
||||
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { ModalButton } from "#elements/buttons/ModalButton";
|
||||
import { WizardPage } from "#elements/wizard/WizardPage";
|
||||
|
||||
@@ -38,45 +40,43 @@ export class Wizard extends ModalButton {
|
||||
* Whether the wizard can be cancelled.
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
canCancel = true;
|
||||
public cancelable = true;
|
||||
|
||||
/**
|
||||
* Whether the wizard can go back to the previous step.
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
canBack = true;
|
||||
public previousNavigation = true;
|
||||
|
||||
/**
|
||||
* Header title of the wizard.
|
||||
*/
|
||||
@property()
|
||||
header?: string;
|
||||
public header?: string;
|
||||
|
||||
/**
|
||||
* Description of the wizard.
|
||||
*/
|
||||
@property()
|
||||
description?: string;
|
||||
public description?: string;
|
||||
|
||||
/**
|
||||
* Whether the wizard is valid and can proceed to the next step.
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
isValid = false;
|
||||
public valid = false;
|
||||
|
||||
/**
|
||||
* Actions to display at the end of the wizard.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
actions: WizardAction[] = [];
|
||||
public actions: WizardAction[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
finalHandler = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
public finalHandler?: () => Promise<void>;
|
||||
|
||||
@property({ attribute: false })
|
||||
state: { [key: string]: unknown } = {};
|
||||
public state: { [key: string]: unknown } = {};
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -86,7 +86,7 @@ export class Wizard extends ModalButton {
|
||||
* Memoized step tag names.
|
||||
*/
|
||||
@state()
|
||||
_steps: string[] = [];
|
||||
protected _steps: string[] = [];
|
||||
|
||||
/**
|
||||
* Step tag names present in the wizard.
|
||||
@@ -123,25 +123,25 @@ export class Wizard extends ModalButton {
|
||||
/**
|
||||
* Initial steps to reset to.
|
||||
*/
|
||||
_initialSteps: string[] = [];
|
||||
#initialSteps: string[] = [];
|
||||
|
||||
@state()
|
||||
_activeStep?: WizardPage;
|
||||
protected activeStep: WizardPage | null = null;
|
||||
|
||||
set activeStepElement(nextActiveStepElement: WizardPage | undefined) {
|
||||
this._activeStep = nextActiveStepElement;
|
||||
set activeStepElement(nextActiveStepElement: WizardPage | null) {
|
||||
this.activeStep = nextActiveStepElement;
|
||||
|
||||
if (!this._activeStep) return;
|
||||
if (!this.activeStep) return;
|
||||
|
||||
this._activeStep.activeCallback();
|
||||
this._activeStep.requestUpdate();
|
||||
this.activeStep.activeCallback();
|
||||
this.activeStep.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* The active step element being displayed.
|
||||
*/
|
||||
get activeStepElement(): WizardPage | undefined {
|
||||
return this._activeStep;
|
||||
get activeStepElement(): WizardPage | null {
|
||||
return this.activeStep;
|
||||
}
|
||||
|
||||
getStepElementByIndex(stepIndex: number): WizardPage | null {
|
||||
@@ -154,69 +154,7 @@ export class Wizard extends ModalButton {
|
||||
return this.querySelector<WizardPage>(`[slot=${stepName}]`);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
firstUpdated(): void {
|
||||
this._initialSteps = this._steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add action to the beginning of the list.
|
||||
*/
|
||||
addActionBefore(displayName: string, run: () => Promise<boolean>): void {
|
||||
this.actions.unshift({
|
||||
displayName,
|
||||
run,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add action at the end of the list.
|
||||
*
|
||||
* @todo: Is this used?
|
||||
*/
|
||||
addActionAfter(displayName: string, run: () => Promise<boolean>): void {
|
||||
this.actions.push({
|
||||
displayName,
|
||||
run,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the wizard to it's initial state.
|
||||
*/
|
||||
reset = (ev?: Event) => {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
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?.();
|
||||
}
|
||||
|
||||
this.steps = this._initialSteps;
|
||||
this.actions = [];
|
||||
this.state = {};
|
||||
this.activeStepElement = undefined;
|
||||
this.canBack = true;
|
||||
this.canCancel = true;
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
renderModalInner(): TemplateResult {
|
||||
#gatherSteps() {
|
||||
const firstPage = this.getStepElementByIndex(0);
|
||||
|
||||
if (!this.activeStepElement && firstPage) {
|
||||
@@ -234,7 +172,78 @@ export class Wizard extends ModalButton {
|
||||
lastPage = activeStepIndex === this.steps.length - 1;
|
||||
}
|
||||
|
||||
const navigateToPreviousStep = () => {
|
||||
return {
|
||||
firstPage,
|
||||
activeStepIndex,
|
||||
lastPage,
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
public firstUpdated(): void {
|
||||
this.#initialSteps = this._steps;
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(EVENT_REFRESH, this.#refreshListener);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener(EVENT_REFRESH, this.#refreshListener);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Event Listeners
|
||||
|
||||
/**
|
||||
* Reset the wizard to it's initial state.
|
||||
*/
|
||||
#resetListener = (event?: Event) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
this.open = false;
|
||||
|
||||
for (const element of this.querySelectorAll("[data-wizardmanaged=true]")) {
|
||||
element.remove();
|
||||
}
|
||||
|
||||
for (const step of this.steps) {
|
||||
const stepElement = this.getStepElementByName(step);
|
||||
|
||||
stepElement?.reset?.();
|
||||
}
|
||||
|
||||
this.steps = this.#initialSteps;
|
||||
this.actions = [];
|
||||
this.state = {};
|
||||
this.activeStepElement = null;
|
||||
this.previousNavigation = true;
|
||||
this.cancelable = true;
|
||||
};
|
||||
|
||||
#refreshListener = (event: Event) => {
|
||||
const { lastPage } = this.#gatherSteps();
|
||||
|
||||
if (!lastPage) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
public renderModalInner(): TemplateResult {
|
||||
const { activeStepIndex, lastPage } = this.#gatherSteps();
|
||||
|
||||
const navigatePreviousListener = () => {
|
||||
const prevPage = this.getStepElementByIndex(activeStepIndex - 1);
|
||||
|
||||
if (prevPage) {
|
||||
@@ -242,14 +251,37 @@ export class Wizard extends ModalButton {
|
||||
}
|
||||
};
|
||||
|
||||
const navigateNextListener = async (): Promise<void> => {
|
||||
if (!this.activeStepElement) return;
|
||||
|
||||
if (this.activeStepElement.nextCallback) {
|
||||
const completedStep = await this.activeStepElement.nextCallback();
|
||||
|
||||
if (!completedStep) return;
|
||||
|
||||
if (lastPage) {
|
||||
await this.finalHandler?.();
|
||||
this.#resetListener();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = this.getStepElementByIndex(activeStepIndex + 1);
|
||||
|
||||
if (nextPage) {
|
||||
this.activeStepElement = nextPage;
|
||||
}
|
||||
};
|
||||
|
||||
return html`<div class="pf-c-wizard">
|
||||
<div class="pf-c-wizard__header">
|
||||
${this.canCancel
|
||||
${this.cancelable
|
||||
? html`<button
|
||||
class="pf-c-button pf-m-plain pf-c-wizard__close"
|
||||
type="button"
|
||||
aria-label="${msg("Close")}"
|
||||
@click=${(ev: Event) => this.reset(ev)}
|
||||
@click=${this.#resetListener}
|
||||
>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>`
|
||||
@@ -266,8 +298,6 @@ export class Wizard extends ModalButton {
|
||||
|
||||
if (!stepEl) return html`<p>Unexpected missing step: ${step}</p>`;
|
||||
|
||||
const sidebarLabel = stepEl.sidebarLabel();
|
||||
|
||||
return html`
|
||||
<li class="pf-c-wizard__nav-item">
|
||||
<button
|
||||
@@ -275,12 +305,13 @@ export class Wizard extends ModalButton {
|
||||
"pf-c-wizard__nav-link": true,
|
||||
"pf-m-current": idx === activeStepIndex,
|
||||
})}
|
||||
type="button"
|
||||
?disabled=${activeStepIndex < idx}
|
||||
@click=${() => {
|
||||
this.activeStepElement = stepEl;
|
||||
}}
|
||||
>
|
||||
${sidebarLabel}
|
||||
${stepEl.sidebarLabel || msg("Label Missing")}
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
@@ -296,47 +327,31 @@ export class Wizard extends ModalButton {
|
||||
<footer class="pf-c-wizard__footer">
|
||||
<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
type="submit"
|
||||
?disabled=${!this.isValid}
|
||||
@click=${async () => {
|
||||
const completedStep = await this.activeStepElement?.nextCallback();
|
||||
if (!completedStep) return;
|
||||
|
||||
if (lastPage) {
|
||||
await this.finalHandler();
|
||||
this.reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nextPage = this.getStepElementByIndex(activeStepIndex + 1);
|
||||
|
||||
if (nextPage) {
|
||||
this.activeStepElement = nextPage;
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
?disabled=${!this.valid}
|
||||
@click=${navigateNextListener}
|
||||
>
|
||||
${lastPage ? msg("Finish") : msg("Next")}
|
||||
</button>
|
||||
${(this.activeStepElement
|
||||
? this.steps.indexOf(this.activeStepElement.slot)
|
||||
: 0) > 0 && this.canBack
|
||||
: 0) > 0 && this.previousNavigation
|
||||
? html`
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
type="button"
|
||||
@click=${navigateToPreviousStep}
|
||||
@click=${navigatePreviousListener}
|
||||
>
|
||||
${msg("Back")}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
${this.canCancel
|
||||
${this.cancelable
|
||||
? html`<div class="pf-c-wizard__footer-cancel">
|
||||
<button
|
||||
class="pf-c-button pf-m-link"
|
||||
type="button"
|
||||
@click=${(ev: Event) => this.reset(ev)}
|
||||
@click=${(ev: Event) => this.#resetListener(ev)}
|
||||
>
|
||||
${msg("Cancel")}
|
||||
</button>
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { WizardPage } from "#elements/wizard/WizardPage";
|
||||
|
||||
import { CSSResult, html, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
export abstract class WizardForm extends Form {
|
||||
viewportCheck = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
nextDataCallback!: (data: Record<string, unknown>) => Promise<boolean>;
|
||||
|
||||
/* Override the traditional behavior of the form and instead simply serialize the form and push
|
||||
* it's contents to the next page.
|
||||
*/
|
||||
async submit(): Promise<boolean | undefined> {
|
||||
const data = this.serialize();
|
||||
|
||||
if (!data) return;
|
||||
|
||||
const files = this.files();
|
||||
|
||||
return this.nextDataCallback({
|
||||
...data,
|
||||
...files,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WizardFormPage extends WizardPage {
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFCard,
|
||||
PFButton,
|
||||
PFForm,
|
||||
PFAlert,
|
||||
PFInputGroup,
|
||||
PFFormControl,
|
||||
];
|
||||
|
||||
inputCallback(): void {
|
||||
const form = this.shadowRoot?.querySelector<HTMLFormElement>("form");
|
||||
|
||||
if (!form) return;
|
||||
|
||||
const state = form.checkValidity();
|
||||
this.host.isValid = state;
|
||||
}
|
||||
|
||||
nextCallback = async (): Promise<boolean> => {
|
||||
const form = this.shadowRoot?.querySelector<WizardForm>("ak-wizard-form");
|
||||
|
||||
if (!form) {
|
||||
console.warn("authentik/wizard: could not find form element");
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await form.submit();
|
||||
|
||||
return Boolean(response);
|
||||
};
|
||||
|
||||
nextDataCallback: (data: Record<string, unknown>) => Promise<boolean> =
|
||||
async (): Promise<boolean> => {
|
||||
return false;
|
||||
};
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html``;
|
||||
}
|
||||
|
||||
activeCallback = async () => {
|
||||
this.inputCallback();
|
||||
};
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<ak-wizard-form
|
||||
.nextDataCallback=${this.nextDataCallback}
|
||||
@input=${() => this.inputCallback()}
|
||||
>
|
||||
${this.renderForm()}
|
||||
</ak-wizard-form>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { Wizard } from "#elements/wizard/Wizard";
|
||||
|
||||
import { CSSResult, html, PropertyDeclaration, TemplateResult } from "lit";
|
||||
import { CSSResult, html, LitElement, PropertyDeclaration, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
@@ -19,21 +19,16 @@ export type WizardPageActiveCallback = () => void | Promise<void>;
|
||||
export type WizardPageNextCallback = () => boolean | Promise<boolean>;
|
||||
|
||||
@customElement("ak-wizard-page")
|
||||
export class WizardPage extends AKElement {
|
||||
export abstract class WizardPage extends AKElement {
|
||||
static styles: CSSResult[] = [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()
|
||||
sidebarLabel = (): string => {
|
||||
return "UNNAMED";
|
||||
};
|
||||
public sidebarLabel?: string;
|
||||
|
||||
get host(): Wizard {
|
||||
public get host(): Wizard {
|
||||
return this.parentElement as Wizard;
|
||||
}
|
||||
|
||||
@@ -42,15 +37,13 @@ export class WizardPage extends AKElement {
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
public reset(): void | Promise<void> {
|
||||
console.debug(`authentik/wizard ${this.localName}: reset)`);
|
||||
}
|
||||
public reset?: () => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Called when this is the page brought into view.
|
||||
*/
|
||||
activeCallback: WizardPageActiveCallback = () => {
|
||||
this.host.isValid = false;
|
||||
public activeCallback: WizardPageActiveCallback = () => {
|
||||
this.host.valid = false;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -60,24 +53,23 @@ export class WizardPage extends AKElement {
|
||||
*
|
||||
* @returns `true` if the wizard can proceed to the next page, `false` otherwise.
|
||||
*/
|
||||
nextCallback: WizardPageNextCallback = () => {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
public abstract nextCallback: WizardPageNextCallback | null;
|
||||
|
||||
requestUpdate(
|
||||
public requestUpdate(
|
||||
name?: PropertyKey,
|
||||
oldValue?: unknown,
|
||||
options?: PropertyDeclaration<unknown, unknown>,
|
||||
): void {
|
||||
this.querySelectorAll("*").forEach((el) => {
|
||||
if ("requestUpdate" in el) {
|
||||
(el as AKElement).requestUpdate();
|
||||
for (const element of this.querySelectorAll("*")) {
|
||||
if (element instanceof LitElement) {
|
||||
element.requestUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return super.requestUpdate(name, oldValue, options);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
public render(): TemplateResult {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export class AkRememberMeController implements ReactiveController {
|
||||
) as HTMLInputElement | null;
|
||||
}
|
||||
|
||||
get isValidChallenge() {
|
||||
get validChallenge() {
|
||||
return !(
|
||||
this.host.challenge.responseErrors &&
|
||||
this.host.challenge.responseErrors.non_field_errors &&
|
||||
|
||||
Reference in New Issue
Block a user