Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/types/src/issues/issue.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {TIssuePriorities} from "../issues";
import {TIssueAttachment} from "./issue_attachment";
import {TIssueLink} from "./issue_link";
import {TIssueReaction} from "./issue_reaction";
import { TIssuePriorities } from "../issues";
import { TIssueAttachment } from "./issue_attachment";
import { TIssueLink } from "./issue_link";
import { TIssueReaction } from "./issue_reaction";

// new issue structure types

Expand Down Expand Up @@ -42,7 +42,7 @@ export type TBaseIssue = {
export type TIssue = TBaseIssue & {
description_html?: string;
is_subscribed?: boolean;
parent?: Partial<TIssue>;
parent?: partial<TIssue>;
issue_reactions?: TIssueReaction[];
issue_attachment?: TIssueAttachment[];
issue_link?: TIssueLink[];
Expand Down
23 changes: 16 additions & 7 deletions web/app/[workspaceSlug]/(projects)/profile/[userId]/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,53 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { ChevronDown, PanelRight } from "lucide-react";
import { IUserProfileProjectSegregation } from "@plane/types";
import { Breadcrumbs, CustomMenu } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
// components
import { ProfileIssuesFilter } from "@/components/profile";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { EUserWorkspaceRoles } from "@/constants/workspace";
import { cn } from "@/helpers/common.helper";
import { useAppTheme, useUser } from "@/hooks/store";

type TUserProfileHeader = {
userProjectsData: IUserProfileProjectSegregation | undefined;
type?: string | undefined;
showProfileIssuesFilter?: boolean;
};

export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const { type = undefined } = props;
const { userProjectsData, type = undefined, showProfileIssuesFilter } = props;
// router
const { workspaceSlug, userId } = useParams();
// store hooks
const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme();
const {
membership: { currentWorkspaceRole },
data: currentUser,
} = useUser();
// derived values
const AUTHORIZED_ROLES = [20, 15, 10];
const isAuthorized = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.VIEWER;

if (!currentWorkspaceRole) return null;

const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole);
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;

const userName = `${userProjectsData?.user_data?.first_name} ${userProjectsData?.user_data?.last_name}`;

const isCurrentUser = currentUser?.id === userId;

const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Activity`;

return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div className="flex w-full justify-between">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink href="/profile" label="Activity Overview" />}
/>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={breadcrumbLabel} disableTooltip />} />
</Breadcrumbs>
<div className="hidden md:flex md:items-center">{showProfileIssuesFilter && <ProfileIssuesFilter />}</div>
<div className="flex gap-4 md:hidden">
<CustomMenu
maxHeight={"md"}
Expand Down
61 changes: 44 additions & 17 deletions web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@

import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import useSWR from "swr";
// components
import { AppHeader, ContentWrapper } from "@/components/core";
import { ProfileSidebar } from "@/components/profile";
// constants
import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks
import { useUser } from "@/hooks/store";
import useSize from "@/hooks/use-window-size";
// local components
import { UserService } from "@/services/user.service";
import { UserProfileHeader } from "./header";
import { ProfileIssuesMobileHeader } from "./mobile-header";
import { ProfileNavbar } from "./navbar";

const userService = new UserService();

type Props = {
children: React.ReactNode;
};
Expand All @@ -30,6 +36,16 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
const {
membership: { currentWorkspaceRole },
} = useUser();

const windowSize = useSize();
const isSmallerScreen = windowSize[0] >= 768;

const { data: userProjectsData } = useSWR(
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
workspaceSlug && userId
? () => userService.getUserProfileProjectsSegregation(workspaceSlug.toString(), userId.toString())
: null
);
// derived values
const isAuthorized = currentWorkspaceRole && AUTHORIZED_ROLES.includes(currentWorkspaceRole);
const isAuthorizedPath =
Expand All @@ -43,25 +59,36 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
<>
{/* Passing the type prop from the current route value as we need the header as top most component.
TODO: We are depending on the route path to handle the mobile header type. If the path changes, this logic will break. */}
<AppHeader
header={<UserProfileHeader type={currentTab?.label} />}
mobileHeader={isIssuesTab && <ProfileIssuesMobileHeader />}
/>
<ContentWrapper>
<div className="h-full w-full flex md:overflow-hidden">
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
<ProfileNavbar isAuthorized={!!isAuthorized} showProfileIssuesFilter={isIssuesTab} />
{isAuthorized || !isAuthorizedPath ? (
<div className={`w-full overflow-hidden md:h-full`}>{children}</div>
) : (
<div className="grid h-full w-full place-items-center text-custom-text-200">
You do not have the permission to access this page.
<div className="h-full w-full md:flex md:overflow-hidden">
<div className="h-full w-full md:overflow-hidden">
<AppHeader
header={
<UserProfileHeader
type={currentTab?.label}
userProjectsData={userProjectsData}
showProfileIssuesFilter={isIssuesTab}
/>
}
mobileHeader={isIssuesTab && <ProfileIssuesMobileHeader />}
/>
<ContentWrapper>
<div className="h-full w-full flex flex-row md:flex-col md:overflow-hidden">
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
<ProfileNavbar isAuthorized={!!isAuthorized} />
{isAuthorized || !isAuthorizedPath ? (
<div className={`w-full overflow-hidden h-full`}>{children}</div>
) : (
<div className="grid h-full w-full place-items-center text-custom-text-200">
You do not have the permission to access this page.
</div>
)}
</div>
)}
</div>
<ProfileSidebar />
{!isSmallerScreen && <ProfileSidebar userProjectsData={userProjectsData} />}
</div>
</ContentWrapper>
</div>
</ContentWrapper>
{isSmallerScreen && <ProfileSidebar userProjectsData={userProjectsData} />}
</div>
</>
);
});
Expand Down
11 changes: 4 additions & 7 deletions web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,29 @@ import Link from "next/link";
import { useParams, usePathname } from "next/navigation";

// components
import { ProfileIssuesFilter } from "@/components/profile";
// constants
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";

type Props = {
isAuthorized: boolean;
showProfileIssuesFilter?: boolean;
};

export const ProfileNavbar: React.FC<Props> = (props) => {
const { isAuthorized, showProfileIssuesFilter } = props;
const { isAuthorized } = props;

const { workspaceSlug, userId } = useParams();
const pathname = usePathname();
const pathname = usePathname();

const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;

return (
<div className="sticky -top-0.5 z-10 hidden md:flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
<div className="sticky -top-0.5 hidden md:flex items-center justify-between gap-4 border-b border-custom-border-300 bg-custom-background-100 px-4 sm:px-5 md:static">
<div className="flex items-center overflow-x-scroll">
{tabsList.map((tab) => (
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
<span
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${
pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}/`
pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}`
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent"
}`}
Expand All @@ -38,7 +36,6 @@ const pathname = usePathname();
</Link>
))}
</div>
{showProfileIssuesFilter && <ProfileIssuesFilter />}
</div>
);
};
34 changes: 18 additions & 16 deletions web/core/components/profile/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,49 @@
"use client";

import { useEffect, useRef } from "react";
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import useSWR from "swr";
// icons
import { ChevronDown, Pencil } from "lucide-react";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// types
import { IUserProfileProjectSegregation } from "@plane/types";
// plane ui
import { Loader, Tooltip } from "@plane/ui";
// components
import { Logo } from "@/components/common";
// fetch-keys
import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
// helpers
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useAppTheme, useProject, useUser } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
// services
import { UserService } from "@/services/user.service";
// components
import { ProfileSidebarTime } from "./time";

// services
const userService = new UserService();

export const ProfileSidebar = observer(() => {
type TProfileSidebar = {
userProjectsData: IUserProfileProjectSegregation | undefined;
className?: string;
};

export const ProfileSidebar: FC<TProfileSidebar> = observer((props) => {
const { userProjectsData, className = "" } = props;
// refs
const ref = useRef<HTMLDivElement>(null);
// router
const { workspaceSlug, userId } = useParams();
const { userId } = useParams();
// store hooks
const { data: currentUser } = useUser();
const { profileSidebarCollapsed, toggleProfileSidebar } = useAppTheme();
const { getProjectById } = useProject();
const { isMobile } = usePlatformOS();
const { data: userProjectsData } = useSWR(
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
workspaceSlug && userId
? () => userService.getUserProfileProjectsSegregation(workspaceSlug.toString(), userId.toString())
: null
);

useOutsideClickDetector(ref, () => {
if (profileSidebarCollapsed === false) {
Expand Down Expand Up @@ -82,12 +81,15 @@ export const ProfileSidebar = observer(() => {

return (
<div
className={`vertical-scrollbar scrollbar-md fixed z-[5] h-full w-full flex-shrink-0 overflow-hidden overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 shadow-custom-shadow-sm transition-all md:relative md:w-[300px]`}
className={cn(
`vertical-scrollbar scrollbar-md fixed z-[5] h-full w-full flex-shrink-0 overflow-hidden overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 shadow-custom-shadow-sm transition-all md:relative md:w-[300px]`,
className
)}
style={profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
>
{userProjectsData ? (
<>
<div className="relative h-32">
<div className="relative h-[110px]">
{currentUser?.id === userId && (
<div className="absolute right-3.5 top-3.5 grid h-5 w-5 place-items-center rounded bg-white">
<Link href="/profile">
Expand All @@ -100,7 +102,7 @@ export const ProfileSidebar = observer(() => {
<img
src={userProjectsData.user_data?.cover_image ?? "/users/user-profile-cover-default-img.png"}
alt={userProjectsData.user_data?.display_name}
className="h-32 w-full object-cover"
className="h-[110px] w-full object-cover"
/>
<div className="absolute -bottom-[26px] left-5 h-[52px] w-[52px] rounded">
{userProjectsData.user_data?.avatar && userProjectsData.user_data?.avatar !== "" ? (
Expand Down
11 changes: 5 additions & 6 deletions web/core/components/workspace/sidebar/user-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export const SidebarUserMenu = observer(() => {
// computed
const workspaceMemberInfo = currentWorkspaceRole || EUserWorkspaceRoles.GUEST;

const getHref = (link: any) =>
`/${workspaceSlug}${link.href}${link.key === "your-work" ? `/${currentUser?.id}` : ""}`;

const handleLinkClick = (itemKey: string) => {
if (window.innerWidth < 768) {
toggleSidebar();
Expand Down Expand Up @@ -67,14 +70,10 @@ export const SidebarUserMenu = observer(() => {
disabled={!sidebarCollapsed}
isMobile={isMobile}
>
<Link
href={`/${workspaceSlug}${link.href}${link.key === "my-work" ? `/${currentUser?.id}` : ""}`}
onClick={() => handleLinkClick(link.key)}
>
<Link key={link.key} href={getHref(link)} onClick={() => handleLinkClick(link.key)}>
<SidebarNavItem
key={link.key}
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
isActive={link.highlight(pathname, `/${workspaceSlug}`)}
isActive={link.highlight(pathname, `/${workspaceSlug}`, { userId: currentUser?.id })}
>
<div className="flex items-center gap-1.5 py-[1px]">
<link.Icon className="size-4 flex-shrink-0" />
Expand Down
9 changes: 7 additions & 2 deletions web/core/constants/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,16 @@ export const SIDEBAR_WORKSPACE_MENU_ITEMS: {
},
];

type TLinkOptions = {
userId: string | undefined;
};

export const SIDEBAR_USER_MENU_ITEMS: {
key: string;
label: string;
href: string;
access: EUserWorkspaceRoles;
highlight: (pathname: string, baseUrl: string) => boolean;
highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) => boolean;
Icon: React.FC<Props>;
}[] = [
{
Expand All @@ -314,7 +318,8 @@ export const SIDEBAR_USER_MENU_ITEMS: {
label: "Your work",
href: "/profile",
access: EUserWorkspaceRoles.GUEST,
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/profile/`),
highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) =>
options?.userId ? pathname.includes(`${baseUrl}/profile/${options?.userId}`) : false,
Icon: UserActivityIcon,
},
{
Expand Down
Loading