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
This commit is contained in:
		
							parent
							
								
									a1e42b0cf3
								
							
						
					
					
						commit
						901f1435d7
					
				| @ -29,7 +29,12 @@ module.exports = { | |||||||
|         destructuredArrayIgnorePattern: "^_", |         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/consistent-type-exports": "error", | ||||||
|     "@typescript-eslint/no-explicit-any": "warn", |     "@typescript-eslint/no-explicit-any": "warn", | ||||||
|   }, |   }, | ||||||
|  | |||||||
							
								
								
									
										75
									
								
								frontend/src/controllers/navigate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								frontend/src/controllers/navigate.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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
 | ||||||
|  |    * <a href="/" @click=${this.navigate.link}>go</a> | ||||||
|  |    * ``` | ||||||
|  |    * @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); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								frontend/src/controllers/notify.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								frontend/src/controllers/notify.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | |||||||
|  | import type { | ||||||
|  |   ReactiveController, | ||||||
|  |   ReactiveControllerHost, | ||||||
|  |   TemplateResult, | ||||||
|  | } from "lit"; | ||||||
|  | 
 | ||||||
|  | export interface NotifyEvent extends CustomEvent { | ||||||
|  |   detail: { | ||||||
|  |     /** | ||||||
|  |      * Notification message body. | ||||||
|  |      * Example: | ||||||
|  |      * ```ts
 | ||||||
|  |      * message: html`<strong>Look!</strong>` | ||||||
|  |      * ``` | ||||||
|  |      * | ||||||
|  |      * 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`<button @click=${this.onClick.bind(this)}>Go!</button>` | ||||||
|  |      * ``` | ||||||
|  |      * Or: | ||||||
|  |      * ```ts
 | ||||||
|  |      * message: html`<button @click=${(e) => this.onClick(e)}>Go!</button>` | ||||||
|  |      * ``` | ||||||
|  |      **/ | ||||||
|  |     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, | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -25,7 +25,7 @@ import queryString from "query-string"; | |||||||
| import { RelativeDuration } from "@/components/ui/relative-duration"; | import { RelativeDuration } from "@/components/ui/relative-duration"; | ||||||
| import type { Crawl } from "@/types/crawler"; | import type { Crawl } from "@/types/crawler"; | ||||||
| import { srOnly, truncate } from "@/utils/css"; | 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"; | import type { OverflowDropdown } from "@/components/ui/overflow-dropdown"; | ||||||
| 
 | 
 | ||||||
| const mediumBreakpointCss = css`30rem`; | const mediumBreakpointCss = css`30rem`; | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ import { msg, localized, str } from "@lit/localize"; | |||||||
| import { RelativeDuration } from "@/components/ui/relative-duration"; | import { RelativeDuration } from "@/components/ui/relative-duration"; | ||||||
| import type { ListWorkflow } from "@/types/crawler"; | import type { ListWorkflow } from "@/types/crawler"; | ||||||
| import { srOnly, truncate } from "@/utils/css"; | import { srOnly, truncate } from "@/utils/css"; | ||||||
| import type { NavigateEvent } from "@/utils/LiteElement"; | import type { NavigateEvent } from "@/controllers/navigate"; | ||||||
| import { humanizeSchedule } from "@/utils/cron"; | import { humanizeSchedule } from "@/utils/cron"; | ||||||
| import { numberFormatter } from "@/utils/number"; | import { numberFormatter } from "@/utils/number"; | ||||||
| import type { OverflowDropdown } from "@/components/ui/overflow-dropdown"; | import type { OverflowDropdown } from "@/components/ui/overflow-dropdown"; | ||||||
|  | |||||||
| @ -11,7 +11,8 @@ import "tailwindcss/tailwind.css"; | |||||||
| import "./utils/polyfills"; | import "./utils/polyfills"; | ||||||
| import appState, { use, AppStateService } from "./utils/state"; | import appState, { use, AppStateService } from "./utils/state"; | ||||||
| import type { OrgTab } from "./pages/org"; | 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 LiteElement, { html } from "./utils/LiteElement"; | ||||||
| import APIRouter from "./utils/APIRouter"; | import APIRouter from "./utils/APIRouter"; | ||||||
| import AuthService from "./utils/AuthService"; | import AuthService from "./utils/AuthService"; | ||||||
|  | |||||||
| @ -1,123 +1,53 @@ | |||||||
| import { LitElement, html } from "lit"; | import { LitElement, html } from "lit"; | ||||||
| import type { TemplateResult } from "lit"; |  | ||||||
| 
 | 
 | ||||||
| import { APIController } from "@/controllers/api"; | import { APIController } from "@/controllers/api"; | ||||||
|  | import { NotifyController } from "@/controllers/notify"; | ||||||
|  | import { NavigateController } from "@/controllers/navigate"; | ||||||
| import appState, { use } from "./state"; | 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`<strong>Look!</strong>` |  | ||||||
|      * ``` |  | ||||||
|      * |  | ||||||
|      * 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`<button @click=${this.onClick.bind(this)}>Go!</button>` |  | ||||||
|      * ``` |  | ||||||
|      * Or: |  | ||||||
|      * ```ts
 |  | ||||||
|      * message: html`<button @click=${(e) => this.onClick(e)}>Go!</button>` |  | ||||||
|      * ``` |  | ||||||
|      **/ |  | ||||||
|     message: string | TemplateResult; |  | ||||||
|     /** Notification title */ |  | ||||||
|     title?: string; |  | ||||||
|     /** Shoelace icon name */ |  | ||||||
|     icon?: string; |  | ||||||
|     variant?: "success" | "warning" | "danger" | "primary" | "info"; |  | ||||||
|     duration?: number; |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export { html }; | export { html }; | ||||||
| 
 | 
 | ||||||
| export default class LiteElement extends LitElement { | export default class LiteElement extends LitElement { | ||||||
|   @use() |   @use() | ||||||
|   appState = appState; |   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() { |   protected get orgBasePath() { | ||||||
|     const slug = this.appState.orgSlug; |     return this.navigateController.orgBasePath; | ||||||
|     if (slug) { |  | ||||||
|       return `/orgs/${slug}`; |  | ||||||
|     } |  | ||||||
|     return "/"; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   createRenderRoot() { |   createRenderRoot() { | ||||||
|     return this; |     return this; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   navTo(url: string, state?: object): void { |   /** | ||||||
|     const evt: NavigateEvent = new CustomEvent("navigate", { |    * @deprecated New components should use NavigateController directly | ||||||
|       detail: { url, state }, |    */ | ||||||
|       bubbles: true, |   navTo(...args: Parameters<NavigateController["to"]>) { | ||||||
|       composed: true, |     return this.navigateController.to(...args); | ||||||
|     }); |  | ||||||
|     this.dispatchEvent(evt); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Bind to anchor tag to prevent full page navigation |    * @deprecated New components should use NavigateController directly | ||||||
|    * @example |  | ||||||
|    * ```ts
 |  | ||||||
|    * <a href="/" @click=${this.navLink}>go</a> |  | ||||||
|    * ``` |  | ||||||
|    * @param event Click event |  | ||||||
|    */ |    */ | ||||||
|   navLink(event: MouseEvent, _href?: string): void { |   navLink(...args: Parameters<NavigateController["link"]>) { | ||||||
|     if ( |     return this.navigateController.link(...args); | ||||||
|       // 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); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Emit global notification |    * @deprecated New components should use NotifyController directly | ||||||
|    */ |    */ | ||||||
|   notify(detail: NotifyEvent["detail"]) { |   notify(...args: Parameters<NotifyController["toast"]>) { | ||||||
|     this.dispatchEvent( |     return this.notifyController.toast(...args); | ||||||
|       new CustomEvent("notify", { |  | ||||||
|         bubbles: true, |  | ||||||
|         composed: true, |  | ||||||
|         detail, |  | ||||||
|       }) |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * @deprecated New components should use APIController directly |    * @deprecated New components should use APIController directly | ||||||
|    */ |    */ | ||||||
|   async apiFetch<T = unknown>(...args: Parameters<APIController["fetch"]>) { |   async apiFetch<T = unknown>(...args: Parameters<APIController["fetch"]>) { | ||||||
|     return this.api.fetch<T>(...args); |     return this.apiController.fetch<T>(...args); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user