Make exclusion table cells editable (#379)

This commit is contained in:
sua yoo 2022-11-23 09:43:52 -08:00 committed by GitHub
parent 3ad47a4c8c
commit da8260a028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 411 additions and 129 deletions

View File

@ -95,7 +95,7 @@ export class Details extends LitElement {
@toggle=${this.onToggle}
aria-disabled=${this.disabled ? "true" : "false"}
>
<summary>
<summary tabindex=${this.disabled ? "-1" : "0"}>
<div class="summary-content">
<div class="title">
<slot name="title"></slot>

View File

@ -1,6 +1,7 @@
import { property, state } from "lit/decorators.js";
import { msg, localized, str } from "@lit/localize";
import type { ExclusionRemoveEvent } from "./queue-exclusion-table";
import type {
ExclusionAddEvent,
ExclusionChangeEvent,
@ -92,7 +93,7 @@ export class ExclusionEditor extends LiteElement {
return html`
${this.config
? html`<btrix-queue-exclusion-table
?editable=${this.isActiveCrawl}
?removable=${this.isActiveCrawl}
.exclusions=${this.config.exclude || []}
@on-remove=${this.deleteExclusion}
>
@ -144,12 +145,12 @@ export class ExclusionEditor extends LiteElement {
}
}
private async deleteExclusion(e: CustomEvent) {
const { value } = e.detail;
private async deleteExclusion(e: ExclusionRemoveEvent) {
const { regex } = e.detail;
try {
const data = await this.apiFetch(
`/archives/${this.archiveId}/crawls/${this.crawlId}/exclusions?regex=${value}`,
`/archives/${this.archiveId}/crawls/${this.crawlId}/exclusions?regex=${regex}`,
this.authState!,
{
method: "DELETE",
@ -158,7 +159,7 @@ export class ExclusionEditor extends LiteElement {
if (data.new_cid) {
this.notify({
message: msg(html`Removed exclusion: <code>${value}</code>`),
message: msg(html`Removed exclusion: <code>${regex}</code>`),
variant: "success",
icon: "check2-circle",
});

View File

@ -1,6 +1,6 @@
import { state, property } from "lit/decorators.js";
import { html as staticHtml, unsafeStatic } from "lit/static-html.js";
import { msg, localized } from "@lit/localize";
import { msg, localized, str } from "@lit/localize";
import RegexColorize from "regex-colorize";
import type { CrawlConfig } from "../pages/archive/types";
@ -8,10 +8,24 @@ import LiteElement, { html } from "../utils/LiteElement";
import { regexEscape } from "../utils/string";
import type { Exclusion } from "./queue-exclusion-form";
export type ExclusionRemoveEvent = CustomEvent<{
export type ExclusionChangeEvent = CustomEvent<{
index: number;
regex: string;
}>;
export type ExclusionRemoveEvent = CustomEvent<{
index: number;
regex: string;
}>;
type SLInputElement = HTMLInputElement & { invalid: boolean };
const MIN_LENGTH = 2;
function formatValue(type: Exclusion["type"], value: Exclusion["value"]) {
return type == "text" ? regexEscape(value) : value;
}
/**
* Crawl queue exclusion table
*
@ -23,6 +37,7 @@ export type ExclusionRemoveEvent = CustomEvent<{
* </btrix-queue-exclusion-table>
* ```
*
* @event on-change ExclusionChangeEvent
* @event on-remove ExclusionRemoveEvent
*/
@localized()
@ -30,18 +45,21 @@ export class QueueExclusionTable extends LiteElement {
@property({ type: Array })
exclusions?: CrawlConfig["exclude"];
@property({ type: Number })
pageSize: number = 5;
@property({ type: Boolean })
editable = false;
@property({ type: Boolean })
removable = false;
@state()
private results: Exclusion[] = [];
@state()
private page: number = 1;
@state()
private pageSize: number = 5;
@state()
private exclusionToRemove?: string;
@ -51,7 +69,7 @@ export class QueueExclusionTable extends LiteElement {
willUpdate(changedProperties: Map<string, any>) {
if (changedProperties.has("exclusions") && this.exclusions) {
this.exclusionToRemove = "";
this.exclusionToRemove = undefined;
const prevVal = changedProperties.get("exclusions");
if (prevVal) {
@ -86,45 +104,58 @@ export class QueueExclusionTable extends LiteElement {
render() {
const [typeColClass, valueColClass, actionColClass] =
this.getColumnClassNames(0, this.results.length);
this.getColumnClassNames(0, this.results.length, true);
return html`<btrix-details open disabled>
<h4 slot="title">${msg("Exclusions")}</h4>
<div slot="summary-description">
${this.total && this.total > this.pageSize
? html`<btrix-pagination
page=${this.page}
size=${this.pageSize}
totalCount=${this.total}
@page-change=${(e: CustomEvent) => {
this.page = e.detail.page;
}}
>
</btrix-pagination>`
: ""}
</div>
<table
class="w-full leading-none border-separate"
style="border-spacing: 0;"
>
<thead class="text-xs font-mono text-neutral-600 uppercase">
<tr class="h-10 text-left">
<th class="font-normal px-2 w-40 bg-slate-50 ${typeColClass}">
${msg("Exclusion Type")}
</th>
<th class="font-normal px-2 bg-slate-50 ${valueColClass}">
${msg("Exclusion Value")}
</th>
<th class="font-normal px-2 w-10 bg-slate-50 ${actionColClass}">
<span class="sr-only">Row actions</span>
</th>
</tr>
</thead>
<tbody>
${this.results.map(this.renderItem)}
</tbody>
</table>
</btrix-details> `;
return html`
<style>
btrix-queue-exclusion-table sl-input {
--sl-input-border-radius-medium: 0;
--sl-input-font-family: var(--sl-font-mono);
--sl-input-spacing-medium: var(--sl-spacing-small);
}
btrix-queue-exclusion-table sl-input:not([invalid]) {
--sl-input-border-width: 0;
}
</style>
<btrix-details open disabled>
<h4 slot="title">${msg("Exclusions")}</h4>
<div slot="summary-description">
${this.total && this.total > this.pageSize
? html`<btrix-pagination
page=${this.page}
size=${this.pageSize}
totalCount=${this.total}
@page-change=${(e: CustomEvent) => {
this.page = e.detail.page;
}}
>
</btrix-pagination>`
: ""}
</div>
<table
class="w-full leading-none border-separate"
style="border-spacing: 0;"
>
<thead class="text-xs font-mono text-neutral-600 uppercase">
<tr class="h-10 text-left">
<th class="font-normal px-2 w-40 bg-slate-50 ${typeColClass}">
${msg("Exclusion Type")}
</th>
<th class="font-normal px-2 bg-slate-50 ${valueColClass}">
${msg("Exclusion Value")}
</th>
<th class="font-normal px-2 w-10 bg-slate-50 ${actionColClass}">
<span class="sr-only">Row actions</span>
</th>
</tr>
</thead>
<tbody>
${this.results.map(this.renderItem)}
</tbody>
</table>
</btrix-details>
`;
}
private renderItem = (
@ -135,44 +166,135 @@ export class QueueExclusionTable extends LiteElement {
const [typeColClass, valueColClass, actionColClass] =
this.getColumnClassNames(index + 1, arr.length);
let typeLabel: string = exclusion.type;
let value: any = exclusion.value;
switch (exclusion.type) {
case "regex":
typeLabel = msg("Regex");
value = staticHtml`<span class="regex">${unsafeStatic(
new RegexColorize().colorizeText(exclusion.value)
)}</span>`;
break;
case "text":
typeLabel = msg("Matches Text");
break;
default:
break;
}
return html`
<tr
class="h-10 ${this.exclusionToRemove === value
class="h-10 ${this.exclusionToRemove === exclusion.value
? "text-neutral-200"
: "text-neutral-600"}"
>
<td class="py-2 px-3 whitespace-nowrap ${typeColClass}">
${typeLabel}
<td class="whitespace-nowrap ${typeColClass}">
${this.renderType({ exclusion, index })}
</td>
<td class="font-mono ${valueColClass}">
${this.renderValue({ exclusion, index })}
</td>
<td class="p-2 font-mono ${valueColClass}">${value}</td>
<td class="text-[1rem] text-center ${actionColClass}">
<btrix-icon-button
name="trash3"
@click=${() => this.removeExclusion(exclusion)}
@click=${() => this.removeExclusion(exclusion, index)}
></btrix-icon-button>
</td>
</tr>
`;
};
private getColumnClassNames(index: number, count: number) {
private renderType({
exclusion,
index,
}: {
exclusion: Exclusion;
index: number;
}) {
let typeLabel: string = exclusion.type;
if (exclusion.type === "text") typeLabel = msg("Matches Text");
if (exclusion.type === "regex") typeLabel = msg("Regex");
if (this.editable) {
return html`
<sl-select
placeholder=${msg("Select Type")}
size="small"
.value=${exclusion.type}
@sl-hide=${this.stopProp}
@sl-after-hide=${this.stopProp}
@sl-select=${(e: Event) => {
this.updateExclusion({
type: (e.target as HTMLSelectElement).value as Exclusion["type"],
value: exclusion.value,
index,
});
}}
hoist
>
<sl-menu-item value="text">${msg("Matches Text")}</sl-menu-item>
<sl-menu-item value="regex">${msg("Regex")}</sl-menu-item>
</sl-select>
`;
}
return typeLabel;
}
private renderValue({
exclusion,
index,
}: {
exclusion: Exclusion;
index: number;
}) {
let value: any = exclusion.value;
if (this.editable) {
return html`
<sl-input
name="exclusion-${index}"
placeholder=${msg("Enter value")}
class="m-0"
value=${exclusion.value}
autocomplete="off"
autocorrect="off"
minlength=${MIN_LENGTH}
@sl-clear=${() => {
this.updateExclusion({
type: exclusion.type,
value: "",
index,
});
}}
@sl-input=${(e: CustomEvent) => {
const inputElem = e.target as SLInputElement;
const validityMessage = this.getInputValidity(inputElem) || "";
inputElem.classList.remove("invalid");
inputElem.setCustomValidity(validityMessage);
this.checkSiblingRowValidity(e);
}}
@sl-change=${(e: CustomEvent) => {
const inputElem = e.target as SLInputElement;
const values = this.getCurrentValues(inputElem);
const params = {
type: values.type || exclusion.type,
value: values.value,
index,
};
if (inputElem.invalid) {
inputElem.classList.add("invalid");
}
inputElem.reportValidity();
this.updateExclusion(params);
}}
></sl-input>
`;
}
if (exclusion.type === "regex") {
value = staticHtml`<span class="regex">${unsafeStatic(
new RegexColorize().colorizeText(exclusion.value)
)}</span>`;
}
return value;
}
private getColumnClassNames(
index: number,
count: number,
isHeader?: boolean
) {
let typeColClass = "border-t border-x";
let valueColClass = "border-t border-r";
let actionColClass = "border-t border-r";
@ -180,7 +302,7 @@ export class QueueExclusionTable extends LiteElement {
if (index === 0) {
typeColClass += " rounded-tl";
if (this.editable) {
if (this.removable) {
actionColClass += " rounded-tr";
} else {
valueColClass += " rounded-tr";
@ -190,7 +312,7 @@ export class QueueExclusionTable extends LiteElement {
if (index === count) {
typeColClass += " border-b rounded-bl";
if (this.editable) {
if (this.removable) {
valueColClass += " border-b";
actionColClass += " border-b rounded-br";
} else {
@ -198,22 +320,125 @@ export class QueueExclusionTable extends LiteElement {
}
}
if (!this.editable) {
if (!this.removable) {
actionColClass += " hidden";
}
if (!isHeader) {
if (this.editable) {
typeColClass += " px-[3px]";
} else {
typeColClass += " py-2 px-3";
valueColClass += " p-2";
}
}
return [typeColClass, valueColClass, actionColClass];
}
private removeExclusion({ value, type }: Exclusion) {
private getCurrentValues(inputElem: SLInputElement) {
// Get latest exclusion type value from select
const typeSelectElem = inputElem.closest("tr")?.querySelector("sl-select");
const exclusionType = typeSelectElem?.value;
return {
type: exclusionType as Exclusion["type"],
value: inputElem.value,
};
}
private getInputDuplicateValidity(inputElem: SLInputElement) {
const siblingElems = inputElem
.closest("table")
?.querySelectorAll(`sl-input:not([name="${inputElem.name}"])`);
if (!siblingElems) {
console.debug("getInputDuplicateValidity no matching siblings");
return;
}
const siblingValues = Array.from(siblingElems).map(
(elem) => (elem as SLInputElement).value
);
const { type, value } = this.getCurrentValues(inputElem);
const formattedValue = formatValue(type, value);
if (siblingValues.includes(formattedValue)) {
return msg("Exclusion already exists. Please edit or remove to continue");
}
}
private getInputValidity(inputElem: SLInputElement): string | void {
const { type, value } = this.getCurrentValues(inputElem);
if (!value) return;
if (value.length < MIN_LENGTH) {
return msg(str`Please enter ${MIN_LENGTH} or more characters`);
}
if (type === "regex") {
try {
// Check if valid regex
new RegExp(value);
} catch (err: any) {
return msg(
"Please enter a valid Regular Expression constructor pattern"
);
}
}
return this.getInputDuplicateValidity(inputElem);
}
private checkSiblingRowValidity(e: CustomEvent) {
// Check if any sibling inputs are now valid
// after fixing duplicate values
const inputElem = e.target as HTMLInputElement;
const table = inputElem.closest("table") as HTMLTableElement;
Array.from(table?.querySelectorAll("sl-input[invalid]")).map((elem) => {
if (elem !== inputElem) {
const validityMessage =
this.getInputDuplicateValidity(elem as SLInputElement) || "";
(elem as SLInputElement).setCustomValidity(validityMessage);
(elem as SLInputElement).reportValidity();
}
});
}
private removeExclusion({ value, type }: Exclusion, index: number) {
this.exclusionToRemove = value;
this.dispatchEvent(
new CustomEvent("on-remove", {
detail: {
regex: type == "text" ? regexEscape(value) : value,
regex: formatValue(type, value),
index,
},
}) as ExclusionRemoveEvent
);
}
private updateExclusion({
type,
value,
index,
}: {
type: Exclusion["type"];
value: Exclusion["value"];
index: number;
}) {
this.dispatchEvent(
new CustomEvent("on-change", {
detail: {
index,
regex: formatValue(type, value),
},
}) as ExclusionChangeEvent
);
}
/**
* Stop propgation of sl-select events.
* Prevents bug where sl-dialog closes when dropdown closes
* https://github.com/shoelace-style/shoelace/issues/170
*/
private stopProp(e: CustomEvent) {
e.stopPropagation();
}
}

View File

@ -3,7 +3,10 @@ import { state, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { msg, localized, str } from "@lit/localize";
import { parse as yamlToJson, stringify as jsonToYaml } from "yaml";
import compact from "lodash/fp/compact";
import merge from "lodash/fp/merge";
import flow from "lodash/fp/flow";
import uniq from "lodash/fp/uniq";
import ISO6391 from "iso-639-1";
import type { AuthState } from "../../utils/AuthService";
@ -12,11 +15,18 @@ import type { InitialCrawlTemplate } from "./crawl-templates-new";
import type { CrawlTemplate, CrawlConfig } from "./types";
import { getUTCSchedule, humanizeSchedule } from "../../utils/cron";
import "../../components/crawl-scheduler";
import { ExclusionRemoveEvent } from "../../components/queue-exclusion-table";
import { ExclusionAddEvent } from "../../components/queue-exclusion-form";
import {
ExclusionRemoveEvent,
ExclusionChangeEvent,
} from "../../components/queue-exclusion-table";
const SEED_URLS_MAX = 3;
// Show default empty editable rows
const defaultExclusions = [""];
const trimExclusions = flow(uniq, compact);
/**
* Usage:
* ```ts
@ -48,7 +58,7 @@ export class CrawlTemplatesDetail extends LiteElement {
private configCode: string = "";
@state()
private exclusions: CrawlConfig["exclude"] = [];
private exclusions: CrawlConfig["exclude"] = defaultExclusions;
private browserLanguage: CrawlConfig["lang"] = null;
@ -69,12 +79,12 @@ export class CrawlTemplatesDetail extends LiteElement {
if (this.isConfigCodeView) {
this.configCode = jsonToYaml(
merge(this.crawlTemplate.config, {
exclude: this.exclusions,
exclude: trimExclusions(this.exclusions),
})
);
} else if (this.isConfigCodeView === false) {
this.exclusions =
(yamlToJson(this.configCode) as CrawlConfig).exclude || [];
const exclude = (yamlToJson(this.configCode) as CrawlConfig).exclude;
this.exclusions = exclude?.length ? exclude : defaultExclusions;
}
}
}
@ -94,7 +104,9 @@ export class CrawlTemplatesDetail extends LiteElement {
this.isConfigCodeView = true;
}
this.configCode = jsonToYaml(this.crawlTemplate.config);
this.exclusions = this.crawlTemplate.config.exclude || [];
if (this.crawlTemplate.config.exclude?.length) {
this.exclusions = this.crawlTemplate.config.exclude;
}
} catch (e: any) {
this.notify({
message:
@ -1111,23 +1123,19 @@ export class CrawlTemplatesDetail extends LiteElement {
return html`
<btrix-queue-exclusion-table
.exclusions=${this.exclusions}
pageSize="50"
editable
@on-remove=${(e: ExclusionRemoveEvent) => {
if (!this.exclusions) return;
const { regex: value } = e.detail;
this.exclusions = this.exclusions.filter((v) => v !== value);
}}
removable
@on-remove=${this.handleRemoveRegex}
@on-change=${this.handleChangeRegex}
></btrix-queue-exclusion-table>
<div class="mt-2">
<btrix-queue-exclusion-form
@on-add=${(e: ExclusionAddEvent) => {
const { regex, onSuccess } = e.detail;
this.exclusions = [...(this.exclusions || []), regex];
onSuccess();
}}
>
</btrix-queue-exclusion-form>
</div>
<sl-button
class="w-full mt-1"
@click=${() => (this.exclusions = [...(this.exclusions || []), ""])}
>
<sl-icon slot="prefix" name="plus-lg"></sl-icon>
<span class="text-neutral-600">${msg("Add More")}</span>
</sl-button>
`;
}
@ -1163,6 +1171,26 @@ export class CrawlTemplatesDetail extends LiteElement {
});
}
private handleRemoveRegex(e: ExclusionRemoveEvent) {
const { index } = e.detail;
if (!this.exclusions) {
this.exclusions = defaultExclusions;
} else {
this.exclusions = [
...this.exclusions.slice(0, index),
...this.exclusions.slice(index + 1),
];
}
}
private handleChangeRegex(e: ExclusionChangeEvent) {
const { regex, index } = e.detail;
const nextExclusions = [...this.exclusions!];
nextExclusions[index] = regex;
this.exclusions = nextExclusions;
}
private async handleSubmitEditName(e: SubmitEvent) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
@ -1185,7 +1213,13 @@ export class CrawlTemplatesDetail extends LiteElement {
private async handleSubmitEditConfiguration(e: SubmitEvent) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const form = e.target as HTMLFormElement;
if (form.querySelector("[invalid]")) {
return;
}
const formData = new FormData(form);
const profileId = (formData.get("browserProfile") as string) || null;
let config: CrawlConfig;
@ -1203,7 +1237,7 @@ export class CrawlTemplatesDetail extends LiteElement {
scopeType: formData.get("scopeType") as string,
limit: pageLimit ? +pageLimit : 0,
extraHops: formData.get("extraHopsOne") ? 1 : 0,
exclude: this.exclusions,
exclude: trimExclusions(this.exclusions),
lang: this.browserLanguage,
};
}

View File

@ -2,10 +2,15 @@ import { state, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { msg, localized, str } from "@lit/localize";
import { parse as yamlToJson, stringify as jsonToYaml } from "yaml";
import compact from "lodash/fp/compact";
import merge from "lodash/fp/merge";
import flow from "lodash/fp/flow";
import uniq from "lodash/fp/uniq";
import type { ExclusionAddEvent } from "../../components/queue-exclusion-form";
import type { ExclusionRemoveEvent } from "../../components/queue-exclusion-table";
import type {
ExclusionRemoveEvent,
ExclusionChangeEvent,
} from "../../components/queue-exclusion-table";
import type { AuthState } from "../../utils/AuthService";
import LiteElement, { html } from "../../utils/LiteElement";
import { ScheduleInterval, humanizeNextDate } from "../../utils/cron";
@ -36,7 +41,8 @@ const defaultValue = {
config: {
seeds: [],
scopeType: "prefix",
exclude: [],
// Show default empty editable rows
exclude: Array.from({ length: 3 }).map(() => ""),
},
} as InitialCrawlTemplate;
const hours = Array.from({ length: 12 }).map((x, i) => ({
@ -48,6 +54,8 @@ const minutes = Array.from({ length: 60 }).map((x, i) => ({
label: `${i}`.padStart(2, "0"),
}));
const trimExclusions = flow(uniq, compact);
/**
* Usage:
* ```ts
@ -129,7 +137,9 @@ export class CrawlTemplatesNew extends LiteElement {
this.isConfigCodeView = true;
}
this.configCode = jsonToYaml(this.initialCrawlTemplate.config);
this.exclusions = this.initialCrawlTemplate.config.exclude;
if (this.initialCrawlTemplate.config.exclude?.length) {
this.exclusions = this.initialCrawlTemplate.config.exclude;
}
this.browserProfileId = this.initialCrawlTemplate.profileid;
super.connectedCallback();
}
@ -139,12 +149,14 @@ export class CrawlTemplatesNew extends LiteElement {
if (this.isConfigCodeView) {
this.configCode = jsonToYaml(
merge(this.initialCrawlTemplate.config, {
exclude: this.exclusions,
exclude: trimExclusions(this.exclusions),
})
);
} else if (this.isConfigCodeView === false) {
this.exclusions =
(yamlToJson(this.configCode) as CrawlConfig).exclude || [];
const exclude = (yamlToJson(this.configCode) as CrawlConfig).exclude;
this.exclusions = exclude?.length
? exclude
: defaultValue.config.exclude;
}
}
}
@ -483,16 +495,19 @@ export class CrawlTemplatesNew extends LiteElement {
return html`
<btrix-queue-exclusion-table
.exclusions=${this.exclusions}
pageSize="50"
editable
removable
@on-remove=${this.handleRemoveRegex}
@on-change=${this.handleChangeRegex}
></btrix-queue-exclusion-table>
<div class="mt-2">
<btrix-queue-exclusion-form
fieldErrorMessage=${this.exclusionFieldErrorMessage || ""}
@on-add=${this.handleAddRegex}
>
</btrix-queue-exclusion-form>
</div>
<sl-button
class="w-full mt-1"
@click=${() => (this.exclusions = [...(this.exclusions || []), ""])}
>
<sl-icon slot="prefix" name="plus-lg"></sl-icon>
<span class="text-neutral-600">${msg("Add More")}</span>
</sl-button>
`;
}
@ -549,7 +564,7 @@ export class CrawlTemplatesNew extends LiteElement {
scopeType: formData.get("scopeType") as string,
limit: pageLimit ? +pageLimit : 0,
extraHops: formData.get("extraHopsOne") ? 1 : 0,
exclude: this.exclusions,
exclude: trimExclusions(this.exclusions),
};
}
@ -557,26 +572,33 @@ export class CrawlTemplatesNew extends LiteElement {
}
private handleRemoveRegex(e: ExclusionRemoveEvent) {
const { regex } = e.detail;
if (!this.exclusions || !regex) return;
this.exclusions = this.exclusions.filter((v) => v !== regex);
const { index } = e.detail;
if (!this.exclusions) {
this.exclusions = defaultValue.config.exclude;
} else {
this.exclusions = [
...this.exclusions.slice(0, index),
...this.exclusions.slice(index + 1),
];
}
}
private handleAddRegex(e: ExclusionAddEvent) {
this.exclusionFieldErrorMessage = "";
const { regex, onSuccess } = e.detail;
if (this.exclusions && this.exclusions.indexOf(regex) > -1) {
this.exclusionFieldErrorMessage = msg("Exclusion already exists");
return;
}
private handleChangeRegex(e: ExclusionChangeEvent) {
const { regex, index } = e.detail;
this.exclusions = [...(this.exclusions || []), regex];
onSuccess();
const nextExclusions = [...this.exclusions!];
nextExclusions[index] = regex;
this.exclusions = nextExclusions;
}
private async onSubmit(event: SubmitEvent) {
event.preventDefault();
if (!this.authState) return;
const form = event.target as HTMLFormElement;
if (form.querySelector("[invalid]")) {
return;
}
const formData = new FormData(event.target as HTMLFormElement);
const params = this.parseTemplate(formData);