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\";",
"",
"@localized()",
"@customElement(\"btrix-${1:component}\")",
"@localized()",
"export class ${2:Component} extends BtrixElement {",
"\trender() {",
"\t\treturn html``;",

View File

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

View File

@ -6,7 +6,7 @@ import { NavigateController } from "./controllers/navigate";
import { NotifyController } from "./controllers/notify";
import { type AppSettings } from "./utils/app";
import AuthService from "./utils/AuthService";
import appState, { AppStateService } from "./utils/state";
import { AppStateService } from "./utils/state";
import { formatAPIUser } from "./utils/user";
import { App, type APIUser } from ".";
@ -62,6 +62,22 @@ describe("browsertrix-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 () => {
stub(AuthService, "initSessionStorage").returns(
Promise.resolve({
@ -72,8 +88,9 @@ describe("browsertrix-app", () => {
);
// @ts-expect-error checkFreshness is private
stub(AuthService.prototype, "checkFreshness");
stub(appState, "settings").returns(mockAppSettings);
const el = await fixture<App>(html` <browsertrix-app></browsertrix-app>`);
const el = await fixture<App>(
html` <browsertrix-app .settings=${mockAppSettings}></browsertrix-app>`,
);
await el.updateComplete;
expect(el.shadowRoot?.querySelector("btrix-home")).to.exist;
});
@ -82,7 +99,6 @@ describe("browsertrix-app", () => {
stub(AuthService, "initSessionStorage").returns(Promise.resolve(null));
// @ts-expect-error checkFreshness is private
stub(AuthService.prototype, "checkFreshness");
stub(appState, "settings").returns(mockAppSettings);
stub(NavigateController, "createNavigateEvent").callsFake(
() =>
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;
});

View File

@ -40,7 +40,7 @@ import {
translatedLocales,
type TranslatedLocaleEnum,
} from "@/types/localization";
import { getAppSettings, type AppSettings } from "@/utils/app";
import { type AppSettings } from "@/utils/app";
import { DEFAULT_MAX_SCALE } from "@/utils/crawler";
import localize from "@/utils/localize";
import { toast } from "@/utils/notify";
@ -70,21 +70,33 @@ export interface UserGuideEventMap {
"btrix-user-guide-show": CustomEvent<{ path?: string }>;
}
@localized()
@customElement("browsertrix-app")
@localized()
export class App extends BtrixElement {
/**
* Browsertrix app version to display in the UI
*/
@property({ type: String })
version?: string;
/**
* Base URL for user guide documentation
*/
@property({ type: String })
docsUrl = "/docs/";
/**
* App settings from `/api/settings`
*/
@property({ type: Object })
settings?: AppSettings;
private readonly router = new APIRouter(ROUTES);
authService = new AuthService();
@state()
private translationReady = false;
@state()
private viewState!: ViewState<typeof ROUTES>;
@ -116,19 +128,6 @@ export class App extends BtrixElement {
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();
this.addEventListener("btrix-navigate", this.onNavigateTo);
@ -157,6 +156,10 @@ export class App extends BtrixElement {
willUpdate(changedProperties: Map<string, unknown>) {
if (changedProperties.has("settings")) {
AppStateService.updateSettings(this.settings || null);
if (this.settings && !changedProperties.get("settings")) {
void this.initTranslation();
}
}
if (changedProperties.has("viewState")) {
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() {
return window.location.pathname;
}
@ -264,6 +274,8 @@ export class App extends BtrixElement {
}
render() {
if (!this.translationReady) return;
return html`
<div class="min-w-screen flex min-h-screen flex-col">
${this.renderNavBar()} ${this.renderAlertBanner()}

View File

@ -20,11 +20,19 @@ import {
} from "@/types/localization";
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({
sourceLocale,
targetLocales,
loadLocale: async (locale: string) =>
import(`/src/__generated__/locales/${locale}.ts`),
localizedTemplates.get(locale as (typeof targetLocales)[number]),
});
const defaultDateOptions: Intl.DateTimeFormatOptions = {