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