Allow superadmins to set role when inviting user (#1801)
- Adds user role select to superadmin dashboard - Requires org selection (unless there's only one org) to prevent accidental crawler invites to default testing org - Shows link to org members after invite, retaining form for quick re-invite - Refactor `invite-form` into `TailwindComponent`
This commit is contained in:
parent
3b2e382ba0
commit
24b20215d0
@ -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<OrgData> | null = null;
|
||||
|
||||
@state()
|
||||
@ -32,18 +41,7 @@ export class InviteForm extends LiteElement {
|
||||
@state()
|
||||
private serverError?: string;
|
||||
|
||||
@state()
|
||||
private selectedOrgId?: string;
|
||||
|
||||
willUpdate(changedProperties: PropertyValues<this> & Map<string, unknown>) {
|
||||
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 {
|
||||
>
|
||||
<div class="mb-5">
|
||||
<sl-select
|
||||
name="orgId"
|
||||
label=${msg("Organization")}
|
||||
placeholder=${msg("Select an org")}
|
||||
value=${ifDefined(
|
||||
this.defaultOrg ? this.defaultOrg.id : sortedOrgs[0]?.id,
|
||||
this.defaultOrg?.id ||
|
||||
(this.orgs?.length === 1 ? this.orgs[0].id : undefined),
|
||||
)}
|
||||
@sl-change=${(e: Event) => {
|
||||
this.selectedOrgId = (e.target as HTMLSelectElement).value;
|
||||
}}
|
||||
?disabled=${sortedOrgs.length === 1}
|
||||
required
|
||||
>
|
||||
@ -85,6 +83,17 @@ export class InviteForm extends LiteElement {
|
||||
)}
|
||||
</sl-select>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<sl-select
|
||||
label=${msg("Role")}
|
||||
value=${AccessCode.owner}
|
||||
name="inviteRole"
|
||||
>
|
||||
<sl-option value=${AccessCode.owner}>${"Admin"}</sl-option>
|
||||
<sl-option value=${AccessCode.crawler}>${"Crawler"}</sl-option>
|
||||
<sl-option value=${AccessCode.viewer}>${"Viewer"}</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<sl-input
|
||||
id="inviteEmail"
|
||||
@ -107,7 +116,7 @@ export class InviteForm extends LiteElement {
|
||||
size="small"
|
||||
type="submit"
|
||||
?loading=${this.isSubmitting}
|
||||
?disabled=${!this.selectedOrgId || this.isSubmitting}
|
||||
?disabled=${this.isSubmitting}
|
||||
>${msg("Invite")}</sl-button
|
||||
>
|
||||
</div>
|
||||
@ -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<SlSelect>('[name="orgId"]')!.value = orgId;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("success", {
|
||||
new CustomEvent<InviteSuccessDetail>("btrix-invite-success", {
|
||||
detail: {
|
||||
inviteEmail,
|
||||
orgId,
|
||||
isExistingUser: data.invited === "existing_user",
|
||||
},
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@ -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`
|
||||
<sl-button @click=${() => (this.isInviteComplete = false)}
|
||||
>${msg("Send another invite")}</sl-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
const defaultOrg = this.userInfo?.orgs.find(
|
||||
(org) => org.default === true,
|
||||
) || { name: "" };
|
||||
return html`
|
||||
<btrix-invite-form
|
||||
.authState=${this.authState}
|
||||
.orgs=${this.orgList}
|
||||
.defaultOrg=${defaultOrg}
|
||||
@success=${() => (this.isInviteComplete = true)}
|
||||
@btrix-invite-success=${(e: CustomEvent<InviteSuccessDetail>) => {
|
||||
const org = this.orgList?.find(({ id }) => id === e.detail.orgId);
|
||||
|
||||
this.notify({
|
||||
message: html`
|
||||
${msg("Invite sent!")}
|
||||
<br />
|
||||
<a
|
||||
class="underline hover:no-underline"
|
||||
href="/orgs/${org?.slug || e.detail.orgId}/settings/members"
|
||||
@click=${this.navLink.bind(this)}
|
||||
>
|
||||
${msg("View org members")}
|
||||
</a>
|
||||
`,
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
});
|
||||
}}
|
||||
></btrix-invite-form>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -46,10 +46,7 @@ export class UsersInvite extends LiteElement {
|
||||
<btrix-invite-form
|
||||
.authState=${this.authState}
|
||||
.orgs=${this.userInfo?.orgs || []}
|
||||
.defaultOrg=${this.userInfo?.orgs.find(
|
||||
(org) => org.default === true,
|
||||
) ?? null}
|
||||
@success=${this.onSuccess}
|
||||
@btrix-invite-success=${this.onSuccess}
|
||||
></btrix-invite-form>
|
||||
</main>
|
||||
</div>`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user