From 901f1435d78df4c546bf7abfc2baba6646c3ce48 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 5 Dec 2023 15:30:10 -0800 Subject: [PATCH] Refactor `LiteElement` into reactive controllers (#1423) - Copies navigation and notification utility methods into separate controllers - Adds deprecation notice to `LitElement` methods - Default type import start to inline --- frontend/.eslintrc.js | 7 +- frontend/src/controllers/navigate.ts | 75 +++++++++++++ frontend/src/controllers/notify.ts | 60 ++++++++++ .../src/features/archived-items/crawl-list.ts | 2 +- .../features/crawl-workflows/workflow-list.ts | 2 +- frontend/src/index.ts | 3 +- frontend/src/utils/LiteElement.ts | 106 +++--------------- 7 files changed, 163 insertions(+), 92 deletions(-) create mode 100644 frontend/src/controllers/navigate.ts create mode 100644 frontend/src/controllers/notify.ts diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 12ed4774..15080333 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -29,7 +29,12 @@ module.exports = { destructuredArrayIgnorePattern: "^_", }, ], - "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/consistent-type-imports": [ + "error", + { + fixStyle: "inline-type-imports", + }, + ], "@typescript-eslint/consistent-type-exports": "error", "@typescript-eslint/no-explicit-any": "warn", }, diff --git a/frontend/src/controllers/navigate.ts b/frontend/src/controllers/navigate.ts new file mode 100644 index 00000000..36b42283 --- /dev/null +++ b/frontend/src/controllers/navigate.ts @@ -0,0 +1,75 @@ +import type { ReactiveController, ReactiveControllerHost } from "lit"; + +import appState from "@/utils/state"; + +const NAVIGATE_EVENT_NAME = "navigate"; + +export interface NavigateEvent extends CustomEvent { + detail: { + url: string; + state?: object; + }; +} + +/** + * Manage app navigation + */ +export class NavigateController implements ReactiveController { + private host: ReactiveControllerHost & EventTarget; + + get orgBasePath() { + const slug = appState.orgSlug; + if (slug) { + return `/orgs/${slug}`; + } + return "/"; + } + + constructor(host: NavigateController["host"]) { + this.host = host; + host.addController(this); + } + + hostConnected() {} + hostDisconnected() {} + + to(url: string, state?: object): void { + const evt: NavigateEvent = new CustomEvent(NAVIGATE_EVENT_NAME, { + detail: { url, state }, + bubbles: true, + composed: true, + }); + this.host.dispatchEvent(evt); + } + + /** + * Bind to anchor tag to prevent full page navigation + * @example + * ```ts + * go + * ``` + * @param event Click event + */ + link(event: MouseEvent, _href?: string): void { + if ( + // Detect keypress for opening in a new tab + event.ctrlKey || + event.shiftKey || + event.metaKey || + (event.button && event.button == 1) || + // Account for event prevented on anchor tag + event.defaultPrevented + ) { + return; + } + + event.preventDefault(); + + const evt: NavigateEvent = new CustomEvent(NAVIGATE_EVENT_NAME, { + detail: { url: (event.currentTarget as HTMLAnchorElement).href }, + bubbles: true, + composed: true, + }); + this.host.dispatchEvent(evt); + } +} diff --git a/frontend/src/controllers/notify.ts b/frontend/src/controllers/notify.ts new file mode 100644 index 00000000..5f3b28b1 --- /dev/null +++ b/frontend/src/controllers/notify.ts @@ -0,0 +1,60 @@ +import type { + ReactiveController, + ReactiveControllerHost, + TemplateResult, +} from "lit"; + +export interface NotifyEvent extends CustomEvent { + detail: { + /** + * Notification message body. + * Example: + * ```ts + * message: html`Look!` + * ``` + * + * Note: In order for `this` methods to work, you'll + * need to bind `this` or use a fat arrow function. + * For example: + * ```ts + * message: html`` + * ``` + * Or: + * ```ts + * message: html`` + * ``` + **/ + message: string | TemplateResult; + /** Notification title */ + title?: string; + /** Shoelace icon name */ + icon?: string; + variant?: "success" | "warning" | "danger" | "primary" | "info"; + duration?: number; + }; +} + +/** + * Manage global app notifications + */ +export class NotifyController implements ReactiveController { + private host: ReactiveControllerHost & EventTarget; + + constructor(host: NotifyController["host"]) { + this.host = host; + host.addController(this); + } + + hostConnected() {} + hostDisconnected() {} + + toast(detail: NotifyEvent["detail"]) { + this.host.dispatchEvent( + new CustomEvent("notify", { + bubbles: true, + composed: true, + detail, + }) + ); + } +} diff --git a/frontend/src/features/archived-items/crawl-list.ts b/frontend/src/features/archived-items/crawl-list.ts index 307a0593..19ef6a04 100644 --- a/frontend/src/features/archived-items/crawl-list.ts +++ b/frontend/src/features/archived-items/crawl-list.ts @@ -25,7 +25,7 @@ import queryString from "query-string"; import { RelativeDuration } from "@/components/ui/relative-duration"; import type { Crawl } from "@/types/crawler"; import { srOnly, truncate } from "@/utils/css"; -import type { NavigateEvent } from "@/utils/LiteElement"; +import type { NavigateEvent } from "@/controllers/navigate"; import type { OverflowDropdown } from "@/components/ui/overflow-dropdown"; const mediumBreakpointCss = css`30rem`; diff --git a/frontend/src/features/crawl-workflows/workflow-list.ts b/frontend/src/features/crawl-workflows/workflow-list.ts index 136d1649..e925d910 100644 --- a/frontend/src/features/crawl-workflows/workflow-list.ts +++ b/frontend/src/features/crawl-workflows/workflow-list.ts @@ -24,7 +24,7 @@ import { msg, localized, str } from "@lit/localize"; import { RelativeDuration } from "@/components/ui/relative-duration"; import type { ListWorkflow } from "@/types/crawler"; import { srOnly, truncate } from "@/utils/css"; -import type { NavigateEvent } from "@/utils/LiteElement"; +import type { NavigateEvent } from "@/controllers/navigate"; import { humanizeSchedule } from "@/utils/cron"; import { numberFormatter } from "@/utils/number"; import type { OverflowDropdown } from "@/components/ui/overflow-dropdown"; diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 371dbae5..57227e20 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -11,7 +11,8 @@ import "tailwindcss/tailwind.css"; import "./utils/polyfills"; import appState, { use, AppStateService } from "./utils/state"; import type { OrgTab } from "./pages/org"; -import type { NotifyEvent, NavigateEvent } from "./utils/LiteElement"; +import type { NavigateEvent } from "@/controllers/navigate"; +import type { NotifyEvent } from "@/controllers/notify"; import LiteElement, { html } from "./utils/LiteElement"; import APIRouter from "./utils/APIRouter"; import AuthService from "./utils/AuthService"; diff --git a/frontend/src/utils/LiteElement.ts b/frontend/src/utils/LiteElement.ts index 803cd9ef..6ce23f76 100644 --- a/frontend/src/utils/LiteElement.ts +++ b/frontend/src/utils/LiteElement.ts @@ -1,123 +1,53 @@ import { LitElement, html } from "lit"; -import type { TemplateResult } from "lit"; import { APIController } from "@/controllers/api"; +import { NotifyController } from "@/controllers/notify"; +import { NavigateController } from "@/controllers/navigate"; import appState, { use } from "./state"; -export interface NavigateEvent extends CustomEvent { - detail: { - url: string; - state?: object; - }; -} - -export interface NotifyEvent extends CustomEvent { - detail: { - /** - * Notification message body. - * Example: - * ```ts - * message: html`Look!` - * ``` - * - * Note: In order for `this` methods to work, you'll - * need to bind `this` or use a fat arrow function. - * For example: - * ```ts - * message: html`` - * ``` - * Or: - * ```ts - * message: html`` - * ``` - **/ - message: string | TemplateResult; - /** Notification title */ - title?: string; - /** Shoelace icon name */ - icon?: string; - variant?: "success" | "warning" | "danger" | "primary" | "info"; - duration?: number; - }; -} - export { html }; export default class LiteElement extends LitElement { @use() appState = appState; - private api = new APIController(this); + private apiController = new APIController(this); + private notifyController = new NotifyController(this); + private navigateController = new NavigateController(this); protected get orgBasePath() { - const slug = this.appState.orgSlug; - if (slug) { - return `/orgs/${slug}`; - } - return "/"; + return this.navigateController.orgBasePath; } createRenderRoot() { return this; } - navTo(url: string, state?: object): void { - const evt: NavigateEvent = new CustomEvent("navigate", { - detail: { url, state }, - bubbles: true, - composed: true, - }); - this.dispatchEvent(evt); + /** + * @deprecated New components should use NavigateController directly + */ + navTo(...args: Parameters) { + return this.navigateController.to(...args); } /** - * Bind to anchor tag to prevent full page navigation - * @example - * ```ts - * go - * ``` - * @param event Click event + * @deprecated New components should use NavigateController directly */ - navLink(event: MouseEvent, _href?: string): void { - if ( - // Detect keypress for opening in a new tab - event.ctrlKey || - event.shiftKey || - event.metaKey || - (event.button && event.button == 1) || - // Account for event prevented on anchor tag - event.defaultPrevented - ) { - return; - } - - event.preventDefault(); - - const evt: NavigateEvent = new CustomEvent("navigate", { - detail: { url: (event.currentTarget as HTMLAnchorElement).href }, - bubbles: true, - composed: true, - }); - this.dispatchEvent(evt); + navLink(...args: Parameters) { + return this.navigateController.link(...args); } /** - * Emit global notification + * @deprecated New components should use NotifyController directly */ - notify(detail: NotifyEvent["detail"]) { - this.dispatchEvent( - new CustomEvent("notify", { - bubbles: true, - composed: true, - detail, - }) - ); + notify(...args: Parameters) { + return this.notifyController.toast(...args); } /** * @deprecated New components should use APIController directly */ async apiFetch(...args: Parameters) { - return this.api.fetch(...args); + return this.apiController.fetch(...args); } }