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 { state, property } from "lit/decorators.js";
|
||||||
import { msg, localized } from "@lit/localize";
|
import { msg, localized } from "@lit/localize";
|
||||||
|
import sortBy from "lodash/fp/sortBy";
|
||||||
|
|
||||||
import type { AuthState } from "../utils/AuthService";
|
import type { AuthState } from "../utils/AuthService";
|
||||||
import LiteElement, { html } from "../utils/LiteElement";
|
import LiteElement, { html } from "../utils/LiteElement";
|
||||||
|
import { OrgData } from "../types/org";
|
||||||
|
|
||||||
|
const sortByName = sortBy("name");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @event success
|
* @event success
|
||||||
@ -12,12 +16,31 @@ export class InviteForm extends LiteElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
authState?: AuthState;
|
authState?: AuthState;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
orgs: OrgData[] = [];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
defaultOrg: OrgData | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private isSubmitting: boolean = false;
|
private isSubmitting: boolean = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private serverError?: string;
|
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() {
|
render() {
|
||||||
let formError;
|
let formError;
|
||||||
|
|
||||||
@ -31,12 +54,31 @@ export class InviteForm extends LiteElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortedOrgs = sortByName(this.orgs) as any as OrgData[];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<form
|
<form
|
||||||
class="max-w-md"
|
class="max-w-md"
|
||||||
@submit=${this.onSubmit}
|
@submit=${this.onSubmit}
|
||||||
aria-describedby="formError"
|
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">
|
<div class="mb-5">
|
||||||
<sl-input
|
<sl-input
|
||||||
id="inviteEmail"
|
id="inviteEmail"
|
||||||
@ -53,12 +95,13 @@ export class InviteForm extends LiteElement {
|
|||||||
|
|
||||||
${formError}
|
${formError}
|
||||||
|
|
||||||
<div>
|
<div class="text-right">
|
||||||
<sl-button
|
<sl-button
|
||||||
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
type="submit"
|
type="submit"
|
||||||
?loading=${this.isSubmitting}
|
?loading=${this.isSubmitting}
|
||||||
?disabled=${this.isSubmitting}
|
?disabled=${!this.selectedOrgId || this.isSubmitting}
|
||||||
>${msg("Invite")}</sl-button
|
>${msg("Invite")}</sl-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -68,7 +111,10 @@ export class InviteForm extends LiteElement {
|
|||||||
|
|
||||||
async onSubmit(event: SubmitEvent) {
|
async onSubmit(event: SubmitEvent) {
|
||||||
event.preventDefault();
|
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.serverError = undefined;
|
||||||
this.isSubmitting = true;
|
this.isSubmitting = true;
|
||||||
@ -77,12 +123,17 @@ export class InviteForm extends LiteElement {
|
|||||||
const inviteEmail = formData.get("inviteEmail") as string;
|
const inviteEmail = formData.get("inviteEmail") as string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await this.apiFetch(`/users/invite`, this.authState, {
|
const data = await this.apiFetch(
|
||||||
method: "POST",
|
`/orgs/${this.selectedOrgId}/invite`,
|
||||||
body: JSON.stringify({
|
this.authState,
|
||||||
email: inviteEmail,
|
{
|
||||||
}),
|
method: "POST",
|
||||||
});
|
body: JSON.stringify({
|
||||||
|
email: inviteEmail,
|
||||||
|
role: 10,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("success", {
|
new CustomEvent("success", {
|
||||||
@ -102,4 +153,9 @@ export class InviteForm extends LiteElement {
|
|||||||
|
|
||||||
this.isSubmitting = false;
|
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 { 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 type { OrgData } from "../utils/orgs";
|
||||||
import LiteElement, { html } from "../utils/LiteElement";
|
import LiteElement, { html } from "../utils/LiteElement";
|
||||||
|
|
||||||
@ -15,6 +15,9 @@ export class OrgsList extends LiteElement {
|
|||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
orgList: OrgData[] = [];
|
orgList: OrgData[] = [];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
defaultOrg?: UserOrg;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
skeleton? = false;
|
skeleton? = false;
|
||||||
|
|
||||||
@ -25,32 +28,38 @@ export class OrgsList extends LiteElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ul class="border rounded-lg overflow-hidden">
|
<ul class="border rounded-lg overflow-hidden">
|
||||||
${this.orgList?.map(
|
${this.orgList?.map(this.renderOrg)}
|
||||||
(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>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ul>
|
</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() {
|
private renderSkeleton() {
|
||||||
return html`
|
return html`
|
||||||
<div class="border rounded-lg overflow-hidden">
|
<div class="border rounded-lg overflow-hidden">
|
||||||
|
|||||||
@ -621,6 +621,7 @@ export class App extends LiteElement {
|
|||||||
return html`<btrix-users-invite
|
return html`<btrix-users-invite
|
||||||
class="w-full max-w-screen-lg mx-auto p-2 md:py-8 box-border"
|
class="w-full max-w-screen-lg mx-auto p-2 md:py-8 box-border"
|
||||||
@navigate="${this.onNavigateTo}"
|
@navigate="${this.onNavigateTo}"
|
||||||
|
@logged-in=${this.onLoggedIn}
|
||||||
@need-login="${this.onNeedLogin}"
|
@need-login="${this.onNeedLogin}"
|
||||||
.authState="${this.authService.authState}"
|
.authState="${this.authService.authState}"
|
||||||
.userInfo="${this.userInfo}"
|
.userInfo="${this.userInfo}"
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { state, property } from "lit/decorators.js";
|
import { state, property } from "lit/decorators.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
import { msg, localized, str } from "@lit/localize";
|
import { msg, localized, str } from "@lit/localize";
|
||||||
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
||||||
|
|
||||||
@ -138,8 +139,8 @@ export class Home extends LiteElement {
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-8">
|
<div class="grid grid-cols-5 gap-8">
|
||||||
<div class="col-span-3 md:col-span-2">
|
<div class="col-span-5 md:col-span-3">
|
||||||
<section>
|
<section>
|
||||||
<header class="flex items-start justify-between">
|
<header class="flex items-start justify-between">
|
||||||
<h2 class="text-lg font-medium mb-3 mt-2">
|
<h2 class="text-lg font-medium mb-3 mt-2">
|
||||||
@ -156,12 +157,17 @@ export class Home extends LiteElement {
|
|||||||
<btrix-orgs-list
|
<btrix-orgs-list
|
||||||
.userInfo=${this.userInfo}
|
.userInfo=${this.userInfo}
|
||||||
.orgList=${this.orgList}
|
.orgList=${this.orgList}
|
||||||
|
.defaultOrg=${ifDefined(
|
||||||
|
this.userInfo?.orgs.find((org) => org.default === true)
|
||||||
|
)}
|
||||||
></btrix-orgs-list>
|
></btrix-orgs-list>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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">
|
<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()}
|
${this.renderInvite()}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -239,9 +245,14 @@ export class Home extends LiteElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultOrg = this.userInfo?.orgs.find(
|
||||||
|
(org) => org.default === true
|
||||||
|
) || { name: "" };
|
||||||
return html`
|
return html`
|
||||||
<btrix-invite-form
|
<btrix-invite-form
|
||||||
.authState=${this.authState}
|
.authState=${this.authState}
|
||||||
|
.orgs=${this.orgList}
|
||||||
|
.defaultOrg=${defaultOrg || null}
|
||||||
@success=${() => (this.isInviteComplete = true)}
|
@success=${() => (this.isInviteComplete = true)}
|
||||||
></btrix-invite-form>
|
></btrix-invite-form>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { state, property } from "lit/decorators.js";
|
import { state, property } from "lit/decorators.js";
|
||||||
import { msg, localized, str } from "@lit/localize";
|
import { msg, localized, str } from "@lit/localize";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import type { AuthState } from "../utils/AuthService";
|
import type { AuthState } from "../utils/AuthService";
|
||||||
import LiteElement, { html } from "../utils/LiteElement";
|
import LiteElement, { html } from "../utils/LiteElement";
|
||||||
import { needLogin } from "../utils/auth";
|
import { needLogin } from "../utils/auth";
|
||||||
|
import { CurrentUser } from "../types/user";
|
||||||
|
|
||||||
@needLogin
|
@needLogin
|
||||||
@localized()
|
@localized()
|
||||||
@ -11,6 +13,9 @@ export class UsersInvite extends LiteElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
authState?: AuthState;
|
authState?: AuthState;
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
userInfo?: CurrentUser;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private invitedEmail?: string;
|
private invitedEmail?: string;
|
||||||
|
|
||||||
@ -40,6 +45,10 @@ export class UsersInvite extends LiteElement {
|
|||||||
<h2 class="text-lg font-medium mb-4">${msg("Invite Users")}</h2>
|
<h2 class="text-lg font-medium mb-4">${msg("Invite Users")}</h2>
|
||||||
<btrix-invite-form
|
<btrix-invite-form
|
||||||
.authState=${this.authState}
|
.authState=${this.authState}
|
||||||
|
.orgs=${this.userInfo?.orgs || []}
|
||||||
|
.defaultOrg=${ifDefined(
|
||||||
|
this.userInfo?.orgs.find((org) => org.default === true)
|
||||||
|
)}
|
||||||
@success=${this.onSuccess}
|
@success=${this.onSuccess}
|
||||||
></btrix-invite-form>
|
></btrix-invite-form>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user