fix: merge conflicts resolved from develop

This commit is contained in:
Aaryan Khandelwal 2024-02-08 23:47:15 +05:30
commit 2e444c1426
27 changed files with 667 additions and 372 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-neutral-text-subtle" aria-hidden="true" /> <ChevronRight className="h-3.5 w-3.5 flex-shrink-0 text-neutral-text-subtle" 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-neutral-text-medium" ? "cursor-not-allowed text-neutral-text-medium"
: "cursor-pointer hover:bg-neutral-component-surface-dark" : "cursor-pointer hover:bg-neutral-component-surface-dark"
} ${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-neutral-border-medium rounded bg-neutral-component-surface-dark grid" className="relative border-[0.5px] border-neutral-border-medium rounded bg-neutral-component-surface-dark 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-neutral-text-subtle focus:outline-none", "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-neutral-text-subtle focus:outline-none transition duration-500",
"transition duration-500",
{ {
"text-neutral-text-strong bg-custom-background-100": selectedTab === tab.key, "text-neutral-text-strong bg-custom-background-100": selectedTab === tab.key,
"hover:text-neutral-text-medium": selectedTab !== tab.key, "hover:text-neutral-text-medium": 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,93 @@
// 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 = {
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-neutral-border-medium bg-sidebar-neutral-component-surface-light p-4"> <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-neutral-border-medium bg-sidebar-neutral-component-surface-light 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-neutral-component-surface-dark p-0.5 text-sm hover:bg-neutral-component-surface-light"> <div className="flex w-full items-center gap-x-2 rounded bg-neutral-component-surface-dark p-0.5 text-sm hover:bg-neutral-component-surface-light">
<Clock className="h-3.5 w-3.5 text-neutral-text-medium" /> <Clock className="h-3.5 w-3.5 text-neutral-text-medium" />

View File

@ -22,7 +22,7 @@ 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-neutral-border-medium bg-neutral-component-surface-light 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-neutral-border-medium bg-neutral-component-surface-light 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}`}>

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-neutral-border-subtle"> <div
className={`flex-shrink-0 overflow-hidden overflow-y-auto shadow-custom-shadow-sm border-l border-sidebar-neutral-border-subtle bg-sidebar-neutral-component-surface-light 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">
@ -134,10 +164,10 @@ export const ProfileSidebar = observer(() => {
<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-danger-component-surface-dark text-danger-text-medium" ? "bg-danger-component-surface-medium text-danger-text-medium"
: completedIssuePercentage <= 70 : completedIssuePercentage <= 70
? "bg-yellow-500/10 text-yellow-500" ? "bg-yellow-500/10 text-yellow-500"
: "bg-success-component-surface-dark text-success-text-medium" : "bg-success-component-surface-medium text-success-text-medium"
}`} }`}
> >
{completedIssuePercentage}% {completedIssuePercentage}%

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-neutral-component-surface-light"> <main className="relative flex h-full w-full flex-col overflow-hidden bg-neutral-component-surface-light">

View File

@ -19,7 +19,7 @@ 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-neutral-text-subtle">Preference</span> <span className="text-xs font-semibold text-neutral-text-subtle">Preference</span>
<div className="flex w-full flex-col gap-2"> <div className="flex w-full flex-col gap-2">

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,11 +103,14 @@ 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-sidebar-neutral-border-medium bg-sidebar-neutral-component-surface-light 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-sidebar-neutral-border-medium bg-sidebar-neutral-component-surface-light 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 ${
@ -101,7 +134,7 @@ 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 ${
@ -132,6 +165,7 @@ export const ProfileLayoutSidebar = observer(() => {
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 <span
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-sidebar-neutral-component-surface-dark ${ className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-sidebar-neutral-component-surface-dark ${
@ -163,7 +197,7 @@ 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-sidebar-neutral-text-medium outline-none hover:bg-sidebar-neutral-component-surface-dark focus:bg-sidebar-neutral-component-surface-dark ${ className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-sidebar-neutral-text-medium outline-none hover:bg-sidebar-neutral-component-surface-dark focus:bg-sidebar-neutral-component-surface-dark ${

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,9 @@ 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"> <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-neutral-border-subtle pb-3.5"> <div className="flex items-center border-b border-neutral-border-subtle 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 ? (

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"
@ -174,6 +179,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-neutral-border-medium 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">
@ -206,14 +212,14 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
<div className="item-center mt-4 flex justify-between px-8"> <div className="item-center mt-4 flex justify-between px-8">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="item-center flex text-lg font-semibold text-neutral-text-strong"> <div className="item-center flex text-lg font-semibold text-neutral-text-subtle">
<span>{`${watch("first_name")} ${watch("last_name")}`}</span> <span>{`${watch("first_name")} ${watch("last_name")}`}</span>
</div> </div>
<span className="text-sm tracking-tight">{watch("email")}</span> <span className="text-sm tracking-tight">{watch("email")}</span>
</div> </div>
{/* <Link href={`/profile/${myProfile.id}`}> {/* <Link href={`/profile/${myProfile.id}`}>
<span className="flex item-center gap-1 text-sm text-primary-text-subtle underline font-medium"> <span className="flex item-center gap-1 text-sm text-custom-primary-100 underline font-medium">
<ExternalLink className="h-4 w-4" /> <ExternalLink className="h-4 w-4" />
Activity Overview Activity Overview
</span> </span>
@ -223,7 +229,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
<div className="grid grid-cols-1 gap-6 px-8 lg:grid-cols-2 2xl:grid-cols-3"> <div className="grid grid-cols-1 gap-6 px-8 lg:grid-cols-2 2xl:grid-cols-3">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm"> <h4 className="text-sm">
First name<span className="text-danger-text-medium">*</span> First name<span className="text-red-500">*</span>
</h4> </h4>
<Controller <Controller
control={control} control={control}
@ -245,7 +251,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
/> />
)} )}
/> />
{errors.first_name && <span className="text-xs text-danger-text-medium">Please enter first name</span>} {errors.first_name && <span className="text-xs text-red-500">Please enter first name</span>}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@ -272,7 +278,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm"> <h4 className="text-sm">
Email<span className="text-danger-text-medium">*</span> Email<span className="text-red-500">*</span>
</h4> </h4>
<Controller <Controller
control={control} control={control}
@ -300,7 +306,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm"> <h4 className="text-sm">
Role<span className="text-danger-text-medium">*</span> Role<span className="text-red-500">*</span>
</h4> </h4>
<Controller <Controller
name="role" name="role"
@ -324,12 +330,12 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
</CustomSelect> </CustomSelect>
)} )}
/> />
{errors.role && <span className="text-xs text-danger-text-medium">Please select a role</span>} {errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm"> <h4 className="text-sm">
Display name<span className="text-danger-text-medium">*</span> Display name<span className="text-red-500">*</span>
</h4> </h4>
<Controller <Controller
control={control} control={control}
@ -364,14 +370,12 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
/> />
)} )}
/> />
{errors.display_name && ( {errors.display_name && <span className="text-xs text-red-500">Please enter display name</span>}
<span className="text-xs text-danger-text-medium">Please enter display name</span>
)}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h4 className="text-sm"> <h4 className="text-sm">
Timezone<span className="text-danger-text-medium">*</span> Timezone<span className="text-red-500">*</span>
</h4> </h4>
<Controller <Controller
@ -381,7 +385,9 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<CustomSearchSelect <CustomSearchSelect
value={value} value={value}
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"} label={
value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"
}
options={timeZoneOptions} options={timeZoneOptions}
onChange={onChange} onChange={onChange}
optionsClassName="w-full" optionsClassName="w-full"
@ -391,7 +397,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
/> />
)} )}
/> />
{errors.role && <span className="text-xs text-danger-text-medium">Please select a time zone</span>} {errors.role && <span className="text-xs text-red-500">Please select a time zone</span>}
</div> </div>
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
@ -405,7 +411,11 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
<Disclosure as="div" className="border-t border-neutral-border-subtle px-8"> <Disclosure as="div" className="border-t border-neutral-border-subtle px-8">
{({ open }) => ( {({ open }) => (
<> <>
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4"> <Disclosure.Button
as="button"
type="button"
className="flex w-full items-center justify-between py-4"
>
<span className="text-lg tracking-tight">Deactivate account</span> <span className="text-lg tracking-tight">Deactivate account</span>
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} /> <ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
</Disclosure.Button> </Disclosure.Button>
@ -422,8 +432,8 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<span className="text-sm tracking-tight"> <span className="text-sm tracking-tight">
The danger zone of the profile page is a critical area that requires careful consideration and The danger zone of the profile page is a critical area that requires careful consideration and
attention. When deactivating an account, all of the data and resources within that account will be attention. When deactivating an account, all of the data and resources within that account
permanently removed and cannot be recovered. will be permanently removed and cannot be recovered.
</span> </span>
<div> <div>
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}> <Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>
@ -437,6 +447,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-neutral-border-subtle pb-3.5"> <div className="flex items-center border-b border-neutral-border-subtle 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