chore: Prevent blocking connected callback (#2244)

Moves language initialization to `willUpdate` to prevent blocking
connected callback and attaching listeners
This commit is contained in:
sua yoo 2024-12-17 09:29:51 -08:00 committed by GitHub
parent 02eeaca245
commit 9597cb1062
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 61 additions and 22 deletions

View File

@ -9,8 +9,8 @@
"", "",
"import { BtrixElement } from \"@/classes/BtrixElement\";", "import { BtrixElement } from \"@/classes/BtrixElement\";",
"", "",
"@localized()",
"@customElement(\"btrix-${1:component}\")", "@customElement(\"btrix-${1:component}\")",
"@localized()",
"export class ${2:Component} extends BtrixElement {", "export class ${2:Component} extends BtrixElement {",
"\trender() {", "\trender() {",
"\t\treturn html``;", "\t\treturn html``;",

View File

@ -22,6 +22,7 @@
</head> </head>
<body> <body>
<script> <script>
// Fetch API settings in parallel with dynamically loaded components
window window
.fetch("/api/settings", { .fetch("/api/settings", {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@ -6,7 +6,7 @@ import { NavigateController } from "./controllers/navigate";
import { NotifyController } from "./controllers/notify"; import { NotifyController } from "./controllers/notify";
import { type AppSettings } from "./utils/app"; import { type AppSettings } from "./utils/app";
import AuthService from "./utils/AuthService"; import AuthService from "./utils/AuthService";
import appState, { AppStateService } from "./utils/state"; import { AppStateService } from "./utils/state";
import { formatAPIUser } from "./utils/user"; import { formatAPIUser } from "./utils/user";
import { App, type APIUser } from "."; import { App, type APIUser } from ".";
@ -62,6 +62,22 @@ describe("browsertrix-app", () => {
expect(el).instanceOf(App); expect(el).instanceOf(App);
}); });
it("blocks render if settings aren't defined", async () => {
stub(AuthService, "initSessionStorage").returns(
Promise.resolve({
headers: { Authorization: "_fake_headers_" },
tokenExpiresAt: 0,
username: "test-auth@example.com",
}),
);
// @ts-expect-error checkFreshness is private
stub(AuthService.prototype, "checkFreshness");
const el = await fixture<App>(html` <browsertrix-app></browsertrix-app>`);
await el.updateComplete;
expect(el.shadowRoot?.childElementCount).to.equal(0);
});
it("renders home when authenticated", async () => { it("renders home when authenticated", async () => {
stub(AuthService, "initSessionStorage").returns( stub(AuthService, "initSessionStorage").returns(
Promise.resolve({ Promise.resolve({
@ -72,8 +88,9 @@ describe("browsertrix-app", () => {
); );
// @ts-expect-error checkFreshness is private // @ts-expect-error checkFreshness is private
stub(AuthService.prototype, "checkFreshness"); stub(AuthService.prototype, "checkFreshness");
stub(appState, "settings").returns(mockAppSettings); const el = await fixture<App>(
const el = await fixture<App>(html` <browsertrix-app></browsertrix-app>`); html` <browsertrix-app .settings=${mockAppSettings}></browsertrix-app>`,
);
await el.updateComplete; await el.updateComplete;
expect(el.shadowRoot?.querySelector("btrix-home")).to.exist; expect(el.shadowRoot?.querySelector("btrix-home")).to.exist;
}); });
@ -82,7 +99,6 @@ describe("browsertrix-app", () => {
stub(AuthService, "initSessionStorage").returns(Promise.resolve(null)); stub(AuthService, "initSessionStorage").returns(Promise.resolve(null));
// @ts-expect-error checkFreshness is private // @ts-expect-error checkFreshness is private
stub(AuthService.prototype, "checkFreshness"); stub(AuthService.prototype, "checkFreshness");
stub(appState, "settings").returns(mockAppSettings);
stub(NavigateController, "createNavigateEvent").callsFake( stub(NavigateController, "createNavigateEvent").callsFake(
() => () =>
new CustomEvent("x-ignored", { new CustomEvent("x-ignored", {
@ -90,7 +106,9 @@ describe("browsertrix-app", () => {
}), }),
); );
const el = await fixture<App>(html` <browsertrix-app></browsertrix-app>`); const el = await fixture<App>(
html` <browsertrix-app .settings=${mockAppSettings}></browsertrix-app>`,
);
expect(el.shadowRoot?.querySelector("btrix-home")).to.exist; expect(el.shadowRoot?.querySelector("btrix-home")).to.exist;
}); });

View File

@ -40,7 +40,7 @@ import {
translatedLocales, translatedLocales,
type TranslatedLocaleEnum, type TranslatedLocaleEnum,
} from "@/types/localization"; } from "@/types/localization";
import { getAppSettings, type AppSettings } from "@/utils/app"; import { type AppSettings } from "@/utils/app";
import { DEFAULT_MAX_SCALE } from "@/utils/crawler"; import { DEFAULT_MAX_SCALE } from "@/utils/crawler";
import localize from "@/utils/localize"; import localize from "@/utils/localize";
import { toast } from "@/utils/notify"; import { toast } from "@/utils/notify";
@ -70,21 +70,33 @@ export interface UserGuideEventMap {
"btrix-user-guide-show": CustomEvent<{ path?: string }>; "btrix-user-guide-show": CustomEvent<{ path?: string }>;
} }
@localized()
@customElement("browsertrix-app") @customElement("browsertrix-app")
@localized()
export class App extends BtrixElement { export class App extends BtrixElement {
/**
* Browsertrix app version to display in the UI
*/
@property({ type: String }) @property({ type: String })
version?: string; version?: string;
/**
* Base URL for user guide documentation
*/
@property({ type: String }) @property({ type: String })
docsUrl = "/docs/"; docsUrl = "/docs/";
/**
* App settings from `/api/settings`
*/
@property({ type: Object }) @property({ type: Object })
settings?: AppSettings; settings?: AppSettings;
private readonly router = new APIRouter(ROUTES); private readonly router = new APIRouter(ROUTES);
authService = new AuthService(); authService = new AuthService();
@state()
private translationReady = false;
@state() @state()
private viewState!: ViewState<typeof ROUTES>; private viewState!: ViewState<typeof ROUTES>;
@ -116,19 +128,6 @@ export class App extends BtrixElement {
void this.fetchAndUpdateUserInfo(); void this.fetchAndUpdateUserInfo();
} }
try {
this.settings = await getAppSettings();
} catch (e) {
console.error(e);
this.notify.toast({
message: msg("Couldnt initialize Browsertrix correctly."),
variant: "danger",
icon: "exclamation-octagon",
id: "get-app-settings-error",
});
} finally {
await localize.initLanguage();
}
super.connectedCallback(); super.connectedCallback();
this.addEventListener("btrix-navigate", this.onNavigateTo); this.addEventListener("btrix-navigate", this.onNavigateTo);
@ -157,6 +156,10 @@ export class App extends BtrixElement {
willUpdate(changedProperties: Map<string, unknown>) { willUpdate(changedProperties: Map<string, unknown>) {
if (changedProperties.has("settings")) { if (changedProperties.has("settings")) {
AppStateService.updateSettings(this.settings || null); AppStateService.updateSettings(this.settings || null);
if (this.settings && !changedProperties.get("settings")) {
void this.initTranslation();
}
} }
if (changedProperties.has("viewState")) { if (changedProperties.has("viewState")) {
if (this.viewState.route === "orgs") { if (this.viewState.route === "orgs") {
@ -170,6 +173,13 @@ export class App extends BtrixElement {
} }
} }
async initTranslation() {
await localize.initLanguage();
// TODO We might want to set this in a lit-localize-status event listener
// see https://lit.dev/docs/localization/runtime-mode/#example-of-using-the-status-event
this.translationReady = true;
}
getLocationPathname() { getLocationPathname() {
return window.location.pathname; return window.location.pathname;
} }
@ -264,6 +274,8 @@ export class App extends BtrixElement {
} }
render() { render() {
if (!this.translationReady) return;
return html` return html`
<div class="min-w-screen flex min-h-screen flex-col"> <div class="min-w-screen flex min-h-screen flex-col">
${this.renderNavBar()} ${this.renderAlertBanner()} ${this.renderNavBar()} ${this.renderAlertBanner()}

View File

@ -20,11 +20,19 @@ import {
} from "@/types/localization"; } from "@/types/localization";
import appState from "@/utils/state"; import appState from "@/utils/state";
// Pre-load all locales
const localizedTemplates = new Map(
targetLocales.map((locale) => [
locale,
import(`/src/__generated__/locales/${locale}.ts`),
]),
);
const { getLocale, setLocale } = configureLocalization({ const { getLocale, setLocale } = configureLocalization({
sourceLocale, sourceLocale,
targetLocales, targetLocales,
loadLocale: async (locale: string) => loadLocale: async (locale: string) =>
import(`/src/__generated__/locales/${locale}.ts`), localizedTemplates.get(locale as (typeof targetLocales)[number]),
}); });
const defaultDateOptions: Intl.DateTimeFormatOptions = { const defaultDateOptions: Intl.DateTimeFormatOptions = {