chore: app dir headers re-implementation (#4751)

* chore: header refactor.

* fix: core imports

* chore: refactor profile activity header and fix all other header imports.

* fix: import fixes

* chore: header refactor.

* fix: app dir header reimplementation

* fix: removing parllel headers

* fix: adding route groups to handle pages

* fix: disabling sentry for temp

* chore: update default exports in layouts & headers for consistency.

* fix: bugfixes

* fix: build errors

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
Prateek Shourya 2024-06-11 02:23:19 +05:30 committed by GitHub
parent 423bc15119
commit 05de4d83f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
150 changed files with 887 additions and 648 deletions

View file

@ -0,0 +1,72 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// ui
import { Button } from "@plane/ui";
// components
import { DownloadActivityButton, WorkspaceActivityListPage } from "@/components/profile";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks
import { useUser } from "@/hooks/store";
const PER_PAGE = 100;
const ProfileActivityPage = observer(() => {
// states
const [pageCount, setPageCount] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [resultsCount, setResultsCount] = useState(0);
// router
const { userId } = useParams();
// store hooks
const { data: currentUser } = useUser();
const {
membership: { currentWorkspaceRole },
} = useUser();
const updateTotalPages = (count: number) => setTotalPages(count);
const updateResultsCount = (count: number) => setResultsCount(count);
const handleLoadMore = () => setPageCount((prev) => prev + 1);
const activityPages: JSX.Element[] = [];
for (let i = 0; i < pageCount; i++)
activityPages.push(
<WorkspaceActivityListPage
key={i}
cursor={`${PER_PAGE}:${i}:0`}
perPage={PER_PAGE}
updateResultsCount={updateResultsCount}
updateTotalPages={updateTotalPages}
/>
);
const canDownloadActivity =
currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<div className="flex h-full w-full flex-col overflow-hidden py-5">
<div className="flex items-center justify-between gap-2 px-5 md:px-9">
<h3 className="text-lg font-medium">Recent activity</h3>
{canDownloadActivity && <DownloadActivityButton />}
</div>
<div className="vertical-scrollbar scrollbar-md flex h-full flex-col overflow-y-auto px-5 md:px-9">
{activityPages}
{pageCount < totalPages && resultsCount !== 0 && (
<div className="flex w-full items-center justify-center text-xs">
<Button variant="accent-primary" size="sm" onClick={handleLoadMore}>
Load more
</Button>
</div>
)}
</div>
</div>
);
});
export default ProfileActivityPage;

View file

@ -0,0 +1,92 @@
"use client";
// ui
import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { ChevronDown, PanelRight } from "lucide-react";
import { Breadcrumbs, CustomMenu } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
// components
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { cn } from "@/helpers/common.helper";
import { useAppTheme, useUser } from "@/hooks/store";
type TUserProfileHeader = {
type?: string | undefined;
};
export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const { type = undefined } = props;
// router
const { workspaceSlug, userId } = useParams();
// store hooks
const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme();
const {
membership: { currentWorkspaceRole },
} = useUser();
// derived values
const AUTHORIZED_ROLES = [20, 15, 10];
if (!currentWorkspaceRole) return null;
const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole);
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
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>
<div className="flex gap-4 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={
<div className="flex items-center gap-2 rounded-md border border-custom-border-400 px-2 py-1.5">
<span className="flex flex-grow justify-center text-sm text-custom-text-200">{type}</span>
<ChevronDown className="h-4 w-4 text-custom-text-400" />
</div>
}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
<></>
{tabsList.map((tab) => (
<CustomMenu.MenuItem className="flex items-center gap-2" key={tab.route}>
<Link
key={tab.route}
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
className="w-full text-custom-text-300"
>
{tab.label}
</Link>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<button
className="block transition-all md:hidden"
onClick={() => {
toggleProfileSidebar();
}}
>
<PanelRight
className={cn(
"block h-4 w-4 md:hidden",
!profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200"
)}
/>
</button>
</div>
</div>
</div>
</div>
);
});

View file

@ -0,0 +1,69 @@
"use client";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// components
import { AppHeader, ContentWrapper } from "@/components/core";
import { ProfileSidebar } from "@/components/profile";
// constants
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks
import { useUser } from "@/hooks/store";
// local components
import { UserProfileHeader } from "./header";
import { ProfileIssuesMobileHeader } from "./mobile-header";
import { ProfileNavbar } from "./navbar";
type Props = {
children: React.ReactNode;
};
const AUTHORIZED_ROLES = [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.VIEWER];
const UseProfileLayout: React.FC<Props> = observer((props) => {
const { children } = props;
// router
const { workspaceSlug, userId } = useParams();
const pathname = usePathname();
// store hooks
const {
membership: { currentWorkspaceRole },
} = useUser();
// derived values
const isAuthorized = currentWorkspaceRole && AUTHORIZED_ROLES.includes(currentWorkspaceRole);
const isAuthorizedPath =
pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed");
const isIssuesTab = pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed");
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
const currentTab = tabsList.find((tab) => pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}`);
return (
<>
{/* 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>
)}
</div>
<ProfileSidebar />
</div>
</ContentWrapper>
</>
);
});
export default UseProfileLayout;

View file

@ -0,0 +1,189 @@
"use client";
import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { ChevronDown } from "lucide-react";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
// components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
import {
EIssueFilterType,
EIssueLayoutTypes,
EIssuesStoreType,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
ISSUE_LAYOUTS,
} from "@/constants/issue";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useIssues, useLabel } from "@/hooks/store";
export const ProfileIssuesMobileHeader = observer(() => {
// router
const { workspaceSlug, userId } = useParams();
// store hook
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROFILE);
const { workspaceLabels } = useLabel();
// derived values
const states = undefined;
// const members = undefined;
// const activeLayout = issueFilters?.displayFilters?.layout;
// const states = undefined;
const members = undefined;
const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !userId) return;
updateFilters(
workspaceSlug.toString(),
undefined,
EIssueFilterType.DISPLAY_FILTERS,
{ layout: layout as EIssueLayoutTypes | undefined },
userId.toString()
);
},
[workspaceSlug, updateFilters, userId]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !userId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(
workspaceSlug.toString(),
undefined,
EIssueFilterType.FILTERS,
{ [key]: newValues },
userId.toString()
);
},
[workspaceSlug, issueFilters, updateFilters, userId]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !userId) return;
updateFilters(
workspaceSlug.toString(),
undefined,
EIssueFilterType.DISPLAY_FILTERS,
updatedDisplayFilter,
userId.toString()
);
},
[workspaceSlug, updateFilters, userId]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !userId) return;
updateFilters(
workspaceSlug.toString(),
undefined,
EIssueFilterType.DISPLAY_PROPERTIES,
property,
userId.toString()
);
},
[workspaceSlug, updateFilters, userId]
);
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
return (
<div className="flex justify-evenly border-b border-custom-border-200 py-2 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-sm text-custom-text-200"
placement="bottom-start"
customButton={<span className="flex flex-grow justify-center text-sm text-custom-text-200">Layout</span>}
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
closeOnSelect
>
{ISSUE_LAYOUTS.map((layout, index) => {
if (layout.key === "spreadsheet" || layout.key === "gantt_chart" || layout.key === "calendar") return;
return (
<CustomMenu.MenuItem
key={index}
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<layout.icon className="h-3 w-3" />
<div className="text-custom-text-300">{layout.title}</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title="Filters"
placement="bottom-end"
menuButton={
<span className="flex items-center text-sm text-custom-text-200">
Filters
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
isFiltersApplied={isFiltersApplied}
>
<FilterSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
states={states}
labels={workspaceLabels}
memberIds={members}
/>
</FiltersDropdown>
</div>
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
<FiltersDropdown
title="Display"
placement="bottom-end"
menuButton={
<span className="flex items-center text-sm text-custom-text-200">
Display
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
</span>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.profile_issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</div>
</div>
);
});

View file

@ -0,0 +1,44 @@
import React from "react";
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 { workspaceSlug, userId } = useParams();
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="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}`
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent"
}`}
>
{tab.label}
</span>
</Link>
))}
</div>
{showProfileIssuesFilter && <ProfileIssuesFilter />}
</div>
);
};

View file

@ -0,0 +1,54 @@
"use client";
import { useParams } from "next/navigation";
import useSWR from "swr";
// types
import { IUserStateDistribution, TStateGroups } from "@plane/types";
// components
import { PageHead } from "@/components/core";
import {
ProfileActivity,
ProfilePriorityDistribution,
ProfileStateDistribution,
ProfileStats,
ProfileWorkload,
} from "@/components/profile";
// constants
import { USER_PROFILE_DATA } from "@/constants/fetch-keys";
import { GROUP_CHOICES } from "@/constants/project";
// services
import { UserService } from "@/services/user.service";
// services
const userService = new UserService();
export default function ProfileOverviewPage() {
const { workspaceSlug, userId } = useParams();
const { data: userProfile } = useSWR(
workspaceSlug && userId ? USER_PROFILE_DATA(workspaceSlug.toString(), userId.toString()) : null,
workspaceSlug && userId ? () => userService.getUserProfileData(workspaceSlug.toString(), userId.toString()) : null
);
const stateDistribution: IUserStateDistribution[] = Object.keys(GROUP_CHOICES).map((key) => {
const group = userProfile?.state_distribution.find((g) => g.state_group === key);
if (group) return group;
else return { state_group: key as TStateGroups, state_count: 0 };
});
return (
<>
<PageHead title="Profile - Summary" />
<div className="h-full w-full space-y-7 overflow-y-auto px-5 py-5 md:px-9 vertical-scrollbar scrollbar-md">
<ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} />
<div className="grid grid-cols-1 items-stretch gap-5 xl:grid-cols-2">
<ProfilePriorityDistribution userProfile={userProfile} />
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
</div>
<ProfileActivity />
</div>
</>
);
}