Improve superadmin invite UI (#581)
This commit is contained in:
parent
103d91556f
commit
a180b92f4a
@ -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, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: inviteEmail,
|
||||
}),
|
||||
});
|
||||
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]");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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>
|
||||
`;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user