diff --git a/web/package-lock.json b/web/package-lock.json index 89313ba2257d..a61f6518dbdf 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3647,9 +3647,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.21.0.tgz", - "integrity": "sha512-v50lnVbzS8mpMSnEVxR+G75XpvxHKtkJaQrNPE8+/fF6Ppr5z4bcdcBhcP8LPfEW+4BZcic6VifMXRwTopc+kw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.22.0.tgz", + "integrity": "sha512-AYuihByeAkW17tuf40nKhnnxDBkshr5An3XjEJoUiN1OPU3w+iVVWB4f0g3XC1TBWFDLnChYH9ODaSq7IkpjPQ==", "dev": true, "optional": true, "bin": { @@ -10198,9 +10198,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.31", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz", - "integrity": "sha512-QcDoBbQeYt0+3CWcK/rEbuHvwpbT/8SV9T3OSgs6cX1FlcUAkgrkqbg9zLnDrMM/rLamzQwal4LYFCiWk861Tg==", + "version": "1.5.32", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", + "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", "dev": true }, "node_modules/emoji-regex": { diff --git a/web/src/common/utils.ts b/web/src/common/utils.ts index 7c5dfff92ad5..f9e19234fadb 100644 --- a/web/src/common/utils.ts +++ b/web/src/common/utils.ts @@ -25,6 +25,12 @@ export function convertToSlug(text: string): string { .replace(/[^\w-]+/g, ""); } +export function isSlug(text: string): boolean { + const lowered = text.toLowerCase(); + const forbidden = /([^\w-]|\s)/.test(lowered); + return lowered === text && !forbidden; +} + /** * Truncate a string based on maximum word count */ diff --git a/web/src/components/HorizontalLightComponent.ts b/web/src/components/HorizontalLightComponent.ts index 7d34c833d615..2f0dcccaf9ec 100644 --- a/web/src/components/HorizontalLightComponent.ts +++ b/web/src/components/HorizontalLightComponent.ts @@ -1,11 +1,12 @@ import { AKElement } from "@goauthentik/elements/Base"; +import "@goauthentik/elements/forms/HorizontalFormElement.js"; import { TemplateResult, html, nothing } from "lit"; import { property } from "lit/decorators.js"; type HelpType = TemplateResult | typeof nothing; -export class HorizontalLightComponent extends AKElement { +export class HorizontalLightComponent extends AKElement { // Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but // we're not actually using that and, for the meantime, we need the form handlers to be able to // find the children of this component. @@ -41,6 +42,9 @@ export class HorizontalLightComponent extends AKElement { @property({ attribute: false }) errorMessages: string[] = []; + @property({ attribute: false }) + value?: T; + renderControl() { throw new Error("Must be implemented in a subclass"); } diff --git a/web/src/components/ak-number-input.ts b/web/src/components/ak-number-input.ts index 917165bef102..726dca472662 100644 --- a/web/src/components/ak-number-input.ts +++ b/web/src/components/ak-number-input.ts @@ -5,13 +5,19 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { HorizontalLightComponent } from "./HorizontalLightComponent"; @customElement("ak-number-input") -export class AkNumberInput extends HorizontalLightComponent { +export class AkNumberInput extends HorizontalLightComponent { @property({ type: Number, reflect: true }) - value = 0; + value = NaN; renderControl() { + const setValue = (ev: InputEvent) => { + const value = (ev.target as HTMLInputElement).value; + this.value = value.trim() === "" ? NaN : parseInt(value, 10); + }; + return html` extends HorizontalLightComponent { +export class AkRadioInput extends HorizontalLightComponent { @property({ type: Object }) value!: T; diff --git a/web/src/components/ak-slug-input.ts b/web/src/components/ak-slug-input.ts index 5ad7e21a0c43..449e985c7986 100644 --- a/web/src/components/ak-slug-input.ts +++ b/web/src/components/ak-slug-input.ts @@ -7,7 +7,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { HorizontalLightComponent } from "./HorizontalLightComponent"; @customElement("ak-slug-input") -export class AkSlugInput extends HorizontalLightComponent { +export class AkSlugInput extends HorizontalLightComponent { @property({ type: String, reflect: true }) value = ""; diff --git a/web/src/components/ak-text-input.ts b/web/src/components/ak-text-input.ts index 72dbc3af5bee..d31f90e148f8 100644 --- a/web/src/components/ak-text-input.ts +++ b/web/src/components/ak-text-input.ts @@ -5,13 +5,18 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { HorizontalLightComponent } from "./HorizontalLightComponent"; @customElement("ak-text-input") -export class AkTextInput extends HorizontalLightComponent { +export class AkTextInput extends HorizontalLightComponent { @property({ type: String, reflect: true }) value = ""; renderControl() { + const setValue = (ev: InputEvent) => { + this.value = (ev.target as HTMLInputElement).value; + }; + return html` { @property({ type: String, reflect: true }) value = ""; diff --git a/web/src/elements/cards/tests/QuickActionCard.test.ts b/web/src/elements/cards/tests/QuickActionCard.test.ts index c4cba27965e0..47beaa80d9ba 100644 --- a/web/src/elements/cards/tests/QuickActionCard.test.ts +++ b/web/src/elements/cards/tests/QuickActionCard.test.ts @@ -35,7 +35,8 @@ describe("ak-quick-actions-card", () => { >`, ); const component = await $("ak-quick-actions-card"); - const items = await component.$$(">>>.pf-c-list li").getElements(); + const items = await component.$$(">>>.pf-c-list li"); + // @ts-expect-error "Another ChainablePromise mistake" await expect(Array.from(items).length).toEqual(5); await expect(await component.$(">>>.pf-c-list li:nth-of-type(4)")).toHaveText( "Manage users", diff --git a/web/src/elements/forms/SearchSelect/ak-search-select-ez.ts b/web/src/elements/forms/SearchSelect/ak-search-select-ez.ts index 416039044369..1f55bb32c8c8 100644 --- a/web/src/elements/forms/SearchSelect/ak-search-select-ez.ts +++ b/web/src/elements/forms/SearchSelect/ak-search-select-ez.ts @@ -9,7 +9,7 @@ export interface ISearchSelectApi { renderDescription?: (element: T) => string | TemplateResult; value: (element: T | undefined) => unknown; selected?: (element: T, elements: T[]) => boolean; - groupBy: (items: T[]) => [string, T[]][]; + groupBy?: (items: T[]) => [string, T[]][]; } export interface ISearchSelectEz extends ISearchSelectBase { @@ -58,7 +58,9 @@ export class SearchSelectEz extends SearchSelectBase implements ISearchSel this.renderDescription = this.config.renderDescription; this.value = this.config.value; this.selected = this.config.selected; - this.groupBy = this.config.groupBy; + if (this.config.groupBy !== undefined) { + this.groupBy = this.config.groupBy; + } super.connectedCallback(); } } diff --git a/web/src/elements/forms/SearchSelect/ak-search-select.ts b/web/src/elements/forms/SearchSelect/ak-search-select.ts index 36b8b4332537..284ae02098dc 100644 --- a/web/src/elements/forms/SearchSelect/ak-search-select.ts +++ b/web/src/elements/forms/SearchSelect/ak-search-select.ts @@ -21,14 +21,12 @@ export interface ISearchSelect extends ISearchSelectBase { * The API layer of ak-search-select * * - @prop fetchObjects (Function): The function by which objects are retrieved by the API. - * - @prop renderElement (Function | string): Either a function that can retrieve the string - * "label" of the element, or the name of the field from which the label can be retrieved.¹ - * - @prop renderDescription (Function | string): Either a function that can retrieve the string - * or TemplateResult "description" of the element, or the name of the field from which the - * description can be retrieved.¹ - * - @prop value (Function | string): Either a function that can retrieve the value (the current - * API object's primary key) selected or the name of the field from which the value can be - * retrieved.¹ + * - @prop renderElement (Function): A function that can retrieve the string + * "label" of the element + * - @prop renderDescription (Function): A function that can retrieve the string + * or TemplateResult "description" of the element + * - @prop value (Function | string): A function that can retrieve the value (the current + * API object's primary key) selected. * - @prop selected (Function): A function that retrieves the current "live" value from the list of objects fetched by the function above. * - @prop groupBy (Function): A function that can group the objects fetched from the API by @@ -41,11 +39,6 @@ export interface ISearchSelect extends ISearchSelectBase { * shown if `blankable` * - @attr selectedObject (Object): The current object, or undefined, selected * - * ¹ Due to a limitation in the parsing of properties-vs-attributes, these must be defined as - * properties, not attributes. As a consequence, they must be declared in property syntax. - * Example: - * - * `.renderElement=${"name"}` * * - @fires ak-change - When a value from the collection has been positively chosen, either as a * consequence of the user typing or when selecting from the list. 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 3a230b7d83cb..ea5a0edb28d9 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 @@ -39,7 +39,8 @@ export class AkSearchSelectViewDriver { ); } const id = await element.getAttribute("data-ouia-component-id"); - const menu = await $(`[data-ouia-component-id="menu-${id}"]`).getElement(); + const menu = await $(`[data-ouia-component-id="menu-${id}"]`); + // @ts-expect-error "Another ChainablePromise mistake" return new AkSearchSelectViewDriver(element, menu); } @@ -52,7 +53,7 @@ export class AkSearchSelectViewDriver { } async listElements() { - return await this.menu.$$(">>>li").getElements(); + return await this.menu.$$(">>>li"); } async focusOnInput() { 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 bdeeadbc1f8c..b159ee06055e 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 @@ -21,9 +21,8 @@ describe("Search select: Test Input Field", () => { html` `, document.body, ); - select = await AkSearchSelectViewDriver.build( - await $("ak-search-select-view").getElement(), - ); + // @ts-expect-error "Another ChainablePromise mistake" + select = await AkSearchSelectViewDriver.build(await $("ak-search-select-view")); }); it("should open the menu when the input is clicked", async () => { @@ -58,9 +57,8 @@ describe("Search select: Test Input Field", () => { expect(await select.open).toBe(false); expect(await select.menuIsVisible()).toBe(false); await browser.keys("A"); - select = await AkSearchSelectViewDriver.build( - await $("ak-search-select-view").getElement(), - ); + // @ts-expect-error "Another ChainablePromise mistake" + select = await AkSearchSelectViewDriver.build(await $("ak-search-select-view")); expect(await select.open).toBe(true); expect(await select.menuIsVisible()).toBe(true); }); @@ -69,6 +67,7 @@ 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,6 +76,7 @@ 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); 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 9d176d909183..d47f5d1de73c 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 @@ -97,9 +97,8 @@ describe("Search select: event driven startup", () => { mock?.dispatchEvent(new Event("resolve")); }); expect(await $(">>>ak-search-select-loading-indicator")).not.toBeDisplayed(); - select = await AkSearchSelectViewDriver.build( - await $(">>>ak-search-select-view").getElement(), - ); + // @ts-expect-error "Another ChainablePromise mistake" + select = await AkSearchSelectViewDriver.build(await $(">>>ak-search-select-view")); expect(await select).toBeExisting(); }); diff --git a/web/wdio.conf.ts b/web/wdio.conf.ts index e142bbc54edc..e5fab5f1f082 100644 --- a/web/wdio.conf.ts +++ b/web/wdio.conf.ts @@ -42,7 +42,14 @@ export const config: WebdriverIO.Config = { }, ], - tsConfigPath: "./tsconfig.json", + // @ts-expect-error TS2353: The types are not up-to-date with Wdio9. + autoCompileOpts: { + autoCompile: true, + tsNodeOpts: { + project: "./tsconfig.json", + transpileOnly: true, + }, + }, // // ================== @@ -141,11 +148,11 @@ export const config: WebdriverIO.Config = { // baseUrl: 'http://localhost:8080', // // Default timeout for all waitFor* commands. - waitforTimeout: 10000, + waitforTimeout: 12000, // // Default timeout in milliseconds for request // if browser driver or grid doesn't send response - connectionRetryTimeout: 120000, + connectionRetryTimeout: 12000, // // Default request retries count connectionRetryCount: 3,