diff --git a/frontend/src/components/invite-form.ts b/frontend/src/components/invite-form.ts index 5c9eb396..65f921b7 100644 --- a/frontend/src/components/invite-form.ts +++ b/frontend/src/components/invite-form.ts @@ -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) { + 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`
+
+ { + this.selectedOrgId = e.detail.item.value; + }} + ?disabled=${sortedOrgs.length === 1} + required + > + ${sortedOrgs.map( + (org) => html` + ${org.name} + ` + )} + +
+
${msg("Invite")}
@@ -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]"); + } } diff --git a/frontend/src/components/orgs-list.ts b/frontend/src/components/orgs-list.ts index f925a1aa..75c4c795 100644 --- a/frontend/src/components/orgs-list.ts +++ b/frontend/src/components/orgs-list.ts @@ -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`
    - ${this.orgList?.map( - (org) => - html` -
  • - ${org.name} - ${this.userInfo && - org.users && - (this.userInfo.isAdmin || - isAdmin(org.users[this.userInfo.id].role)) - ? html`${msg("Admin")}` - : ""} -
  • - ` - )} + ${this.orgList?.map(this.renderOrg)}
`; } + private renderOrg = (org: OrgData) => { + let defaultLabel: any; + if (this.defaultOrg && org.id === this.defaultOrg.id) { + defaultLabel = html`${msg("Default")}`; + } + const memberCount = Object.keys(org.users || {}).length; + + return html` +
  • +
    + ${defaultLabel}${org.name} +
    +
    + ${memberCount === 1 + ? msg(`1 member`) + : msg(str`${memberCount} members`)} +
    +
  • + `; + }; + private renderSkeleton() { return html`
    diff --git a/frontend/src/index.ts b/frontend/src/index.ts index f6e45f66..b02e128d 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -621,6 +621,7 @@ export class App extends LiteElement { return html` -
    -
    +
    +

    @@ -156,12 +157,17 @@ export class Home extends LiteElement { org.default === true) + )} >

    -
    +
    -

    ${msg("Invite a User")}

    +

    + ${msg("Invite User to Org")} +

    ${this.renderInvite()}
    @@ -239,9 +245,14 @@ export class Home extends LiteElement { `; } + const defaultOrg = this.userInfo?.orgs.find( + (org) => org.default === true + ) || { name: "" }; return html` (this.isInviteComplete = true)} > `; diff --git a/frontend/src/pages/users-invite.ts b/frontend/src/pages/users-invite.ts index aeb8187f..84e91134 100644 --- a/frontend/src/pages/users-invite.ts +++ b/frontend/src/pages/users-invite.ts @@ -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 {

    ${msg("Invite Users")}

    org.default === true) + )} @success=${this.onSuccess} >