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);
}
}