diff --git a/apiserver/plane/db/migrations/0059_auto_20240208_0957.py b/apiserver/plane/db/migrations/0059_auto_20240208_0957.py new file mode 100644 index 000000000..c4c43fa4b --- /dev/null +++ b/apiserver/plane/db/migrations/0059_auto_20240208_0957.py @@ -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) + ] diff --git a/packages/types/src/dashboard.d.ts b/packages/types/src/dashboard.d.ts index 7cfa6aa85..407b5cd79 100644 --- a/packages/types/src/dashboard.d.ts +++ b/packages/types/src/dashboard.d.ts @@ -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 = diff --git a/packages/ui/src/breadcrumbs/breadcrumbs.tsx b/packages/ui/src/breadcrumbs/breadcrumbs.tsx index f5a4f9f0e..657521731 100644 --- a/packages/ui/src/breadcrumbs/breadcrumbs.tsx +++ b/packages/ui/src/breadcrumbs/breadcrumbs.tsx @@ -10,7 +10,7 @@ type BreadcrumbsProps = { const Breadcrumbs = ({ children }: BreadcrumbsProps) => (
{React.Children.map(children, (child, index) => ( -
+
{child} {index !== React.Children.count(children) - 1 && (
diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index 296f69377..a5caae06b 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -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 = observer((props) => { // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(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) => { if (!widgetDetails) return; @@ -43,7 +43,7 @@ export const CreatedIssuesWidget: React.FC = 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 = 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 }); }} />
diff --git a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx index 93492160b..f94720a71 100644 --- a/web/components/dashboard/widgets/issue-panels/tabs-list.tsx +++ b/web/components/dashboard/widgets/issue-panels/tabs-list.tsx @@ -16,42 +16,40 @@ export const TabsList: React.FC = 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 (
{tabsList.map((tab) => ( diff --git a/web/components/dashboard/widgets/issues-by-priority.tsx b/web/components/dashboard/widgets/issues-by-priority.tsx index eebd448bb..fc44b22a5 100644 --- a/web/components/dashboard/widgets/issues-by-priority.tsx +++ b/web/components/dashboard/widgets/issues-by-priority.tsx @@ -73,8 +73,10 @@ export const IssuesByPriorityWidget: React.FC = 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(workspaceSlug, dashboardId, WIDGET_KEY); + const selectedDuration = widgetDetails?.widget_filters.duration ?? "none"; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -84,7 +86,7 @@ export const IssuesByPriorityWidget: React.FC = 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 = 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 = observer((props) => Assigned by priority handleUpdateFilters({ - target_date: val, + duration: val, }) } /> diff --git a/web/components/dashboard/widgets/issues-by-state-group.tsx b/web/components/dashboard/widgets/issues-by-state-group.tsx index f5f3c35a8..7689cb039 100644 --- a/web/components/dashboard/widgets/issues-by-state-group.tsx +++ b/web/components/dashboard/widgets/issues-by-state-group.tsx @@ -34,6 +34,7 @@ export const IssuesByStateGroupWidget: React.FC = observer((props) // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const selectedDuration = widgetDetails?.widget_filters.duration ?? "none"; const handleUpdateFilters = async (filters: Partial) => { if (!widgetDetails) return; @@ -43,7 +44,7 @@ export const IssuesByStateGroupWidget: React.FC = 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 = 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 = observer((props) Assigned by state handleUpdateFilters({ - target_date: val, + duration: val, }) } /> diff --git a/web/components/headers/user-profile.tsx b/web/components/headers/user-profile.tsx index 9924177af..c9af893e1 100644 --- a/web/components/headers/user-profile.tsx +++ b/web/components/headers/user-profile.tsx @@ -1,18 +1,93 @@ // 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 = () => ( -
-
- -
- - } /> - +type TUserProfileHeader = { + type?: string | undefined; +}; + +export const UserProfileHeader: FC = 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 ( +
+
+ +
+ + } + /> + +
+ + {type} + +
+ } + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + <> + {tabsList.map((tab) => ( + + + {tab.label} + + + ))} + + +
+
-
-); + ); +}); diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index 3ac859b97..8a11a0a17 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -208,9 +208,6 @@ export const NotificationCard: React.FC = (props) => { void }) => { - e.stopPropagation(); - }} customButton={
diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index cf4352eb4..e59602c31 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -22,7 +22,7 @@ export const ProfileNavbar: React.FC = (props) => { const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB; return ( -
+
{tabsList.map((tab) => ( diff --git a/web/components/profile/sidebar.tsx b/web/components/profile/sidebar.tsx index 8386c890f..4207a12ac 100644 --- a/web/components/profile/sidebar.tsx +++ b/web/components/profile/sidebar.tsx @@ -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(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,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 ( -
+
{userProjectsData ? ( <>
@@ -134,10 +164,10 @@ export const ProfileSidebar = observer(() => {
{completedIssuePercentage}% diff --git a/web/helpers/dashboard.helper.ts b/web/helpers/dashboard.helper.ts index 8003f15e3..90319a90b 100644 --- a/web/helpers/dashboard.helper.ts +++ b/web/helpers/dashboard.helper.ts @@ -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"; + } +}; diff --git a/web/layouts/settings-layout/profile/preferences/layout.tsx b/web/layouts/settings-layout/profile/preferences/layout.tsx index f58efbc34..18274fd1c 100644 --- a/web/layouts/settings-layout/profile/preferences/layout.tsx +++ b/web/layouts/settings-layout/profile/preferences/layout.tsx @@ -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 = (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 ( - + + + + {showMenuItem()} + +
+ } + customButtonClassName="flex flex-grow justify-start text-custom-text-200 text-sm" + > + <> + {profilePreferenceLinks.map((link) => ( + + {link.label} + + ))} + +
+ }>
diff --git a/web/layouts/settings-layout/profile/preferences/sidebar.tsx b/web/layouts/settings-layout/profile/preferences/sidebar.tsx index 7a78497ec..6bb32ba0d 100644 --- a/web/layouts/settings-layout/profile/preferences/sidebar.tsx +++ b/web/layouts/settings-layout/profile/preferences/sidebar.tsx @@ -19,7 +19,7 @@ export const ProfilePreferenceSettingsSidebar = () => { }, ]; return ( -
+
Preference
diff --git a/web/layouts/settings-layout/profile/sidebar.tsx b/web/layouts/settings-layout/profile/sidebar.tsx index e76be036b..1904553c2 100644 --- a/web/layouts/settings-layout/profile/sidebar.tsx +++ b/web/layouts/settings-layout/profile/sidebar.tsx @@ -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(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,11 +103,14 @@ export const ProfileLayoutSidebar = observer(() => { return (
-
+
{ if (link.key === "change-password" && currentUser?.is_password_autoset) return null; return ( - +
{ className={`flex flex-grow cursor-pointer select-none items-center truncate text-left text-sm font-medium ${ sidebarCollapsed ? "justify-center" : `justify-between` }`} + onClick={handleItemClick} > { )}
{WORKSPACE_ACTION_LINKS.map((link) => ( - +
= observer((props) => { const isAuthorizedPath = router.pathname.includes("assigned" || "created" || "subscribed"); return ( -
- -
+
+
{isAuthorized || !isAuthorizedPath ? (
{children}
@@ -40,6 +39,8 @@ export const ProfileAuthWrapper: React.FC = observer((props) => {
)}
+ +
); }); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index fe5de0454..0808b9503 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -12,7 +12,7 @@ const ProfileAssignedIssuesPage: NextPageWithLayout = () => }> + }> {page} ); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx index d91dd8d65..76f4c1fba 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx @@ -14,7 +14,7 @@ const ProfileCreatedIssuesPage: NextPageWithLayout = () => }> + }> {page} ); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx index f56a8c14f..486d8d7e3 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/index.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/index.tsx @@ -56,7 +56,7 @@ const ProfileOverviewPage: NextPageWithLayout = () => { ProfileOverviewPage.getLayout = function getLayout(page: ReactElement) { return ( - }> + }> {page} ); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx index 44d802a1b..257c23655 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx @@ -14,7 +14,7 @@ const ProfileSubscribedIssuesPage: NextPageWithLayout = () => }> + }> {page} ); diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index 13a84b6c6..d6d1e2ac2 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -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,9 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => { const { currentUser } = useUser(); return ( -
-
+
+
+

Activity

{userActivity ? ( diff --git a/web/pages/profile/change-password.tsx b/web/pages/profile/change-password.tsx index 5a30c01cd..4769cca0d 100644 --- a/web/pages/profile/change-password.tsx +++ b/web/pages/profile/change-password.tsx @@ -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 ( +
+
+ +
{
+
); }); diff --git a/web/pages/profile/index.tsx b/web/pages/profile/index.tsx index 1c2b1020d..cb36e4d7e 100644 --- a/web/pages/profile/index.tsx +++ b/web/pages/profile/index.tsx @@ -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 = { avatar: "", @@ -136,306 +137,317 @@ const ProfileSettingsPage: NextPageWithLayout = observer(() => { return ( <> - ( - setIsImageUploadModalOpen(false)} - isRemoving={isRemoving} - handleDelete={() => handleDelete(myProfile?.avatar, true)} - onSuccess={(url) => { - onChange(url); - handleSubmit(onSubmit)(); - setIsImageUploadModalOpen(false); - }} - value={value && value.trim() !== "" ? value : null} - /> - )} - /> - setDeactivateAccountModal(false)} /> -
-
-
-
- {myProfile?.first_name +
+ +
+
+ ( + setIsImageUploadModalOpen(false)} + isRemoving={isRemoving} + handleDelete={() => handleDelete(myProfile?.avatar, true)} + onSuccess={(url) => { + onChange(url); + handleSubmit(onSubmit)(); + setIsImageUploadModalOpen(false); + }} + value={value && value.trim() !== "" ? value : null} /> -
-
-
- +
+
+
+ +
+ ( + onChange(imageUrl)} + control={control} + value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"} + /> )} - + />
-
-
- ( - onChange(imageUrl)} - control={control} - value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"} - /> - )} - /> -
-
+
+
+
+ {`${watch("first_name")} ${watch("last_name")}`} +
+ {watch("email")} +
-
-
-
- {`${watch("first_name")} ${watch("last_name")}`} -
- {watch("email")} -
- - {/* - + {/* + Activity Overview */} -
+
-
-
-

- First name* -

- ( - +
+

+ First name* +

+ ( + + )} /> - )} - /> - {errors.first_name && Please enter first name} -
- -
-

Last name

- - ( - - )} - /> -
- -
-

- Email* -

- ( - - )} - /> -
- -
-

- Role* -

- ( - - {USER_ROLES.map((item) => ( - - {item.label} - - ))} - - )} - /> - {errors.role && Please select a role} -
- -
-

- Display name* -

- { - 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 } }) => ( - - )} - /> - {errors.display_name && ( - Please enter display name - )} -
- -
-

- Timezone* -

- - ( - 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-neutral-border-medium" - input - /> - )} - /> - {errors.role && Please select a time zone} -
- -
- -
-
-
-
- - {({ open }) => ( - <> - - Deactivate account - - - - -
- - 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. - -
- -
+ {errors.first_name && Please enter first name}
-
-
- - )} -
+ +
+

Last name

+ + ( + + )} + /> +
+ +
+

+ Email* +

+ ( + + )} + /> +
+ +
+

+ Role* +

+ ( + + {USER_ROLES.map((item) => ( + + {item.label} + + ))} + + )} + /> + {errors.role && Please select a role} +
+ +
+

+ Display name* +

+ { + 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 } }) => ( + + )} + /> + {errors.display_name && Please enter display name} +
+ +
+

+ Timezone* +

+ + ( + 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-neutral-border-medium" + input + /> + )} + /> + {errors.role && Please select a time zone} +
+ +
+ +
+
+
+ + + {({ open }) => ( + <> + + Deactivate account + + + + +
+ + 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. + +
+ +
+
+
+
+ + )} +
+
+
); diff --git a/web/pages/profile/preferences/theme.tsx b/web/pages/profile/preferences/theme.tsx index dac1ba779..9971db9c6 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/pages/profile/preferences/theme.tsx @@ -48,7 +48,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { return ( <> {currentUser ? ( -
+

Preferences

diff --git a/web/store/application/theme.store.ts b/web/store/application/theme.store.ts index 1c6f792eb..7ecc0e770 100644 --- a/web/store/application/theme.store.ts +++ b/web/store/application/theme.store.ts @@ -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