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_VERSION=latest" >> $GITHUB_OUTPUT
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=local" >> $GITHUB_OUTPUT
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
fi
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
export type TAssignedIssuesWidgetFilters = {
target_date?: TDurationFilterOptions;
duration?: TDurationFilterOptions;
tab?: TIssuesListTypes;
};
export type TCreatedIssuesWidgetFilters = {
target_date?: TDurationFilterOptions;
duration?: TDurationFilterOptions;
tab?: TIssuesListTypes;
};
export type TIssuesByStateGroupsWidgetFilters = {
target_date?: TDurationFilterOptions;
duration?: TDurationFilterOptions;
};
export type TIssuesByPriorityWidgetFilters = {
target_date?: TDurationFilterOptions;
duration?: TDurationFilterOptions;
};
export type TWidgetFiltersFormData =

View File

@ -10,7 +10,7 @@ type BreadcrumbsProps = {
const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
<div className="flex items-center space-x-2">
{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}
{index !== React.Children.count(children) - 1 && (
<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 handleOnClick = () => {
if (closeOnSelect) closeDropdown();
};
useOutsideClickDetector(dropdownRef, closeDropdown);
let menuItems = (
@ -101,7 +106,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
tabIndex={tabIndex}
className={cn("relative w-min text-left", className)}
onKeyDownCapture={handleKeyDown}
onChange={handleOnChange}
onClick={handleOnClick}
>
{({ open }) => (
<>
@ -110,7 +115,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
<button
ref={setReferenceElement}
type="button"
onClick={() => {
onClick={(e) => {
e.stopPropagation();
openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}}
@ -127,7 +133,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
<button
ref={setReferenceElement}
type="button"
onClick={() => {
onClick={(e) => {
e.stopPropagation();
openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}}
@ -152,7 +159,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={() => {
onClick={(e) => {
e.stopPropagation();
openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}}

View File

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

View File

@ -150,7 +150,7 @@ export const ActiveCycleDetails: React.FC<IActiveCycleDetails> = observer((props
color: group.color,
}));
const daysLeft = findHowManyDaysLeft(activeCycle.end_date ?? new Date());
const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0;
return (
<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 (
<div>

View File

@ -140,7 +140,7 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
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 (
<>

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@ export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observ
const blockedByIssueProjectDetails =
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 (
<ControlLink
@ -212,7 +212,7 @@ export const CreatedOverdueIssueListItem: React.FC<IssueListItemProps> = observe
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 (
<ControlLink

View File

@ -16,42 +16,40 @@ export const TabsList: React.FC<Props> = observer((props) => {
const { durationFilter, selectedTab } = props;
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 (
<Tab.List
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={{
gridTemplateColumns: `repeat(${tabsList.length}, 1fr)`,
}}
>
<div
className={cn("absolute bg-custom-background-100 rounded transition-all duration-500 ease-in-out", {
// 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,
})}
className={cn(
"absolute top-1/2 left-[1px] bg-custom-background-100 rounded-[3px] transition-all duration-500 ease-in-out",
{
// 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,
}
)}
style={{
height: "calc(100% - 1px)",
width: `${100 / tabsList.length}%`,
transform: `translateX(${selectedTabIndex * 100}%)`,
height: "calc(100% - 2px)",
width: `calc(${100 / tabsList.length}% - 1px)`,
transform: `translate(${selectedTabIndex * 100}%, -50%)`,
}}
/>
{tabsList.map((tab) => (
<Tab
key={tab.key}
className={cn(
"relative z-[1] font-semibold text-xs rounded py-1.5 text-custom-text-400 focus:outline-none",
"transition duration-500",
"relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500",
{
"text-custom-text-100 bg-custom-background-100": 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;
// store hooks
const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard();
// derived values
const widgetDetails = getWidgetDetails(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>) => {
if (!widgetDetails) return;
@ -84,7 +86,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
filters,
});
const filterDates = getCustomDates(filters.target_date ?? widgetDetails.widget_filters.target_date ?? "none");
const filterDates = getCustomDates(filters.duration ?? selectedDuration);
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
@ -92,7 +94,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
};
useEffect(() => {
const filterDates = getCustomDates(widgetDetails?.widget_filters.target_date ?? "none");
const filterDates = getCustomDates(selectedDuration);
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
...(filterDates.trim() !== "" ? { target_date: filterDates } : {}),
@ -139,10 +141,10 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
Assigned by priority
</Link>
<DurationFilterDropdown
value={widgetDetails.widget_filters.target_date ?? "none"}
value={selectedDuration}
onChange={(val) =>
handleUpdateFilters({
target_date: val,
duration: val,
})
}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,78 @@
// ui
import { Breadcrumbs } from "@plane/ui";
import { Breadcrumbs, CustomMenu } from "@plane/ui";
import { BreadcrumbLink } from "components/common";
// components
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 = () => (
<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 TUserProfileHeader = {
type?: string | undefined
}
export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const { type = undefined } = props
const router = useRouter();
const { workspaceSlug, userId } = router.query;
const AUTHORIZED_ROLES = [20, 15, 10];
const {
membership: { currentWorkspaceRole },
} = useUser();
if (!currentWorkspaceRole) return null;
const isAuthorized = AUTHORIZED_ROLES.includes(currentWorkspaceRole);
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
const { theme: themStore } = useApplication();
return (<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-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">
<SidebarHamburgerToggle />
<div>
<div className="flex justify-between w-full">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink href="/profile" label="Activity Overview" />} />
</Breadcrumbs>
<div className="flex gap-4 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-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>)
});

View File

@ -1,6 +1,5 @@
export * from "./attachment";
export * from "./issue-modal";
export * from "./view-select";
export * from "./delete-issue-modal";
export * from "./description-form";
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="flex items-center gap-x-2">
{/* Don't show project selection if editing an issue */}
@ -699,13 +699,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
</Fragment>
)}
<Button
variant="primary"
size="sm"
loading={isSubmitting}
tabIndex={isDraft ? 19 : 18}
onClick={handleSubmit((data) => handleFormSubmit(data))}
>
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={isDraft ? 19 : 18}>
{data?.id ? (isSubmitting ? "Updating" : "Update issue") : isSubmitting ? "Creating" : "Create issue"}
</Button>
</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 { workspaceSlug } = router.query;
// store
const { projectModuleIds, moduleMap } = useModule();
const { currentProjectDetails } = useProject();
const { projectModuleIds, moduleMap, updateModuleDetails } = useModule();
const handleModuleUpdate = (module: IModule, payload: IBlockUpdateData) => {
if (!workspaceSlug) return;
// FIXME
//updateModuleGanttStructure(workspaceSlug.toString(), module.project, module, payload);
const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => {
if (!workspaceSlug || !module) return;
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[]) =>
blocks && blocks.length > 0
? blocks
.filter((blockId) => {
const block = moduleMap[blockId];
return block.start_date && block.target_date && new Date(block.start_date) <= new Date(block.target_date);
})
.map((blockId) => {
const block = moduleMap[blockId];
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 ?? ""),
};
})
: [];
blocks?.map((blockId) => {
const block = moduleMap[blockId];
return {
data: block,
id: block.id,
sort_order: block.sort_order,
start_date: block.start_date ? new Date(block.start_date) : null,
target_date: block.target_date ? new Date(block.target_date) : null,
};
});
const isAllowed = currentProjectDetails?.member_role === 20 || currentProjectDetails?.member_role === 15;
const modules = projectModuleIds;
return (
<div className="h-full w-full overflow-y-auto">
<GanttChartRoot
@ -57,6 +52,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => {
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={isAllowed}
showAllBlocks
/>
</div>
);

View File

@ -208,9 +208,6 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
<Tooltip tooltipContent="Snooze">
<CustomMenu
className="flex items-center"
menuButtonOnClick={(e: { stopPropagation: () => void }) => {
e.stopPropagation();
}}
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">
<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;
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">
{tabsList.map((tab) => (
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
<span
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${
router.pathname === tab.selected
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent"
}`}
className={`flex whitespace-nowrap border-b-2 p-4 text-sm font-medium outline-none ${router.pathname === tab.selected
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent"
}`}
>
{tab.label}
</span>

View File

@ -4,7 +4,7 @@ import useSWR from "swr";
import { Disclosure, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
// hooks
import { useUser } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
// services
import { UserService } from "services/user.service";
// components
@ -18,6 +18,8 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { renderEmoji } from "helpers/emoji.helper";
// 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
const userService = new UserService();
@ -28,6 +30,8 @@ export const ProfileSidebar = observer(() => {
const { workspaceSlug, userId } = router.query;
// store hooks
const { currentUser } = useUser();
const { theme: themStore } = useApplication();
const ref = useRef<HTMLDivElement>(null);
const { data: userProjectsData } = useSWR(
workspaceSlug && userId ? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString()) : null,
@ -36,6 +40,14 @@ export const ProfileSidebar = observer(() => {
: null
);
useOutsideClickDetector(ref, () => {
if (themStore.profileSidebarCollapsed === false) {
if (window.innerWidth < 768) {
themStore.toggleProfileSidebar();
}
}
});
const userDetails = [
{
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 (
<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 ? (
<>

View File

@ -4,6 +4,10 @@ import { renderFormattedPayloadDate } from "./date-time.helper";
// types
import { TDurationFilterOptions, TIssuesListTypes } from "@plane/types";
/**
* @description returns date range based on the duration filter
* @param duration
*/
export const getCustomDates = (duration: TDurationFilterOptions): string => {
const today = new Date();
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 => {
const today = renderFormattedPayloadDate(new Date());
@ -44,3 +52,20 @@ export const getRedirectionFilters = (type: TIssuesListTypes): string => {
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
*/
export const findTotalDaysInRange = (
startDate: Date | string,
endDate: Date | string,
startDate: Date | string | undefined | null,
endDate: Date | string | undefined | null,
inclusive: boolean = true
): number => {
if (!startDate || !endDate) return 0;
): number | undefined => {
if (!startDate || !endDate) return undefined;
// Parse the dates to check if they are valid
const parsedStartDate = new Date(startDate);
const parsedEndDate = new Date(endDate);
@ -110,8 +110,11 @@ export const findTotalDaysInRange = (
* @param {boolean} inclusive (optional) // default true
* @example findHowManyDaysLeft("2024-01-01") // 3
*/
export const findHowManyDaysLeft = (date: string | Date, inclusive: boolean = true): number => {
if (!date) return 0;
export const findHowManyDaysLeft = (
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
return findTotalDaysInRange(new Date(), date, inclusive);
};

View File

@ -2,6 +2,11 @@ import { FC, ReactNode } from "react";
// layout
import { ProfileSettingsLayout } from "layouts/settings-layout";
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 {
children: ReactNode;
@ -10,9 +15,57 @@ interface IProfilePreferenceSettingsLayout {
export const ProfilePreferenceSettingsLayout: FC<IProfilePreferenceSettingsLayout> = (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 (
<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">
<ProfilePreferenceSettingsSidebar />
<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;
href: string;
}> = [
{
label: "Theme",
href: `/profile/preferences/theme`,
},
{
label: "Email",
href: `/profile/preferences/email`,
},
];
{
label: "Theme",
href: `/profile/preferences/theme`,
},
{
label: "Email",
href: `/profile/preferences/email`,
},
];
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">
<span className="text-xs font-semibold text-custom-text-400">Preference</span>
<div className="flex w-full flex-col gap-2">
{profilePreferenceLinks.map((link) => (
<Link key={link.href} href={link.href}>
<div
className={`rounded-md px-4 py-2 text-sm font-medium ${
(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
? "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"
}`}
className={`rounded-md px-4 py-2 text-sm font-medium ${(link.label === "Import" ? router.asPath.includes(link.href) : router.asPath === link.href)
? "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"
}`}
>
{link.label}
</div>

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { mutate } from "swr";
import Link from "next/link";
import { useRouter } from "next/router";
@ -12,6 +12,7 @@ import useToast from "hooks/use-toast";
import { Tooltip } from "@plane/ui";
// constants
import { PROFILE_ACTION_LINKS } from "constants/profile";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
const WORKSPACE_ACTION_LINKS = [
{
@ -52,6 +53,35 @@ export const ProfileLayoutSidebar = observer(() => {
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 () => {
setIsSigningOut(true);
@ -73,16 +103,18 @@ export const ProfileLayoutSidebar = observer(() => {
return (
<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 ${
sidebarCollapsed ? "" : "md:w-[280px]"
} ${sidebarCollapsed ? "left-0" : "-left-full md:left-0"}`}
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 ? "-ml-[280px]" : ""}
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}`}>
<div
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${
sidebarCollapsed ? "justify-center" : ""
}`}
className={`flex flex-shrink-0 items-center gap-2 truncate px-4 pt-4 ${sidebarCollapsed ? "justify-center" : ""
}`}
>
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
<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;
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}>
<div
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)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
} ${sidebarCollapsed ? "justify-center" : ""}`}
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)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
} ${sidebarCollapsed ? "justify-center" : ""}`}
>
{<link.Icon className="h-4 w-4" />}
{!sidebarCollapsed && link.label}
@ -129,19 +160,17 @@ export const ProfileLayoutSidebar = observer(() => {
<Link
key={workspace.id}
href={`/${workspace.slug}`}
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${
sidebarCollapsed ? "justify-center" : `justify-between`
}`}
className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${sidebarCollapsed ? "justify-center" : `justify-between`
}`}
onClick={handleItemClick}
>
<span
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${
sidebarCollapsed ? "justify-center" : ""
}`}
className={`flex w-full flex-grow items-center gap-x-2 truncate rounded-md px-3 py-1 hover:bg-custom-sidebar-background-80 ${sidebarCollapsed ? "justify-center" : ""
}`}
>
<span
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"
}`}
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${!workspace?.logo && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo && workspace.logo !== "" ? (
<img
@ -163,12 +192,11 @@ export const ProfileLayoutSidebar = observer(() => {
)}
<div className="mt-1.5">
{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}>
<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 ${
sidebarCollapsed ? "justify-center" : ""
}`}
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" : ""
}`}
>
{<link.Icon className="h-4 w-4" />}
{!sidebarCollapsed && link.label}
@ -180,9 +208,8 @@ export const ProfileLayoutSidebar = observer(() => {
</div>
<div className="flex flex-shrink-0 flex-grow items-end px-6 py-2">
<div
className={`flex w-full ${
sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
}`}
className={`flex w-full ${sidebarCollapsed ? "flex-col justify-center gap-2" : "items-center justify-between gap-2"
}`}
>
<button
type="button"
@ -202,9 +229,8 @@ export const ProfileLayoutSidebar = observer(() => {
</button>
<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 ${
sidebarCollapsed ? "w-full" : ""
}`}
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" : ""
}`}
onClick={() => toggleSidebar()}
>
<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");
return (
<div className="h-full w-full md:flex md:flex-row-reverse md:overflow-hidden">
<ProfileSidebar />
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
<div className="h-full w-full realtive flex flex-row">
<div className="w-full realtive flex flex-col">
<ProfileNavbar isAuthorized={isAuthorized} showProfileIssuesFilter={showProfileIssuesFilter} />
{isAuthorized || !isAuthorizedPath ? (
<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>
<ProfileSidebar />
</div>
);
});

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import { USER_ACTIVITY } from "constants/fetch-keys";
import { calculateTimeAgo } from "helpers/date-time.helper";
// type
import { NextPageWithLayout } from "lib/types";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
const userService = new UserService();
@ -30,8 +31,10 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
const { currentUser } = useUser();
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>
</div>
{userActivity ? (
@ -94,12 +97,12 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
const message =
activityItem.verb === "created" &&
activityItem.field !== "cycles" &&
activityItem.field !== "modules" &&
activityItem.field !== "attachment" &&
activityItem.field !== "link" &&
activityItem.field !== "estimate" &&
!activityItem.field ? (
activityItem.field !== "cycles" &&
activityItem.field !== "modules" &&
activityItem.field !== "attachment" &&
activityItem.field !== "link" &&
activityItem.field !== "estimate" &&
!activityItem.field ? (
<span>
created <IssueLink activity={activityItem} />
</span>
@ -187,6 +190,7 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => {
</Loader>
)}
</section>
);
});

View File

@ -14,6 +14,7 @@ import { ProfileSettingsLayout } from "layouts/settings-layout";
import { Button, Input, Spinner } from "@plane/ui";
// types
import { NextPageWithLayout } from "lib/types";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
interface FormValues {
old_password: string;
@ -86,6 +87,10 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
);
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
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"
@ -168,6 +173,7 @@ const ChangePasswordPage: NextPageWithLayout = observer(() => {
</Button>
</div>
</form>
</div>
);
});

View File

@ -23,6 +23,7 @@ import type { NextPageWithLayout } from "lib/types";
// constants
import { USER_ROLES } from "constants/workspace";
import { TIME_ZONES } from "constants/timezones";
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
const defaultValues: Partial<IUser> = {
avatar: "",
@ -56,7 +57,7 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
// store hooks
const { currentUser: myProfile, updateCurrentUser, currentUserLoader } = useUser();
// custom hooks
const {} = useUserAuth({ user: myProfile, isLoading: currentUserLoader });
const { } = useUserAuth({ user: myProfile, isLoading: currentUserLoader });
useEffect(() => {
reset({ ...defaultValues, ...myProfile });
@ -136,304 +137,310 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => {
return (
<>
<Controller
control={control}
name="avatar"
render={({ field: { onChange, value } }) => (
<UserImageUploadModal
isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isRemoving}
handleDelete={() => handleDelete(myProfile?.avatar, true)}
onSuccess={(url) => {
onChange(url);
handleSubmit(onSubmit)();
setIsImageUploadModalOpen(false);
}}
value={value && value.trim() !== "" ? value : null}
/>
)}
/>
<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">
<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="flex flex-col h-full">
<div className="block md:hidden flex-shrink-0 border-b border-custom-border-200 p-4">
<SidebarHamburgerToggle />
</div>
<div className="overflow-hidden">
<Controller
control={control}
name="avatar"
render={({ field: { onChange, value } }) => (
<UserImageUploadModal
isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isRemoving}
handleDelete={() => handleDelete(myProfile?.avatar, true)}
onSuccess={(url) => {
onChange(url);
handleSubmit(onSubmit)();
setIsImageUploadModalOpen(false);
}}
value={value && value.trim() !== "" ? value : null}
/>
<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">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? (
<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>
)}
/>
<DeactivateAccountModal isOpen={deactivateAccountModal} onClose={() => setDeactivateAccountModal(false)} />
<div className="mx-auto flex h-full w-full flex-col space-y-10 overflow-y-auto pt-10 md:pt-16 px-8 pb-8 lg:w-3/5">
<form onSubmit={handleSubmit(onSubmit)}>
<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">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!watch("avatar") || watch("avatar") === "" ? (
<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 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"}
/>
)}
/>
</div>
</div>
<div className="item-center mt-4 flex justify-between px-8">
<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>
<div className="item-center mt-4 flex justify-between px-8">
<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}`}>
{/* <Link href={`/profile/${myProfile.id}`}>
<span className="flex item-center gap-1 text-sm text-custom-primary-100 underline font-medium">
<ExternalLink className="h-4 w-4" />
Activity Overview
</span>
</Link> */}
</div>
</div>
<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">
<h4 className="text-sm">
First name<span className="text-red-500">*</span>
</h4>
<Controller
control={control}
name="first_name"
rules={{
required: "First name is required.",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
<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">
<h4 className="text-sm">
First name<span className="text-red-500">*</span>
</h4>
<Controller
control={control}
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" : ""}`}
rules={{
required: "First name is required.",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
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>}
</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>
{errors.first_name && <span className="text-xs text-red-500">Please enter first name</span>}
</div>
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
<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>
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
</div>
</div>
</div>
</>
);

View File

@ -48,7 +48,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => {
return (
<>
{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">
<h3 className="text-xl font-medium">Preferences</h3>
</div>

View File

@ -7,15 +7,18 @@ export interface IThemeStore {
// observables
theme: string | null;
sidebarCollapsed: boolean | undefined;
profileSidebarCollapsed: boolean | undefined;
// actions
toggleSidebar: (collapsed?: boolean) => void;
setTheme: (theme: any) => void;
toggleProfileSidebar: (collapsed?: boolean) => void;
}
export class ThemeStore implements IThemeStore {
// observables
sidebarCollapsed: boolean | undefined = undefined;
theme: string | null = null;
profileSidebarCollapsed: boolean | undefined = undefined;
// root store
rootStore;
@ -24,9 +27,11 @@ export class ThemeStore implements IThemeStore {
// observable
sidebarCollapsed: observable.ref,
theme: observable.ref,
profileSidebarCollapsed: observable.ref,
// action
toggleSidebar: action,
setTheme: action,
toggleProfileSidebar: action,
// computed
});
// root store
@ -46,6 +51,19 @@ export class ThemeStore implements IThemeStore {
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
* @param _theme

View File

@ -103,7 +103,7 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
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);
return allCycleIds;
}
@ -118,7 +118,7 @@ export class CycleStore implements ICycleStore {
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
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);
return completedCycleIds;
}
@ -133,7 +133,7 @@ export class CycleStore implements ICycleStore {
const isStartDateUpcoming = isFuture(new Date(c.start_date ?? ""));
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);
return upcomingCycleIds;
}
@ -148,7 +148,7 @@ export class CycleStore implements ICycleStore {
const hasEndDatePassed = isPast(new Date(c.end_date ?? ""));
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);
return incompleteCycleIds;
}
@ -162,7 +162,7 @@ export class CycleStore implements ICycleStore {
let draftCycles = Object.values(this.cycleMap ?? {}).filter(
(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);
return draftCycleIds;
}
@ -203,7 +203,7 @@ export class CycleStore implements ICycleStore {
if (!this.fetchedMap[projectId]) return null;
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);
return cycleIds || null;
});

View File

@ -100,7 +100,7 @@ export class ModulesStore implements IModuleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
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);
return projectModuleIds || null;
}
@ -120,7 +120,7 @@ export class ModulesStore implements IModuleStore {
if (!this.fetchedMap[projectId]) return null;
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);
return projectModuleIds;
});