Duplicate crawl config from list (#99)
This commit is contained in:
parent
3a461d86d4
commit
2666b6f6aa
@ -6,12 +6,12 @@ import type { SlDialog } from "@shoelace-style/shoelace";
|
|||||||
import "tailwindcss/tailwind.css";
|
import "tailwindcss/tailwind.css";
|
||||||
|
|
||||||
import type { ArchiveTab } from "./pages/archive";
|
import type { ArchiveTab } from "./pages/archive";
|
||||||
import type { NotifyEvent } from "./utils/LiteElement";
|
import type { NotifyEvent, NavigateEvent } from "./utils/LiteElement";
|
||||||
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";
|
||||||
import type { LoggedInEvent } from "./utils/AuthService";
|
import type { LoggedInEvent } from "./utils/AuthService";
|
||||||
import type { ViewState, NavigateEvent } from "./utils/APIRouter";
|
import type { ViewState } from "./utils/APIRouter";
|
||||||
import type { CurrentUser } from "./types/user";
|
import type { CurrentUser } from "./types/user";
|
||||||
import type { AuthState } from "./utils/AuthService";
|
import type { AuthState } from "./utils/AuthService";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
@ -145,7 +145,7 @@ export class App extends LiteElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(newViewPath: string) {
|
navigate(newViewPath: string, state?: object) {
|
||||||
if (newViewPath.startsWith("http")) {
|
if (newViewPath.startsWith("http")) {
|
||||||
const url = new URL(newViewPath);
|
const url = new URL(newViewPath);
|
||||||
newViewPath = `${url.pathname}${url.search}`;
|
newViewPath = `${url.pathname}${url.search}`;
|
||||||
@ -158,6 +158,8 @@ export class App extends LiteElement {
|
|||||||
this.viewState = this.router.match(newViewPath);
|
this.viewState = this.router.match(newViewPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.viewState.data = state;
|
||||||
|
|
||||||
window.history.pushState(this.viewState, "", this.viewState.pathname);
|
window.history.pushState(this.viewState, "", this.viewState.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,6 +379,7 @@ export class App extends LiteElement {
|
|||||||
@notify="${this.onNotify}"
|
@notify="${this.onNotify}"
|
||||||
.authState=${this.authService.authState}
|
.authState=${this.authService.authState}
|
||||||
.userInfo=${this.userInfo}
|
.userInfo=${this.userInfo}
|
||||||
|
.viewStateData=${this.viewState.data}
|
||||||
archiveId=${this.viewState.params.id}
|
archiveId=${this.viewState.params.id}
|
||||||
archiveTab=${this.viewState.params.crawlConfigId
|
archiveTab=${this.viewState.params.crawlConfigId
|
||||||
? "crawl-templates"
|
? "crawl-templates"
|
||||||
@ -472,7 +475,7 @@ export class App extends LiteElement {
|
|||||||
onNavigateTo(event: NavigateEvent) {
|
onNavigateTo(event: NavigateEvent) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
this.navigate(event.detail);
|
this.navigate(event.detail.url, event.detail.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserInfoChange(event: CustomEvent<Partial<CurrentUser>>) {
|
onUserInfoChange(event: CustomEvent<Partial<CurrentUser>>) {
|
||||||
|
@ -109,24 +109,24 @@ export class CrawlTemplatesDetail extends LiteElement {
|
|||||||
? " border-t"
|
? " border-t"
|
||||||
: ""}"
|
: ""}"
|
||||||
role="row"
|
role="row"
|
||||||
title=${seed.url}
|
title=${typeof seed === "string" ? seed : seed.url}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="col-span-3 break-all leading-tight"
|
class="col-span-3 break-all leading-tight"
|
||||||
role="cell"
|
role="cell"
|
||||||
>
|
>
|
||||||
${seed.url}
|
${typeof seed === "string" ? seed : seed.url}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="col-span-1 uppercase text-0-500 text-xs"
|
class="col-span-1 uppercase text-0-500 text-xs"
|
||||||
role="cell"
|
role="cell"
|
||||||
>${seed.scopeType ||
|
>${(typeof seed !== "string" && seed.scopeType) ||
|
||||||
this.crawlTemplate?.config.scopeType}</span
|
this.crawlTemplate?.config.scopeType}</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="col-span-1 uppercase text-0-500 text-xs font-mono"
|
class="col-span-1 uppercase text-0-500 text-xs font-mono"
|
||||||
role="cell"
|
role="cell"
|
||||||
>${seed.limit ||
|
>${(typeof seed !== "string" && seed.limit) ||
|
||||||
this.crawlTemplate?.config.limit}</span
|
this.crawlTemplate?.config.limit}</span
|
||||||
>
|
>
|
||||||
</li>`
|
</li>`
|
||||||
@ -290,22 +290,7 @@ export class CrawlTemplatesDetail extends LiteElement {
|
|||||||
this.authState!
|
this.authState!
|
||||||
);
|
);
|
||||||
|
|
||||||
const { config, ...template } = data;
|
return data;
|
||||||
|
|
||||||
return {
|
|
||||||
...template,
|
|
||||||
config: {
|
|
||||||
...config,
|
|
||||||
// Normalize seed format
|
|
||||||
seeds: config.seeds.map((seed) =>
|
|
||||||
typeof seed === "string"
|
|
||||||
? {
|
|
||||||
url: seed,
|
|
||||||
}
|
|
||||||
: seed
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runNow(): Promise<void> {
|
private async runNow(): Promise<void> {
|
||||||
|
@ -110,9 +110,22 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
style="font-size: 1rem"
|
style="font-size: 1rem"
|
||||||
></sl-icon-button>
|
></sl-icon-button>
|
||||||
|
|
||||||
<ul role="menu">
|
<ul class="text-sm whitespace-nowrap" role="menu">
|
||||||
<li
|
<li
|
||||||
class="px-4 py-2 text-danger hover:bg-danger hover:text-white cursor-pointer"
|
class="p-2 hover:bg-zinc-100 cursor-pointer"
|
||||||
|
role="menuitem"
|
||||||
|
@click=${() => this.duplicateConfig(t)}
|
||||||
|
>
|
||||||
|
<sl-icon
|
||||||
|
class="inline-block align-middle px-1"
|
||||||
|
name="files"
|
||||||
|
></sl-icon>
|
||||||
|
<span class="inline-block align-middle pr-2"
|
||||||
|
>${msg("Duplicate crawl config")}</span
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="p-2 text-danger hover:bg-danger hover:text-white cursor-pointer"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
@click=${(e: any) => {
|
@click=${(e: any) => {
|
||||||
// Close dropdown before deleting template
|
// Close dropdown before deleting template
|
||||||
@ -121,7 +134,13 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
this.deleteTemplate(t);
|
this.deleteTemplate(t);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${msg("Delete")}
|
<sl-icon
|
||||||
|
class="inline-block align-middle px-1"
|
||||||
|
name="file-earmark-x"
|
||||||
|
></sl-icon>
|
||||||
|
<span class="inline-block align-middle pr-2"
|
||||||
|
>${msg("Delete")}</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</sl-dropdown>
|
</sl-dropdown>
|
||||||
@ -131,14 +150,20 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
<div class="grid gap-2 text-xs leading-none">
|
<div class="grid gap-2 text-xs leading-none">
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<sl-tooltip
|
<sl-tooltip
|
||||||
content=${t.config.seeds.map(({ url }) => url).join(", ")}
|
content=${t.config.seeds
|
||||||
|
.map((seed) =>
|
||||||
|
typeof seed === "string" ? seed : seed.url
|
||||||
|
)
|
||||||
|
.join(", ")}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="font-mono whitespace-nowrap truncate text-0-500"
|
class="font-mono whitespace-nowrap truncate text-0-500"
|
||||||
>
|
>
|
||||||
<span class="underline decoration-dashed"
|
<span class="underline decoration-dashed"
|
||||||
>${t.config.seeds
|
>${t.config.seeds
|
||||||
.map(({ url }) => url)
|
.map((seed) =>
|
||||||
|
typeof seed === "string" ? seed : seed.url
|
||||||
|
)
|
||||||
.join(", ")}</span
|
.join(", ")}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -251,44 +276,43 @@ export class CrawlTemplatesList extends LiteElement {
|
|||||||
* associated with the crawl templates
|
* associated with the crawl templates
|
||||||
**/
|
**/
|
||||||
private async getCrawlTemplates(): Promise<CrawlTemplate[]> {
|
private async getCrawlTemplates(): Promise<CrawlTemplate[]> {
|
||||||
type CrawlConfig = Omit<CrawlTemplate, "config"> & {
|
const data: { crawlConfigs: CrawlTemplate[] } = await this.apiFetch(
|
||||||
config: Omit<CrawlTemplate["config"], "seeds"> & {
|
|
||||||
seeds: (string | { url: string })[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const data: { crawlConfigs: CrawlConfig[] } = await this.apiFetch(
|
|
||||||
`/archives/${this.archiveId}/crawlconfigs`,
|
`/archives/${this.archiveId}/crawlconfigs`,
|
||||||
this.authState!
|
this.authState!
|
||||||
);
|
);
|
||||||
|
|
||||||
const crawlConfigs: CrawlTemplate[] = [];
|
|
||||||
const runningCrawlsMap: RunningCrawlsMap = {};
|
const runningCrawlsMap: RunningCrawlsMap = {};
|
||||||
|
|
||||||
data.crawlConfigs.forEach(({ config, ...configMeta }) => {
|
data.crawlConfigs.forEach(({ id, currCrawlId }) => {
|
||||||
crawlConfigs.push({
|
if (currCrawlId) {
|
||||||
...configMeta,
|
runningCrawlsMap[id] = currCrawlId;
|
||||||
config: {
|
|
||||||
...config,
|
|
||||||
// Normalize seed format
|
|
||||||
seeds: config.seeds.map((seed) =>
|
|
||||||
typeof seed === "string"
|
|
||||||
? {
|
|
||||||
url: seed,
|
|
||||||
}
|
|
||||||
: seed
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (configMeta.currCrawlId) {
|
|
||||||
runningCrawlsMap[configMeta.id] = configMeta.currCrawlId;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.runningCrawlsMap = runningCrawlsMap;
|
this.runningCrawlsMap = runningCrawlsMap;
|
||||||
|
|
||||||
return crawlConfigs;
|
return data.crawlConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new template using existing template data
|
||||||
|
*/
|
||||||
|
private async duplicateConfig(template: CrawlTemplate) {
|
||||||
|
const crawlConfig: CrawlTemplate["config"] = {
|
||||||
|
seeds: template.config.seeds,
|
||||||
|
scopeType: template.config.scopeType,
|
||||||
|
limit: template.config.limit,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.navTo(`/archives/${this.archiveId}/crawl-templates/new`, {
|
||||||
|
crawlConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.notify({
|
||||||
|
message: msg(str`Copied crawl configuration to new template.`),
|
||||||
|
type: "success",
|
||||||
|
icon: "check2-circle",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteTemplate(template: CrawlTemplate): Promise<void> {
|
private async deleteTemplate(template: CrawlTemplate): Promise<void> {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { state, property } from "lit/decorators.js";
|
import { state, property } from "lit/decorators.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
import { msg, localized, str } from "@lit/localize";
|
import { msg, localized, str } from "@lit/localize";
|
||||||
import cronParser from "cron-parser";
|
import cronParser from "cron-parser";
|
||||||
|
|
||||||
@ -23,10 +24,8 @@ const initialValues = {
|
|||||||
config: {
|
config: {
|
||||||
seeds: [],
|
seeds: [],
|
||||||
scopeType: "prefix",
|
scopeType: "prefix",
|
||||||
limit: 0,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const initialSeedsJson = JSON.stringify(initialValues.config, null, 2);
|
|
||||||
const hours = Array.from({ length: 12 }).map((x, i) => ({
|
const hours = Array.from({ length: 12 }).map((x, i) => ({
|
||||||
value: i + 1,
|
value: i + 1,
|
||||||
label: `${i + 1}`,
|
label: `${i + 1}`,
|
||||||
@ -50,6 +49,9 @@ export class CrawlTemplatesNew extends LiteElement {
|
|||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
archiveId!: string;
|
archiveId!: string;
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
initialCrawlConfig?: CrawlConfig;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private isRunNow: boolean = initialValues.runNow;
|
private isRunNow: boolean = initialValues.runNow;
|
||||||
|
|
||||||
@ -69,7 +71,7 @@ export class CrawlTemplatesNew extends LiteElement {
|
|||||||
private isSeedsJsonView: boolean = false;
|
private isSeedsJsonView: boolean = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private seedsJson: string = initialSeedsJson;
|
private seedsJson: string = "";
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private invalidSeedsJsonMessage: string = "";
|
private invalidSeedsJsonMessage: string = "";
|
||||||
@ -111,6 +113,24 @@ export class CrawlTemplatesNew extends LiteElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
// Show JSON editor view if complex initial config is specified
|
||||||
|
// (e.g. cloning a template) since form UI doesn't support
|
||||||
|
// all available fields in the config
|
||||||
|
const isComplexConfig = this.initialCrawlConfig?.seeds.some(
|
||||||
|
(seed: any) => typeof seed !== "string"
|
||||||
|
);
|
||||||
|
if (isComplexConfig) {
|
||||||
|
this.isSeedsJsonView = true;
|
||||||
|
}
|
||||||
|
this.initialCrawlConfig = {
|
||||||
|
...initialValues.config,
|
||||||
|
...this.initialCrawlConfig,
|
||||||
|
};
|
||||||
|
this.seedsJson = JSON.stringify(this.initialCrawlConfig, null, 2);
|
||||||
|
super.connectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<h2 class="text-xl font-bold mb-3">${msg("New Crawl Template")}</h2>
|
<h2 class="text-xl font-bold mb-3">${msg("New Crawl Template")}</h2>
|
||||||
@ -340,12 +360,13 @@ export class CrawlTemplatesNew extends LiteElement {
|
|||||||
"Required. Separate URLs with a new line, space or comma."
|
"Required. Separate URLs with a new line, space or comma."
|
||||||
)}
|
)}
|
||||||
rows="3"
|
rows="3"
|
||||||
|
value=${this.initialCrawlConfig!.seeds.join("\n")}
|
||||||
required
|
required
|
||||||
></sl-textarea>
|
></sl-textarea>
|
||||||
<sl-select
|
<sl-select
|
||||||
name="scopeType"
|
name="scopeType"
|
||||||
label=${msg("Scope type")}
|
label=${msg("Scope type")}
|
||||||
value=${initialValues.config.scopeType}
|
value=${this.initialCrawlConfig!.scopeType!}
|
||||||
>
|
>
|
||||||
<sl-menu-item value="page">Page</sl-menu-item>
|
<sl-menu-item value="page">Page</sl-menu-item>
|
||||||
<sl-menu-item value="page-spa">Page SPA</sl-menu-item>
|
<sl-menu-item value="page-spa">Page SPA</sl-menu-item>
|
||||||
@ -357,6 +378,7 @@ export class CrawlTemplatesNew extends LiteElement {
|
|||||||
name="limit"
|
name="limit"
|
||||||
label=${msg("Page limit")}
|
label=${msg("Page limit")}
|
||||||
type="number"
|
type="number"
|
||||||
|
value=${ifDefined(this.initialCrawlConfig!.limit)}
|
||||||
placeholder=${msg("unlimited")}
|
placeholder=${msg("unlimited")}
|
||||||
>
|
>
|
||||||
<span slot="suffix">${msg("pages")}</span>
|
<span slot="suffix">${msg("pages")}</span>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { state, property } from "lit/decorators.js";
|
import { state, property } from "lit/decorators.js";
|
||||||
import { msg, localized, str } from "@lit/localize";
|
import { msg, localized, str } from "@lit/localize";
|
||||||
|
|
||||||
|
import type { ViewState } from "../../utils/APIRouter";
|
||||||
import type { AuthState } from "../../utils/AuthService";
|
import type { AuthState } from "../../utils/AuthService";
|
||||||
import type { CurrentUser } from "../../types/user";
|
import type { CurrentUser } from "../../types/user";
|
||||||
import type { ArchiveData } from "../../utils/archives";
|
import type { ArchiveData } from "../../utils/archives";
|
||||||
@ -24,6 +25,9 @@ export class Archive extends LiteElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
userInfo?: CurrentUser;
|
userInfo?: CurrentUser;
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
viewStateData?: ViewState["data"];
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
archiveId?: string;
|
archiveId?: string;
|
||||||
|
|
||||||
@ -144,6 +148,8 @@ export class Archive extends LiteElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderCrawlTemplates() {
|
private renderCrawlTemplates() {
|
||||||
|
const crawlConfig = this.viewStateData?.crawlConfig;
|
||||||
|
|
||||||
if (this.isNewResourceTab || this.crawlConfigId) {
|
if (this.isNewResourceTab || this.crawlConfigId) {
|
||||||
return html`
|
return html`
|
||||||
<div class="md:grid grid-cols-6 gap-6">
|
<div class="md:grid grid-cols-6 gap-6">
|
||||||
@ -171,6 +177,7 @@ export class Archive extends LiteElement {
|
|||||||
class="col-span-5 mt-6"
|
class="col-span-5 mt-6"
|
||||||
.authState=${this.authState!}
|
.authState=${this.authState!}
|
||||||
.archiveId=${this.archiveId!}
|
.archiveId=${this.archiveId!}
|
||||||
|
.initialCrawlConfig=${crawlConfig}
|
||||||
></btrix-crawl-templates-new>`}
|
></btrix-crawl-templates-new>`}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -4,7 +4,7 @@ type SeedConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CrawlConfig = {
|
export type CrawlConfig = {
|
||||||
seeds: ({ url: string } & SeedConfig)[];
|
seeds: (string | ({ url: string } & SeedConfig))[];
|
||||||
} & SeedConfig;
|
} & SeedConfig;
|
||||||
|
|
||||||
export type CrawlTemplate = {
|
export type CrawlTemplate = {
|
||||||
|
@ -14,8 +14,9 @@ export type ViewState = {
|
|||||||
// e.g. "/users/:id"
|
// e.g. "/users/:id"
|
||||||
// e.g. "/redirect?url"
|
// e.g. "/redirect?url"
|
||||||
params: { [key: string]: string };
|
params: { [key: string]: string };
|
||||||
|
// arbitrary data to pass between routes
|
||||||
|
data?: { [key: string]: any };
|
||||||
};
|
};
|
||||||
export interface NavigateEvent extends CustomEvent {}
|
|
||||||
|
|
||||||
export default class APIRouter {
|
export default class APIRouter {
|
||||||
routes: Routes;
|
routes: Routes;
|
||||||
|
@ -3,6 +3,13 @@ import { LitElement, html } from "lit";
|
|||||||
import type { Auth } from "../utils/AuthService";
|
import type { Auth } from "../utils/AuthService";
|
||||||
import { APIError } from "./api";
|
import { APIError } from "./api";
|
||||||
|
|
||||||
|
export interface NavigateEvent extends CustomEvent {
|
||||||
|
detail: {
|
||||||
|
url: string;
|
||||||
|
state?: object;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotifyEvent extends CustomEvent {
|
export interface NotifyEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
/**
|
/**
|
||||||
@ -27,21 +34,31 @@ export default class LiteElement extends LitElement {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
navTo(url: string) {
|
navTo(url: string, state?: object): void {
|
||||||
this.dispatchEvent(
|
const evt: NavigateEvent = new CustomEvent("navigate", {
|
||||||
new CustomEvent("navigate", { detail: url, bubbles: true })
|
detail: { url, state },
|
||||||
);
|
bubbles: true,
|
||||||
|
});
|
||||||
|
this.dispatchEvent(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
navLink(event: Event) {
|
/**
|
||||||
|
* Bind to anchor tag to prevent full page navigation
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* <a href="/" @click=${this.navLink}>go</a>
|
||||||
|
* ```
|
||||||
|
* @param event Click event
|
||||||
|
*/
|
||||||
|
navLink(event: Event): void {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent("navigate", {
|
const evt: NavigateEvent = new CustomEvent("navigate", {
|
||||||
detail: (event.currentTarget as HTMLAnchorElement).href,
|
detail: { url: (event.currentTarget as HTMLAnchorElement).href },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
})
|
});
|
||||||
);
|
this.dispatchEvent(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user