Improve superadmin invite UI (#581)

This commit is contained in:
sua yoo 2023-02-12 10:12:53 -08:00 committed by GitHub
parent 103d91556f
commit a180b92f4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 37 deletions

View File

@ -1,8 +1,12 @@
import { state, property } from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
import sortBy from "lodash/fp/sortBy";
import type { AuthState } from "../utils/AuthService";
import LiteElement, { html } from "../utils/LiteElement";
import { OrgData } from "../types/org";
const sortByName = sortBy("name");
/**
* @event success
@ -12,12 +16,31 @@ export class InviteForm extends LiteElement {
@property({ type: Object })
authState?: AuthState;
@property({ type: Array })
orgs: OrgData[] = [];
@property({ type: Object })
defaultOrg: OrgData | null = null;
@state()
private isSubmitting: boolean = false;
@state()
private serverError?: string;
@state()
private selectedOrgId?: string;
willUpdate(changedProperties: Map<string, any>) {
if (
changedProperties.has("defaultOrg") &&
this.defaultOrg &&
!this.selectedOrgId
) {
this.selectedOrgId = this.defaultOrg.id;
}
}
render() {
let formError;
@ -31,12 +54,31 @@ export class InviteForm extends LiteElement {
`;
}
const sortedOrgs = sortByName(this.orgs) as any as OrgData[];
return html`
<form
class="max-w-md"
@submit=${this.onSubmit}
aria-describedby="formError"
>
<div class="mb-5">
<sl-select
label=${msg("Organization")}
value=${this.defaultOrg ? this.defaultOrg.id : sortedOrgs[0]?.id}
@sl-select=${(e: CustomEvent) => {
this.selectedOrgId = e.detail.item.value;
}}
?disabled=${sortedOrgs.length === 1}
required
>
${sortedOrgs.map(
(org) => html`
<sl-menu-item value=${org.id}>${org.name}</sl-menu-item>
`
)}
</sl-select>
</div>
<div class="mb-5">
<sl-input
id="inviteEmail"
@ -53,12 +95,13 @@ export class InviteForm extends LiteElement {
${formError}
<div>
<div class="text-right">
<sl-button
variant="primary"
size="small"
type="submit"
?loading=${this.isSubmitting}
?disabled=${this.isSubmitting}
?disabled=${!this.selectedOrgId || this.isSubmitting}
>${msg("Invite")}</sl-button
>
</div>
@ -68,7 +111,10 @@ export class InviteForm extends LiteElement {
async onSubmit(event: SubmitEvent) {
event.preventDefault();
if (!this.authState) return;
if (!this.authState || !this.selectedOrgId) return;
const formEl = event.target as HTMLFormElement;
if (!(await this.checkFormValidity(formEl))) return;
this.serverError = undefined;
this.isSubmitting = true;
@ -77,12 +123,17 @@ export class InviteForm extends LiteElement {
const inviteEmail = formData.get("inviteEmail") as string;
try {
const data = await this.apiFetch(`/users/invite`, this.authState, {
const data = await this.apiFetch(
`/orgs/${this.selectedOrgId}/invite`,
this.authState,
{
method: "POST",
body: JSON.stringify({
email: inviteEmail,
role: 10,
}),
});
}
);
this.dispatchEvent(
new CustomEvent("success", {
@ -102,4 +153,9 @@ export class InviteForm extends LiteElement {
this.isSubmitting = false;
}
async checkFormValidity(formEl: HTMLFormElement) {
await this.updateComplete;
return !formEl.querySelector("[data-invalid]");
}
}

View File

@ -1,7 +1,7 @@
import { state, property } from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
import { msg, localized, str } from "@lit/localize";
import type { CurrentUser } from "../types/user";
import type { CurrentUser, UserOrg } from "../types/user";
import type { OrgData } from "../utils/orgs";
import LiteElement, { html } from "../utils/LiteElement";
@ -15,6 +15,9 @@ export class OrgsList extends LiteElement {
@property({ type: Array })
orgList: OrgData[] = [];
@property({ type: Object })
defaultOrg?: UserOrg;
@property({ type: Boolean })
skeleton? = false;
@ -25,32 +28,38 @@ export class OrgsList extends LiteElement {
return html`
<ul class="border rounded-lg overflow-hidden">
${this.orgList?.map(
(org) =>
html`
<li
class="p-3 md:p-6 bg-white border-t first:border-t-0 text-primary hover:text-indigo-400"
role="button"
@click=${this.makeOnOrgClick(org)}
>
<span class="font-medium mr-2 transition-colors"
>${org.name}</span
>
${this.userInfo &&
org.users &&
(this.userInfo.isAdmin ||
isAdmin(org.users[this.userInfo.id].role))
? html`<sl-tag size="small" variant="primary"
>${msg("Admin")}</sl-tag
>`
: ""}
</li>
`
)}
${this.orgList?.map(this.renderOrg)}
</ul>
`;
}
private renderOrg = (org: OrgData) => {
let defaultLabel: any;
if (this.defaultOrg && org.id === this.defaultOrg.id) {
defaultLabel = html`<sl-tag size="small" variant="primary" class="mr-2"
>${msg("Default")}</sl-tag
>`;
}
const memberCount = Object.keys(org.users || {}).length;
return html`
<li
class="p-3 bg-white border-t first:border-t-0 text-primary hover:text-indigo-400 flex items-center justify-between"
role="button"
@click=${this.makeOnOrgClick(org)}
>
<div class="font-medium mr-2 transition-colors">
${defaultLabel}${org.name}
</div>
<div class="text-xs text-neutral-400">
${memberCount === 1
? msg(`1 member`)
: msg(str`${memberCount} members`)}
</div>
</li>
`;
};
private renderSkeleton() {
return html`
<div class="border rounded-lg overflow-hidden">

View File

@ -621,6 +621,7 @@ export class App extends LiteElement {
return html`<btrix-users-invite
class="w-full max-w-screen-lg mx-auto p-2 md:py-8 box-border"
@navigate="${this.onNavigateTo}"
@logged-in=${this.onLoggedIn}
@need-login="${this.onNeedLogin}"
.authState="${this.authService.authState}"
.userInfo="${this.userInfo}"

View File

@ -1,4 +1,5 @@
import { state, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { msg, localized, str } from "@lit/localize";
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
@ -138,8 +139,8 @@ export class Home extends LiteElement {
</form>
</section>
<div class="grid grid-cols-3 gap-8">
<div class="col-span-3 md:col-span-2">
<div class="grid grid-cols-5 gap-8">
<div class="col-span-5 md:col-span-3">
<section>
<header class="flex items-start justify-between">
<h2 class="text-lg font-medium mb-3 mt-2">
@ -156,12 +157,17 @@ export class Home extends LiteElement {
<btrix-orgs-list
.userInfo=${this.userInfo}
.orgList=${this.orgList}
.defaultOrg=${ifDefined(
this.userInfo?.orgs.find((org) => org.default === true)
)}
></btrix-orgs-list>
</section>
</div>
<div class="col-span-3 md:col-span-1">
<div class="col-span-5 md:col-span-2">
<section class="md:border md:rounded-lg md:bg-white p-3 md:p-8">
<h2 class="text-lg font-medium mb-4">${msg("Invite a User")}</h2>
<h2 class="text-lg font-medium mb-3">
${msg("Invite User to Org")}
</h2>
${this.renderInvite()}
</section>
</div>
@ -239,9 +245,14 @@ export class Home extends LiteElement {
`;
}
const defaultOrg = this.userInfo?.orgs.find(
(org) => org.default === true
) || { name: "" };
return html`
<btrix-invite-form
.authState=${this.authState}
.orgs=${this.orgList}
.defaultOrg=${defaultOrg || null}
@success=${() => (this.isInviteComplete = true)}
></btrix-invite-form>
`;

View File

@ -1,9 +1,11 @@
import { state, property } from "lit/decorators.js";
import { msg, localized, str } from "@lit/localize";
import { ifDefined } from "lit/directives/if-defined.js";
import type { AuthState } from "../utils/AuthService";
import LiteElement, { html } from "../utils/LiteElement";
import { needLogin } from "../utils/auth";
import { CurrentUser } from "../types/user";
@needLogin
@localized()
@ -11,6 +13,9 @@ export class UsersInvite extends LiteElement {
@property({ type: Object })
authState?: AuthState;
@property({ type: Object })
userInfo?: CurrentUser;
@state()
private invitedEmail?: string;
@ -40,6 +45,10 @@ export class UsersInvite extends LiteElement {
<h2 class="text-lg font-medium mb-4">${msg("Invite Users")}</h2>
<btrix-invite-form
.authState=${this.authState}
.orgs=${this.userInfo?.orgs || []}
.defaultOrg=${ifDefined(
this.userInfo?.orgs.find((org) => org.default === true)
)}
@success=${this.onSuccess}
></btrix-invite-form>
</main>