("input[autofocus]").forEach((input) => {
@@ -55,7 +59,7 @@ export class FormElement extends AKElement {
: html``}
- ${(this._errors || []).map((error) => {
+ ${(this.#errors || []).map((error) => {
return html`
extends Form {
+ /**
+ * Load the instance from the server.
+ *
+ * @param primaryKey The primary key of the instance to load.
+ */
+ public abstract loadInstance(primaryKey: PrimaryKey): Promise;
-export abstract class ModelForm extends Form {
- abstract loadInstance(pk: PKT): Promise;
-
+ /**
+ * Load the form data.
+ *
+ * Override this method to load any additional data needed for the form.
+ */
async load(): Promise {
return Promise.resolve();
}
@property({ attribute: false })
- set instancePk(value: PKT) {
+ set instancePk(value: PrimaryKey) {
this._instancePk = value;
if (this.viewportCheck && !this.isInViewport) {
return;
@@ -38,7 +46,11 @@ export abstract class ModelForm extends Form
});
}
- private _instancePk?: PKT;
+ get instancePk(): PrimaryKey | undefined {
+ return this._instancePk;
+ }
+
+ private _instancePk?: PrimaryKey;
// Keep track if we've loaded the model instance
private _initialLoad = false;
@@ -48,14 +60,15 @@ export abstract class ModelForm extends Form
private _isLoading = false;
@property({ attribute: false })
- instance?: T = this.defaultInstance;
+ public instance?: Data = this.defaultInstance;
- get defaultInstance(): T | undefined {
+ get defaultInstance(): Data | undefined {
return undefined;
}
constructor() {
super();
+
this.addEventListener(EVENT_REFRESH, () => {
if (!this._instancePk) return;
this.loadInstance(this._instancePk).then((instance) => {
diff --git a/web/src/elements/forms/ProxyForm.ts b/web/src/elements/forms/ProxyForm.ts
index c0b33a74cb..08d89089cc 100644
--- a/web/src/elements/forms/ProxyForm.ts
+++ b/web/src/elements/forms/ProxyForm.ts
@@ -8,6 +8,7 @@ export abstract class ProxyForm extends Form {
@property()
type!: string;
+ // TODO: Is this used anywhere?
@property({ attribute: false })
args: Record = {};
@@ -28,24 +29,29 @@ export abstract class ProxyForm extends Form {
return this.innerElement?.getSuccessMessage() || "";
}
- async requestUpdate(name?: PropertyKey | undefined, oldValue?: unknown): Promise {
- const result = await super.requestUpdate(name, oldValue);
- await this.innerElement?.requestUpdate();
+ requestUpdate(name?: PropertyKey | undefined, oldValue?: unknown): void {
+ const result = super.requestUpdate(name, oldValue);
+ this.innerElement?.requestUpdate();
return result;
}
renderVisible(): TemplateResult {
let elementName = this.type;
+
if (this.type in this.typeMap) {
elementName = this.typeMap[this.type];
}
+
if (!this.innerElement) {
this.innerElement = document.createElement(elementName) as Form;
}
+
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)[k] = this.args[k];
+
+ for (const [key, value] of Object.entries(this.args)) {
+ this.innerElement.setAttribute(key, value as string);
+
+ (this.innerElement as unknown as Record)[key] = value as string;
}
return html`${this.innerElement}`;
}
diff --git a/web/src/elements/forms/SearchSelect/ak-portal.ts b/web/src/elements/forms/SearchSelect/ak-portal.ts
index 026b9ef154..0ffc192462 100644
--- a/web/src/elements/forms/SearchSelect/ak-portal.ts
+++ b/web/src/elements/forms/SearchSelect/ak-portal.ts
@@ -67,9 +67,9 @@ export class Portal extends LitElement implements IPortal {
this.setAttribute("data-ouia-component-type", "ak-portal");
this.setAttribute("data-ouia-component-id", this.getAttribute("id") || randomId());
this.dropdownContainer = document.createElement("div");
- this.dropdownContainer.dataset["managedBy"] = "ak-portal";
+ this.dropdownContainer.dataset.managedBy = "ak-portal";
if (this.name) {
- this.dropdownContainer.dataset["managedFor"] = this.name;
+ this.dropdownContainer.dataset.managedFor = this.name;
}
document.body.append(this.dropdownContainer);
if (!this.anchor) {
diff --git a/web/src/elements/forms/SearchSelect/ak-search-select.ts b/web/src/elements/forms/SearchSelect/ak-search-select.ts
index 52af07c266..eada400423 100644
--- a/web/src/elements/forms/SearchSelect/ak-search-select.ts
+++ b/web/src/elements/forms/SearchSelect/ak-search-select.ts
@@ -54,28 +54,28 @@ export class SearchSelect extends SearchSelectBase implements ISearchSelec
// A function which takes the query state object (accepting that it may be empty) and returns a
// new collection of objects.
@property({ attribute: false })
- fetchObjects!: (query?: string) => Promise;
+ declare fetchObjects: (query?: string) => Promise;
// A function passed to this object that extracts a string representation of items of the
// collection under search.
@property({ attribute: false })
- renderElement!: (element: T) => string;
+ declare renderElement: (element: T) => string;
// A function passed to this object that extracts an HTML representation of additional
// information for items of the collection under search.
@property({ attribute: false })
- renderDescription?: (element: T) => string | TemplateResult;
+ declare renderDescription?: (element: T) => string | TemplateResult;
// A function which returns the currently selected object's primary key, used for serialization
// into forms.
@property({ attribute: false })
- value!: (element: T | undefined) => string;
+ declare value: (element: T | undefined) => string;
// A function passed to this object that determines an object in the collection under search
// should be automatically selected. Only used when the search itself is responsible for
// fetching the data; sets an initial default value.
@property({ attribute: false })
- selected?: (element: T, elements: T[]) => boolean;
+ declare selected?: (element: T, elements: T[]) => boolean;
// A function passed to this object (or using the default below) that groups objects in the
// collection under search into categories.
diff --git a/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.comp.ts b/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.comp.ts
index ea5a0edb28..157b825186 100644
--- a/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.comp.ts
+++ b/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.comp.ts
@@ -1,16 +1,25 @@
import { $, browser } from "@wdio/globals";
+import type { ChainablePromiseElement } from "webdriverio";
browser.addCommand(
"focus",
- function () {
- browser.execute(function (domElement) {
- domElement.focus();
- // @ts-ignore
- }, this);
+ () => {
+ browser.execute(function applyFocus(this: HTMLElement) {
+ this?.focus?.();
+ });
},
- true,
+ true, // Extend to all elements
);
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace WebdriverIO {
+ interface Element {
+ focus: () => Promise;
+ }
+ }
+}
+
/**
* Search Select View Driver
*
@@ -24,13 +33,13 @@ browser.addCommand(
export class AkSearchSelectViewDriver {
constructor(
- public element: WebdriverIO.Element,
- public menu: WebdriverIO.Element,
+ public element: ChainablePromiseElement,
+ public menu: ChainablePromiseElement,
) {
/* no op */
}
- static async build(element: WebdriverIO.Element) {
+ static async build(element: ChainablePromiseElement) {
const tagname = await element.getTagName();
const comptype = await element.getAttribute("data-ouia-component-type");
if (comptype !== "ak-search-select-view") {
@@ -39,8 +48,8 @@ export class AkSearchSelectViewDriver {
);
}
const id = await element.getAttribute("data-ouia-component-id");
- const menu = await $(`[data-ouia-component-id="menu-${id}"]`);
- // @ts-expect-error "Another ChainablePromise mistake"
+ const menu = $(`[data-ouia-component-id="menu-${id}"]`);
+
return new AkSearchSelectViewDriver(element, menu);
}
@@ -48,17 +57,16 @@ export class AkSearchSelectViewDriver {
return this.element.getProperty("open");
}
- async input() {
- return await this.element.$(">>>input");
+ input() {
+ return this.element.$(">>>input");
}
async listElements() {
- return await this.menu.$$(">>>li");
+ return this.menu.$$(">>>li");
}
async focusOnInput() {
- // @ts-ignore
- await (await this.input()).focus();
+ await (await this.input().getElement()).focus();
}
async inputIsVisible() {
@@ -70,6 +78,6 @@ export class AkSearchSelectViewDriver {
}
async clickInput() {
- return await (await this.input()).click();
+ return await this.input().click();
}
}
diff --git a/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.test.ts b/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.test.ts
index 9089bef697..dee5a0e8cf 100644
--- a/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.test.ts
+++ b/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.test.ts
@@ -6,6 +6,7 @@ import { Key } from "webdriverio";
import { html } from "lit";
import "../ak-search-select-view.js";
+import type { SearchSelectView } from "../ak-search-select-view.js";
import { sampleData } from "../stories/sampleData.js";
import { AkSearchSelectViewDriver } from "./ak-search-select-view.comp.js";
@@ -20,10 +21,9 @@ describe("Search select: Test Input Field", () => {
beforeEach(async () => {
render(
html` `,
- document.body,
+ document,
);
- // @ts-expect-error "Another ChainablePromise mistake"
- select = await AkSearchSelectViewDriver.build(await $("ak-search-select-view"));
+ select = await AkSearchSelectViewDriver.build($("ak-search-select-view"));
});
it("should open the menu when the input is clicked", async () => {
@@ -58,8 +58,7 @@ describe("Search select: Test Input Field", () => {
expect(await select.open).toBe(false);
expect(await select.menuIsVisible()).toBe(false);
await browser.keys("A");
- // @ts-expect-error "Another ChainablePromise mistake"
- select = await AkSearchSelectViewDriver.build(await $("ak-search-select-view"));
+ select = await AkSearchSelectViewDriver.build($("ak-search-select-view"));
expect(await select.open).toBe(true);
expect(await select.menuIsVisible()).toBe(true);
});
@@ -68,7 +67,6 @@ describe("Search select: Test Input Field", () => {
await select.focusOnInput();
await browser.keys("Ap");
await expect(await select.menuIsVisible()).toBe(true);
- // @ts-expect-error "Another ChainablePromise mistake"
const elements = Array.from(await select.listElements());
await expect(elements.length).toBe(2);
});
@@ -77,7 +75,6 @@ describe("Search select: Test Input Field", () => {
await select.focusOnInput();
await browser.keys("Ap");
await expect(await select.menuIsVisible()).toBe(true);
- // @ts-expect-error "Another ChainablePromise mistake"
const elements = Array.from(await select.listElements());
await expect(elements.length).toBe(2);
await browser.keys(Key.Tab);
@@ -89,7 +86,7 @@ describe("Search select: Test Input Field", () => {
"afterbegin",
'',
);
- const input = await browser.$("#a-separate-component");
+ const input = browser.$("#a-separate-component");
await select.clickInput();
expect(await select.open).toBe(true);
@@ -97,14 +94,13 @@ describe("Search select: Test Input Field", () => {
expect(await select.open).toBe(false);
});
- afterEach(async () => {
- await browser.execute(() => {
+ afterEach(() => {
+ return browser.execute(() => {
document.body.querySelector("#a-separate-component")?.remove();
- document.body.querySelector("ak-search-select-view")?.remove();
- // @ts-expect-error expression of type '"_$litPart$"' is added by Lit
- if (document.body["_$litPart$"]) {
- // @ts-expect-error expression of type '"_$litPart$"' is added by Lit
- delete document.body["_$litPart$"];
+ document.body.querySelector("ak-search-select-view")?.remove();
+
+ if ("_$litPart$" in document.body) {
+ delete document.body._$litPart$;
}
});
});
diff --git a/web/src/elements/forms/SearchSelect/tests/ak-search-select.test.ts b/web/src/elements/forms/SearchSelect/tests/ak-search-select.test.ts
index 83402dfb60..8d11cdea8e 100644
--- a/web/src/elements/forms/SearchSelect/tests/ak-search-select.test.ts
+++ b/web/src/elements/forms/SearchSelect/tests/ak-search-select.test.ts
@@ -5,6 +5,7 @@ import { render } from "@goauthentik/elements/tests/utils.js";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { $, browser, expect } from "@wdio/globals";
import { slug } from "github-slugger";
+import type { ChainablePromiseElement } from "webdriverio";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
@@ -82,34 +83,31 @@ export class MockSearch extends CustomListenerElement(AKElement) {
describe("Search select: event driven startup", () => {
let select: AkSearchSelectViewDriver;
- let wrapper: SearchSelect;
+ let wrapper: ChainablePromiseElement;
beforeEach(async () => {
- await render(html``, document.body);
- // @ts-ignore
- wrapper = await $(">>>ak-search-select");
+ render(html``, document);
+ wrapper = $(">>>ak-search-select");
});
it("should shift from the loading indicator to search select view on fetch event completed", async () => {
- expect(await wrapper).toBeExisting();
- expect(await $(">>>ak-search-select-loading-indicator")).toBeDisplayed();
+ expect(wrapper).toBeExisting();
+ expect($(">>>ak-search-select-loading-indicator")).toBeDisplayed();
await browser.execute(() => {
const mock = document.querySelector("ak-mock-search-group");
mock?.dispatchEvent(new Event("resolve"));
});
- expect(await $(">>>ak-search-select-loading-indicator")).not.toBeDisplayed();
- // @ts-expect-error "Another ChainablePromise mistake"
- select = await AkSearchSelectViewDriver.build(await $(">>>ak-search-select-view"));
- expect(await select).toBeExisting();
+ expect($(">>>ak-search-select-loading-indicator")).not.toBeDisplayed();
+ select = await AkSearchSelectViewDriver.build($(">>>ak-search-select-view"));
+ expect(select.element).toBeExisting();
});
- afterEach(async () => {
- await browser.execute(() => {
+ afterEach(() => {
+ return browser.execute(() => {
document.body.querySelector("ak-mock-search-group")?.remove();
- // @ts-expect-error expression of type '"_$litPart$"' is added by Lit
- if (document.body["_$litPart$"]) {
- // @ts-expect-error expression of type '"_$litPart$"' is added by Lit
- delete document.body["_$litPart$"];
+
+ if ("_$litPart$" in document.body) {
+ delete document.body._$litPart$;
}
});
});
diff --git a/web/src/elements/forms/SearchSelect/tests/is-visible.ts b/web/src/elements/forms/SearchSelect/tests/is-visible.ts
index b2b78ea0fa..2d9d5faafa 100644
--- a/web/src/elements/forms/SearchSelect/tests/is-visible.ts
+++ b/web/src/elements/forms/SearchSelect/tests/is-visible.ts
@@ -8,7 +8,7 @@ function computedStyleIsVisible(element: HTMLElement) {
return (
isStyledVisible(computedStyle) &&
(isDisplayContents(computedStyle) ||
- !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length))
+ Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length))
);
}
diff --git a/web/src/elements/messages/MessageContainer.ts b/web/src/elements/messages/MessageContainer.ts
index 9f706079c8..f5fd1c4ab6 100644
--- a/web/src/elements/messages/MessageContainer.ts
+++ b/web/src/elements/messages/MessageContainer.ts
@@ -59,7 +59,7 @@ export class MessageContainer extends AKElement {
addMessage(message: APIMessage, unique = false): void {
if (unique) {
- const matchingMessages = this.messages.filter((m) => m.message == message.message);
+ const matchingMessages = this.messages.filter((m) => m.message === message.message);
if (matchingMessages.length > 0) {
return;
}
diff --git a/web/src/elements/router/RouteMatch.ts b/web/src/elements/router/RouteMatch.ts
index 95657b0845..5070e3458d 100644
--- a/web/src/elements/router/RouteMatch.ts
+++ b/web/src/elements/router/RouteMatch.ts
@@ -24,26 +24,29 @@ export class RouteMatch {
}
}
-export function getURLParam(key: string, fallback: T): T {
- const params = getURLParams();
- if (key in params) {
- return params[key] as T;
- }
- return fallback;
-}
-
export function getURLParams(): { [key: string]: unknown } {
const params = {};
- if (window.location.hash.includes(ROUTE_SEPARATOR)) {
- const urlParts = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR, 2);
- const rawParams = decodeURIComponent(urlParts[1]);
- try {
- return JSON.parse(rawParams);
- } catch {
- return params;
- }
+
+ if (!window.location.hash.includes(ROUTE_SEPARATOR)) return params;
+
+ const urlParts = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR, 2);
+ const rawParams = decodeURIComponent(urlParts[1]);
+
+ try {
+ return JSON.parse(rawParams);
+ } catch {
+ return params;
}
- return params;
+}
+
+export function getURLParam(key: string, fallback: T): T {
+ const params = getURLParams();
+
+ if (Object.hasOwn(params, key)) {
+ return params[key] as T;
+ }
+
+ return fallback;
}
export function setURLParams(params: { [key: string]: unknown }, replace = true): void {
@@ -59,8 +62,10 @@ export function setURLParams(params: { [key: string]: unknown }, replace = true)
export function updateURLParams(params: { [key: string]: unknown }, replace = true): void {
const currentParams = getURLParams();
- for (const key in params) {
- currentParams[key] = params[key] as string;
+
+ for (const [key, value] of Object.entries(params)) {
+ currentParams[key] = value;
}
+
setURLParams(currentParams, replace);
}
diff --git a/web/src/elements/router/RouterOutlet.ts b/web/src/elements/router/RouterOutlet.ts
index 1ef5abd087..596110108a 100644
--- a/web/src/elements/router/RouterOutlet.ts
+++ b/web/src/elements/router/RouterOutlet.ts
@@ -9,24 +9,27 @@ import { customElement, property } from "lit/decorators.js";
// Poliyfill for hashchange.newURL,
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
+// TODO: Do we still support browsers that don't have this?
window.addEventListener("load", () => {
- if (!window.HashChangeEvent)
- (function () {
- let lastURL = document.URL;
- window.addEventListener("hashchange", function (event) {
- Object.defineProperty(event, "oldURL", {
- enumerable: true,
- configurable: true,
- value: lastURL,
- });
- Object.defineProperty(event, "newURL", {
- enumerable: true,
- configurable: true,
- value: document.URL,
- });
- lastURL = document.URL;
- });
- })();
+ if (window.HashChangeEvent) return;
+
+ let lastURL = document.URL;
+
+ window.addEventListener("hashchange", (event) => {
+ Object.defineProperty(event, "oldURL", {
+ enumerable: true,
+ configurable: true,
+ value: lastURL,
+ });
+
+ Object.defineProperty(event, "newURL", {
+ enumerable: true,
+ configurable: true,
+ value: document.URL,
+ });
+
+ lastURL = document.URL;
+ });
});
export function paramURL(url: string, params?: { [key: string]: unknown }): string {
@@ -38,6 +41,7 @@ export function paramURL(url: string, params?: { [key: string]: unknown }): stri
}
return finalUrl;
}
+
export function navigate(url: string, params?: { [key: string]: unknown }): void {
window.location.assign(paramURL(url, params));
}
diff --git a/web/src/elements/sidebar/SidebarVersion.ts b/web/src/elements/sidebar/SidebarVersion.ts
index f5e0fbed05..c2431b5489 100644
--- a/web/src/elements/sidebar/SidebarVersion.ts
+++ b/web/src/elements/sidebar/SidebarVersion.ts
@@ -46,7 +46,7 @@ export class SidebarVersion extends WithLicenseSummary(WithVersion(AKElement)) {
return nothing;
}
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle;
- if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
+ if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`