web/elements: fix search select inconsistency (#4989)
* web/elements: fix search-select inconsistency Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web/common: fix config having to be json converted everywhere Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web/elements: refactor form without iron-form Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web/admin: fix misc Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> # Conflicts: # web/package-lock.json
This commit is contained in:
parent
13fd1afbb9
commit
bb575fcc10
File diff suppressed because it is too large
Load Diff
|
@ -74,8 +74,6 @@
|
|||
"@lingui/detect-locale": "^3.17.2",
|
||||
"@lingui/macro": "^3.17.2",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@polymer/iron-form": "^3.0.1",
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
|
|
|
@ -61,6 +61,9 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
|
|||
metrics.healthy += 1;
|
||||
}
|
||||
});
|
||||
if (health.length < 1) {
|
||||
metrics.unsynced += 1;
|
||||
}
|
||||
} catch {
|
||||
metrics.unsynced += 1;
|
||||
}
|
||||
|
|
|
@ -10,9 +10,8 @@ import "@goauthentik/elements/forms/SearchSelect";
|
|||
import { t } from "@lingui/macro";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
import {
|
||||
CertificateKeyPair,
|
||||
|
@ -20,6 +19,7 @@ import {
|
|||
CoreGroupsListRequest,
|
||||
CryptoApi,
|
||||
CryptoCertificatekeypairsListRequest,
|
||||
CurrentTenant,
|
||||
Flow,
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
|
@ -32,10 +32,14 @@ import {
|
|||
|
||||
@customElement("ak-provider-ldap-form")
|
||||
export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
||||
loadInstance(pk: number): Promise<LDAPProvider> {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({
|
||||
@state()
|
||||
tenant?: CurrentTenant;
|
||||
async loadInstance(pk: number): Promise<LDAPProvider> {
|
||||
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({
|
||||
id: pk,
|
||||
});
|
||||
this.tenant = await tenant();
|
||||
return provider;
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
|
@ -75,22 +79,16 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
${until(
|
||||
tenant().then((t) => {
|
||||
return html`
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Flow[]> => {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation:
|
||||
FlowsInstancesListDesignationEnum.Authentication,
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const flows = await new FlowsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).flowsInstancesList(args);
|
||||
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
|
||||
return flows.results;
|
||||
}}
|
||||
.renderElement=${(flow: Flow): string => {
|
||||
|
@ -103,7 +101,7 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
return flow?.pk;
|
||||
}}
|
||||
.selected=${(flow: Flow): boolean => {
|
||||
let selected = flow.pk === t.flowAuthentication;
|
||||
let selected = flow.pk === this.tenant?.flowAuthentication;
|
||||
if (this.instance?.authorizationFlow === flow.pk) {
|
||||
selected = true;
|
||||
}
|
||||
|
@ -111,10 +109,6 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
|||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
`;
|
||||
}),
|
||||
html`<option>${t`Loading...`}</option>`,
|
||||
)}
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`}
|
||||
</p>
|
||||
|
|
|
@ -7,19 +7,9 @@ import { EVENT_REFRESH, VERSION } from "@goauthentik/common/constants";
|
|||
import { globalAK } from "@goauthentik/common/global";
|
||||
import { activateLocale } from "@goauthentik/common/ui/locale";
|
||||
|
||||
import {
|
||||
Config,
|
||||
ConfigFromJSON,
|
||||
Configuration,
|
||||
CoreApi,
|
||||
CurrentTenant,
|
||||
CurrentTenantFromJSON,
|
||||
RootApi,
|
||||
} from "@goauthentik/api";
|
||||
import { Config, Configuration, CoreApi, CurrentTenant, RootApi } from "@goauthentik/api";
|
||||
|
||||
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(
|
||||
ConfigFromJSON(globalAK()?.config),
|
||||
);
|
||||
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
|
||||
export function config(): Promise<Config> {
|
||||
if (!globalConfigPromise) {
|
||||
globalConfigPromise = new RootApi(DEFAULT_CONFIG).rootConfigRetrieve();
|
||||
|
@ -52,9 +42,7 @@ export function tenantSetLocale(tenant: CurrentTenant) {
|
|||
activateLocale(tenant.defaultLocale);
|
||||
}
|
||||
|
||||
let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve(
|
||||
CurrentTenantFromJSON(globalAK()?.tenant),
|
||||
);
|
||||
let globalTenantPromise: Promise<CurrentTenant> | undefined = Promise.resolve(globalAK().tenant);
|
||||
export function tenant(): Promise<CurrentTenant> {
|
||||
if (!globalTenantPromise) {
|
||||
globalTenantPromise = new CoreApi(DEFAULT_CONFIG)
|
||||
|
@ -82,7 +70,7 @@ export const DEFAULT_CONFIG = new Configuration({
|
|||
middleware: [
|
||||
new CSRFMiddleware(),
|
||||
new EventMiddleware(),
|
||||
new LoggingMiddleware(CurrentTenantFromJSON(globalAK()?.tenant)),
|
||||
new LoggingMiddleware(globalAK().tenant),
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Config, CurrentTenant } from "@goauthentik/api";
|
||||
import { Config, ConfigFromJSON, CurrentTenant, CurrentTenantFromJSON } from "@goauthentik/api";
|
||||
|
||||
export interface GlobalAuthentik {
|
||||
_converted?: boolean;
|
||||
locale?: string;
|
||||
flow?: {
|
||||
layout: string;
|
||||
|
@ -13,11 +14,17 @@ export interface GlobalAuthentik {
|
|||
}
|
||||
|
||||
export interface AuthentikWindow {
|
||||
authentik?: GlobalAuthentik;
|
||||
authentik: GlobalAuthentik;
|
||||
}
|
||||
|
||||
export function globalAK(): GlobalAuthentik | undefined {
|
||||
return (window as unknown as AuthentikWindow).authentik;
|
||||
export function globalAK(): GlobalAuthentik {
|
||||
const ak = (window as unknown as AuthentikWindow).authentik;
|
||||
if (ak && !ak._converted) {
|
||||
ak._converted = true;
|
||||
ak.tenant = CurrentTenantFromJSON(ak.tenant);
|
||||
ak.config = ConfigFromJSON(ak.config);
|
||||
}
|
||||
return ak;
|
||||
}
|
||||
|
||||
export function docLink(path: string): string {
|
||||
|
|
|
@ -172,6 +172,6 @@ export class Interface extends AKElement {
|
|||
|
||||
async getTheme(): Promise<UiThemeEnum> {
|
||||
const config = await uiConfig();
|
||||
return config.theme.base;
|
||||
return config.theme?.base || UiThemeEnum.Automatic;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,6 @@ import { AKElement } from "@goauthentik/elements/Base";
|
|||
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
|
||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||
import "@polymer/iron-form/iron-form";
|
||||
import { IronFormElement } from "@polymer/iron-form/iron-form";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
@ -110,63 +107,76 @@ export class Form<T> extends AKElement {
|
|||
* Reset the inner iron-form
|
||||
*/
|
||||
resetForm(): void {
|
||||
const ironForm = this.shadowRoot?.querySelector("iron-form");
|
||||
ironForm?.reset();
|
||||
const form = this.shadowRoot?.querySelector<HTMLFormElement>("form");
|
||||
form?.reset();
|
||||
}
|
||||
|
||||
getFormFiles(): { [key: string]: File } {
|
||||
const ironForm = this.shadowRoot?.querySelector("iron-form");
|
||||
const files: { [key: string]: File } = {};
|
||||
if (!ironForm) {
|
||||
return files;
|
||||
}
|
||||
const elements = ironForm._getSubmittableElements();
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i] as HTMLInputElement;
|
||||
if (element.tagName.toLowerCase() === "input" && element.type === "file") {
|
||||
if ((element.files || []).length < 1) {
|
||||
const element = elements[i];
|
||||
element.requestUpdate();
|
||||
const inputElement = element.querySelector<HTMLInputElement>("[name]");
|
||||
if (!inputElement) {
|
||||
continue;
|
||||
}
|
||||
files[element.name] = (element.files || [])[0];
|
||||
if (inputElement.tagName.toLowerCase() === "input" && inputElement.type === "file") {
|
||||
if ((inputElement.files || []).length < 1) {
|
||||
continue;
|
||||
}
|
||||
files[element.name] = (inputElement.files || [])[0];
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
serializeForm(): T | undefined {
|
||||
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
|
||||
if (!form) {
|
||||
console.warn("authentik/forms: failed to find iron-form");
|
||||
return;
|
||||
}
|
||||
const elements: HTMLInputElement[] = form._getSubmittableElements();
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
const json: { [key: string]: unknown } = {};
|
||||
elements.forEach((element) => {
|
||||
const values = form._serializeElementValues(element);
|
||||
if (element.hidden) {
|
||||
element.requestUpdate();
|
||||
const inputElement = element.querySelector<HTMLInputElement>("[name]");
|
||||
if (element.hidden || !inputElement) {
|
||||
return;
|
||||
}
|
||||
if (element.tagName.toLowerCase() === "select" && "multiple" in element.attributes) {
|
||||
json[element.name] = values;
|
||||
} else if (element.tagName.toLowerCase() === "input" && element.type === "date") {
|
||||
json[element.name] = element.valueAsDate;
|
||||
} else if (
|
||||
element.tagName.toLowerCase() === "input" &&
|
||||
element.type === "datetime-local"
|
||||
if (
|
||||
inputElement.tagName.toLowerCase() === "select" &&
|
||||
"multiple" in inputElement.attributes
|
||||
) {
|
||||
json[element.name] = new Date(element.valueAsNumber);
|
||||
const selectElement = inputElement as unknown as HTMLSelectElement;
|
||||
json[element.name] = Array.from(selectElement.selectedOptions).map((v) => v.value);
|
||||
} else if (
|
||||
element.tagName.toLowerCase() === "input" &&
|
||||
"type" in element.dataset &&
|
||||
element.dataset["type"] === "datetime-local"
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "date"
|
||||
) {
|
||||
json[element.name] = inputElement.valueAsDate;
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "datetime-local"
|
||||
) {
|
||||
json[element.name] = new Date(inputElement.valueAsNumber);
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
"type" in inputElement.dataset &&
|
||||
inputElement.dataset["type"] === "datetime-local"
|
||||
) {
|
||||
// Workaround for Firefox <93, since 92 and older don't support
|
||||
// datetime-local fields
|
||||
json[element.name] = new Date(element.value);
|
||||
} else if (element.tagName.toLowerCase() === "input" && element.type === "checkbox") {
|
||||
json[element.name] = element.checked;
|
||||
} else if (element.tagName.toLowerCase() === "ak-search-select") {
|
||||
const select = element as unknown as SearchSelect<unknown>;
|
||||
json[element.name] = new Date(inputElement.value);
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "checkbox"
|
||||
) {
|
||||
json[element.name] = inputElement.checked;
|
||||
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
|
||||
const select = inputElement as unknown as SearchSelect<unknown>;
|
||||
let value: unknown;
|
||||
try {
|
||||
value = select.toForm();
|
||||
|
@ -179,9 +189,7 @@ export class Form<T> extends AKElement {
|
|||
}
|
||||
json[element.name] = value;
|
||||
} else {
|
||||
for (let v = 0; v < values.length; v++) {
|
||||
this.serializeFieldRecursive(element, values[v], json);
|
||||
}
|
||||
this.serializeFieldRecursive(inputElement, inputElement.value, json);
|
||||
}
|
||||
});
|
||||
return json as unknown as T;
|
||||
|
@ -213,11 +221,6 @@ export class Form<T> extends AKElement {
|
|||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
|
||||
if (!form) {
|
||||
console.warn("authentik/forms: failed to find iron-form");
|
||||
return;
|
||||
}
|
||||
return this.send(data)
|
||||
.then((r) => {
|
||||
showMessage({
|
||||
|
@ -244,8 +247,12 @@ export class Form<T> extends AKElement {
|
|||
throw errorMessage;
|
||||
}
|
||||
// assign all input-related errors to their elements
|
||||
const elements: HorizontalFormElement[] = form._getSubmittableElements();
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal",
|
||||
) || [];
|
||||
elements.forEach((element) => {
|
||||
element.requestUpdate();
|
||||
const elementName = element.name;
|
||||
if (!elementName) return;
|
||||
if (camelToSnake(elementName) in errorMessage) {
|
||||
|
@ -296,13 +303,7 @@ export class Form<T> extends AKElement {
|
|||
}
|
||||
|
||||
renderVisible(): TemplateResult {
|
||||
return html`<iron-form
|
||||
@iron-form-presubmit=${(ev: Event) => {
|
||||
this.submit(ev);
|
||||
}}
|
||||
>
|
||||
${this.renderNonFieldErrors()} ${this.renderForm()}
|
||||
</iron-form>`;
|
||||
return html` ${this.renderNonFieldErrors()} ${this.renderForm()}`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
|
|
|
@ -69,6 +69,10 @@ export class HorizontalFormElement extends AKElement {
|
|||
@property()
|
||||
name = "";
|
||||
|
||||
firstUpdated(): void {
|
||||
this.updated();
|
||||
}
|
||||
|
||||
updated(): void {
|
||||
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
|
||||
input.focus();
|
||||
|
@ -89,7 +93,7 @@ export class HorizontalFormElement extends AKElement {
|
|||
case "ak-chip-group":
|
||||
case "ak-search-select":
|
||||
case "ak-radio":
|
||||
(input as HTMLInputElement).name = this.name;
|
||||
input.setAttribute("name", this.name);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
@ -108,6 +112,7 @@ export class HorizontalFormElement extends AKElement {
|
|||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
this.updated();
|
||||
return html`<div class="pf-c-form__group">
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label">
|
||||
|
|
|
@ -70,6 +70,7 @@ export class SearchSelect<T> extends AKElement {
|
|||
observer: IntersectionObserver;
|
||||
dropdownUID: string;
|
||||
dropdownContainer: HTMLDivElement;
|
||||
isFetchingData = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -103,13 +104,18 @@ export class SearchSelect<T> extends AKElement {
|
|||
}
|
||||
|
||||
updateData(): void {
|
||||
if (this.isFetchingData) {
|
||||
return;
|
||||
}
|
||||
this.isFetchingData = true;
|
||||
this.fetchObjects(this.query).then((objects) => {
|
||||
this.objects = objects;
|
||||
this.objects.forEach((obj) => {
|
||||
objects.forEach((obj) => {
|
||||
if (this.selected && this.selected(obj, this.objects || [])) {
|
||||
this.selectedObject = obj;
|
||||
}
|
||||
});
|
||||
this.objects = objects;
|
||||
this.isFetchingData = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -200,9 +206,10 @@ export class SearchSelect<T> extends AKElement {
|
|||
render(
|
||||
html`<div
|
||||
class="pf-c-dropdown pf-m-expanded"
|
||||
?hidden=${!this.open}
|
||||
style="position: fixed; inset: 0px auto auto 0px; z-index: 9999; transform: translate(${pos.x}px, ${pos.y +
|
||||
this.offsetHeight}px); width: ${pos.width}px;"
|
||||
this.offsetHeight}px); width: ${pos.width}px; ${this.open
|
||||
? ""
|
||||
: "visibility: hidden;"}"
|
||||
>
|
||||
<ul
|
||||
class="pf-c-dropdown__menu pf-m-static"
|
||||
|
@ -249,6 +256,14 @@ export class SearchSelect<T> extends AKElement {
|
|||
|
||||
render(): TemplateResult {
|
||||
this.renderMenu();
|
||||
let value = "";
|
||||
if (!this.objects) {
|
||||
value = t`Loading...`;
|
||||
} else if (this.selectedObject) {
|
||||
value = this.renderElement(this.selectedObject);
|
||||
} else if (this.blankable) {
|
||||
value = this.emptyOption;
|
||||
}
|
||||
return html`<div class="pf-c-select">
|
||||
<div class="pf-c-select__toggle pf-m-typeahead">
|
||||
<div class="pf-c-select__toggle-wrapper">
|
||||
|
@ -256,6 +271,7 @@ export class SearchSelect<T> extends AKElement {
|
|||
class="pf-c-form-control pf-c-select__toggle-typeahead"
|
||||
type="text"
|
||||
placeholder=${this.placeholder}
|
||||
spellcheck="false"
|
||||
@input=${(ev: InputEvent) => {
|
||||
this.query = (ev.target as HTMLInputElement).value;
|
||||
this.updateData();
|
||||
|
@ -285,11 +301,7 @@ export class SearchSelect<T> extends AKElement {
|
|||
this.open = false;
|
||||
this.renderMenu();
|
||||
}}
|
||||
.value=${this.selectedObject
|
||||
? this.renderElement(this.selectedObject)
|
||||
: this.blankable
|
||||
? this.emptyOption
|
||||
: ""}
|
||||
.value=${value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -37,6 +37,7 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
|||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
ChallengeChoices,
|
||||
|
@ -119,7 +120,7 @@ export class FlowExecutor extends Interface implements StageHost {
|
|||
ws: WebsocketClient;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFLogin, PFDrawer, PFButton, PFTitle, PFList, PFBackgroundImage].concat(css`
|
||||
return [PFBase, PFLogin, PFDrawer, PFButton, PFTitle, PFList, PFBackgroundImage].concat(css`
|
||||
.pf-c-background-image::before {
|
||||
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
||||
|
|
|
@ -16,6 +16,7 @@ import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
|||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
AuthenticatorValidationChallenge,
|
||||
|
@ -76,7 +77,7 @@ export class AuthenticatorValidateStage
|
|||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
|
||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
|
||||
ul {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
|||
if (this.timer) {
|
||||
console.debug("authentik/stages/password: cleared focus timer");
|
||||
window.clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// @ts-ignore
|
||||
window["polymerSkipLoadingFontRoboto"] = true;
|
||||
import "construct-style-sheets-polyfill";
|
||||
import "@webcomponents/webcomponentsjs";
|
||||
import "lit/polyfill-support.js";
|
||||
|
|
Reference in New Issue