Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion static/js/publisher/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "react-query";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";

import { importComponent } from "./utils/importComponent";
import BrandStoreRoute from "./components/BrandStoreRoute/BrandStoreRoute";
import PublisherLayout from "./layouts/PublisherLayout";
import AccountDetails from "./pages/AccountDetails";
import AccountSnaps from "./pages/AccountSnaps";
import BrandStoreSettings from "./pages/BrandStoreSettings";
import Build from "./pages/Build";
Expand All @@ -29,6 +29,8 @@ import ValidationSet from "./pages/ValidationSet";
import ValidationSets from "./pages/ValidationSets";
import AccountKeys from "./pages/AccountKeys";

const AccountDetails = importComponent(() => import("./pages/AccountDetails"));

Sentry.init({
dsn: window.SENTRY_DSN,
integrations: [Sentry.browserTracingIntegration()],
Expand Down
5 changes: 5 additions & 0 deletions static/js/publisher/pages/AccountDetails/AccountDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@canonical/react-components";

import { publisherState } from "../../state/publisherState";
import { setPageTitle } from "../../utils";

function AccountDetails(): React.JSX.Element {
const [subscriptionPreferencesChanged, setSubscriptionPreferencesChanged] =
Expand All @@ -21,6 +22,10 @@ function AccountDetails(): React.JSX.Element {
const [isSaving, setIsSaving] = useState(false);
const publisher = useAtomValue(publisherState);

useEffect(() => {
setPageTitle(`Account details`);
}, []);

useEffect(() => {
setSubscribeToNewsletter(
publisher?.subscriptions ? publisher?.subscriptions?.newsletter : false,
Expand Down
47 changes: 47 additions & 0 deletions static/js/publisher/utils/__tests__/importComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
render,
screen,
act,
waitForElementToBeRemoved,
} from "@testing-library/react";
import "@testing-library/jest-dom";

import { importComponent } from "../importComponent";

function renderComponent() {
const sleep = (n: number) => new Promise<void>((r) => setTimeout(r, n));

function MockComponent() {
return <h1>Mock Component!</h1>;
}

const LazyComponent = importComponent(async () => {
await sleep(100);

return {
default: MockComponent,
};
});

return render(<LazyComponent />);
}

describe("importComponent", () => {
test("renders loader", () => {
act(() => {
renderComponent();
});

expect(screen.getByText(/Loading/)).toBeInTheDocument();
});

test("renders component", async () => {
await act(async () => {
renderComponent();
});

await waitForElementToBeRemoved(() => screen.getByText(/Loading/));

expect(screen.getByText(/Mock Component!/)).toBeInTheDocument();
});
});
34 changes: 34 additions & 0 deletions static/js/publisher/utils/importComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { forwardRef, lazy, Suspense } from "react";
import { Spinner } from "@canonical/react-components";

type LoaderFn<TComponent extends React.ComponentType> = () => Promise<{
default: TComponent;
}>;

/**
* Helper function that uses `React.lazy` to load a component and wrap it in a `React.Suspense` boundary
*
* @param {LoaderFn<TComponent>} loader function that executes the lazy import for `Component`
* @param {React.ReactNode} fallback element that will be displayed while waiting for `Component` to load, optional
* @return a `Suspense` boundary that wraps `Component`
*/
export function importComponent<TComponent extends React.ComponentType>(
loader: LoaderFn<TComponent>,
fallback?: React.ReactNode,
) {
const Component = lazy(loader);

return forwardRef<unknown, React.ComponentPropsWithRef<TComponent>>(
(props, ref) => (
<Suspense fallback={fallback ? fallback : <Spinner text="Loading..." />}>
{/*
Something is wrong with the types for Component and its props,
but it does actually work at runtime and types are inferred properly
outisde of this file, so...
*/}
{/* @ts-expect-error: let's just ignore this error for the moment */}
<Component {...props} ref={ref} />
</Suspense>
),
);
}
1 change: 1 addition & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default defineConfig({
},
],
},
base: "./", // use the script's URL path as base when loading assets in dynamic imports
build: {
manifest: true,
modulePreload: false,
Expand Down
Loading