feat: Improve UX when user doesn't belong to any orgs (#1953)

Directs user that doesn't belong to any orgs to account settings page,
with banner.

Also contains some minor out-of-scope changes:
- Refactors `isAdmin` key to `isSuperAdmin` for more legibility on
whether current user is superadmin or regular user without orgs
- Adds "cancel" button to change password form
This commit is contained in:
sua yoo 2024-07-23 16:51:28 -07:00 committed by GitHub
parent a02f7a6826
commit 8c4e481bd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 69 additions and 32 deletions

View File

@ -257,6 +257,15 @@ export class AccountSettings extends LiteElement {
str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.`,
)}
</p>
<sl-button
type="reset"
size="small"
variant="text"
class="mx-2"
@click=${() => (this.isChangingPassword = false)}
>
${msg("Cancel")}
</sl-button>
<sl-button
type="submit"
size="small"
@ -270,7 +279,7 @@ export class AccountSettings extends LiteElement {
</form>
`,
() => html`
<div class="flex items-center justify-between px-4 py-3">
<div class="flex items-center justify-between px-4 py-2.5">
<h2 class="text-lg font-semibold leading-none">
${msg("Password")}
</h2>

View File

@ -103,7 +103,7 @@ describe("browsertrix-app", () => {
email: "test-user@example.com",
name: "Test User",
isVerified: false,
isAdmin: false,
isSuperAdmin: false,
orgs: [
{
id: "test_org_id",

View File

@ -1,4 +1,4 @@
import { localized, msg } from "@lit/localize";
import { localized, msg, str } from "@lit/localize";
import type { SlDialog } from "@shoelace-style/shoelace";
import { nothing, render, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
@ -149,7 +149,7 @@ export class App extends LiteElement {
if (
orgs.length &&
!this.appState.userInfo!.isAdmin &&
!this.appState.userInfo!.isSuperAdmin &&
!this.appState.orgSlug
) {
const firstOrg = orgs[0].slug;
@ -232,7 +232,7 @@ export class App extends LiteElement {
render() {
return html`
<div class="min-w-screen flex min-h-screen flex-col">
${this.renderNavBar()}
${this.renderNavBar()} ${this.renderAlertBanner()}
<main class="relative flex flex-auto">${this.renderPage()}</main>
<div class="border-t border-neutral-100">${this.renderFooter()}</div>
</div>
@ -247,10 +247,41 @@ export class App extends LiteElement {
`;
}
private renderAlertBanner() {
if (this.appState.userInfo?.orgs && !this.appState.userInfo.orgs.length) {
return this.renderNoOrgsBanner();
}
}
private renderNoOrgsBanner() {
return html`
<div class="border-b bg-slate-100 py-5">
<div class="mx-auto box-border w-full max-w-screen-desktop px-3">
<sl-alert variant="warning" open>
<sl-icon slot="icon" name="exclamation-triangle-fill"></sl-icon>
<strong class="block font-semibold">
${msg("Your account isn't quite set up yet")}
</strong>
${msg(
"You must belong to at least one org in order to access Browsertrix features.",
)}
${this.appState.settings?.salesEmail
? msg(
str`If you haven't received an invitation to an org, please contact us at ${this.appState.settings.salesEmail}.`,
)
: msg(
str`If you haven't received an invitation to an org, please contact your Browsertrix administrator.`,
)}
</sl-alert>
</div>
</div>
`;
}
private renderNavBar() {
const isAdmin = this.appState.userInfo?.isAdmin;
const isSuperAdmin = this.appState.userInfo?.isSuperAdmin;
let homeHref = "/";
if (!isAdmin && this.appState.orgSlug) {
if (!isSuperAdmin && this.appState.orgSlug) {
homeHref = `/orgs/${this.appState.orgSlug}`;
}
@ -263,7 +294,7 @@ export class App extends LiteElement {
aria-label="home"
href=${homeHref}
@click=${(e: MouseEvent) => {
if (isAdmin) {
if (isSuperAdmin) {
this.clearSelectedOrg();
}
this.navLink(e);
@ -271,7 +302,7 @@ export class App extends LiteElement {
>
<img class="h-6" alt="Browsertrix logo" src=${brandLockupColor} />
</a>
${isAdmin
${isSuperAdmin
? html`
<div
class="grid grid-flow-col items-center gap-3 text-xs md:gap-5 md:text-sm"
@ -316,7 +347,7 @@ export class App extends LiteElement {
<sl-icon slot="prefix" name="gear"></sl-icon>
${msg("Account Settings")}
</sl-menu-item>
${this.appState.userInfo?.isAdmin
${this.appState.userInfo?.isSuperAdmin
? html` <sl-menu-item
@click=${() => this.navigate(ROUTES.usersInvite)}
>
@ -387,7 +418,7 @@ export class App extends LiteElement {
}}
>
${when(
this.appState.userInfo.isAdmin,
this.appState.userInfo.isSuperAdmin,
() => html`
<sl-menu-item
type="checkbox"
@ -415,7 +446,7 @@ export class App extends LiteElement {
private renderMenuUserInfo() {
if (!this.appState.userInfo) return;
if (this.appState.userInfo.isAdmin) {
if (this.appState.userInfo.isSuperAdmin) {
return html`
<div class="mb-2">
<sl-tag class="uppercase" variant="primary" size="small"
@ -614,7 +645,7 @@ export class App extends LiteElement {
case "usersInvite": {
if (this.appState.userInfo) {
if (this.appState.userInfo.isAdmin) {
if (this.appState.userInfo.isSuperAdmin) {
return html`<btrix-users-invite
class="mx-auto box-border w-full max-w-screen-desktop p-2 md:py-8"
.authState="${this.authService.authState}"
@ -631,7 +662,7 @@ export class App extends LiteElement {
case "crawls":
case "crawl": {
if (this.appState.userInfo) {
if (this.appState.userInfo.isAdmin) {
if (this.appState.userInfo.isSuperAdmin) {
return html`<btrix-crawls
class="w-full"
@notify=${this.onNotify}

View File

@ -60,8 +60,12 @@ export class Home extends LiteElement {
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("slug") && this.slug) {
this.navTo(`/orgs/${this.slug}`);
} else if (changedProperties.has("authState") && this.authState) {
void this.fetchOrgs();
} else if (changedProperties.has("userInfo") && this.userInfo) {
if (this.userInfo.isSuperAdmin) {
void this.fetchOrgs();
} else {
this.navTo(`/account/settings`);
}
}
}
@ -71,7 +75,7 @@ export class Home extends LiteElement {
const orgListUpdated = changedProperties.has("orgList") && this.orgList;
const userInfoUpdated = changedProperties.has("userInfo") && this.userInfo;
if (orgListUpdated || userInfoUpdated) {
if (this.userInfo?.isAdmin && this.orgList && !this.orgList.length) {
if (this.userInfo?.isSuperAdmin && this.orgList && !this.orgList.length) {
this.isAddingOrg = true;
}
}
@ -89,7 +93,7 @@ export class Home extends LiteElement {
let title: string | undefined;
let content: TemplateResult<1> | undefined;
if (this.userInfo.isAdmin) {
if (this.userInfo.isSuperAdmin) {
title = msg("Welcome");
content = this.renderAdminOrgs();
} else {

View File

@ -197,16 +197,7 @@ export class Org extends LiteElement {
if (org) {
this.navTo(`/orgs/${org.slug}`);
} else {
// Handle edge case where user does not belong
// to any orgs but is attempting to log in
// TODO check if hosted instance and show support email if so
this.notify({
message: msg(
"You must belong to at least one org in order to log in. Please contact your Browsertrix admin to resolve the issue.",
),
variant: "danger",
icon: "exclamation-octagon",
});
this.navTo(`/account/settings`);
}
return;
@ -376,7 +367,7 @@ export class Org extends LiteElement {
path: "browser-profiles",
}),
)}
${when(this.isAdmin || this.userInfo?.isAdmin, () =>
${when(this.isAdmin || this.userInfo?.isSuperAdmin, () =>
this.renderNavTab({
tabName: "settings",
label: msg("Org Settings"),

View File

@ -30,6 +30,6 @@ export type CurrentUser = {
email: string;
name: string;
isVerified: boolean;
isAdmin: boolean;
isSuperAdmin: boolean;
orgs: UserOrg[];
};

View File

@ -54,7 +54,9 @@ export function maxLengthValidator(maxLength: number): MaxLengthValidator {
el.setCustomValidity(
isInvalid
? msg(str`Please shorten this text to ${maxLength} or fewer characters.`)
? msg(
str`Please shorten this text to ${maxLength} or fewer characters.`,
)
: "",
);

View File

@ -7,7 +7,7 @@ export function formatAPIUser(userData: APIUser): CurrentUser {
email: userData.email,
name: userData.name,
isVerified: userData.is_verified,
isAdmin: userData.is_superuser,
isSuperAdmin: userData.is_superuser,
orgs: userData.orgs,
};
}