diff --git a/frontend/src/features/accounts/invite-form.ts b/frontend/src/features/accounts/invite-form.ts index 56ba740a..66953f13 100644 --- a/frontend/src/features/accounts/invite-form.ts +++ b/frontend/src/features/accounts/invite-form.ts @@ -1,29 +1,38 @@ import { localized, msg } from "@lit/localize"; -import { type PropertyValues } from "lit"; +import type { SlSelect } from "@shoelace-style/shoelace"; +import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js"; +import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import sortBy from "lodash/fp/sortBy"; -import type { OrgData } from "@/types/org"; +import { TailwindElement } from "@/classes/TailwindElement"; +import { APIController } from "@/controllers/api"; +import { AccessCode, type OrgData } from "@/types/org"; import { isApiError } from "@/utils/api"; import type { AuthState } from "@/utils/AuthService"; -import LiteElement, { html } from "@/utils/LiteElement"; + +export type InviteSuccessDetail = { + inviteEmail: string; + orgId: string; + isExistingUser: boolean; +}; const sortByName = sortBy("name"); /** - * @event success + * @event btrix-invite-success */ @localized() @customElement("btrix-invite-form") -export class InviteForm extends LiteElement { - @property({ type: Object }) +export class InviteForm extends TailwindElement { + @property({ type: Object, attribute: false }) authState?: AuthState; - @property({ type: Array }) + @property({ type: Array, attribute: false }) orgs?: OrgData[] = []; - @property({ type: Object }) + @property({ type: Object, attribute: false }) defaultOrg: Partial | null = null; @state() @@ -32,18 +41,7 @@ export class InviteForm extends LiteElement { @state() private serverError?: string; - @state() - private selectedOrgId?: string; - - willUpdate(changedProperties: PropertyValues & Map) { - if ( - changedProperties.has("defaultOrg") && - this.defaultOrg && - !this.selectedOrgId - ) { - this.selectedOrgId = this.defaultOrg.id; - } - } + private readonly api = new APIController(this); render() { let formError; @@ -68,13 +66,13 @@ export class InviteForm extends LiteElement { >
{ - this.selectedOrgId = (e.target as HTMLSelectElement).value; - }} ?disabled=${sortedOrgs.length === 1} required > @@ -85,6 +83,17 @@ export class InviteForm extends LiteElement { )}
+
+ + ${"Admin"} + ${"Crawler"} + ${"Viewer"} + +
${msg("Invite")}
@@ -116,37 +125,45 @@ export class InviteForm extends LiteElement { } async onSubmit(event: SubmitEvent) { - event.preventDefault(); - if (!this.authState || !this.selectedOrgId) return; - const formEl = event.target as HTMLFormElement; + event.preventDefault(); + if (!(await this.checkFormValidity(formEl))) return; this.serverError = undefined; this.isSubmitting = true; - const formData = new FormData(event.target as HTMLFormElement); - const inviteEmail = formData.get("inviteEmail") as string; + const { orgId, inviteEmail, inviteRole } = serialize(formEl) as { + orgId: string; + inviteEmail: string; + inviteRole: string; + }; try { - const data = await this.apiFetch<{ invited: string }>( - `/orgs/${this.selectedOrgId}/invite`, - this.authState, + const data = await this.api.fetch<{ invited: string }>( + `/orgs/${orgId}/invite`, + this.authState!, { method: "POST", body: JSON.stringify({ email: inviteEmail, - role: 10, + role: +inviteRole, }), }, ); + // Reset fields except selected org ID + formEl.reset(); + formEl.querySelector('[name="orgId"]')!.value = orgId; + this.dispatchEvent( - new CustomEvent("success", { + new CustomEvent("btrix-invite-success", { detail: { inviteEmail, + orgId, isExistingUser: data.invited === "existing_user", }, + composed: true, }), ); } catch (e) { diff --git a/frontend/src/pages/home.ts b/frontend/src/pages/home.ts index b04b17bb..979c34ce 100644 --- a/frontend/src/pages/home.ts +++ b/frontend/src/pages/home.ts @@ -3,6 +3,7 @@ import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js"; import { type PropertyValues, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; +import type { InviteSuccessDetail } from "@/features/accounts/invite-form"; import type { APIPaginatedList } from "@/types/api"; import type { CurrentUser } from "@/types/user"; import { isApiError } from "@/utils/api"; @@ -23,9 +24,6 @@ export class Home extends LiteElement { @property({ type: String }) slug?: string; - @state() - private isInviteComplete?: boolean; - @state() private orgList?: OrgData[]; @@ -249,23 +247,29 @@ export class Home extends LiteElement { } private renderInvite() { - if (this.isInviteComplete) { - return html` - (this.isInviteComplete = false)} - >${msg("Send another invite")} - `; - } - - const defaultOrg = this.userInfo?.orgs.find( - (org) => org.default === true, - ) || { name: "" }; return html` (this.isInviteComplete = true)} + @btrix-invite-success=${(e: CustomEvent) => { + const org = this.orgList?.find(({ id }) => id === e.detail.orgId); + + this.notify({ + message: html` + ${msg("Invite sent!")} +
+ + ${msg("View org members")} + + `, + variant: "success", + icon: "check2-circle", + }); + }} >
`; } diff --git a/frontend/src/pages/users-invite.ts b/frontend/src/pages/users-invite.ts index 24dc95e9..149a8020 100644 --- a/frontend/src/pages/users-invite.ts +++ b/frontend/src/pages/users-invite.ts @@ -46,10 +46,7 @@ export class UsersInvite extends LiteElement { org.default === true, - ) ?? null} - @success=${this.onSuccess} + @btrix-invite-success=${this.onSuccess} > `;