fix: merge conflicts resolved

This commit is contained in:
sriram veeraghanta 2024-02-08 18:50:04 +05:30
commit eb4c3a4db5
28 changed files with 686 additions and 420 deletions

View File

@ -0,0 +1,33 @@
# Generated by Django 4.2.7 on 2024-02-08 09:57
from django.db import migrations
def widgets_filter_change(apps, schema_editor):
Widget = apps.get_model("db", "Widget")
widgets_to_update = []
# Define the filter dictionaries for each widget key
filters_mapping = {
"assigned_issues": {"duration": "none", "tab": "pending"},
"created_issues": {"duration": "none", "tab": "pending"},
"issues_by_state_groups": {"duration": "none"},
"issues_by_priority": {"duration": "none"},
}
# Iterate over widgets and update filters if applicable
for widget in Widget.objects.all():
if widget.key in filters_mapping:
widget.filters = filters_mapping[widget.key]
widgets_to_update.append(widget)
# Bulk update the widgets
Widget.objects.bulk_update(widgets_to_update, ["filters"], batch_size=10)
class Migration(migrations.Migration):
dependencies = [
('db', '0058_alter_moduleissue_issue_and_more'),
]
operations = [
migrations.RunPython(widgets_filter_change)
]

View File

@ -24,21 +24,21 @@ export type TDurationFilterOptions =
// widget filters // widget filters
export type TAssignedIssuesWidgetFilters = { export type TAssignedIssuesWidgetFilters = {
target_date?: TDurationFilterOptions; duration?: TDurationFilterOptions;
tab?: TIssuesListTypes; tab?: TIssuesListTypes;
}; };
export type TCreatedIssuesWidgetFilters = { export type TCreatedIssuesWidgetFilters = {
target_date?: TDurationFilterOptions; duration?: TDurationFilterOptions;
tab?: TIssuesListTypes; tab?: TIssuesListTypes;
}; };
export type TIssuesByStateGroupsWidgetFilters = { export type TIssuesByStateGroupsWidgetFilters = {
target_date?: TDurationFilterOptions; duration?: TDurationFilterOptions;
}; };
export type TIssuesByPriorityWidgetFilters = { export type TIssuesByPriorityWidgetFilters = {
target_date?: TDurationFilterOptions; duration?: TDurationFilterOptions;
}; };
export type TWidgetFiltersFormData = export type TWidgetFiltersFormData =

View File

@ -10,7 +10,7 @@ type BreadcrumbsProps = {
const Breadcrumbs = ({ children }: BreadcrumbsProps) => ( const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{React.Children.map(children, (child, index) => ( {React.Children.map(children, (child, index) => (
<div key={index} className="flex flex-wrap items-center gap-2.5"> <div key={index} className="flex items-center gap-2.5">
{child} {child}
{index !== React.Children.count(children) - 1 && ( {index !== React.Children.count(children) - 1 && (
<ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" /> <ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-400" aria-hidden="true" />

View File

@ -66,6 +66,11 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
}; };
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen, selectActiveItem); const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen, selectActiveItem);
const handleOnClick = () => {
if (closeOnSelect) closeDropdown();
};
useOutsideClickDetector(dropdownRef, closeDropdown); useOutsideClickDetector(dropdownRef, closeDropdown);
let menuItems = ( let menuItems = (
@ -101,7 +106,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
tabIndex={tabIndex} tabIndex={tabIndex}
className={cn("relative w-min text-left", className)} className={cn("relative w-min text-left", className)}
onKeyDownCapture={handleKeyDown} onKeyDownCapture={handleKeyDown}
onChange={handleOnChange} onClick={handleOnClick}
> >
{({ open }) => ( {({ open }) => (
<> <>
@ -110,7 +115,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
<button <button
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
onClick={() => { onClick={(e) => {
e.stopPropagation();
openDropdown(); openDropdown();
if (menuButtonOnClick) menuButtonOnClick(); if (menuButtonOnClick) menuButtonOnClick();
}} }}
@ -127,7 +133,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
<button <button
ref={setReferenceElement} ref={setReferenceElement}
type="button" type="button"
onClick={() => { onClick={(e) => {
e.stopPropagation();
openDropdown(); openDropdown();
if (menuButtonOnClick) menuButtonOnClick(); if (menuButtonOnClick) menuButtonOnClick();
}} }}
@ -152,7 +159,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
? "cursor-not-allowed text-custom-text-200" ? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => { onClick={(e) => {
e.stopPropagation();
openDropdown(); openDropdown();
if (menuButtonOnClick) menuButtonOnClick(); if (menuButtonOnClick) menuButtonOnClick();
}} }}

View File

@ -13,7 +13,7 @@ import {
WidgetProps, WidgetProps,
} from "components/dashboard/widgets"; } from "components/dashboard/widgets";
// helpers // helpers
import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper"; import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper";
// types // types
import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types"; import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types";
// constants // constants
@ -30,8 +30,8 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// derived values // derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats<TAssignedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const selectedTab = widgetDetails?.widget_filters.tab ?? "pending"; const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none";
const selectedDurationFilter = widgetDetails?.widget_filters.target_date ?? "none"; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
const handleUpdateFilters = async (filters: Partial<TAssignedIssuesWidgetFilters>) => { const handleUpdateFilters = async (filters: Partial<TAssignedIssuesWidgetFilters>) => {
if (!widgetDetails) return; if (!widgetDetails) return;
@ -43,7 +43,7 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
filters, filters,
}); });
const filterDates = getCustomDates(filters.target_date ?? selectedDurationFilter); const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter);
fetchWidgetStats(workspaceSlug, dashboardId, { fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY, widget_key: WIDGET_KEY,
issue_type: filters.tab ?? selectedTab, issue_type: filters.tab ?? selectedTab,
@ -86,19 +86,19 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// switch to pending tab if target date is changed to none // switch to pending tab if target date is changed to none
if (val === "none" && selectedTab !== "completed") { if (val === "none" && selectedTab !== "completed") {
handleUpdateFilters({ target_date: val, tab: "pending" }); handleUpdateFilters({ duration: val, tab: "pending" });
return; return;
} }
// switch to upcoming tab if target date is changed to other than none // switch to upcoming tab if target date is changed to other than none
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") { if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
handleUpdateFilters({ handleUpdateFilters({
target_date: val, duration: val,
tab: "upcoming", tab: "upcoming",
}); });
return; return;
} }
handleUpdateFilters({ target_date: val }); handleUpdateFilters({ duration: val });
}} }}
/> />
</div> </div>

View File

@ -13,7 +13,7 @@ import {
WidgetProps, WidgetProps,
} from "components/dashboard/widgets"; } from "components/dashboard/widgets";
// helpers // helpers
import { getCustomDates, getRedirectionFilters } from "helpers/dashboard.helper"; import { getCustomDates, getRedirectionFilters, getTabKey } from "helpers/dashboard.helper";
// types // types
import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types"; import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types";
// constants // constants
@ -30,8 +30,8 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// derived values // derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats<TCreatedIssuesWidgetResponse>(workspaceSlug, dashboardId, WIDGET_KEY);
const selectedTab = widgetDetails?.widget_filters.tab ?? "pending"; const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? "none";
const selectedDurationFilter = widgetDetails?.widget_filters.target_date ?? "none"; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab);
const handleUpdateFilters = async (filters: Partial<TCreatedIssuesWidgetFilters>) => { const handleUpdateFilters = async (filters: Partial<TCreatedIssuesWidgetFilters>) => {
if (!widgetDetails) return; if (!widgetDetails) return;
@ -43,7 +43,7 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
filters, filters,
}); });
const filterDates = getCustomDates(filters.target_date ?? selectedDurationFilter); const filterDates = getCustomDates(filters.duration ?? selectedDurationFilter);
fetchWidgetStats(workspaceSlug, dashboardId, { fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY, widget_key: WIDGET_KEY,
issue_type: filters.tab ?? selectedTab, issue_type: filters.tab ?? selectedTab,
@ -83,19 +83,19 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
// switch to pending tab if target date is changed to none // switch to pending tab if target date is changed to none
if (val === "none" && selectedTab !== "completed") { if (val === "none" && selectedTab !== "completed") {
handleUpdateFilters({ target_date: val, tab: "pending" }); handleUpdateFilters({ duration: val, tab: "pending" });
return; return;
} }
// switch to upcoming tab if target date is changed to other than none // switch to upcoming tab if target date is changed to other than none
if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") { if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") {
handleUpdateFilters({ handleUpdateFilters({
target_date: val, duration: val,
tab: "upcoming", tab: "upcoming",
}); });
return; return;
} }
handleUpdateFilters({ target_date: val }); handleUpdateFilters({ duration: val });
}} }}
/> />
</div> </div>

View File

@ -16,42 +16,40 @@ export const TabsList: React.FC<Props> = observer((props) => {
const { durationFilter, selectedTab } = props; const { durationFilter, selectedTab } = props;
const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const tabsList = durationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST;
const selectedTabIndex = tabsList.findIndex((tab) => tab.key === (selectedTab ?? "pending")); const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab);
return ( return (
<Tab.List <Tab.List
as="div" as="div"
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 grid" className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 p-[1px] grid"
style={{ style={{
gridTemplateColumns: `repeat(${tabsList.length}, 1fr)`, gridTemplateColumns: `repeat(${tabsList.length}, 1fr)`,
}} }}
> >
<div <div
className={cn("absolute bg-custom-background-100 rounded transition-all duration-500 ease-in-out", { className={cn(
"absolute top-1/2 left-[1px] bg-custom-background-100 rounded-[3px] transition-all duration-500 ease-in-out",
{
// right shadow // right shadow
"shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1, "shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
// left shadow // left shadow
"shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0, "shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
})} }
)}
style={{ style={{
height: "calc(100% - 1px)", height: "calc(100% - 2px)",
width: `${100 / tabsList.length}%`, width: `calc(${100 / tabsList.length}% - 1px)`,
transform: `translateX(${selectedTabIndex * 100}%)`, transform: `translate(${selectedTabIndex * 100}%, -50%)`,
}} }}
/> />
{tabsList.map((tab) => ( {tabsList.map((tab) => (
<Tab <Tab
key={tab.key} key={tab.key}
className={cn( className={cn(
"relative z-[1] font-semibold text-xs rounded py-1.5 text-custom-text-400 focus:outline-none", "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
"transition duration-500",
{ {
"text-custom-text-100 bg-custom-background-100": selectedTab === tab.key, "text-custom-text-100 bg-custom-background-100": selectedTab === tab.key,
"hover:text-custom-text-300": selectedTab !== tab.key, "hover:text-custom-text-300": selectedTab !== tab.key,
// // right shadow
// "shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1,
// // left shadow
// "shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0,
} }
)} )}
> >

View File

@ -73,8 +73,10 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
const { dashboardId, workspaceSlug } = props; const { dashboardId, workspaceSlug } = props;
// store hooks // store hooks
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard();
// derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TIssuesByPriorityWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats<TIssuesByPriorityWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
const selectedDuration = widgetDetails?.widget_filters.duration ?? "none";
const handleUpdateFilters = async (filters: Partial<TIssuesByPriorityWidgetFilters>) => { const handleUpdateFilters = async (filters: Partial<TIssuesByPriorityWidgetFilters>) => {
if (!widgetDetails) return; if (!widgetDetails) return;
@ -84,7 +86,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
filters, filters,
}); });
const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none"); const filterDates = getCustomDates(filters.duration ?? selectedDuration);
fetchWidgetStats(workspaceSlug, dashboardId, { fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY, widget_key: WIDGET_KEY,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
@ -92,7 +94,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
}; };
useEffect(() => { useEffect(() => {
const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none"); const filterDates = getCustomDates(selectedDuration);
fetchWidgetStats(workspaceSlug, dashboardId, { fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY, widget_key: WIDGET_KEY,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
@ -139,10 +141,10 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
Assigned by priority Assigned by priority
</Link> </Link>
<DurationFilterDropdown <DurationFilterDropdown
value={widgetDetails.widget_filters.target_date ?? "none"} value={selectedDuration}
onChange={(val) => onChange={(val) =>
handleUpdateFilters({ handleUpdateFilters({
target_date: val, duration: val,
}) })
} }
/> />

View File

@ -34,6 +34,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
// derived values // derived values
const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY);
const widgetStats = getWidgetStats<TIssuesByStateGroupsWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats<TIssuesByStateGroupsWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
const selectedDuration = widgetDetails?.widget_filters.duration ?? "none";
const handleUpdateFilters = async (filters: Partial<TIssuesByStateGroupsWidgetFilters>) => { const handleUpdateFilters = async (filters: Partial<TIssuesByStateGroupsWidgetFilters>) => {
if (!widgetDetails) return; if (!widgetDetails) return;
@ -43,7 +44,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
filters, filters,
}); });
const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none"); const filterDates = getCustomDates(filters.duration ?? selectedDuration);
fetchWidgetStats(workspaceSlug, dashboardId, { fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY, widget_key: WIDGET_KEY,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
@ -52,7 +53,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
// fetch widget stats // fetch widget stats
useEffect(() => { useEffect(() => {
const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none"); const filterDates = getCustomDates(selectedDuration);
fetchWidgetStats(workspaceSlug, dashboardId, { fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY, widget_key: WIDGET_KEY,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}), ...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
@ -138,10 +139,10 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
Assigned by state Assigned by state
</Link> </Link>
<DurationFilterDropdown <DurationFilterDropdown
value={widgetDetails.widget_filters.target_date ?? "none"} value={selectedDuration}
onChange={(val) => onChange={(val) =>
handleUpdateFilters({ handleUpdateFilters({
target_date: val, duration: val,
}) })
} }
/> />

View File

@ -1,18 +1,78 @@
// ui // ui
import { Breadcrumbs } from "@plane/ui"; import { Breadcrumbs, CustomMenu } from "@plane/ui";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
// components // components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { cn } from "helpers/common.helper";
import { FC } from "react";
import { useApplication, useUser } from "hooks/store";
import { ChevronDown, PanelRight } from "lucide-react";
import { observer } from "mobx-react-lite";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "constants/profile";
import Link from "next/link";
import { useRouter } from "next/router";
export const UserProfileHeader = () => ( type TUserProfileHeader = {
<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 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4"> type?: string | undefined
}
export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const { type = undefined } = props
const router = useRouter();
const { workspaceSlug, userId } = router.query;
const AUTHORIZED_ROLES = [20, 15, 10];
const {
membership: { currentWorkspaceRole },
} = useUser();
if (!currentWorkspaceRole) return null;
const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole);
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
const { theme: themStore } = useApplication();
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 border-b border-custom-border-200 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 flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle /> <SidebarHamburgerToggle />
<div> <div className="flex justify-between w-full">
<Breadcrumbs> <Breadcrumbs>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink href="/profile" label="Activity Overview" />} /> <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink href="/profile" label="Activity Overview" />} />
</Breadcrumbs> </Breadcrumbs>
<div className="flex gap-4 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={
<div className="flex gap-2 items-center px-2 py-1.5 border border-custom-border-400 rounded-md">
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{type}</span>
<ChevronDown className="w-4 h-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"
>
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`} className="text-custom-text-300 w-full">{tab.label}</Link>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<button className="transition-all block md:hidden" onClick={() => { themStore.toggleProfileSidebar(); console.log(themStore.profileSidebarCollapsed) }}>
<PanelRight className={
cn("w-4 h-4 block md:hidden", !themStore.profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")
} />
</button>
</div> </div>
</div> </div>
</div> </div>
); </div>)
});

View File

@ -208,9 +208,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
<Tooltip tooltipContent="Snooze"> <Tooltip tooltipContent="Snooze">
<CustomMenu <CustomMenu
className="flex items-center" className="flex items-center"
menuButtonOnClick={(e: { stopPropagation: () => void }) => {
e.stopPropagation();
}}
customButton={ customButton={
<div className="flex w-full items-center gap-x-2 rounded bg-custom-background-80 p-0.5 text-sm hover:bg-custom-background-100"> <div className="flex w-full items-center gap-x-2 rounded bg-custom-background-80 p-0.5 text-sm hover:bg-custom-background-100">
<Clock className="h-3.5 w-3.5 text-custom-text-300" /> <Clock className="h-3.5 w-3.5 text-custom-text-300" />

View File

@ -22,13 +22,12 @@ export const ProfileNavbar: React.FC<Props> = (props) => {
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB; const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
return ( return (
<div className="sticky -top-0.5 z-10 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 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"> <div className="flex items-center overflow-x-scroll">
{tabsList.map((tab) => ( {tabsList.map((tab) => (
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}> <Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
<span <span
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${ className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${router.pathname === tab.selected
router.pathname === tab.selected
? "border-custom-primary-100 text-custom-primary-100" ? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent" : "border-transparent"
}`} }`}

View File

@ -4,7 +4,7 @@ import useSWR from "swr";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useUser } from "hooks/store"; import { useApplication, useUser } from "hooks/store";
// services // services
import { UserService } from "services/user.service"; import { UserService } from "services/user.service";
// components // components
@ -18,6 +18,8 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
// fetch-keys // fetch-keys
import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys"; import { USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { useEffect, useRef } from "react";
// services // services
const userService = new UserService(); const userService = new UserService();
@ -28,6 +30,8 @@ export const ProfileSidebar = observer(() => {
const { workspaceSlug, userId } = router.query; const { workspaceSlug, userId } = router.query;
// store hooks // store hooks
const { currentUser } = useUser(); const { currentUser } = useUser();
const { theme: themStore } = useApplication();
const ref = useRef<HTMLDivElement>(null);
const { data: userProjectsData } = useSWR( const { data: userProjectsData } = useSWR(
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null, workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
@ -36,6 +40,14 @@ export const ProfileSidebar = observer(() => {
: null : null
); );
useOutsideClickDetector(ref, () => {
if (themStore.profileSidebarCollapsed === false) {
if (window.innerWidth < 768) {
themStore.toggleProfileSidebar();
}
}
});
const userDetails = [ const userDetails = [
{ {
label: "Joined on", label: "Joined on",
@ -47,8 +59,26 @@ export const ProfileSidebar = observer(() => {
}, },
]; ];
useEffect(() => {
const handleToggleProfileSidebar = () => {
if (window && window.innerWidth < 768) {
themStore.toggleProfileSidebar(true);
}
if (window && themStore.profileSidebarCollapsed && window.innerWidth >= 768) {
themStore.toggleProfileSidebar(false);
}
};
window.addEventListener("resize", handleToggleProfileSidebar);
handleToggleProfileSidebar();
return () => window.removeEventListener("resize", handleToggleProfileSidebar);
}, [themStore]);
return ( return (
<div className="w-full flex-shrink-0 overflow-y-auto shadow-custom-shadow-sm md:h-full md:w-80 border-l border-custom-border-100"> <div
className={`flex-shrink-0 overflow-hidden overflow-y-auto shadow-custom-shadow-sm border-l border-custom-border-100 bg-custom-sidebar-background-100 h-full z-[5] fixed md:relative transition-all w-full md:w-[300px]`}
style={themStore.profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
>
{userProjectsData ? ( {userProjectsData ? (
<> <>
<div className="relative h-32"> <div className="relative h-32">
@ -132,8 +162,7 @@ export const ProfileSidebar = observer(() => {
{project.assigned_issues > 0 && ( {project.assigned_issues > 0 && (
<Tooltip tooltipContent="Completion percentage" position="left"> <Tooltip tooltipContent="Completion percentage" position="left">
<div <div
className={`rounded px-1 py-0.5 text-xs font-medium ${ className={`rounded px-1 py-0.5 text-xs font-medium ${completedIssuePercentage <= 35
completedIssuePercentage <= 35
? "bg-red-500/10 text-red-500" ? "bg-red-500/10 text-red-500"
: completedIssuePercentage <= 70 : completedIssuePercentage <= 70
? "bg-yellow-500/10 text-yellow-500" ? "bg-yellow-500/10 text-yellow-500"

View File

@ -4,6 +4,10 @@ import { renderFormattedPayloadDate } from "./date-time.helper";
// types // types
import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types"; import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types";
/**
* @description returns date range based on the duration filter
* @param duration
*/
export const getCustomDates = (duration: TDurationFilterOptions): string => { export const getCustomDates = (duration: TDurationFilterOptions): string => {
const today = new Date(); const today = new Date();
let firstDay, lastDay; let firstDay, lastDay;
@ -30,6 +34,10 @@ export const getCustomDates = (duration: TDurationFilterOptions): string => {
} }
}; };
/**
* @description returns redirection filters for the issues list
* @param type
*/
export const getRedirectionFilters = (type: TIssuesListTypes): string => { export const getRedirectionFilters = (type: TIssuesListTypes): string => {
const today = renderFormattedPayloadDate(new Date()); const today = renderFormattedPayloadDate(new Date());
@ -44,3 +52,20 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => {
return filterParams; return filterParams;
}; };
/**
* @description returns the tab key based on the duration filter
* @param duration
* @param tab
*/
export const getTabKey = (duration: TDurationFilterOptions, tab: TIssuesListTypes | undefined): TIssuesListTypes => {
if (!tab) return "completed";
if (tab === "completed") return tab;
if (duration === "none") return "pending";
else {
if (["upcoming", "overdue"].includes(tab)) return tab;
else return "upcoming";
}
};

View File

@ -2,6 +2,11 @@ import { FC, ReactNode } from "react";
// layout // layout
import { ProfileSettingsLayout } from "layouts/settings-layout"; import { ProfileSettingsLayout } from "layouts/settings-layout";
import { ProfilePreferenceSettingsSidebar } from "./sidebar"; import { ProfilePreferenceSettingsSidebar } from "./sidebar";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { CustomMenu } from "@plane/ui";
import { ChevronDown } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
interface IProfilePreferenceSettingsLayout { interface IProfilePreferenceSettingsLayout {
children: ReactNode; children: ReactNode;
@ -10,9 +15,57 @@ interface IProfilePreferenceSettingsLayout {
export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayout> = (props) => { export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayout> = (props) => {
const { children, header } = props; const { children, header } = props;
const router = useRouter();
const showMenuItem = () => {
const item = router.asPath.split('/');
let splittedItem = item[item.length - 1];
splittedItem = splittedItem.replace(splittedItem[0], splittedItem[0].toUpperCase());
console.log(splittedItem);
return splittedItem;
}
const profilePreferenceLinks: Array<{
label: string;
href: string;
}> = [
{
label: "Theme",
href: `/profile/preferences/theme`,
},
{
label: "Email",
href: `/profile/preferences/email`,
},
];
return ( return (
<ProfileSettingsLayout> <ProfileSettingsLayout header={
<div className="md:hidden flex flex-shrink-0 gap-4 items-center justify-start border-b border-custom-border-200 p-4">
<SidebarHamburgerToggle />
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-custom-text-200 text-sm"
placement="bottom-start"
customButton={
<div className="flex gap-2 items-center px-2 py-1.5 border rounded-md border-custom-border-400">
<span className="flex flex-grow justify-center text-custom-text-200 text-sm">{showMenuItem()}</span>
<ChevronDown className="w-4 h-4 text-custom-text-400" />
</div>
}
customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm"
>
<></>
{profilePreferenceLinks.map((link) => (
<CustomMenu.MenuItem
className="flex items-center gap-2"
>
<Link key={link.href} href={link.href} className="text-custom-text-300 w-full">{link.label}</Link>
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
}>
<div className="relative flex h-screen w-full overflow-hidden"> <div className="relative flex h-screen w-full overflow-hidden">
<ProfilePreferenceSettingsSidebar /> <ProfilePreferenceSettingsSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100"> <main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">

View File

@ -19,15 +19,14 @@ export const ProfilePreferenceSettingsSidebar = () => {
}, },
]; ];
return ( return (
<div className="flex w-96 flex-col gap-6 px-8 py-12"> <div className="hidden md:flex w-96 flex-col gap-6 px-8 py-12">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<span className="text-xs font-semibold text-custom-text-400">Preference</span> <span className="text-xs font-semibold text-custom-text-400">Preference</span>
<div className="flex w-full flex-col gap-2"> <div className="flex w-full flex-col gap-2">
{profilePreferenceLinks.map((link) => ( {profilePreferenceLinks.map((link) => (
<Link key={link.href} href={link.href}> <Link key={link.href} href={link.href}>
<div <div
className={`rounded-md px-4 py-2 text-sm font-medium ${ className={`rounded-md px-4 py-2 text-sm font-medium ${(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
? "bg-custom-primary-100/10 text-custom-primary-100" ? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80" : "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
}`} }`}

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import { mutate } from "swr"; import { mutate } from "swr";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -12,6 +12,7 @@ import useToast from "hooks/use-toast";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// constants // constants
import { PROFILE_ACTION_LINKS } from "constants/profile"; import { PROFILE_ACTION_LINKS } from "constants/profile";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
const WORKSPACE_ACTION_LINKS = [ const WORKSPACE_ACTION_LINKS = [
{ {
@ -52,6 +53,35 @@ export const ProfileLayoutSidebar = observer(() => {
currentUserSettings?.workspace?.fallback_workspace_slug || currentUserSettings?.workspace?.fallback_workspace_slug ||
""; "";
const ref = useRef<HTMLDivElement>(null);
useOutsideClickDetector(ref, () => {
if (sidebarCollapsed === false) {
if (window.innerWidth < 768) {
toggleSidebar();
}
}
});
useEffect(() => {
const handleResize = () => {
if (window.innerWidth <= 768) {
toggleSidebar(true);
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [toggleSidebar]);
const handleItemClick = () => {
if (window.innerWidth < 768) {
toggleSidebar();
}
};
const handleSignOut = async () => { const handleSignOut = async () => {
setIsSigningOut(true); setIsSigningOut(true);
@ -73,15 +103,17 @@ export const ProfileLayoutSidebar = observer(() => {
return ( return (
<div <div
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 md:relative ${ className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 md:relative
sidebarCollapsed ? "" : "md:w-[280px]" ${sidebarCollapsed ? "-ml-[280px]" : ""}
} ${sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`} sm:${sidebarCollapsed ? "-ml-[280px]" : ""}
md:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
lg:ml-0 ${sidebarCollapsed ? "w-[80px]" : "w-[280px]"}
`}
> >
<div className="flex h-full w-full flex-col gap-y-4"> <div ref={ref} className="flex h-full w-full flex-col gap-y-4">
<Link href={`/${redirectWorkspaceSlug}`}> <Link href={`/${redirectWorkspaceSlug}`}>
<div <div
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${ className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${sidebarCollapsed ? "justify-center" : ""
sidebarCollapsed ? "justify-center" : ""
}`} }`}
> >
<span className="grid h-5 w-5 flex-shrink-0 place-items-center"> <span className="grid h-5 w-5 flex-shrink-0 place-items-center">
@ -101,11 +133,10 @@ export const ProfileLayoutSidebar = observer(() => {
if (link.key === "change-password" && currentUser?.is_password_autoset) return null; if (link.key === "change-password" && currentUser?.is_password_autoset) return null;
return ( return (
<Link key={link.key} href={link.href} className="block w-full"> <Link key={link.key} href={link.href} className="block w-full" onClick={handleItemClick}>
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}> <Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
<div <div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${ className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${link.highlight(router.pathname)
link.highlight(router.pathname)
? "bg-custom-primary-100/10 text-custom-primary-100" ? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80" : "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
} ${sidebarCollapsed ? "justify-center" : ""}`} } ${sidebarCollapsed ? "justify-center" : ""}`}
@ -129,18 +160,16 @@ export const ProfileLayoutSidebar = observer(() => {
<Link <Link
key={workspace.id} key={workspace.id}
href={`/${workspace.slug}`} href={`/${workspace.slug}`}
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${ className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${sidebarCollapsed ? "justify-center" : `justify-between`
sidebarCollapsed ? "justify-center" : `justify-between` }`}
onClick={handleItemClick}
>
<span
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
}`} }`}
> >
<span <span
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${ className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${!workspace?.logo && "rounded bg-custom-primary-500 text-white"
sidebarCollapsed ? "justify-center" : ""
}`}
>
<span
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
}`} }`}
> >
{workspace?.logo && workspace.logo !== "" ? ( {workspace?.logo && workspace.logo !== "" ? (
@ -163,11 +192,10 @@ export const ProfileLayoutSidebar = observer(() => {
)} )}
<div className="mt-1.5"> <div className="mt-1.5">
{WORKSPACE_ACTION_LINKS.map((link) => ( {WORKSPACE_ACTION_LINKS.map((link) => (
<Link className="block w-full" key={link.key} href={link.href}> <Link className="block w-full" key={link.key} href={link.href} onClick={handleItemClick}>
<Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}> <Tooltip tooltipContent={link.label} position="right" className="ml-2" disabled={!sidebarCollapsed}>
<div <div
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${ className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-custom-sidebar-text-200 outline-none hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
sidebarCollapsed ? "justify-center" : ""
}`} }`}
> >
{<link.Icon className="h-4 w-4" />} {<link.Icon className="h-4 w-4" />}
@ -180,8 +208,7 @@ export const ProfileLayoutSidebar = observer(() => {
</div> </div>
<div className="flex flex-shrink-0 flex-grow items-end px-6 py-2"> <div className="flex flex-shrink-0 flex-grow items-end px-6 py-2">
<div <div
className={`flex w-full ${ className={`flex w-full ${sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
}`} }`}
> >
<button <button
@ -202,8 +229,7 @@ export const ProfileLayoutSidebar = observer(() => {
</button> </button>
<button <button
type="button" type="button"
className={`ml-auto hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${ className={`ml-auto hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${sidebarCollapsed ? "w-full" : ""
sidebarCollapsed ? "w-full" : ""
}`} }`}
onClick={() => toggleSidebar()} onClick={() => toggleSidebar()}
> >

View File

@ -28,9 +28,8 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed"); const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed");
return ( return (
<div className="h-full w-full md:flex md:flex-row-reverse md:overflow-hidden"> <div className="h-full w-full realtive flex flex-row">
<ProfileSidebar /> <div className="w-full realtive flex flex-col">
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
<ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} /> <ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} />
{isAuthorized || !isAuthorizedPath ? ( {isAuthorized || !isAuthorizedPath ? (
<div className={`w-full overflow-hidden md:h-full ${className}`}>{children}</div> <div className={`w-full overflow-hidden md:h-full ${className}`}>{children}</div>
@ -40,6 +39,8 @@ export const ProfileAuthWrapper: React.FC<Props> = observer((props) => {
</div> </div>
)} )}
</div> </div>
<ProfileSidebar />
</div> </div>
); );
}); });

View File

@ -12,7 +12,7 @@ const ProfileAssignedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage t
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) { ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<UserProfileHeader />}> <AppLayout header={<UserProfileHeader type="Assigned" />}>
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper> <ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
</AppLayout> </AppLayout>
); );

View File

@ -14,7 +14,7 @@ const ProfileCreatedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage ty
ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) { ProfileCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<UserProfileHeader />}> <AppLayout header={<UserProfileHeader type="Created" />}>
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper> <ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
</AppLayout> </AppLayout>
); );

View File

@ -56,7 +56,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => {
ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) { ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<UserProfileHeader />}> <AppLayout header={<UserProfileHeader type='Summary' />}>
<ProfileAuthWrapper>{page}</ProfileAuthWrapper> <ProfileAuthWrapper>{page}</ProfileAuthWrapper>
</AppLayout> </AppLayout>
); );

View File

@ -14,7 +14,7 @@ const ProfileSubscribedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage
ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) { ProfileSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return ( return (
<AppLayout header={<UserProfileHeader />}> <AppLayout header={<UserProfileHeader type="Subscribed" />}>
<ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper> <ProfileAuthWrapper showProfileIssuesFilter>{page}</ProfileAuthWrapper>
</AppLayout> </AppLayout>
); );

View File

@ -21,6 +21,7 @@ import { USER_ACTIVITY } from "constants/fetch-keys";
import { calculateTimeAgo } from "helpers/date-time.helper"; import { calculateTimeAgo } from "helpers/date-time.helper";
// type // type
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
const userService = new UserService(); const userService = new UserService();
@ -30,8 +31,10 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
const { currentUser } = useUser(); const { currentUser } = useUser();
return ( return (
<section className="mx-auto mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
<div className="flex items-center border-b border-custom-border-100 pb-3.5"> <section className="mx-auto mt-5 md:mt-16 flex h-full w-full flex-col overflow-hidden px-8 pb-8 lg:w-3/5">
<div className="flex items-center border-b border-custom-border-100 gap-4 pb-3.5">
<SidebarHamburgerToggle />
<h3 className="text-xl font-medium">Activity</h3> <h3 className="text-xl font-medium">Activity</h3>
</div> </div>
{userActivity ? ( {userActivity ? (
@ -187,6 +190,7 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
</Loader> </Loader>
)} )}
</section> </section>
); );
}); });

View File

@ -14,6 +14,7 @@ import { ProfileSettingsLayout } from "layouts/settings-layout";
import { Button, Input, Spinner } from "@plane/ui"; import { Button, Input, Spinner } from "@plane/ui";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
interface FormValues { interface FormValues {
old_password: string; old_password: string;
@ -86,6 +87,10 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
); );
return ( return (
<div className="flex flex-col h-full">
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
<SidebarHamburgerToggle />
</div>
<form <form
onSubmit={handleSubmit(handleChangePassword)} onSubmit={handleSubmit(handleChangePassword)}
className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5" className="mx-auto mt-16 flex h-full w-full flex-col gap-8 px-8 pb-8 lg:w-3/5"
@ -168,6 +173,7 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
</Button> </Button>
</div> </div>
</form> </form>
</div>
); );
}); });

View File

@ -23,6 +23,7 @@ import type { NextPageWithLayout } from "lib/types";
// constants // constants
import { USER_ROLES } from "constants/workspace"; import { USER_ROLES } from "constants/workspace";
import { TIME_ZONES } from "constants/timezones"; import { TIME_ZONES } from "constants/timezones";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
const defaultValues: Partial<IUser> = { const defaultValues: Partial<IUser> = {
avatar: "", avatar: "",
@ -136,6 +137,11 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<div className="flex flex-col h-full">
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
<SidebarHamburgerToggle />
</div>
<div className="overflow-hidden">
<Controller <Controller
control={control} control={control}
name="avatar" name="avatar"
@ -155,7 +161,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
)} )}
/> />
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} /> <DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
<div className="mx-auto flex h-full w-full flex-col space-y-10 overflow-y-auto pt-16 px-8 pb-8 lg:w-3/5"> <div className="mx-auto flex h-full w-full flex-col space-y-10 overflow-y-auto pt-10 md:pt-16 px-8 pb-8 lg:w-3/5">
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="flex w-full flex-col gap-8"> <div className="flex w-full flex-col gap-8">
<div className="relative h-44 w-full"> <div className="relative h-44 w-full">
@ -289,8 +295,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
ref={ref} ref={ref}
hasError={Boolean(errors.email)} hasError={Boolean(errors.email)}
placeholder="Enter your email" placeholder="Enter your email"
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${ className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${errors.email ? "border-red-500" : ""
errors.email ? "border-red-500" : ""
}`} }`}
disabled disabled
/> />
@ -435,6 +440,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
)} )}
</Disclosure> </Disclosure>
</div> </div>
</div>
</div>
</> </>
); );
}); });

View File

@ -48,7 +48,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
{currentUser ? ( {currentUser ? (
<div className="mx-auto mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8"> <div className="mx-auto mt-10 md:mt-14 h-full w-full overflow-y-auto px-6 lg:px-20 pb-8">
<div className="flex items-center border-b border-custom-border-100 pb-3.5"> <div className="flex items-center border-b border-custom-border-100 pb-3.5">
<h3 className="text-xl font-medium">Preferences</h3> <h3 className="text-xl font-medium">Preferences</h3>
</div> </div>

View File

@ -7,15 +7,18 @@ export interface IThemeStore {
// observables // observables
theme: string | null; theme: string | null;
sidebarCollapsed: boolean | undefined; sidebarCollapsed: boolean | undefined;
profileSidebarCollapsed: boolean | undefined;
// actions // actions
toggleSidebar: (collapsed?: boolean) => void; toggleSidebar: (collapsed?: boolean) => void;
setTheme: (theme: any) => void; setTheme: (theme: any) => void;
toggleProfileSidebar: (collapsed?: boolean) => void;
} }
export class ThemeStore implements IThemeStore { export class ThemeStore implements IThemeStore {
// observables // observables
sidebarCollapsed: boolean | undefined = undefined; sidebarCollapsed: boolean | undefined = undefined;
theme: string | null = null; theme: string | null = null;
profileSidebarCollapsed: boolean | undefined = undefined;
// root store // root store
rootStore; rootStore;
@ -24,9 +27,11 @@ export class ThemeStore implements IThemeStore {
// observable // observable
sidebarCollapsed: observable.ref, sidebarCollapsed: observable.ref,
theme: observable.ref, theme: observable.ref,
profileSidebarCollapsed: observable.ref,
// action // action
toggleSidebar: action, toggleSidebar: action,
setTheme: action, setTheme: action,
toggleProfileSidebar: action,
// computed // computed
}); });
// root store // root store
@ -46,6 +51,19 @@ export class ThemeStore implements IThemeStore {
localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString()); localStorage.setItem("app_sidebar_collapsed", this.sidebarCollapsed.toString());
}; };
/**
* Toggle the profile sidebar collapsed state
* @param collapsed
*/
toggleProfileSidebar = (collapsed?: boolean) => {
if (collapsed === undefined) {
this.profileSidebarCollapsed = !this.profileSidebarCollapsed;
} else {
this.profileSidebarCollapsed = collapsed;
}
localStorage.setItem("profile_sidebar_collapsed", this.profileSidebarCollapsed.toString());
};
/** /**
* Sets the user theme and applies it to the platform * Sets the user theme and applies it to the platform
* @param _theme * @param _theme