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`
`; } 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