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