Manage org member roles and invites (#558)
- View and delete pending invites - Update user roles for members - Remove members
This commit is contained in:
parent
40fb04b385
commit
7463becdff
131
frontend/src/components/data-table.ts
Normal file
131
frontend/src/components/data-table.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { LitElement, html, css, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
type CellContent = string | TemplateResult;
|
||||
|
||||
/**
|
||||
* Styled data table
|
||||
*
|
||||
* Usage example:
|
||||
* ```ts
|
||||
* <btrix-data-table
|
||||
* .columns=${[html`A`, html`B`, html`C`]}
|
||||
* .rows=${[
|
||||
* [html`1a`, html`1b`, html`1c`],
|
||||
* [html`2a`, html`2b`, html`2c`],
|
||||
* ]}
|
||||
* .columnWidths=${["100%", "20rem"]}
|
||||
* >
|
||||
* </btrix-data-table>
|
||||
* ```
|
||||
*/
|
||||
export class DataTable extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
font-family: var(--font-monostyle-family);
|
||||
font-variation-settings: var(--font-monostyle-variation);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
.tbody {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cell:nth-of-type(n + 2) {
|
||||
border-left: 1px solid var(--sl-panel-border-color);
|
||||
}
|
||||
|
||||
.cell[role="cell"] {
|
||||
border-top: 1px solid var(--sl-panel-border-color);
|
||||
}
|
||||
|
||||
.cell.padSmall {
|
||||
padding: var(--sl-spacing-2x-small);
|
||||
}
|
||||
|
||||
.cell.padded {
|
||||
padding: var(--sl-spacing-x-small);
|
||||
}
|
||||
|
||||
.thead .row {
|
||||
background-color: var(--sl-color-neutral-50);
|
||||
color: var(--sl-color-neutral-700);
|
||||
font-size: var(--sl-font-size-x-small);
|
||||
line-height: 1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: Array })
|
||||
columns: CellContent[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
rows: Array<CellContent[]> = [];
|
||||
|
||||
// Array of CSS widths
|
||||
@property({ type: Array })
|
||||
columnWidths: string[] = [];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div role="table" class="table">
|
||||
<div role="rowgroup" class="thead">
|
||||
<div role="row" class="row">
|
||||
${this.columns.map(this.renderColumnHeader)}
|
||||
</div>
|
||||
</div>
|
||||
<div role="rowgroup" class="tbody">
|
||||
${this.rows.map(this.renderRow)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderColumnHeader = (cell: CellContent, index: number) => html`
|
||||
<div
|
||||
role="columnheader"
|
||||
class="cell padded"
|
||||
style=${this.columnWidths[index]
|
||||
? `width: ${this.columnWidths[index]}`
|
||||
: ""}
|
||||
>
|
||||
${cell}
|
||||
</div>
|
||||
`;
|
||||
|
||||
private renderRow = (cells: CellContent[]) => html`
|
||||
<div role="row" class="row">${cells.map(this.renderCell)}</div>
|
||||
`;
|
||||
|
||||
private renderCell = (cell: CellContent) => {
|
||||
const shouldPadSmall =
|
||||
typeof cell === "string"
|
||||
? false
|
||||
: // TODO better logic to check template component
|
||||
cell.strings[0].startsWith("<sl-");
|
||||
return html`
|
||||
<div role="cell" class="cell ${shouldPadSmall ? "padSmall" : "padded"}">
|
||||
${cell}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
}
|
||||
@ -101,6 +101,9 @@ import("./tag").then(({ Tag }) => {
|
||||
import("./dialog").then(({ Dialog }) => {
|
||||
customElements.define("btrix-dialog", Dialog);
|
||||
});
|
||||
import("./data-table").then(({ DataTable }) => {
|
||||
customElements.define("btrix-data-table", DataTable);
|
||||
});
|
||||
|
||||
customElements.define("btrix-alert", Alert);
|
||||
customElements.define("btrix-input", Input);
|
||||
|
||||
@ -5,7 +5,7 @@ import type { CurrentUser } from "../types/user";
|
||||
import type { OrgData } from "../utils/orgs";
|
||||
import LiteElement, { html } from "../utils/LiteElement";
|
||||
|
||||
import { isOwner } from "../utils/orgs";
|
||||
import { isAdmin } from "../utils/orgs";
|
||||
|
||||
@localized()
|
||||
export class OrgsList extends LiteElement {
|
||||
@ -39,7 +39,7 @@ export class OrgsList extends LiteElement {
|
||||
${this.userInfo &&
|
||||
org.users &&
|
||||
(this.userInfo.isAdmin ||
|
||||
isOwner(org.users[this.userInfo.id].role))
|
||||
isAdmin(org.users[this.userInfo.id].role))
|
||||
? html`<sl-tag size="small" variant="primary"
|
||||
>${msg("Admin")}</sl-tag
|
||||
>`
|
||||
|
||||
@ -18,6 +18,12 @@ import "./browser-profiles-detail";
|
||||
import "./browser-profiles-list";
|
||||
import "./browser-profiles-new";
|
||||
import "./settings";
|
||||
import type {
|
||||
Member,
|
||||
OrgNameChangeEvent,
|
||||
UserRoleChangeEvent,
|
||||
OrgRemoveMemberEvent,
|
||||
} from "./settings";
|
||||
|
||||
export type OrgTab =
|
||||
| "crawls"
|
||||
@ -62,6 +68,9 @@ export class Org extends LiteElement {
|
||||
@state()
|
||||
private org?: OrgData | null;
|
||||
|
||||
@state()
|
||||
private isSavingOrgName = false;
|
||||
|
||||
get userOrg() {
|
||||
if (!this.userInfo) return null;
|
||||
return this.userInfo.orgs.find(({ id }) => id === this.orgId)!;
|
||||
@ -295,6 +304,10 @@ export class Org extends LiteElement {
|
||||
.orgId=${this.orgId}
|
||||
activePanel=${activePanel}
|
||||
?isAddingMember=${isAddingMember}
|
||||
?isSavingOrgName=${this.isSavingOrgName}
|
||||
@org-name-change=${this.onOrgNameChange}
|
||||
@org-user-role-change=${this.onUserRoleChange}
|
||||
@org-remove-member=${this.onOrgRemoveMember}
|
||||
></btrix-org-settings>`;
|
||||
}
|
||||
|
||||
@ -303,4 +316,130 @@ export class Org extends LiteElement {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private async onOrgNameChange(e: OrgNameChangeEvent) {
|
||||
this.isSavingOrgName = true;
|
||||
|
||||
try {
|
||||
await this.apiFetch(`/orgs/${this.org!.id}/rename`, this.authState!, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: e.detail.value }),
|
||||
});
|
||||
|
||||
this.notify({
|
||||
message: msg("Updated organization name."),
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
});
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("update-user-info", { bubbles: true })
|
||||
);
|
||||
} catch (e: any) {
|
||||
this.notify({
|
||||
message: e.isApiError
|
||||
? e.message
|
||||
: msg("Sorry, couldn't update organization name at this time."),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
|
||||
this.isSavingOrgName = false;
|
||||
}
|
||||
|
||||
private async onOrgRemoveMember(e: OrgRemoveMemberEvent) {
|
||||
this.removeMember(e.detail.member);
|
||||
}
|
||||
|
||||
private async onUserRoleChange(e: UserRoleChangeEvent) {
|
||||
const { user, newRole } = e.detail;
|
||||
|
||||
try {
|
||||
await this.apiFetch(`/orgs/${this.orgId}/user-role`, this.authState!, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
email: user.email,
|
||||
role: newRole,
|
||||
}),
|
||||
});
|
||||
|
||||
this.notify({
|
||||
message: msg(
|
||||
str`Successfully updated role for ${user.name || user.email}.`
|
||||
),
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
});
|
||||
this.org = await this.getOrg(this.orgId);
|
||||
} catch (e: any) {
|
||||
console.debug(e);
|
||||
|
||||
this.notify({
|
||||
message: e.isApiError
|
||||
? e.message
|
||||
: msg(
|
||||
str`Sorry, couldn't update role for ${
|
||||
user.name || user.email
|
||||
} at this time.`
|
||||
),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async removeMember(member: Member) {
|
||||
if (!this.org) return;
|
||||
const isSelf = member.email === this.userInfo!.email;
|
||||
if (
|
||||
isSelf &&
|
||||
!window.confirm(
|
||||
msg(
|
||||
str`Are you sure you want to remove yourself from ${this.org.name}?`
|
||||
)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiFetch(`/orgs/${this.orgId}/remove`, this.authState!, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: member.email,
|
||||
}),
|
||||
});
|
||||
|
||||
this.notify({
|
||||
message: msg(
|
||||
str`Successfully removed ${member.name || member.email} from ${
|
||||
this.org.name
|
||||
}.`
|
||||
),
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
});
|
||||
if (isSelf) {
|
||||
// FIXME better UX, this is the only page currently that doesn't require org...
|
||||
this.navTo("/account/settings");
|
||||
} else {
|
||||
this.org = await this.getOrg(this.orgId);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.debug(e);
|
||||
|
||||
this.notify({
|
||||
message: e.isApiError
|
||||
? e.message
|
||||
: msg(
|
||||
str`Sorry, couldn't remove ${
|
||||
member.name || member.email
|
||||
} at this time.`
|
||||
),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,32 @@ import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
||||
|
||||
import type { AuthState } from "../../utils/AuthService";
|
||||
import LiteElement, { html } from "../../utils/LiteElement";
|
||||
import { isOwner, AccessCode } from "../../utils/orgs";
|
||||
import { isAdmin, isCrawler, AccessCode } from "../../utils/orgs";
|
||||
import type { OrgData } from "../../utils/orgs";
|
||||
import type { CurrentUser } from "../../types/user";
|
||||
|
||||
type Tab = "information" | "members";
|
||||
type User = {
|
||||
email: string;
|
||||
role: number;
|
||||
};
|
||||
type Invite = User & {
|
||||
created: string;
|
||||
inviterEmail: string;
|
||||
};
|
||||
export type Member = User & {
|
||||
name: string;
|
||||
};
|
||||
export type OrgNameChangeEvent = CustomEvent<{
|
||||
value: string;
|
||||
}>;
|
||||
export type UserRoleChangeEvent = CustomEvent<{
|
||||
user: Member;
|
||||
newRole: number;
|
||||
}>;
|
||||
export type OrgRemoveMemberEvent = CustomEvent<{
|
||||
member: Member;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
@ -23,6 +44,11 @@ type Tab = "information" | "members";
|
||||
* ?isAddingMember=${isAddingMember}
|
||||
* ></btrix-org-settings>
|
||||
* ```
|
||||
*
|
||||
* @events
|
||||
* org-name-change
|
||||
* org-user-role-change
|
||||
* org-remove-member
|
||||
*/
|
||||
@localized()
|
||||
export class OrgSettings extends LiteElement {
|
||||
@ -44,11 +70,14 @@ export class OrgSettings extends LiteElement {
|
||||
@property({ type: Boolean })
|
||||
isAddingMember = false;
|
||||
|
||||
@state()
|
||||
private isAddMemberFormVisible = false;
|
||||
@property({ type: Boolean })
|
||||
isSavingOrgName = false;
|
||||
|
||||
@state()
|
||||
private isSavingOrgName = false;
|
||||
pendingInvites: Invite[] = [];
|
||||
|
||||
@state()
|
||||
private isAddMemberFormVisible = false;
|
||||
|
||||
@state()
|
||||
private isSubmittingInvite = false;
|
||||
@ -64,6 +93,12 @@ export class OrgSettings extends LiteElement {
|
||||
if (changedProperties.has("isAddingMember") && this.isAddingMember) {
|
||||
this.isAddMemberFormVisible = true;
|
||||
}
|
||||
if (
|
||||
changedProperties.has("activePanel") &&
|
||||
this.activePanel === "members"
|
||||
) {
|
||||
this.fetchPendingInvites();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -73,10 +108,10 @@ export class OrgSettings extends LiteElement {
|
||||
|
||||
<btrix-tab-list activePanel=${this.activePanel} ?hideIndicator=${true}>
|
||||
<header slot="header" class="flex items-end justify-between h-5">
|
||||
<h3>${this.tabLabels[this.activePanel]}</h3>
|
||||
${when(
|
||||
this.activePanel === "members",
|
||||
() => html`
|
||||
<h3>${msg("Active Members")}</h3>
|
||||
<sl-button
|
||||
href=${`/orgs/${this.orgId}/settings/members?invite`}
|
||||
variant="primary"
|
||||
@ -84,7 +119,8 @@ export class OrgSettings extends LiteElement {
|
||||
@click=${this.navLink}
|
||||
>${msg("Invite New Member")}</sl-button
|
||||
>
|
||||
`
|
||||
`,
|
||||
() => html` <h3>${this.tabLabels[this.activePanel]}</h3> `
|
||||
)}
|
||||
</header>
|
||||
${this.renderTab("information", "settings")}
|
||||
@ -146,42 +182,44 @@ export class OrgSettings extends LiteElement {
|
||||
}
|
||||
|
||||
private renderMembers() {
|
||||
const columnWidths = ["100%", "10rem", "1.5rem"];
|
||||
return html`
|
||||
<div role="table" class="rounded border">
|
||||
<div class="border-b bg-neutral-50" role="rowgroup">
|
||||
<div class="flex font-medium" role="row">
|
||||
<div class="flex-1 px-3 py-1" role="columnheader" aria-sort="none">
|
||||
${msg("Name")}
|
||||
</div>
|
||||
<div
|
||||
class="flex-0 w-52 px-3 py-1"
|
||||
role="columnheader"
|
||||
aria-sort="none"
|
||||
>
|
||||
${msg("Role", { desc: "Organization member's role" })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="rowgroup">
|
||||
${Object.entries(this.org.users!).map(
|
||||
([id, user]) => html`
|
||||
<div
|
||||
class="border-b last:border-none flex items-center"
|
||||
role="row"
|
||||
<section class="rounded border overflow-hidden">
|
||||
<btrix-data-table
|
||||
.columns=${[msg("Name"), msg("Role"), ""]}
|
||||
.rows=${Object.entries(this.org.users!).map(([id, user]) => [
|
||||
user.name,
|
||||
this.renderUserRoleSelect(user),
|
||||
this.renderRemoveMemberButton(user),
|
||||
])}
|
||||
.columnWidths=${columnWidths}
|
||||
>
|
||||
</btrix-data-table>
|
||||
</section>
|
||||
|
||||
${when(
|
||||
this.pendingInvites.length,
|
||||
() => html`
|
||||
<section class="mt-7">
|
||||
<h3 class="text-lg font-semibold mb-2">
|
||||
${msg("Pending Invites")}
|
||||
</h3>
|
||||
|
||||
<div class="rounded border overflow-hidden">
|
||||
<btrix-data-table
|
||||
.columns=${[msg("Email"), msg("Role"), ""]}
|
||||
.rows=${this.pendingInvites.map((user) => [
|
||||
user.email,
|
||||
this.renderUserRole(user),
|
||||
this.renderRemoveInviteButton(user),
|
||||
])}
|
||||
.columnWidths=${columnWidths}
|
||||
>
|
||||
<div class="flex-1 p-3" role="cell">${user.name}</div>
|
||||
<div class="flex-0 w-52 p-3" role="cell">
|
||||
${isOwner(user.role)
|
||||
? msg("Admin")
|
||||
: user.role === AccessCode.crawler
|
||||
? msg("Crawler")
|
||||
: msg("Viewer")}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</btrix-data-table>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
)}
|
||||
|
||||
<btrix-dialog
|
||||
label=${msg("Invite New Member")}
|
||||
@ -195,14 +233,67 @@ export class OrgSettings extends LiteElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderUserRole(user: { name: string; role: typeof AccessCode }) {
|
||||
return html`<sl-select value=${user.role} size="small">
|
||||
<sl-menu-item value=${AccessCode.owner}> ${"Admin"} </sl-menu-item>
|
||||
<sl-menu-item value=${AccessCode.crawler}> ${"Crawler"} </sl-menu-item>
|
||||
<sl-menu-item value=${AccessCode.viewer}> ${"Viewer"} </sl-menu-item>
|
||||
private renderUserRole({ role }: User) {
|
||||
if (isAdmin(role)) return msg("Admin");
|
||||
if (isCrawler(role)) return msg("Crawler");
|
||||
return msg("Viewer");
|
||||
}
|
||||
|
||||
private renderUserRoleSelect(user: Member) {
|
||||
// Consider superadmins owners
|
||||
const userRole =
|
||||
user.role === AccessCode.superadmin ? AccessCode.owner : user.role;
|
||||
return html`<sl-select
|
||||
value=${userRole}
|
||||
size="small"
|
||||
@sl-select=${this.selectUserRole(user)}
|
||||
>
|
||||
<sl-menu-item value=${AccessCode.owner}>${"Admin"}</sl-menu-item>
|
||||
<sl-menu-item value=${AccessCode.crawler}>${"Crawler"}</sl-menu-item>
|
||||
<sl-menu-item value=${AccessCode.viewer}>${"Viewer"}</sl-menu-item>
|
||||
</sl-select>`;
|
||||
}
|
||||
|
||||
private renderRemoveMemberButton(member: Member) {
|
||||
let disableButton = false;
|
||||
if (member.email === this.userInfo.email) {
|
||||
const { [this.userInfo.id]: _currentUser, ...otherUsers } =
|
||||
this.org.users!;
|
||||
const hasOtherAdmin = Object.values(otherUsers).some(({ role }) =>
|
||||
isAdmin(role)
|
||||
);
|
||||
if (!hasOtherAdmin) {
|
||||
// Must be another admin in order to remove self
|
||||
disableButton = true;
|
||||
}
|
||||
}
|
||||
return html`<sl-icon-button
|
||||
name="trash"
|
||||
?disabled=${disableButton}
|
||||
style="font-size: 1rem"
|
||||
let
|
||||
disableButton="false;"
|
||||
aria-details=${ifDefined(
|
||||
disableButton === true
|
||||
? msg("Cannot remove only admin member")
|
||||
: undefined
|
||||
)}
|
||||
@click=${() =>
|
||||
this.dispatchEvent(
|
||||
<OrgRemoveMemberEvent>new CustomEvent("org-remove-member", {
|
||||
detail: { member },
|
||||
})
|
||||
)}
|
||||
></sl-icon-button>`;
|
||||
}
|
||||
|
||||
private renderRemoveInviteButton(invite: Invite) {
|
||||
return html`<btrix-icon-button
|
||||
name="trash"
|
||||
@click=${() => this.removeInvite(invite)}
|
||||
></btrix-icon-button>`;
|
||||
}
|
||||
|
||||
private hideInviteDialog() {
|
||||
this.navTo(`/orgs/${this.orgId}/settings/members`);
|
||||
}
|
||||
@ -262,11 +353,31 @@ export class OrgSettings extends LiteElement {
|
||||
`;
|
||||
}
|
||||
|
||||
async checkFormValidity(formEl: HTMLFormElement) {
|
||||
private async checkFormValidity(formEl: HTMLFormElement) {
|
||||
await this.updateComplete;
|
||||
return !formEl.querySelector("[data-invalid]");
|
||||
}
|
||||
|
||||
private getPendingInvites(): Promise<Invite[]> {
|
||||
return this.apiFetch(`/orgs/${this.org.id}/invites`, this.authState!).then(
|
||||
(data) => data.pending_invites
|
||||
);
|
||||
}
|
||||
|
||||
private async fetchPendingInvites() {
|
||||
try {
|
||||
this.pendingInvites = await this.getPendingInvites();
|
||||
} catch (e: any) {
|
||||
console.debug(e);
|
||||
|
||||
this.notify({
|
||||
message: msg("Sorry, couldn't retrieve pending invites at this time."),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async onOrgNameSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -274,38 +385,24 @@ export class OrgSettings extends LiteElement {
|
||||
if (!(await this.checkFormValidity(formEl))) return;
|
||||
|
||||
const { orgName } = serialize(formEl);
|
||||
|
||||
this.isSavingOrgName = true;
|
||||
|
||||
try {
|
||||
await this.apiFetch(`/orgs/${this.org.id}/rename`, this.authState!, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: orgName }),
|
||||
});
|
||||
|
||||
this.notify({
|
||||
message: msg("Updated organization name."),
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
duration: 8000,
|
||||
});
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("update-user-info", { bubbles: true })
|
||||
);
|
||||
} catch (e: any) {
|
||||
this.notify({
|
||||
message: e.isApiError
|
||||
? e.message
|
||||
: msg("Sorry, couldn't update organization name at this time."),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
|
||||
this.isSavingOrgName = false;
|
||||
this.dispatchEvent(
|
||||
<OrgNameChangeEvent>new CustomEvent("org-name-change", {
|
||||
detail: { value: orgName },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private selectUserRole = (user: User) => (e: CustomEvent) => {
|
||||
this.dispatchEvent(
|
||||
<UserRoleChangeEvent>new CustomEvent("org-user-role-change", {
|
||||
detail: {
|
||||
user,
|
||||
newRole: Number(e.detail.item.value),
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
async onOrgInviteSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -333,9 +430,9 @@ export class OrgSettings extends LiteElement {
|
||||
message: msg(str`Successfully invited ${inviteEmail}.`),
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
duration: 8000,
|
||||
});
|
||||
|
||||
this.fetchPendingInvites();
|
||||
this.hideInviteDialog();
|
||||
} catch (e: any) {
|
||||
this.notify({
|
||||
@ -349,6 +446,43 @@ export class OrgSettings extends LiteElement {
|
||||
|
||||
this.isSubmittingInvite = false;
|
||||
}
|
||||
|
||||
private async removeInvite(invite: Invite) {
|
||||
try {
|
||||
await this.apiFetch(
|
||||
`/orgs/${this.orgId}/invites/delete`,
|
||||
this.authState!,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: invite.email,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
this.notify({
|
||||
message: msg(
|
||||
str`Successfully removed ${invite.email} from ${this.org.name}.`
|
||||
),
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
});
|
||||
|
||||
this.pendingInvites = this.pendingInvites.filter(
|
||||
({ email }) => email !== invite.email
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.debug(e);
|
||||
|
||||
this.notify({
|
||||
message: e.isApiError
|
||||
? e.message
|
||||
: msg(str`Sorry, couldn't remove ${invite.email} at this time.`),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("btrix-org-settings", OrgSettings);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user