Skip to content

Commit

Permalink
Merge pull request #1236 from Shelf-nu/729-feature-request-team-membe…
Browse files Browse the repository at this point in the history
…r-page

Created profile page for team's registered users
  • Loading branch information
DonKoko authored Aug 15, 2024
2 parents 9df3016 + b2430db commit c71d49b
Show file tree
Hide file tree
Showing 23 changed files with 973 additions and 314 deletions.
37 changes: 33 additions & 4 deletions app/components/assets/asset-custody-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { TeamMember, User } from "@prisma/client";
import { Link } from "@remix-run/react";
import { useUserRoleHelper } from "~/hooks/user-user-role-helper";
import {
PermissionAction,
PermissionEntity,
} from "~/utils/permissions/permission.data";
import { userHasPermission } from "~/utils/permissions/permission.validator.client";
import { tw } from "~/utils/tw";
import { resolveTeamMemberName } from "~/utils/user";
import { Button } from "../shared/button";
import { Card } from "../shared/card";

/**
Expand Down Expand Up @@ -30,16 +38,24 @@ export function CustodyCard({
dateDisplay: string;
custodian: {
name: string;
userId?: string | null;
user?: Partial<
Pick<User, "firstName" | "lastName" | "profilePicture" | "email">
> | null;
};
} | null;
}) {
const { roles } = useUserRoleHelper();
const canViewTeamMemberUsers = userHasPermission({
roles,
entity: PermissionEntity.teamMemberProfile,
action: PermissionAction.read,
});
/** We return null if user is selfService */
if (!hasPermission) {
if (!hasPermission || !custody) {
return null;
}
const fullName = resolveTeamMemberName(custody.custodian);

/* If custody is present, we render the card showing custody */
if (custody?.dateDisplay) {
Expand All @@ -57,9 +73,22 @@ export function CustodyCard({
<div>
<p className="">
In custody of{" "}
<span className="font-semibold">
{resolveTeamMemberName(custody.custodian)}
</span>
{canViewTeamMemberUsers && custody?.custodian?.userId ? (
<Button
to={`/settings/team/users/${custody.custodian.userId}/assets`}
variant="link"
className={tw(
"mt-px font-semibold text-gray-900 hover:text-gray-700 hover:underline",
"[&_.external-link-icon]:opacity-0 [&_.external-link-icon]:duration-100 [&_.external-link-icon]:ease-in-out [&_.external-link-icon]:hover:opacity-100"
)}
target="_blank"
>
{fullName}
</Button>
) : (
<span className="mt-px">{fullName}</span>
)}
<span className="font-semibold">{}</span>
</p>
<span>Since {custody.dateDisplay}</span>
</div>
Expand Down
44 changes: 39 additions & 5 deletions app/components/dashboard/custodians.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { useLoaderData } from "@remix-run/react";
import { useUserRoleHelper } from "~/hooks/user-user-role-helper";
import type { loader } from "~/routes/_layout+/dashboard";
import {
PermissionAction,
PermissionEntity,
} from "~/utils/permissions/permission.data";
import { userHasPermission } from "~/utils/permissions/permission.validator.client";
import { tw } from "~/utils/tw";
import { resolveTeamMemberName } from "~/utils/user";
import { EmptyState } from "./empty-state";
import { Button } from "../shared/button";
import { InfoTooltip } from "../shared/info-tooltip";
import { Table, Td, Tr } from "../table";

export default function CustodiansList() {
const { custodiansData } = useLoaderData<typeof loader>();

const { roles } = useUserRoleHelper();
const canViewTeamMemberUsers = userHasPermission({
roles,
entity: PermissionEntity.teamMemberProfile,
action: PermissionAction.read,
});
return (
<>
<div className="rounded-t border border-b-0 border-gray-200">
Expand All @@ -33,7 +46,11 @@ export default function CustodiansList() {
<tbody>
{custodiansData.map((cd) => (
<Tr key={cd.id} className="h-[72px]">
<Row custodian={cd.custodian} count={cd.count} />
<Row
custodian={cd.custodian}
count={cd.count}
canNavigate={canViewTeamMemberUsers}
/>
</Tr>
))}
{custodiansData.length < 5 &&
Expand All @@ -58,17 +75,22 @@ export default function CustodiansList() {
function Row({
custodian,
count,
canNavigate,
}: {
custodian: {
name: string;
userId?: string | null;
user?: {
firstName?: string | null;
lastName?: string | null;
profilePicture?: string | null;
} | null;
};
count: number;
/** Does the current user have permissions to acess this teamMember page */
canNavigate: boolean;
}) {
const teamMemberName = resolveTeamMemberName(custodian);
return (
<>
<Td className="w-full">
Expand All @@ -85,9 +107,21 @@ function Row({
alt={`${resolveTeamMemberName(custodian)}'s profile`}
/>
<div>
<span className="mt-px">
{resolveTeamMemberName(custodian)}
</span>
{canNavigate && custodian.userId ? (
<Button
to={`/settings/team/users/${custodian.userId}/assets`}
variant="link"
className={tw(
"mt-px font-medium text-gray-900 hover:text-gray-700 hover:underline",
"[&_.external-link-icon]:opacity-0 [&_.external-link-icon]:duration-100 [&_.external-link-icon]:ease-in-out [&_.external-link-icon]:hover:opacity-100"
)}
target="_blank"
>
{teamMemberName}
</Button>
) : (
<span className="mt-px">{teamMemberName}</span>
)}
<span className="block text-gray-600">{count} Assets</span>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions app/components/layout/breadcrumbs/breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export function Breadcrumb({
breadcrumb =
<span className="single-crumb">{match?.data?.kit?.name}</span> ||
"Not found";
} else if (match?.data?.userName) {
breadcrumb =
<span className="single-crumb">{match?.data?.userName}</span> ||
"Not found";
} else {
breadcrumb =
<span className="single-crumb">{match?.data?.asset?.title}</span> ||
Expand Down
18 changes: 12 additions & 6 deletions app/components/layout/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { tw } from "~/utils/tw";
import type { HeaderData } from "./types";
import { Breadcrumbs } from "../breadcrumbs";

type SlotKeys = "left-of-title";
type SlotKeys = "left-of-title" | "right-of-title" | "append-to-title";

export default function Header({
title = null,
Expand All @@ -21,13 +21,15 @@ export default function Header({
/** Pass a title to replace the default route title set in the loader
* This is very useful for interactive adjustments of the title
*/
title?: string | null;
title?: string | ReactNode | null;
children?: React.ReactNode;
subHeading?: React.ReactNode;
hidePageDescription?: boolean;
hideBreadcrumbs?: boolean;
classNames?: string;
slots?: Record<SlotKeys, ReactNode>;
slots?: {
[key in SlotKeys]?: ReactNode;
};
}) {
const data = useLoaderData<{
header?: HeaderData;
Expand Down Expand Up @@ -57,15 +59,19 @@ export default function Header({
<div className={`flex items-center border-b border-gray-200 px-4 py-3`}>
{slots?.["left-of-title"] || null}
<div>
<Heading as="h2" className="break-all text-[20px] font-semibold">
{title || header?.title}
</Heading>
<div className="flex items-center gap-2">
<Heading as="h2" className="break-all text-[20px] font-semibold">
{title || header?.title}
</Heading>
{slots?.["append-to-title"] || null}
</div>
{subHeading ? (
<SubHeading>{subHeading}</SubHeading>
) : (
header?.subHeading && <SubHeading>{header.subHeading}</SubHeading>
)}
</div>
{slots?.["right-of-title"] || null}
</div>
)}
</header>
Expand Down
13 changes: 11 additions & 2 deletions app/components/layout/sidebar/menu-items.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { FetcherWithComponents } from "@remix-run/react";
import { NavLink, useLoaderData, useLocation } from "@remix-run/react";
import {
NavLink,
useLoaderData,
useLocation,
useMatches,
} from "@remix-run/react";
import { motion } from "framer-motion";
import { useAtom } from "jotai";
import { switchingWorkspaceAtom } from "~/atoms/switching-workspace";
Expand All @@ -20,7 +25,10 @@ const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents<any> }) => {
const { menuItemsTop, menuItemsBottom } = useMainMenuItems();
const location = useLocation();
const [workspaceSwitching] = useAtom(switchingWorkspaceAtom);

const matches = useMatches();
const currentRoute = matches.at(-1);
// @ts-expect-error
const handle = currentRoute?.handle?.name;
return (
<div className="flex h-full flex-col">
<div className="flex h-full flex-col justify-between">
Expand Down Expand Up @@ -63,6 +71,7 @@ const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents<any> }) => {
: "my-0 text-gray-500 hover:bg-gray-50 hover:text-gray-500",
/** We need to do this becasue of a special way we handle the bookings link that doesnt allow us to use NavLink currently */
location.pathname.includes(item.to) &&
handle !== "$userId.bookings" &&
!location.pathname.includes("assets")
? "active bg-primary-50 text-primary-600"
: ""
Expand Down
14 changes: 8 additions & 6 deletions app/components/list/list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ export interface ListItemData {
[x: string]: any;
}

export interface ListItemProps {
item: ListItemData;
children: React.ReactNode;
navigate?: (id: string, item: ListItemData) => void;
className?: string;
}

export const ListItem = ({
item,
children,
navigate,
className,
}: {
item: ListItemData;
children: React.ReactNode;
navigate?: (id: string, item: ListItemData) => void;
className?: string;
}) => (
}: ListItemProps) => (
<tr
// onClick={navigate ? () => navigate(item.id, item) : undefined}
onClick={(event) => {
Expand Down
4 changes: 3 additions & 1 deletion app/components/shared/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ export const Button = React.forwardRef<HTMLElement, ButtonProps>(
)}
>
<span>{children}</span>{" "}
{newTab && <ExternalLinkIcon className="mt-px" />}
{newTab && (
<ExternalLinkIcon className="external-link-icon mt-px" />
)}
</span>
) : null}
</Component>
Expand Down
33 changes: 25 additions & 8 deletions app/components/workspace/users-actions-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ReactNode } from "react";
import type { InviteStatuses, User } from "@prisma/client";
import { useFetcher } from "@remix-run/react";
import {
Expand All @@ -23,28 +24,43 @@ export function TeamUsersActionsDropdown({
name,
teamMemberId,
email,
isSSO,
customTrigger,
}: {
userId: User["id"] | null;
inviteStatus: InviteStatuses;
name?: string;
teamMemberId?: string;
email: string;
isSSO: boolean;
customTrigger?: (disabled: boolean) => ReactNode;
}) {
const fetcher = useFetcher();
const disabled = isFormProcessing(fetcher.state);
const { ref, open, setOpen } = useControlledDropdownMenu();

return (
/** Most users will have an invite, however we have to handle SSO case:
*
* 1. If the user has an invite, we show the "Resend invite" and "Cancel invite" buttons.
* 2. If the user has accepted the invite or doesn't have an invite but has userId(SSO), we show the "Revoke access" button.
*/
const hasInvite = !!inviteStatus;

return hasInvite || (!hasInvite && isSSO) ? (
<>
<DropdownMenu
modal={false}
onOpenChange={(open) => setOpen(open)}
open={open}
>
<DropdownMenuTrigger className="size-6 pr-2 outline-none focus-visible:border-0">
<i className="inline-block px-3 py-0 text-gray-400 ">
{disabled ? <Spinner className="size-4" /> : <VerticalDotsIcon />}
</i>
<DropdownMenuTrigger className="w-full " asChild>
{customTrigger ? (
customTrigger(disabled)
) : (
<Button variant="tertiary" width="full" className="border-0 pr-0">
{disabled ? <Spinner className="size-4" /> : <VerticalDotsIcon />}
</Button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
Expand All @@ -59,7 +75,7 @@ export function TeamUsersActionsDropdown({
}}
>
{/* Only show resend button if the invite is not accepted */}
{inviteStatus !== "ACCEPTED" ? (
{inviteStatus && inviteStatus !== "ACCEPTED" ? (
<>
<input type="hidden" name="name" value={name} />
<input type="hidden" name="email" value={email} />
Expand Down Expand Up @@ -92,7 +108,8 @@ export function TeamUsersActionsDropdown({
</Button>
</>
) : null}
{inviteStatus === "ACCEPTED" ? (
{(hasInvite && inviteStatus === "ACCEPTED") ||
(!hasInvite && isSSO) ? (
<>
{userId ? (
<input type="hidden" name="userId" value={userId} />
Expand All @@ -116,5 +133,5 @@ export function TeamUsersActionsDropdown({
</DropdownMenuContent>
</DropdownMenu>
</>
);
) : null;
}
6 changes: 6 additions & 0 deletions app/hooks/use-main-menu-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export function useMainMenuItems() {
to: "bookings",
title: "Bookings",
},
{
icon: <Icon icon="user" />,
to: "/settings/team",
title: "Team",
},
];
let menuItemsBottom = [
{
Expand Down Expand Up @@ -80,6 +85,7 @@ export function useMainMenuItems() {
"tags",
"locations",
"settings",
"team",
];
menuItemsTop = menuItemsTop.filter(
(item) => !itemsToRemove.includes(item.to)
Expand Down
Loading

0 comments on commit c71d49b

Please sign in to comment.