import { localized, msg, str } from "@lit/localize"; import { Task, TaskStatus } from "@lit/task"; import type { SlInput } from "@shoelace-style/shoelace"; import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js"; import { html } from "lit"; import { customElement, property, query } from "lit/decorators.js"; import slugify from "slugify"; import { TailwindElement } from "@/classes/TailwindElement"; import { APIController } from "@/controllers/api"; import { NotifyController } from "@/controllers/notify"; import { type APIUser } from "@/index"; import { isApiError } from "@/utils/api"; import type { AuthState } from "@/utils/AuthService"; import { maxLengthValidator } from "@/utils/form"; import { AppStateService } from "@/utils/state"; import { formatAPIUser } from "@/utils/user"; type FormValues = { orgName: string; orgSlug: string; }; export type OrgUpdatedDetail = { data: { name: string; slug: string }; }; /** * @fires btrix-org-updated */ @localized() @customElement("btrix-org-form") export class OrgForm extends TailwindElement { @property({ type: Object }) authState?: AuthState; @property({ type: String }) orgId?: string; @property({ type: String }) name = ""; @property({ type: String }) slug = ""; @query("#orgForm") private readonly form?: HTMLFormElement | null; readonly _api = new APIController(this); readonly _notify = new NotifyController(this); private readonly validateOrgNameMax = maxLengthValidator(40); readonly _renameOrgTask = new Task(this, { autoRun: false, task: async ([id, name, slug]) => { if (!id) throw new Error("Missing args"); const inviteInfo = await this._renameOrg(id, { name, slug }); return inviteInfo; }, args: () => [this.orgId, this.name, this.slug] as const, }); render() { const helpText = (slug: unknown) => msg( str`Your org dashboard will be ${window.location.protocol}//${window.location.hostname}/orgs/${slug || ""}`, ); return html`
{ const input = e.target as SlInput; input.helpText = helpText(slugify(input.value, { strict: true })); }} >
${this._renameOrgTask.render({ error: (err) => html`
${err instanceof Error ? err.message : err}
`, })} ${msg("Go to Dashboard")}
`; } private async onSubmit(e: SubmitEvent) { e.preventDefault(); const form = e.target as HTMLFormElement; if (!(await this.checkFormValidity(form))) return; const params = serialize(form) as FormValues; const orgName = params.orgName; const orgSlug = slugify(params.orgSlug, { strict: true }); void this._renameOrgTask.run([this.orgId, orgName, orgSlug]); } async _renameOrg(id: string, params: { name?: string; slug?: string }) { const name = params.name || this.name; const slug = params.slug || this.slug; const payload = { name, slug }; try { await this._api.fetch(`/orgs/${id}/rename`, this.authState!, { method: "POST", body: JSON.stringify(payload), }); this._notify.toast({ message: msg("Org successfully updated."), variant: "success", icon: "check2-circle", }); await this.onRenameSuccess(payload); } catch (e) { console.debug(e); if (isApiError(e)) { let error: Error | null = null; let fieldName = ""; if (e.details === "duplicate_org_name") { fieldName = "orgName"; error = new Error( msg(str`The org name "${name}" is already taken, try another one.`), ); } else if (e.details === "duplicate_org_slug") { fieldName = "orgSlug"; error = new Error( msg(str`The org URL "${slug}" is already taken, try another one.`), ); } else if (e.details === "invalid_slug") { fieldName = "orgSlug"; error = new Error( msg( str`The org URL "${slug}" is not a valid URL. Please use alphanumeric characters and dashes (-) only`, ), ); } if (error) { if (fieldName) { this.highlightErrorField(fieldName, error); } throw error; } } this._notify.toast({ message: msg( "Sorry, couldn't rename organization at this time. Try again later from org settings.", ), variant: "danger", icon: "exclamation-octagon", }); } } private highlightErrorField(fieldName: string, error: Error) { const input = this.form?.querySelector(`[name="${fieldName}"]`); if (input) { input.setCustomValidity(error.message); const onOneInput = () => { input.setCustomValidity(""); input.removeEventListener("sl-input", onOneInput); }; input.addEventListener("sl-input", onOneInput); } } private async checkFormValidity(formEl: HTMLFormElement) { await this.updateComplete; return !formEl.querySelector("[data-invalid]"); } private async onRenameSuccess(data: OrgUpdatedDetail["data"]) { try { const user = await this._getCurrentUser(); AppStateService.updateUserInfo(formatAPIUser(user)); AppStateService.updateOrgSlug(data.slug); } catch (e) { console.debug(e); } await this.updateComplete; this.dispatchEvent( new CustomEvent("btrix-org-updated", { detail: { data }, }), ); } async _getCurrentUser(): Promise { return this._api.fetch("/users/me", this.authState!); } }