conflict: merge conlficts resolved

This commit is contained in:
gurusainath 2024-02-08 23:40:49 +05:30
commit 84d3d34e14
50 changed files with 758 additions and 748 deletions

View File

@ -37,7 +37,7 @@ jobs:
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=local" >> $GITHUB_OUTPUT echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
fi fi
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT

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

@ -3,7 +3,7 @@ import { Menu } from "lucide-react";
import { useApplication } from "hooks/store"; import { useApplication } from "hooks/store";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
export const SidebarHamburgerToggle: FC = observer (() => { export const SidebarHamburgerToggle: FC = observer(() => {
const { theme: themStore } = useApplication(); const { theme: themStore } = useApplication();
return ( return (
<div <div

View File

@ -150,7 +150,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
color: group.color, color: group.color,
})); }));
const daysLeft = findHowManyDaysLeft(activeCycle.end_date ?? new Date()); const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
return ( return (
<div className="grid-row-2 grid divide-y rounded-[10px] border border-custom-border-200 bg-custom-background-100 shadow"> <div className="grid-row-2 grid divide-y rounded-[10px] border border-custom-border-200 bg-custom-background-100 shadow">

View File

@ -137,7 +137,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
}); });
}; };
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date ?? new Date()); const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
return ( return (
<div> <div>

View File

@ -140,7 +140,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus); const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date ?? new Date()); const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
return ( return (
<> <>

View File

@ -1,11 +1,8 @@
import { FC } from "react"; import { FC } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { KeyedMutator } from "swr";
// hooks // hooks
import { useCycle, useUser } from "hooks/store"; import { useCycle, useUser } from "hooks/store";
// services
import { CycleService } from "services/cycle.service";
// components // components
import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart";
import { CycleGanttBlock } from "components/cycles"; import { CycleGanttBlock } from "components/cycles";
@ -17,14 +14,10 @@ import { EUserProjectRoles } from "constants/project";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
cycleIds: string[]; cycleIds: string[];
mutateCycles?: KeyedMutator<ICycle[]>;
}; };
// services
const cycleService = new CycleService();
export const CyclesListGanttChartView: FC<Props> = observer((props) => { export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const { cycleIds, mutateCycles } = props; const { cycleIds } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -32,38 +25,15 @@ export const CyclesListGanttChartView: FC<Props> = observer((props) => {
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { getCycleById } = useCycle(); const { getCycleById, updateCycleDetails } = useCycle();
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => { const handleCycleUpdate = async (cycle: ICycle, data: IBlockUpdateData) => {
if (!workspaceSlug) return; if (!workspaceSlug || !cycle) return;
mutateCycles &&
mutateCycles((prevData: any) => {
if (!prevData) return prevData;
const newList = prevData.map((p: any) => ({ const payload: any = { ...data };
...p, if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
...(p.id === cycle.id
? {
start_date: payload.start_date ? payload.start_date : p.start_date,
target_date: payload.target_date ? payload.target_date : p.end_date,
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
}
: {}),
}));
if (payload.sort_order) { await updateCycleDetails(workspaceSlug.toString(), cycle.project, cycle.id, payload);
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
}
return newList;
}, false);
const newPayload: any = { ...payload };
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
cycleService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload);
}; };
const blockFormat = (blocks: (ICycle | null)[]) => { const blockFormat = (blocks: (ICycle | null)[]) => {

View File

@ -318,6 +318,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const issueCount = const issueCount =
cycleDetails.total_issues === 0 ? "0 Issue" : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; cycleDetails.total_issues === 0 ? "0 Issue" : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@ -375,8 +376,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
backgroundColor: `${currentCycle.color}20`, backgroundColor: `${currentCycle.color}20`,
}} }}
> >
{currentCycle.value === "current" {currentCycle.value === "current" && daysLeft !== undefined
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}` ? `${daysLeft} ${currentCycle.label}`
: `${currentCycle.label}`} : `${currentCycle.label}`}
</span> </span>
)} )}

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

@ -83,7 +83,7 @@ export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observ
const blockedByIssueProjectDetails = const blockedByIssueProjectDetails =
blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null; blockedByIssues.length === 1 ? getProjectById(blockedByIssues[0]?.project_id ?? "") : null;
const dueBy = findTotalDaysInRange(new Date(issueDetails.target_date ?? ""), new Date(), false); const dueBy = findTotalDaysInRange(new Date(issueDetails.target_date ?? ""), new Date(), false) ?? 0;
return ( return (
<ControlLink <ControlLink
@ -212,7 +212,7 @@ export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observe
const projectDetails = getProjectById(issue.project_id); const projectDetails = getProjectById(issue.project_id);
const dueBy = findTotalDaysInRange(new Date(issue.target_date ?? ""), new Date(), false); const dueBy = findTotalDaysInRange(new Date(issue.target_date ?? ""), new Date(), false) ?? 0;
return ( return (
<ControlLink <ControlLink

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(
// right shadow "absolute top-1/2 left-[1px] bg-custom-background-100 rounded-[3px] transition-all duration-500 ease-in-out",
"shadow-[2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== tabsList.length - 1, {
// left shadow // right shadow
"shadow-[-2px_0_8px_rgba(167,169,174,0.15)]": selectedTabIndex !== 0, "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,
}
)}
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

@ -3,8 +3,7 @@ import { TIssue } from "@plane/types";
import { IGanttBlock } from "components/gantt-chart"; import { IGanttBlock } from "components/gantt-chart";
export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] => export const renderIssueBlocksStructure = (blocks: TIssue[]): IGanttBlock[] =>
blocks && blocks?.map((block) => ({
blocks.map((block) => ({
data: block, data: block,
id: block.id, id: block.id,
sort_order: block.sort_order, sort_order: block.sort_order,

View File

@ -93,7 +93,7 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
<> <>
{blocks ? ( {blocks ? (
blocks.map((block, index) => { blocks.map((block, index) => {
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? ""); const duration = findTotalDaysInRange(block.start_date, block.target_date);
return ( return (
<Draggable <Draggable
@ -130,9 +130,11 @@ export const CycleGanttSidebar: React.FC<Props> = (props) => {
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<CycleGanttSidebarBlock data={block.data} /> <CycleGanttSidebarBlock data={block.data} />
</div> </div>
<div className="flex-shrink-0 text-sm text-custom-text-200"> {duration !== undefined && (
{duration} day{duration > 1 ? "s" : ""} <div className="flex-shrink-0 text-sm text-custom-text-200">
</div> {duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -93,7 +93,7 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
<> <>
{blocks ? ( {blocks ? (
blocks.map((block, index) => { blocks.map((block, index) => {
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? ""); const duration = findTotalDaysInRange(block.start_date, block.target_date);
return ( return (
<Draggable <Draggable
@ -130,9 +130,11 @@ export const ModuleGanttSidebar: React.FC<Props> = (props) => {
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<ModuleGanttSidebarBlock data={block.data} /> <ModuleGanttSidebarBlock data={block.data} />
</div> </div>
<div className="flex-shrink-0 text-sm text-custom-text-200"> {duration !== undefined && (
{duration} day{duration > 1 ? "s" : ""} <div className="flex-shrink-0 text-sm text-custom-text-200">
</div> {duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -94,7 +94,7 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
<> <>
{blocks ? ( {blocks ? (
blocks.map((block, index) => { blocks.map((block, index) => {
const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? ""); const duration = findTotalDaysInRange(block.start_date, block.target_date);
return ( return (
<Draggable <Draggable
@ -131,9 +131,11 @@ export const ProjectViewGanttSidebar: React.FC<Props> = (props) => {
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<IssueGanttSidebarBlock data={block.data} /> <IssueGanttSidebarBlock data={block.data} />
</div> </div>
<div className="flex-shrink-0 text-sm text-custom-text-200"> {duration !== undefined && (
{duration} day{duration > 1 ? "s" : ""} <div className="flex-shrink-0 text-sm text-custom-text-200">
</div> {duration} day{duration > 1 ? "s" : ""}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -119,10 +119,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false // hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!showAllBlocks && !isBlockVisibleOnSidebar) return; if (!showAllBlocks && !isBlockVisibleOnSidebar) return;
const duration = const duration = findTotalDaysInRange(block.start_date, block.target_date);
!block.start_date || !block.target_date
? null
: findTotalDaysInRange(block.start_date, block.target_date);
return ( return (
<Draggable <Draggable
@ -166,13 +163,13 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<IssueGanttSidebarBlock data={block.data} /> <IssueGanttSidebarBlock data={block.data} />
</div> </div>
<div className="flex-shrink-0 text-sm text-custom-text-200"> {duration !== undefined && (
{duration && ( <div className="flex-shrink-0 text-sm text-custom-text-200">
<span> <span>
{duration} day{duration > 1 ? "s" : ""} {duration} day{duration > 1 ? "s" : ""}
</span> </span>
)} </div>
</div> )}
</div> </div>
</div> </div>
</div> </div>

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

@ -1,6 +1,5 @@
export * from "./attachment"; export * from "./attachment";
export * from "./issue-modal"; export * from "./issue-modal";
export * from "./view-select";
export * from "./delete-issue-modal"; export * from "./delete-issue-modal";
export * from "./description-form"; export * from "./description-form";
export * from "./issue-layouts"; export * from "./issue-layouts";

View File

@ -250,7 +250,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
}} }}
/> />
)} )}
<form> <form onSubmit={handleSubmit((data) => handleFormSubmit(data))}>
<div className="space-y-5"> <div className="space-y-5">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
{/* Don't show project selection if editing an issue */} {/* Don't show project selection if editing an issue */}
@ -699,13 +699,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
</Fragment> </Fragment>
)} )}
<Button <Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={isDraft ? 19 : 18}>
variant="primary"
size="sm"
loading={isSubmitting}
tabIndex={isDraft ? 19 : 18}
onClick={handleSubmit((data) => handleFormSubmit(data))}
>
{data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"} {data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"}
</Button> </Button>
</div> </div>

View File

@ -1,81 +0,0 @@
// ui
import { CustomDatePicker } from "components/ui";
import { Tooltip } from "@plane/ui";
import { CalendarCheck } from "lucide-react";
// helpers
import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
onChange: (date: string | null) => void;
handleOnOpen?: () => void;
handleOnClose?: () => void;
tooltipPosition?: "top" | "bottom";
className?: string;
noBorder?: boolean;
disabled: boolean;
};
export const ViewDueDateSelect: React.FC<Props> = ({
issue,
onChange,
handleOnOpen,
handleOnClose,
tooltipPosition = "top",
className = "",
noBorder = false,
disabled,
}) => {
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
return (
<Tooltip
tooltipHeading="Due date"
tooltipContent={issue.target_date ? renderFormattedDate(issue.target_date) ?? "N/A" : "N/A"}
position={tooltipPosition}
>
<div
className={`group max-w-[6.5rem] flex-shrink-0 ${className} ${
issue.target_date === null
? ""
: issue.target_date < new Date().toISOString()
? "text-red-600"
: findHowManyDaysLeft(issue.target_date) <= 3 && "text-orange-400"
}`}
>
<CustomDatePicker
value={issue?.target_date}
onChange={onChange}
className={`bg-transparent ${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"}`}
customInput={
<div
className={`flex items-center justify-center gap-2 rounded border border-custom-border-200 px-2 py-1 text-xs shadow-sm duration-200 hover:bg-custom-background-80 ${
issue.target_date ? "pr-6 text-custom-text-300" : "text-custom-text-400"
} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
>
{issue.target_date ? (
<>
<CalendarCheck className="h-3.5 w-3.5 flex-shrink-0" />
<span>{renderFormattedDate(issue.target_date) ?? "_ _"}</span>
</>
) : (
<>
<CalendarCheck className="h-3.5 w-3.5 flex-shrink-0" />
<span>Due Date</span>
</>
)}
</div>
}
minDate={minDate ?? undefined}
noBorder={noBorder}
handleOnOpen={handleOnOpen}
handleOnClose={handleOnClose}
disabled={disabled}
/>
</div>
</Tooltip>
);
};

View File

@ -1,64 +0,0 @@
import React from "react";
import { observer } from "mobx-react-lite";
import { Triangle } from "lucide-react";
import sortBy from "lodash/sortBy";
// store hooks
import { useEstimate } from "hooks/store";
// ui
import { CustomSelect, Tooltip } from "@plane/ui";
// types
import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
onChange: (data: number) => void;
tooltipPosition?: "top" | "bottom";
customButton?: boolean;
disabled: boolean;
};
export const ViewEstimateSelect: React.FC<Props> = observer((props) => {
const { issue, onChange, tooltipPosition = "top", customButton = false, disabled } = props;
const { areEstimatesEnabledForCurrentProject, activeEstimateDetails, getEstimatePointValue } = useEstimate();
const estimateValue = getEstimatePointValue(issue.estimate_point, issue.project_id);
const estimateLabels = (
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>
<div className="flex items-center gap-1 text-custom-text-200">
<Triangle className="h-3 w-3" />
{estimateValue ?? "None"}
</div>
</Tooltip>
);
if (!areEstimatesEnabledForCurrentProject) return null;
return (
<CustomSelect
value={issue.estimate_point}
onChange={onChange}
{...(customButton ? { customButton: estimateLabels } : { label: estimateLabels })}
maxHeight="md"
noChevron
disabled={disabled}
>
<CustomSelect.Option value={null}>
<>
<span>
<Triangle className="h-3 w-3" />
</span>
None
</>
</CustomSelect.Option>
{sortBy(activeEstimateDetails?.points, "key")?.map((estimate) => (
<CustomSelect.Option key={estimate.id} value={estimate.key}>
<>
<Triangle className="h-3 w-3" />
{estimate.value}
</>
</CustomSelect.Option>
))}
</CustomSelect>
);
});

View File

@ -1,3 +0,0 @@
export * from "./due-date";
export * from "./estimate";
export * from "./start-date";

View File

@ -1,72 +0,0 @@
// ui
import { CustomDatePicker } from "components/ui";
import { Tooltip } from "@plane/ui";
import { CalendarClock } from "lucide-react";
// helpers
import { renderFormattedDate } from "helpers/date-time.helper";
// types
import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
onChange: (date: string | null) => void;
handleOnOpen?: () => void;
handleOnClose?: () => void;
tooltipPosition?: "top" | "bottom";
className?: string;
noBorder?: boolean;
disabled: boolean;
};
export const ViewStartDateSelect: React.FC<Props> = ({
issue,
onChange,
handleOnOpen,
handleOnClose,
tooltipPosition = "top",
className = "",
noBorder = false,
disabled,
}) => {
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
return (
<Tooltip
tooltipHeading="Start date"
tooltipContent={issue.start_date ? renderFormattedDate(issue.start_date) ?? "N/A" : "N/A"}
position={tooltipPosition}
>
<div className={`group max-w-[6.5rem] flex-shrink-0 ${className}`}>
<CustomDatePicker
value={issue?.start_date}
onChange={onChange}
maxDate={maxDate ?? undefined}
noBorder={noBorder}
handleOnOpen={handleOnOpen}
customInput={
<div
className={`flex items-center justify-center gap-2 rounded border border-custom-border-200 px-2 py-1 text-xs shadow-sm duration-200 hover:bg-custom-background-80 ${
issue?.start_date ? "pr-6 text-custom-text-300" : "text-custom-text-400"
} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
>
{issue?.start_date ? (
<>
<CalendarClock className="h-3.5 w-3.5 flex-shrink-0" />
<span>{renderFormattedDate(issue?.start_date ?? "_ _")}</span>
</>
) : (
<>
<CalendarClock className="h-3.5 w-3.5 flex-shrink-0" />
<span>Start Date</span>
</>
)}
</div>
}
handleOnClose={handleOnClose}
disabled={disabled}
/>
</div>
</Tooltip>
);
};

View File

@ -13,37 +13,32 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store // store
const { projectModuleIds, moduleMap } = useModule();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectModuleIds, moduleMap, updateModuleDetails } = useModule();
const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => { const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => {
if (!workspaceSlug) return; if (!workspaceSlug || !module) return;
// FIXME
//updateModuleGanttStructure(workspaceSlug.toString(), module.project, module, payload); const payload: any = { ...data };
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
await updateModuleDetails(workspaceSlug.toString(), module.project, module.id, payload);
}; };
const blockFormat = (blocks: string[]) => const blockFormat = (blocks: string[]) =>
blocks && blocks.length > 0 blocks?.map((blockId) => {
? blocks const block = moduleMap[blockId];
.filter((blockId) => { return {
const block = moduleMap[blockId]; data: block,
return block.start_date && block.target_date && new Date(block.start_date) <= new Date(block.target_date); id: block.id,
}) sort_order: block.sort_order,
.map((blockId) => { start_date: block.start_date ? new Date(block.start_date) : null,
const block = moduleMap[blockId]; target_date: block.target_date ? new Date(block.target_date) : null,
return { };
data: block, });
id: block.id,
sort_order: block.sort_order,
start_date: new Date(block.start_date ?? ""),
target_date: new Date(block.target_date ?? ""),
};
})
: [];
const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15; const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15;
const modules = projectModuleIds;
return ( return (
<div className="h-full w-full overflow-y-auto"> <div className="h-full w-full overflow-y-auto">
<GanttChartRoot <GanttChartRoot
@ -57,6 +52,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
enableBlockRightResize={isAllowed} enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed} enableBlockMove={isAllowed}
enableReorder={isAllowed} enableReorder={isAllowed}
showAllBlocks
/> />
</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,16 +22,15 @@ 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" }`}
}`}
> >
{tab.label} {tab.label}
</span> </span>

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,9 +59,25 @@ 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 <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`} 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 ? (
<> <>

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

@ -87,11 +87,11 @@ export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" |
* @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8 * @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8
*/ */
export const findTotalDaysInRange = ( export const findTotalDaysInRange = (
startDate: Date | string, startDate: Date | string | undefined | null,
endDate: Date | string, endDate: Date | string | undefined | null,
inclusive: boolean = true inclusive: boolean = true
): number => { ): number | undefined => {
if (!startDate || !endDate) return 0; if (!startDate || !endDate) return undefined;
// Parse the dates to check if they are valid // Parse the dates to check if they are valid
const parsedStartDate = new Date(startDate); const parsedStartDate = new Date(startDate);
const parsedEndDate = new Date(endDate); const parsedEndDate = new Date(endDate);
@ -110,8 +110,11 @@ export const findTotalDaysInRange = (
* @param {boolean} inclusive (optional) // default true * @param {boolean} inclusive (optional) // default true
* @example findHowManyDaysLeft("2024-01-01") // 3 * @example findHowManyDaysLeft("2024-01-01") // 3
*/ */
export const findHowManyDaysLeft = (date: string | Date, inclusive: boolean = true): number => { export const findHowManyDaysLeft = (
if (!date) return 0; date: Date | string | undefined | null,
inclusive: boolean = true
): number | undefined => {
if (!date) return undefined;
// Pass the date to findTotalDaysInRange function to find the total number of days in range from today // Pass the date to findTotalDaysInRange function to find the total number of days in range from today
return findTotalDaysInRange(new Date(), date, inclusive); return findTotalDaysInRange(new Date(), date, inclusive);
}; };

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

@ -9,28 +9,27 @@ export const ProfilePreferenceSettingsSidebar = () => {
label: string; label: string;
href: string; href: string;
}> = [ }> = [
{ {
label: "Theme", label: "Theme",
href: `/profile/preferences/theme`, href: `/profile/preferences/theme`,
}, },
{ {
label: "Email", label: "Email",
href: `/profile/preferences/email`, href: `/profile/preferences/email`,
}, },
]; ];
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" }`}
}`}
> >
{link.label} {link.label}
</div> </div>

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,16 +103,18 @@ 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">
<ChevronLeft className="h-5 w-5" strokeWidth={1} /> <ChevronLeft className="h-5 w-5" strokeWidth={1} />
@ -101,14 +133,13 @@ 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" : ""}`}
> >
{<link.Icon className="h-4 w-4" />} {<link.Icon className="h-4 w-4" />}
{!sidebarCollapsed && link.label} {!sidebarCollapsed && link.label}
@ -129,19 +160,17 @@ 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 <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={`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" : ""
sidebarCollapsed ? "justify-center" : "" }`}
}`}
> >
<span <span
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${ 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 && "rounded bg-custom-primary-500 text-white" }`}
}`}
> >
{workspace?.logo && workspace.logo !== "" ? ( {workspace?.logo && workspace.logo !== "" ? (
<img <img
@ -163,12 +192,11 @@ 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" />}
{!sidebarCollapsed && link.label} {!sidebarCollapsed && link.label}
@ -180,9 +208,8 @@ 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
type="button" type="button"
@ -202,9 +229,8 @@ 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()}
> >
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${sidebarCollapsed ? "rotate-180" : ""}`} /> <MoveLeft className={`h-3.5 w-3.5 duration-300 ${sidebarCollapsed ? "rotate-180" : ""}`} />

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 ? (
@ -94,12 +97,12 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
const message = const message =
activityItem.verb === "created" && activityItem.verb === "created" &&
activityItem.field !== "cycles" && activityItem.field !== "cycles" &&
activityItem.field !== "modules" && activityItem.field !== "modules" &&
activityItem.field !== "attachment" && activityItem.field !== "attachment" &&
activityItem.field !== "link" && activityItem.field !== "link" &&
activityItem.field !== "estimate" && activityItem.field !== "estimate" &&
!activityItem.field ? ( !activityItem.field ? (
<span> <span>
created <IssueLink activity={activityItem} /> created <IssueLink activity={activityItem} />
</span> </span>
@ -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: "",
@ -56,7 +57,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
// store hooks // store hooks
const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser(); const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser();
// custom hooks // custom hooks
const {} = useUserAuth({ user: myProfile, isLoading: currentUserLoader }); const { } = useUserAuth({ user: myProfile, isLoading: currentUserLoader });
useEffect(() => { useEffect(() => {
reset({ ...defaultValues, ...myProfile }); reset({ ...defaultValues, ...myProfile });
@ -136,304 +137,310 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
return ( return (
<> <>
<Controller <div className="flex flex-col h-full">
control={control} <div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
name="avatar" <SidebarHamburgerToggle />
render={({ field: { onChange, value } }) => ( </div>
<UserImageUploadModal <div className="overflow-hidden">
isOpen={isImageUploadModalOpen} <Controller
onClose={() => setIsImageUploadModalOpen(false)} control={control}
isRemoving={isRemoving} name="avatar"
handleDelete={() => handleDelete(myProfile?.avatar, true)} render={({ field: { onChange, value } }) => (
onSuccess={(url) => { <UserImageUploadModal
onChange(url); isOpen={isImageUploadModalOpen}
handleSubmit(onSubmit)(); onClose={() => setIsImageUploadModalOpen(false)}
setIsImageUploadModalOpen(false); isRemoving={isRemoving}
}} handleDelete={() => handleDelete(myProfile?.avatar, true)}
value={value && value.trim() !== "" ? value : null} onSuccess={(url) => {
/> onChange(url);
)} handleSubmit(onSubmit)();
/> setIsImageUploadModalOpen(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"> value={value && value.trim() !== "" ? value : null}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex w-full flex-col gap-8">
<div className="relative h-44 w-full">
<img
src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
className="h-44 w-full rounded-lg object-cover"
alt={myProfile?.first_name ?? "Cover image"}
/> />
<div className="absolute -bottom-6 left-8 flex items-end justify-between"> )}
<div className="flex gap-3"> />
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-custom-background-90"> <DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}> <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">
{!watch("avatar") || watch("avatar") === "" ? ( <form onSubmit={handleSubmit(onSubmit)}>
<div className="h-16 w-16 rounded-md bg-custom-background-80 p-2"> <div className="flex w-full flex-col gap-8">
<User2 className="h-full w-full text-custom-text-200" /> <div className="relative h-44 w-full">
</div> <img
) : ( src={watch("cover_image") ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
<div className="relative h-16 w-16 overflow-hidden"> className="h-44 w-full rounded-lg object-cover"
<img alt={myProfile?.first_name ?? "Cover image"}
src={watch("avatar")} />
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover" <div className="absolute -bottom-6 left-8 flex items-end justify-between">
onClick={() => setIsImageUploadModalOpen(true)} <div className="flex gap-3">
alt={myProfile.display_name} <div className="flex h-16 w-16 items-center justify-center rounded-lg bg-custom-background-90">
role="button" <button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
/> {!watch("avatar") || watch("avatar") === "" ? (
</div> <div className="h-16 w-16 rounded-md bg-custom-background-80 p-2">
<User2 className="h-full w-full text-custom-text-200" />
</div>
) : (
<div className="relative h-16 w-16 overflow-hidden">
<img
src={watch("avatar")}
className="absolute left-0 top-0 h-full w-full rounded-lg object-cover"
onClick={() => setIsImageUploadModalOpen(true)}
alt={myProfile.display_name}
role="button"
/>
</div>
)}
</button>
</div>
</div>
</div>
<div className="absolute bottom-3 right-3 flex">
<Controller
control={control}
name="cover_image"
render={({ field: { value, onChange } }) => (
<ImagePickerPopover
label={"Change cover"}
onChange={(imageUrl) => onChange(imageUrl)}
control={control}
value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
/>
)} )}
</button> />
</div> </div>
</div> </div>
</div>
<div className="absolute bottom-3 right-3 flex"> <div className="item-center mt-4 flex justify-between px-8">
<Controller <div className="flex flex-col">
control={control} <div className="item-center flex text-lg font-semibold text-custom-text-100">
name="cover_image" <span>{`${watch("first_name")} ${watch("last_name")}`}</span>
render={({ field: { value, onChange } }) => ( </div>
<ImagePickerPopover <span className="text-sm tracking-tight">{watch("email")}</span>
label={"Change cover"} </div>
onChange={(imageUrl) => onChange(imageUrl)}
control={control}
value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"}
/>
)}
/>
</div>
</div>
<div className="item-center mt-4 flex justify-between px-8"> {/* <Link href={`/profile/${myProfile.id}`}>
<div className="flex flex-col">
<div className="item-center flex text-lg font-semibold text-custom-text-100">
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
</div>
<span className="text-sm tracking-tight">{watch("email")}</span>
</div>
{/* <Link href={`/profile/${myProfile.id}`}>
<span className="flex item-center gap-1 text-sm text-custom-primary-100 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>
</Link> */} </Link> */}
</div> </div>
<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-red-500">*</span> First name<span className="text-red-500">*</span>
</h4> </h4>
<Controller <Controller
control={control} control={control}
name="first_name"
rules={{
required: "First name is required.",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
name="first_name" name="first_name"
type="text" rules={{
value={value} required: "First name is required.",
onChange={onChange} }}
ref={ref} render={({ field: { value, onChange, ref } }) => (
hasError={Boolean(errors.first_name)} <Input
placeholder="Enter your first name" id="first_name"
className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`} name="first_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.first_name)}
placeholder="Enter your first name"
className={`w-full rounded-md ${errors.first_name ? "border-red-500" : ""}`}
/>
)}
/> />
)} {errors.first_name && <span className="text-xs text-red-500">Please enter first name</span>}
/>
{errors.first_name && <span className="text-xs text-red-500">Please enter first name</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Last name</h4>
<Controller
control={control}
name="last_name"
render={({ field: { value, onChange, ref } }) => (
<Input
id="last_name"
name="last_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.last_name)}
placeholder="Enter your last name"
className="w-full rounded-md"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">
Email<span className="text-red-500">*</span>
</h4>
<Controller
control={control}
name="email"
rules={{
required: "Email is required.",
}}
render={({ field: { value, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="Enter your email"
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${
errors.email ? "border-red-500" : ""
}`}
disabled
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">
Role<span className="text-red-500">*</span>
</h4>
<Controller
name="role"
control={control}
rules={{ required: "Role is required." }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
buttonClassName={errors.role ? "border-red-500" : "border-none"}
className="rounded-md border-[0.5px] !border-custom-border-200"
optionsClassName="w-full"
input
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">
Display name<span className="text-red-500">*</span>
</h4>
<Controller
control={control}
name="display_name"
rules={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="display_name"
name="display_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.display_name)}
placeholder="Enter your display name"
className={`w-full ${errors.display_name ? "border-red-500" : ""}`}
/>
)}
/>
{errors.display_name && <span className="text-xs text-red-500">Please enter display name</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">
Timezone<span className="text-red-500">*</span>
</h4>
<Controller
name="user_timezone"
control={control}
rules={{ required: "Time zone is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"}
options={timeZoneOptions}
onChange={onChange}
optionsClassName="w-full"
buttonClassName={errors.user_timezone ? "border-red-500" : "border-none"}
className="rounded-md border-[0.5px] !border-custom-border-200"
input
/>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a time zone</span>}
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isLoading}>
{isLoading ? "Saving..." : "Save changes"}
</Button>
</div>
</div>
</div>
</form>
<Disclosure as="div" className="border-t border-custom-border-100 px-8">
{({ open }) => (
<>
<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>
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
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
permanently removed and cannot be recovered.
</span>
<div>
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>
Deactivate account
</Button>
</div>
</div> </div>
</Disclosure.Panel>
</Transition> <div className="flex flex-col gap-1">
</> <h4 className="text-sm">Last name</h4>
)}
</Disclosure> <Controller
control={control}
name="last_name"
render={({ field: { value, onChange, ref } }) => (
<Input
id="last_name"
name="last_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.last_name)}
placeholder="Enter your last name"
className="w-full rounded-md"
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">
Email<span className="text-red-500">*</span>
</h4>
<Controller
control={control}
name="email"
rules={{
required: "Email is required.",
}}
render={({ field: { value, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
ref={ref}
hasError={Boolean(errors.email)}
placeholder="Enter your email"
className={`w-full rounded-md cursor-not-allowed !bg-custom-background-80 ${errors.email ? "border-red-500" : ""
}`}
disabled
/>
)}
/>
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">
Role<span className="text-red-500">*</span>
</h4>
<Controller
name="role"
control={control}
rules={{ required: "Role is required." }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={value ? value.toString() : "Select your role"}
buttonClassName={errors.role ? "border-red-500" : "border-none"}
className="rounded-md border-[0.5px] !border-custom-border-200"
optionsClassName="w-full"
input
>
{USER_ROLES.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a role</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">
Display name<span className="text-red-500">*</span>
</h4>
<Controller
control={control}
name="display_name"
rules={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1)
return "Display name must be at least 1 characters long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="display_name"
name="display_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.display_name)}
placeholder="Enter your display name"
className={`w-full ${errors.display_name ? "border-red-500" : ""}`}
/>
)}
/>
{errors.display_name && <span className="text-xs text-red-500">Please enter display name</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm">
Timezone<span className="text-red-500">*</span>
</h4>
<Controller
name="user_timezone"
control={control}
rules={{ required: "Time zone is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={value ? TIME_ZONES.find((t) => t.value === value)?.label ?? value : "Select a timezone"}
options={timeZoneOptions}
onChange={onChange}
optionsClassName="w-full"
buttonClassName={errors.user_timezone ? "border-red-500" : "border-none"}
className="rounded-md border-[0.5px] !border-custom-border-200"
input
/>
)}
/>
{errors.role && <span className="text-xs text-red-500">Please select a time zone</span>}
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" type="submit" loading={isLoading}>
{isLoading ? "Saving..." : "Save changes"}
</Button>
</div>
</div>
</div>
</form>
<Disclosure as="div" className="border-t border-custom-border-100 px-8">
{({ open }) => (
<>
<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>
<ChevronDown className={`h-5 w-5 transition-all ${open ? "rotate-180" : ""}`} />
</Disclosure.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="flex flex-col gap-8">
<span className="text-sm tracking-tight">
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
permanently removed and cannot be recovered.
</span>
<div>
<Button variant="danger" onClick={() => setDeactivateAccountModal(true)}>
Deactivate account
</Button>
</div>
</div>
</Disclosure.Panel>
</Transition>
</>
)}
</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

View File

@ -103,7 +103,7 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId; const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null; if (!projectId || !this.fetchedMap[projectId]) return null;
let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project === projectId); let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project === projectId);
allCycles = sortBy(allCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); allCycles = sortBy(allCycles, [(c) => c.sort_order]);
const allCycleIds = allCycles.map((c) => c.id); const allCycleIds = allCycles.map((c) => c.id);
return allCycleIds; return allCycleIds;
} }
@ -118,7 +118,7 @@ export class CycleStore implements ICycleStore {
const hasEndDatePassed = isPast(new Date(c.end_date ?? "")); const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
return c.project === projectId && hasEndDatePassed; return c.project === projectId && hasEndDatePassed;
}); });
completedCycles = sortBy(completedCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
const completedCycleIds = completedCycles.map((c) => c.id); const completedCycleIds = completedCycles.map((c) => c.id);
return completedCycleIds; return completedCycleIds;
} }
@ -133,7 +133,7 @@ export class CycleStore implements ICycleStore {
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? "")); const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
return c.project === projectId && isStartDateUpcoming; return c.project === projectId && isStartDateUpcoming;
}); });
upcomingCycles = sortBy(upcomingCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
const upcomingCycleIds = upcomingCycles.map((c) => c.id); const upcomingCycleIds = upcomingCycles.map((c) => c.id);
return upcomingCycleIds; return upcomingCycleIds;
} }
@ -148,7 +148,7 @@ export class CycleStore implements ICycleStore {
const hasEndDatePassed = isPast(new Date(c.end_date ?? "")); const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
return c.project === projectId && !hasEndDatePassed; return c.project === projectId && !hasEndDatePassed;
}); });
incompleteCycles = sortBy(incompleteCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
const incompleteCycleIds = incompleteCycles.map((c) => c.id); const incompleteCycleIds = incompleteCycles.map((c) => c.id);
return incompleteCycleIds; return incompleteCycleIds;
} }
@ -162,7 +162,7 @@ export class CycleStore implements ICycleStore {
let draftCycles = Object.values(this.cycleMap ?? {}).filter( let draftCycles = Object.values(this.cycleMap ?? {}).filter(
(c) => c.project === projectId && !c.start_date && !c.end_date (c) => c.project === projectId && !c.start_date && !c.end_date
); );
draftCycles = sortBy(draftCycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); draftCycles = sortBy(draftCycles, [(c) => c.sort_order]);
const draftCycleIds = draftCycles.map((c) => c.id); const draftCycleIds = draftCycles.map((c) => c.id);
return draftCycleIds; return draftCycleIds;
} }
@ -203,7 +203,7 @@ export class CycleStore implements ICycleStore {
if (!this.fetchedMap[projectId]) return null; if (!this.fetchedMap[projectId]) return null;
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId); let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId);
cycles = sortBy(cycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); cycles = sortBy(cycles, [(c) => c.sort_order]);
const cycleIds = cycles.map((c) => c.id); const cycleIds = cycles.map((c) => c.id);
return cycleIds || null; return cycleIds || null;
}); });

View File

@ -100,7 +100,7 @@ export class ModulesStore implements IModuleStore {
const projectId = this.rootStore.app.router.projectId; const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null; if (!projectId || !this.fetchedMap[projectId]) return null;
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId); let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
projectModules = sortBy(projectModules, [(m) => !m.is_favorite, (m) => m.name.toLowerCase()]); projectModules = sortBy(projectModules, [(m) => m.sort_order]);
const projectModuleIds = projectModules.map((m) => m.id); const projectModuleIds = projectModules.map((m) => m.id);
return projectModuleIds || null; return projectModuleIds || null;
} }
@ -120,7 +120,7 @@ export class ModulesStore implements IModuleStore {
if (!this.fetchedMap[projectId]) return null; if (!this.fetchedMap[projectId]) return null;
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId); let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
projectModules = sortBy(projectModules, [(m) => !m.is_favorite, (m) => m.name.toLowerCase()]); projectModules = sortBy(projectModules, [(m) => m.sort_order]);
const projectModuleIds = projectModules.map((m) => m.id); const projectModuleIds = projectModules.map((m) => m.id);
return projectModuleIds; return projectModuleIds;
}); });