Update org settings & org invite UI (#528)
This commit is contained in:
parent
3c199419a2
commit
05ce32d898
38
frontend/src/components/dialog.ts
Normal file
38
frontend/src/components/dialog.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { css } from "lit";
|
||||
import SLDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
|
||||
import dialogStyles from "@shoelace-style/shoelace/dist/components/dialog/dialog.styles.js";
|
||||
|
||||
/**
|
||||
* Customized <sl-dialog>
|
||||
*
|
||||
* Usage: see https://shoelace.style/components/dialog
|
||||
*/
|
||||
export class Dialog extends SLDialog {
|
||||
static styles = css`
|
||||
${dialogStyles} .dialog__panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog__header {
|
||||
background-color: var(--sl-color-neutral-50);
|
||||
border-bottom: 1px solid var(--sl-color-neutral-100);
|
||||
}
|
||||
|
||||
.dialog__title {
|
||||
padding-top: var(--sl-spacing-small);
|
||||
padding-bottom: var(--sl-spacing-small);
|
||||
font-size: var(--sl-font-size-medium);
|
||||
font-weight: var(--sl-font-weight-medium);
|
||||
}
|
||||
|
||||
.dialog__close {
|
||||
--header-spacing: var(--sl-spacing-2x-small);
|
||||
}
|
||||
|
||||
.dialog__footer {
|
||||
padding-top: var(--sl-spacing-small);
|
||||
padding-bottom: var(--sl-spacing-small);
|
||||
border-top: 1px solid var(--sl-color-neutral-100);
|
||||
}
|
||||
`;
|
||||
}
|
@ -10,9 +10,6 @@ import("./locale-picker").then(({ LocalePicker }) => {
|
||||
import("./account-settings").then(({ AccountSettings }) => {
|
||||
customElements.define("btrix-account-settings", AccountSettings);
|
||||
});
|
||||
import("./org-invite-form").then(({ OrgInviteForm }) => {
|
||||
customElements.define("btrix-org-invite-form", OrgInviteForm);
|
||||
});
|
||||
import("./config-editor").then(({ ConfigEditor }) => {
|
||||
customElements.define("btrix-config-editor", ConfigEditor);
|
||||
});
|
||||
@ -101,6 +98,9 @@ import("./tag-input").then(({ TagInput }) => {
|
||||
import("./tag").then(({ Tag }) => {
|
||||
customElements.define("btrix-tag", Tag);
|
||||
});
|
||||
import("./dialog").then(({ Dialog }) => {
|
||||
customElements.define("btrix-dialog", Dialog);
|
||||
});
|
||||
|
||||
customElements.define("btrix-alert", Alert);
|
||||
customElements.define("btrix-input", Input);
|
||||
|
@ -1,132 +0,0 @@
|
||||
import { state, property } from "lit/decorators.js";
|
||||
import { msg, localized, str } from "@lit/localize";
|
||||
|
||||
import type { AuthState } from "../utils/AuthService";
|
||||
import LiteElement, { html } from "../utils/LiteElement";
|
||||
import { AccessCode } from "../utils/orgs";
|
||||
|
||||
@localized()
|
||||
export class OrgInviteForm extends LiteElement {
|
||||
@property({ type: String })
|
||||
orgId?: string;
|
||||
|
||||
@property({ type: Object })
|
||||
authState?: AuthState;
|
||||
|
||||
@state()
|
||||
private isSubmitting: boolean = false;
|
||||
|
||||
@state()
|
||||
private serverError?: string;
|
||||
|
||||
render() {
|
||||
let formError;
|
||||
|
||||
if (this.serverError) {
|
||||
formError = html`
|
||||
<div class="mb-5">
|
||||
<btrix-alert id="formError" variant="danger"
|
||||
>${this.serverError}</btrix-alert
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<form
|
||||
class="max-w-md"
|
||||
@submit=${this.onSubmit}
|
||||
aria-describedby="formError"
|
||||
>
|
||||
<div class="mb-5">
|
||||
<sl-input
|
||||
id="inviteEmail"
|
||||
name="inviteEmail"
|
||||
type="email"
|
||||
label=${msg("Email")}
|
||||
placeholder=${msg("org-member@email.com", {
|
||||
desc: "Placeholder text for email to invite",
|
||||
})}
|
||||
required
|
||||
>
|
||||
</sl-input>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<sl-radio-group
|
||||
name="role"
|
||||
label="Select an option"
|
||||
value=${AccessCode.viewer}
|
||||
>
|
||||
<sl-radio value=${AccessCode.owner}>
|
||||
${msg("Admin")} - ${msg("Can manage crawls and invite others")}
|
||||
</sl-radio>
|
||||
<sl-radio value=${AccessCode.crawler}>
|
||||
${msg("Crawler")} - ${msg("Can manage crawls")}
|
||||
</sl-radio>
|
||||
<sl-radio value=${AccessCode.viewer}>
|
||||
${msg("Viewer")} - ${msg("Can view crawls")}
|
||||
</sl-radio>
|
||||
</sl-radio-group>
|
||||
</div>
|
||||
|
||||
${formError}
|
||||
|
||||
<div>
|
||||
<sl-button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
?loading=${this.isSubmitting}
|
||||
?disabled=${this.isSubmitting}
|
||||
>${msg("Invite")}</sl-button
|
||||
>
|
||||
<sl-button
|
||||
variant="text"
|
||||
@click=${() => this.dispatchEvent(new CustomEvent("cancel"))}
|
||||
>${msg("Cancel")}</sl-button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
async onSubmit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
if (!this.authState) return;
|
||||
|
||||
this.isSubmitting = true;
|
||||
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
const inviteEmail = formData.get("inviteEmail") as string;
|
||||
|
||||
try {
|
||||
const data = await this.apiFetch(
|
||||
`/orgs/${this.orgId}/invite`,
|
||||
this.authState,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: inviteEmail,
|
||||
role: Number(formData.get("role")),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("success", {
|
||||
detail: {
|
||||
inviteEmail,
|
||||
isExistingUser: data.invited === "existing_user",
|
||||
},
|
||||
})
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (e?.isApiError) {
|
||||
this.serverError = e?.message;
|
||||
} else {
|
||||
this.serverError = msg("Something unexpected went wrong");
|
||||
}
|
||||
}
|
||||
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
}
|
@ -168,15 +168,19 @@ export class TabList extends LitElement {
|
||||
top: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
ul {
|
||||
.tablist {
|
||||
display: flex;
|
||||
margin: 0 0 0 var(--track-width);
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.show-indicator .tablist {
|
||||
margin-left: var(--track-width);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: ${SCREEN_LG}px) {
|
||||
ul {
|
||||
.tablist {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@ -201,9 +205,9 @@ export class TabList extends LitElement {
|
||||
}
|
||||
|
||||
@media only screen and (min-width: ${SCREEN_LG}px) {
|
||||
ul,
|
||||
.track,
|
||||
.indicator {
|
||||
.tablist,
|
||||
.show-indicator .track,
|
||||
.show-indicator .indicator {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@ -217,6 +221,9 @@ export class TabList extends LitElement {
|
||||
@property({ type: String })
|
||||
progressPanel?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
hideIndicator = false;
|
||||
|
||||
@queryAsync(".track")
|
||||
private trackElem!: HTMLElement;
|
||||
|
||||
@ -233,7 +240,7 @@ export class TabList extends LitElement {
|
||||
}
|
||||
|
||||
private async repositionIndicator(activeTab?: TabElement, animate = true) {
|
||||
if (!activeTab) return;
|
||||
if (!activeTab || this.hideIndicator) return;
|
||||
|
||||
const trackElem = await this.trackElem;
|
||||
const indicatorElem = await this.indicatorElem;
|
||||
@ -274,12 +281,17 @@ export class TabList extends LitElement {
|
||||
@sl-resize=${() =>
|
||||
this.repositionIndicator(this.getTab(this.progressPanel))}
|
||||
>
|
||||
<div class="nav ${this.progressPanel ? "linear" : "nonlinear"}">
|
||||
<div
|
||||
class="nav ${this.progressPanel ? "linear" : "nonlinear"} ${this
|
||||
.hideIndicator
|
||||
? "hide-indicator"
|
||||
: "show-indicator"}"
|
||||
>
|
||||
<div class="track" role="presentation">
|
||||
<div class="indicator" role="presentation"></div>
|
||||
</div>
|
||||
|
||||
<ul role="tablist">
|
||||
<ul class="tablist" role="tablist">
|
||||
<slot name="nav"></slot>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -572,6 +572,9 @@ export class App extends LiteElement {
|
||||
.viewStateData=${this.viewState.data}
|
||||
.params=${this.viewState.params}
|
||||
orgId=${this.viewState.params.orgId}
|
||||
orgPath=${this.viewState.pathname.split(
|
||||
this.viewState.params.orgId
|
||||
)[1]}
|
||||
orgTab=${this.viewState.params.orgTab as OrgTab}
|
||||
></btrix-org>`;
|
||||
|
||||
|
@ -46,6 +46,10 @@ export class Org extends LiteElement {
|
||||
@property({ type: Object })
|
||||
viewStateData?: ViewState["data"];
|
||||
|
||||
// Path after `/orgs/:orgId/`
|
||||
@property({ type: String })
|
||||
orgPath!: string;
|
||||
|
||||
@property({ type: Object })
|
||||
params!: Params;
|
||||
|
||||
@ -259,6 +263,10 @@ export class Org extends LiteElement {
|
||||
}
|
||||
|
||||
private renderOrgSettings() {
|
||||
// const activePanel = this.
|
||||
const activePanel = this.orgPath.includes("/members")
|
||||
? "members"
|
||||
: "information";
|
||||
const isAddingMember = this.params.hasOwnProperty("invite");
|
||||
|
||||
return html`<btrix-org-settings
|
||||
@ -266,6 +274,7 @@ export class Org extends LiteElement {
|
||||
.userInfo=${this.userInfo}
|
||||
.org=${this.org}
|
||||
.orgId=${this.orgId}
|
||||
activePanel=${activePanel}
|
||||
?isAddingMember=${isAddingMember}
|
||||
></btrix-org-settings>`;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { msg, localized, str } from "@lit/localize";
|
||||
import { when } from "lit/directives/when.js";
|
||||
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
||||
import type { SlButton } from "@shoelace-style/shoelace";
|
||||
|
||||
import type { AuthState } from "../../utils/AuthService";
|
||||
import LiteElement, { html } from "../../utils/LiteElement";
|
||||
@ -11,6 +10,8 @@ import { isOwner, AccessCode } from "../../utils/orgs";
|
||||
import type { OrgData } from "../../utils/orgs";
|
||||
import type { CurrentUser } from "../../types/user";
|
||||
|
||||
type Tab = "information" | "members";
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* ```ts
|
||||
@ -37,131 +38,142 @@ export class OrgSettings extends LiteElement {
|
||||
@property({ type: Object })
|
||||
org!: OrgData;
|
||||
|
||||
@property({ type: String })
|
||||
activePanel: Tab = "information";
|
||||
|
||||
@property({ type: Boolean })
|
||||
isAddingMember = false;
|
||||
|
||||
@state()
|
||||
private successfullyInvitedEmail?: string;
|
||||
|
||||
@state()
|
||||
private isEditingOrgName = false;
|
||||
private isAddMemberFormVisible = false;
|
||||
|
||||
@state()
|
||||
private isSavingOrgName = false;
|
||||
|
||||
@state()
|
||||
private isSubmittingInvite = false;
|
||||
|
||||
private get tabLabels() {
|
||||
return {
|
||||
information: msg("Org Information"),
|
||||
members: msg("Members"),
|
||||
};
|
||||
}
|
||||
|
||||
async willUpdate(changedProperties: Map<string, any>) {
|
||||
if (changedProperties.has("isAddingMember") && this.isAddingMember) {
|
||||
this.successfullyInvitedEmail = undefined;
|
||||
this.isAddMemberFormVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.isAddingMember) {
|
||||
return this.renderAddMember();
|
||||
}
|
||||
return html`<header class="mb-5">
|
||||
<h2 class="text-xl leading-10">${msg("Org Settings")}</h2>
|
||||
</header>
|
||||
|
||||
return html`<btrix-section-heading
|
||||
>${msg("Org Information")}</btrix-section-heading
|
||||
>
|
||||
<section class="mt-5 mb-10">${this.renderOrgName()}</section>
|
||||
<btrix-section-heading>${msg("Org Members")}</btrix-section-heading>
|
||||
<section class="mt-5">${this.renderMembers()}</section>`;
|
||||
}
|
||||
|
||||
private renderOrgName() {
|
||||
return html`<form
|
||||
@submit=${this.onOrgNameSubmit}
|
||||
@reset=${() => (this.isEditingOrgName = false)}
|
||||
>
|
||||
<div class="flex items-end">
|
||||
<div class="flex-1 mr-3">
|
||||
<sl-input
|
||||
name="orgName"
|
||||
label=${msg("Org Name")}
|
||||
autocomplete="off"
|
||||
value=${this.org.name}
|
||||
?readonly=${!this.isEditingOrgName}
|
||||
?required=${this.isEditingOrgName}
|
||||
></sl-input>
|
||||
</div>
|
||||
<div class="flex-0">
|
||||
<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.isEditingOrgName,
|
||||
this.activePanel === "members",
|
||||
() => html`
|
||||
<sl-button type="reset" class="mr-1">${msg("Cancel")}</sl-button>
|
||||
<sl-button
|
||||
type="submit"
|
||||
href=${`/orgs/${this.orgId}/settings/members?invite`}
|
||||
variant="primary"
|
||||
?disabled=${this.isSavingOrgName}
|
||||
?loading=${this.isSavingOrgName}
|
||||
>${msg("Save Changes")}</sl-button
|
||||
>
|
||||
`,
|
||||
() => html`
|
||||
<sl-button
|
||||
@click=${(e: MouseEvent) => {
|
||||
this.isEditingOrgName = true;
|
||||
(e.target as SlButton)
|
||||
.closest("form")
|
||||
?.querySelector("sl-input")
|
||||
?.focus();
|
||||
}}
|
||||
>${msg("Edit")}</sl-button
|
||||
size="small"
|
||||
@click=${this.navLink}
|
||||
>${msg("Invite New Member")}</sl-button
|
||||
>
|
||||
`
|
||||
)}
|
||||
</header>
|
||||
${this.renderTab("information", "settings")}
|
||||
${this.renderTab("members", "settings/members")}
|
||||
|
||||
<btrix-tab-panel name="information"
|
||||
>${this.renderInformation()}</btrix-tab-panel
|
||||
>
|
||||
<btrix-tab-panel name="members"
|
||||
>${this.renderMembers()}</btrix-tab-panel
|
||||
>
|
||||
</btrix-tab-list>`;
|
||||
}
|
||||
|
||||
private renderTab(name: Tab, path: string) {
|
||||
const isActive = name === this.activePanel;
|
||||
return html`
|
||||
<a
|
||||
slot="nav"
|
||||
href=${`/orgs/${this.orgId}/${path}`}
|
||||
class="block font-medium rounded-sm mb-2 mr-2 p-2 transition-all ${isActive
|
||||
? "text-blue-600 bg-blue-50 shadow-sm"
|
||||
: "text-neutral-600 hover:bg-neutral-50"}"
|
||||
@click=${this.navLink}
|
||||
aria-selected=${isActive}
|
||||
>
|
||||
${this.tabLabels[name]}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderInformation() {
|
||||
return html`<div class="rounded border p-5">
|
||||
<form @submit=${this.onOrgNameSubmit}>
|
||||
<div class="flex items-end">
|
||||
<div class="flex-1 mr-3">
|
||||
<sl-input
|
||||
name="orgName"
|
||||
size="small"
|
||||
label=${msg("Org Name")}
|
||||
autocomplete="off"
|
||||
value=${this.org.name}
|
||||
required
|
||||
></sl-input>
|
||||
</div>
|
||||
<div class="flex-0">
|
||||
<sl-button
|
||||
type="submit"
|
||||
size="small"
|
||||
variant="primary"
|
||||
?disabled=${this.isSavingOrgName}
|
||||
?loading=${this.isSavingOrgName}
|
||||
>${msg("Save Changes")}</sl-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>`;
|
||||
</form>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderMembers() {
|
||||
let successMessage;
|
||||
|
||||
if (this.successfullyInvitedEmail) {
|
||||
successMessage = html`
|
||||
<div class="my-3">
|
||||
<btrix-alert variant="success"
|
||||
>${msg(
|
||||
str`Successfully invited ${this.successfullyInvitedEmail}`
|
||||
)}</btrix-alert
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`${successMessage}
|
||||
|
||||
<div class="text-right">
|
||||
<sl-button
|
||||
href=${`/orgs/${this.orgId}/settings/members?invite`}
|
||||
@click=${this.navLink}
|
||||
>${msg("Add Member")}</sl-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div role="table">
|
||||
<div class="border-b" role="rowgroup">
|
||||
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="w-1/2 px-3 py-2" role="columnheader" aria-sort="none">
|
||||
<div class="flex-1 px-3 py-1" role="columnheader" aria-sort="none">
|
||||
${msg("Name")}
|
||||
</div>
|
||||
<div class="px-3 py-2" role="columnheader" aria-sort="none">
|
||||
<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, { name, role }]) => html`
|
||||
<div class="border-b flex" role="row">
|
||||
<div class="w-1/2 p-3" role="cell">
|
||||
${name ||
|
||||
html`<span class="text-gray-400">${msg("Member")}</span>`}
|
||||
</div>
|
||||
<div class="p-3" role="cell">
|
||||
${isOwner(role)
|
||||
([id, user]) => html`
|
||||
<div
|
||||
class="border-b last:border-none flex items-center"
|
||||
role="row"
|
||||
>
|
||||
<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")
|
||||
: role === AccessCode.crawler
|
||||
: user.role === AccessCode.crawler
|
||||
? msg("Crawler")
|
||||
: msg("Viewer")}
|
||||
</div>
|
||||
@ -169,43 +181,99 @@ export class OrgSettings extends LiteElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private renderAddMember() {
|
||||
return html`
|
||||
<div class="mb-5">
|
||||
<a
|
||||
class="text-neutral-500 hover:text-neutral-600 text-sm font-medium"
|
||||
href=${`/orgs/${this.orgId}/settings/members`}
|
||||
@click=${this.navLink}
|
||||
>
|
||||
<sl-icon
|
||||
name="arrow-left"
|
||||
class="inline-block align-middle"
|
||||
></sl-icon>
|
||||
<span class="inline-block align-middle"
|
||||
>${msg("Back to Settings")}</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-lg p-4 md:p-8 md:pt-6">
|
||||
<h2 class="text-lg font-medium mb-4">${msg("Add New Member")}</h2>
|
||||
<btrix-org-invite-form
|
||||
@success=${this.onInviteSuccess}
|
||||
@cancel=${() => this.navTo(`/orgs/${this.orgId}/settings/members`)}
|
||||
.authState=${this.authState}
|
||||
.orgId=${this.orgId}
|
||||
></btrix-org-invite-form>
|
||||
<btrix-dialog
|
||||
label=${msg("Invite New Member")}
|
||||
?open=${this.isAddingMember}
|
||||
@sl-request-close=${this.hideInviteDialog}
|
||||
@sl-show=${() => (this.isAddMemberFormVisible = true)}
|
||||
@sl-after-hide=${() => (this.isAddMemberFormVisible = false)}
|
||||
>
|
||||
${this.isAddMemberFormVisible ? this.renderInviteForm() : ""}
|
||||
</btrix-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
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>
|
||||
</sl-select>`;
|
||||
}
|
||||
|
||||
private hideInviteDialog() {
|
||||
this.navTo(`/orgs/${this.orgId}/settings/members`);
|
||||
}
|
||||
|
||||
private renderInviteForm() {
|
||||
return html`
|
||||
<form
|
||||
id="orgInviteForm"
|
||||
@submit=${this.onOrgInviteSubmit}
|
||||
@reset=${this.hideInviteDialog}
|
||||
>
|
||||
<div class="mb-5">
|
||||
<sl-input
|
||||
id="inviteEmail"
|
||||
name="inviteEmail"
|
||||
type="email"
|
||||
label=${msg("Email")}
|
||||
placeholder=${msg("org-member@email.com", {
|
||||
desc: "Placeholder text for email to invite",
|
||||
})}
|
||||
required
|
||||
>
|
||||
</sl-input>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<sl-radio-group
|
||||
name="role"
|
||||
label="Permission"
|
||||
value=${AccessCode.viewer}
|
||||
>
|
||||
<sl-radio value=${AccessCode.owner}>
|
||||
${msg("Admin — Can create crawls and manage org members")}
|
||||
</sl-radio>
|
||||
<sl-radio value=${AccessCode.crawler}>
|
||||
${msg("Crawler — Can create crawls")}
|
||||
</sl-radio>
|
||||
<sl-radio value=${AccessCode.viewer}>
|
||||
${msg("Viewer — Can view crawls")}
|
||||
</sl-radio>
|
||||
</sl-radio-group>
|
||||
</div>
|
||||
</form>
|
||||
<div slot="footer" class="flex justify-between">
|
||||
<sl-button form="orgInviteForm" type="reset" size="small"
|
||||
>${msg("Cancel")}</sl-button
|
||||
>
|
||||
<sl-button
|
||||
form="orgInviteForm"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="small"
|
||||
?loading=${this.isSubmittingInvite}
|
||||
?disabled=${this.isSubmittingInvite}
|
||||
>${msg("Invite")}</sl-button
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async checkFormValidity(formEl: HTMLFormElement) {
|
||||
await this.updateComplete;
|
||||
return !formEl.querySelector("[data-invalid]");
|
||||
}
|
||||
|
||||
private async onOrgNameSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (!this.org) return;
|
||||
const { orgName } = serialize(e.target as HTMLFormElement);
|
||||
|
||||
const formEl = e.target as HTMLFormElement;
|
||||
if (!(await this.checkFormValidity(formEl))) return;
|
||||
|
||||
const { orgName } = serialize(formEl);
|
||||
|
||||
this.isSavingOrgName = true;
|
||||
|
||||
@ -222,19 +290,14 @@ export class OrgSettings extends LiteElement {
|
||||
duration: 8000,
|
||||
});
|
||||
|
||||
this.org = {
|
||||
...this.org,
|
||||
name: orgName as string,
|
||||
};
|
||||
|
||||
this.isEditingOrgName = false;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("update-user-info", { bubbles: true })
|
||||
);
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
} catch (e: any) {
|
||||
this.notify({
|
||||
message: msg("Sorry, couldn't update organization name at this time."),
|
||||
message: e.isApiError
|
||||
? e.message
|
||||
: msg("Sorry, couldn't update organization name at this time."),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
@ -243,12 +306,48 @@ export class OrgSettings extends LiteElement {
|
||||
this.isSavingOrgName = false;
|
||||
}
|
||||
|
||||
private onInviteSuccess(
|
||||
event: CustomEvent<{ inviteEmail: string; isExistingUser: boolean }>
|
||||
) {
|
||||
this.successfullyInvitedEmail = event.detail.inviteEmail;
|
||||
async onOrgInviteSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
this.navTo(`/orgs/${this.orgId}/settings/members`);
|
||||
const formEl = e.target as HTMLFormElement;
|
||||
if (!(await this.checkFormValidity(formEl))) return;
|
||||
|
||||
const { inviteEmail, role } = serialize(formEl);
|
||||
|
||||
this.isSubmittingInvite = true;
|
||||
|
||||
try {
|
||||
const data = await this.apiFetch(
|
||||
`/orgs/${this.orgId}/invite`,
|
||||
this.authState!,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: inviteEmail,
|
||||
role: Number(role),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
this.notify({
|
||||
message: msg(str`Successfully invited ${inviteEmail}.`),
|
||||
variant: "success",
|
||||
icon: "check2-circle",
|
||||
duration: 8000,
|
||||
});
|
||||
|
||||
this.hideInviteDialog();
|
||||
} catch (e: any) {
|
||||
this.notify({
|
||||
message: e.isApiError
|
||||
? e.message
|
||||
: msg("Sorry, couldn't invite user at this time."),
|
||||
variant: "danger",
|
||||
icon: "exclamation-octagon",
|
||||
});
|
||||
}
|
||||
|
||||
this.isSubmittingInvite = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user