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: "^_",
|
||||
},
|
||||
],
|
||||
"@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",
|
||||
},
|
||||
|
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 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`;
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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`<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 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<NavigateController["to"]>) {
|
||||
return this.navigateController.to(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to anchor tag to prevent full page navigation
|
||||
* @example
|
||||
* ```ts
|
||||
* <a href="/" @click=${this.navLink}>go</a>
|
||||
* ```
|
||||
* @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<NavigateController["link"]>) {
|
||||
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<NotifyController["toast"]>) {
|
||||
return this.notifyController.toast(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated New components should use APIController directly
|
||||
*/
|
||||
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