Skip to content

Commit

Permalink
Initial OrgMenu work
Browse files Browse the repository at this point in the history
  • Loading branch information
allanlasser committed Apr 12, 2024
1 parent 7e90406 commit 4761812
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 63 deletions.
49 changes: 48 additions & 1 deletion src/lib/api/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Maybe, User, Org } from "@/api/types";
import { BASE_API_URL } from "@/config/config.js";
import { BASE_API_URL, SQUARELET_BASE } from "@/config/config.js";
import { fetchAll } from "../utils/api";
import { alphabetizeUsers } from "../utils/accounts";

type Fetch = typeof globalThis.fetch;

Expand All @@ -21,3 +23,48 @@ export async function getOrg(fetch: Fetch, id: number): Promise<Org> {
const resp = await fetch(endpoint, { credentials: "include" });
return resp.json();
}

/** Returns a list of users that belong to the given organization.
* Users are returned sorted alphabetically and grouped by admin status.
* If `myId` is provided, then that user is removed from the sorted list.
*/
export async function getOrgUsers(fetch: Fetch, orgId: number, myId?: number) {
if (!orgId) return [];
const endpoint = new URL("users/", BASE_API_URL);
endpoint.searchParams.set("expand", "organization");
endpoint.searchParams.set("organizations", String(orgId));
try {
const users = await fetchAll<User>(fetch, endpoint);
// Sort by admin status, then username
const adminUsers = users
.filter((u) => u.admin_organizations?.includes(orgId))
.sort(alphabetizeUsers);
const regularUsers = users
.filter((u) => !adminUsers.includes(u))
.sort(alphabetizeUsers);
// Remove me from the user list, if id is provided
return [...adminUsers, ...regularUsers].filter((u) => u.id !== myId);
} catch (e) {
return [];
}
}

export function getUpgradeUrl(org) {
if (org.individual) {
// Redirect the user to their Squarelet account settings
return SQUARELET_BASE + `/users/~payment/`;
}
// Redirect the user to the Squarelet organization settings
return SQUARELET_BASE + `/organizations/${org.slug}/payment/`;
}

export async function triggerPremiumUpgradeFlow(org) {
if (org) {
window?.open(getUpgradeUrl(org));
}
}

// TODO: Handle flow for purchasing premium credits (#342)
export async function triggerCreditPurchaseFlow() {
alert("Purchase Credits!");
}
2 changes: 1 addition & 1 deletion src/lib/components/MainLayout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
</slot>
<SignedIn>
<Flex>
<OrgMenu org={$org} />
<OrgMenu org={$org} user={$me} />
<UserMenu user={$me} />
</Flex>
<Button slot="signedOut" mode="primary" href={SIGN_IN_URL}>
Expand Down
31 changes: 31 additions & 0 deletions src/lib/components/common/Avatar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
export let src: string = null;
export let alt: string = "";
</script>

<div class="avatar">
{#if src}
<img {src} {alt} />
{:else}
<slot name="fallback" />
{/if}
</div>

<style>
.avatar {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
width: var(--size, 1.5rem);
height: var(--size, 1.5rem);
border-radius: var(--radius, calc(var(--size, 1.5rem) / 2));
background: var(--background, var(--gray-2, #d8dee2));
fill: var(--gray-4);
}
.avatar img {
height: 100%;
width: 100%;
}
</style>
19 changes: 15 additions & 4 deletions src/lib/components/common/Button.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
export let href: string = null;
export let mode: "standard" | "primary" | "ghost" = "standard";
export let mode: "standard" | "primary" | "premium" | "ghost" = "standard";
export let full = false;
export let disabled = false;
Expand All @@ -10,11 +11,11 @@
</script>

{#if href}
<a {href} {title} on:click class={mode}>
<a {href} {title} on:click class={mode} class:full>
<slot>{label}</slot>
</a>
{:else}
<button {title} on:click class={mode} {disabled} {type}>
<button {title} on:click class={mode} {disabled} {type} class:full>
<slot>{label}</slot>
</button>
{/if}
Expand Down Expand Up @@ -47,11 +48,17 @@
}
.primary {
background: var(--blue-3, #4294f0);
border-color: var(--blue-4, #1367d0);
background: var(--primary, #4294f0);
box-shadow: 0px 2px 0px 0px var(--blue-4, #1367d0);
}
.premium {
background: var(--green);
border-color: var(--green-dark);
box-shadow: 0px 2px 0px 0px var(--green-dark);
}
.ghost {
background: none;
border: none;
Expand Down Expand Up @@ -83,4 +90,8 @@
.ghost:disabled:hover {
background: transparent;
}
.full {
display: flex;
}
</style>
156 changes: 128 additions & 28 deletions src/lib/components/navigation/OrgMenu.svelte
Original file line number Diff line number Diff line change
@@ -1,19 +1,134 @@
<script lang="ts">
import type { Org } from "@/api/types";
import { ChevronDown16 } from "svelte-octicons";
import { _, locale } from "svelte-i18n";
import type { User, Org } from "@/api/types";
import {
ChevronDown16,
Hourglass24,
Organization16,
People24,
Person16,
} from "svelte-octicons";
import Dropdown, { closeDropdown } from "@/common/Dropdown2.svelte";
import Menu from "@/common/Menu.svelte";
import MenuInsert from "@/common/MenuInsert.svelte";
import CreditMeter, {
formatResetDate,
} from "@/premium-credits/CreditMeter.svelte";
import SidebarItem from "../sidebar/SidebarItem.svelte";
import { isOrgAdmin, isPremiumOrg } from "@/lib/utils/accounts";
import Button from "../common/Button.svelte";
import {
getOrg,
getOrgUsers,
triggerPremiumUpgradeFlow,
} from "@/lib/api/accounts";
import Empty from "../common/Empty.svelte";
import { userUrl } from "@/search/search";
import Avatar from "../common/Avatar.svelte";
import Error from "../common/Error.svelte";
export let user: User;
export let org: Org;
let orgMemberPromise = getOrgUsers(globalThis.fetch, org.id, user.id);
let orgSwitchPromise = Promise.all(
user.organizations.map((id) => getOrg(globalThis.fetch, id)),
);
</script>

<div class="container">
<div class="org">
<div class="avatar">
<img alt="MuckRock's avatar" src={org.avatar_url} />
</div>
<p class="name">{org.name}</p>
<Dropdown id="organization">
<SidebarItem slot="title">
<Avatar src={org.avatar_url} alt={`${org.name} logo`} --radius="0.125rem">
<Organization16 slot="fallback" />
</Avatar>
<span class="name">{org.name}</span>
<span class="arrow"><ChevronDown16 /></span>
</div>
</div>
</SidebarItem>
<Menu>
<!-- Premium Credits -->
{#if isPremiumOrg(org)}
<MenuInsert>
<CreditMeter
id="org-credits"
label={$_("authSection.credits.monthlyOrg")}
helpText={$_("authSection.credits.refreshOn", {
values: {
date: formatResetDate(org.credit_reset_date, $locale),
},
})}
value={org.monthly_credits}
max={org.monthly_credit_allowance}
/>
<!-- TODO: Support credit purchases (#342)
<CreditMeter
id="purchased-credits"
label={$_("authSection.credits.purchased")}
helpText={$_("authSection.credits.purchasedHelpText")}
value={activeOrg.purchased_credits}
/>
{#if user.admin_organizations.includes(activeOrg.id)}
<Button
premium
fullWidth={true}
label={$_("authSection.credits.purchaseCreditsButton")}
on:click={triggerCreditPurchaseFlow}
/>
{:else}
<p class="helpText">
{$_("authSection.credits.purchaseCreditsAdminOnly")}
</p>
{/if} -->
</MenuInsert>
<!-- Premium Upgrade -->
{:else if isOrgAdmin(user)}
<MenuInsert>
<div class="freeOrg">
<h3 class="heading">
{$_("authSection.premiumUpgrade.orgHeading")}
</h3>
<p class="description">
{$_("authSection.premiumUpgrade.orgDescription")}
</p>
<Button
label={$_("authSection.premiumUpgrade.cta")}
full
mode="premium"
on:click={() => triggerPremiumUpgradeFlow(org)}
/>
<div class="learnMore">
<Button
mode="ghost"
href="/help/premium"
on:click={() => closeDropdown("organization")}
>
{$_("authSection.premiumUpgrade.docs")}
</Button>
</div>
</div>
</MenuInsert>
{/if}
<!-- Org Member List -->
{#await orgMemberPromise}
<Empty icon={Hourglass24}>Loading org member list…</Empty>
{:then users}
{#each users as user}
<SidebarItem href={userUrl(user)} hover>
<Avatar src={user.avatar_url}>
<Person16 slot="fallback" />
</Avatar>
<span class="name">{user.name}</span>
{#if user.admin_organizations.includes(org.id)}
<span class="badge">{$_("authSection.org.adminRole")}</span>
{/if}
</SidebarItem>
{:else}
<Empty icon={People24}>{$_("authSection.org.memberListEmpty")}</Empty>
{/each}
{:catch}
<Error>{$_("authSection.org.memberListError")}</Error>
{/await}
</Menu>
</Dropdown>

<style>
.container {
Expand All @@ -22,35 +137,20 @@
gap: 0.5rem;
flex: 1 0 0;
}
.org {
display: flex;
align-items: center;
gap: 0.375rem;
flex: 1 0 0;
}
.avatar {
width: 1.5rem;
height: 1.5rem;
overflow: hidden;
border-radius: 0.75rem;
background: var(--gray-2, #d8dee2);
}
.avatar img {
display: block;
height: 100%;
width: 100%;
}
.name {
color: #233944;
font-family: var(--font-sans, "Source Sans Pro");
font-weight: var(--font-semibold, 600);
line-height: 1rem;
}
.arrow {
width: 1rem;
height: 1rem;
fill: #5c717c;
display: flex;
align-items: center;
}
/* unused
Expand Down
27 changes: 4 additions & 23 deletions src/lib/components/navigation/UserMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import Menu from "@/common/Menu.svelte";
import { SQUARELET_BASE, SIGN_OUT_URL } from "@/config/config.js";
import Avatar from "../common/Avatar.svelte";
export let user: User;
Expand All @@ -27,13 +28,9 @@

<Dropdown id={dropdownId} position="right">
<SidebarItem slot="title">
<div class="avatar">
{#if user.avatar_url}
<img src={user.avatar_url} alt="Avatar" />
{:else}
<Person16 fill="var(--gray-4)" />
{/if}
</div>
<Avatar src={user.avatar_url}>
<Person16 slot="fallback" />
</Avatar>
<span class="name">{user.name ?? user.username}</span>
<div class="dropdownArrow"><ChevronDown16 /></div>
</SidebarItem>
Expand All @@ -55,22 +52,6 @@
</Dropdown>

<style>
.avatar {
height: 1.5rem;
width: 1.5rem;
border-radius: 0.75rem;
background: var(--gray-2);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.avatar > img {
height: 100%;
width: 100%;
}
.dropdownArrow {
display: flex;
align-items: center;
Expand Down
Loading

0 comments on commit 4761812

Please sign in to comment.