style: issue detail responsiveness (#3625)

This commit is contained in:
Ramesh Kumar Chandra 2024-02-12 17:01:24 +05:30 committed by GitHub
parent eb0af8de59
commit e29edfc02b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 80 additions and 32 deletions

View File

@ -3,7 +3,7 @@ import useSWR from "swr";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useProject } from "hooks/store"; import { useApplication, useProject } from "hooks/store";
// ui // ui
import { Breadcrumbs, LayersIcon } from "@plane/ui"; import { Breadcrumbs, LayersIcon } from "@plane/ui";
// helpers // helpers
@ -15,6 +15,8 @@ import { ISSUE_DETAILS } from "constants/fetch-keys";
// components // components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { BreadcrumbLink } from "components/common"; import { BreadcrumbLink } from "components/common";
import { PanelRight } from "lucide-react";
import { cn } from "helpers/common.helper";
// services // services
const issueService = new IssueService(); const issueService = new IssueService();
@ -25,6 +27,7 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
const { workspaceSlug, projectId, issueId } = router.query; const { workspaceSlug, projectId, issueId } = router.query;
// store hooks // store hooks
const { currentProjectDetails, getProjectById } = useProject(); const { currentProjectDetails, getProjectById } = useProject();
const { theme: themeStore } = useApplication();
const { data: issueDetails } = useSWR( const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
@ -33,12 +36,14 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
: null : null
); );
const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed;
return ( 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="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>
<Breadcrumbs> <Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem <Breadcrumbs.BreadcrumbItem
type="text" type="text"
link={ link={
@ -85,6 +90,9 @@ export const ProjectIssueDetailsHeader: FC = observer(() => {
</Breadcrumbs> </Breadcrumbs>
</div> </div>
</div> </div>
<button className="block md:hidden" onClick={() => themeStore.toggleIssueDetailSidebar()}>
<PanelRight className={cn("w-4 h-4 ", !isSidebarCollapsed ? "text-custom-primary-100" : " text-custom-text-200")} />
</button>
</div> </div>
); );
}); });

View File

@ -33,7 +33,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
}`} }`}
> >
<div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden={true} /> <div className="absolute left-[13px] top-0 bottom-0 w-0.5 bg-custom-background-80" aria-hidden={true} />
<div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-center z-10 bg-custom-background-80 text-custom-text-200"> <div className="flex-shrink-0 ring-6 w-7 h-7 rounded-full overflow-hidden flex justify-center items-center z-[4] bg-custom-background-80 text-custom-text-200">
{icon ? icon : <Network className="w-3.5 h-3.5" />} {icon ? icon : <Network className="w-3.5 h-3.5" />}
</div> </div>
<div className="w-full text-custom-text-200"> <div className="w-full text-custom-text-200">

View File

@ -9,13 +9,14 @@ import { EmptyState } from "components/common";
// images // images
import emptyIssue from "public/empty-state/issue.svg"; import emptyIssue from "public/empty-state/issue.svg";
// hooks // hooks
import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// constants // constants
import { EUserProjectRoles } from "constants/project"; import { EUserProjectRoles } from "constants/project";
import { EIssuesStoreType } from "constants/issue"; import { EIssuesStoreType } from "constants/issue";
import { observer } from "mobx-react";
export type TIssueOperations = { export type TIssueOperations = {
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -51,7 +52,7 @@ export type TIssueDetailRoot = {
is_archived?: boolean; is_archived?: boolean;
}; };
export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => { export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
const { workspaceSlug, projectId, issueId, is_archived = false } = props; const { workspaceSlug, projectId, issueId, is_archived = false } = props;
// router // router
const router = useRouter(); const router = useRouter();
@ -75,6 +76,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
const { const {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { theme: themeStore } = useApplication();
const issueOperations: TIssueOperations = useMemo( const issueOperations: TIssueOperations = useMemo(
() => ({ () => ({
@ -346,8 +348,8 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
}} }}
/> />
) : ( ) : (
<div className="flex h-full overflow-hidden"> <div className="flex w-full h-full overflow-hidden">
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5"> <div className="h-full w-full max-w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
<IssueMainContent <IssueMainContent
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -356,7 +358,9 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
is_editable={!is_archived && is_editable} is_editable={!is_archived && is_editable}
/> />
</div> </div>
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5"> <div className="h-full w-full sm:w-1/2 md:w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5 fixed md:relative bg-custom-sidebar-background-100 right-0 z-[5]"
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
>
<IssueDetailsSidebar <IssueDetailsSidebar
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
@ -373,4 +377,4 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
<IssuePeekOverview /> <IssuePeekOverview />
</> </>
); );
}; });

View File

@ -187,9 +187,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"} buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
className="w-3/5 flex-grow group" className="w-3/5 flex-grow group"
buttonContainerClassName="w-full text-left" buttonContainerClassName="w-full text-left"
buttonClassName={`text-sm justify-between ${ buttonClassName={`text-sm justify-between ${issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400" }`}
}`}
hideIcon={issue.assignee_ids?.length === 0} hideIcon={issue.assignee_ids?.length === 0}
dropdownArrow dropdownArrow
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
@ -233,8 +232,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
hideIcon hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline" clearIconClassName="h-3 w-3 hidden group-hover:inline"
// TODO: add this logic // TODO: add this logic
// showPlaceholderIcon // showPlaceholderIcon
/> />
</div> </div>
@ -259,8 +258,8 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
hideIcon hideIcon
clearIconClassName="h-3 w-3 hidden group-hover:inline" clearIconClassName="h-3 w-3 hidden group-hover:inline"
// TODO: add this logic // TODO: add this logic
// showPlaceholderIcon // showPlaceholderIcon
/> />
</div> </div>

View File

@ -1,5 +1,5 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { Bell } from "lucide-react"; import { Bell, BellOff } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// UI // UI
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
@ -52,12 +52,20 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
<div> <div>
<Button <Button
size="sm" size="sm"
prependIcon={<Bell className="h-3 w-3" />} prependIcon={subscription?.subscribed ? <BellOff /> : <Bell className="h-3 w-3" />}
variant="outline-primary" variant="outline-primary"
className="hover:!bg-custom-primary-100/20" className="hover:!bg-custom-primary-100/20"
onClick={handleSubscription} onClick={handleSubscription}
> >
{loading ? "Loading..." : subscription?.subscribed ? "Unsubscribe" : "Subscribe"} {loading ? (
<span>
<span className="hidden sm:block">Loading</span>...
</span>
) : subscription?.subscribed ? (
<div className="hidden sm:block">Unsubscribe</div>
) : (
<div className="hidden sm:block">Subscribe</div>
)}
</Button> </Button>
</div> </div>
); );

View File

@ -63,14 +63,14 @@ export const SpreadsheetHeaderColumn = (props: Props) => {
</div> </div>
} }
placement="bottom-end" placement="bottom-end"
closeOnSelect
> >
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}> <CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
<div <div
className={`flex items-center justify-between gap-1.5 px-1 ${ className={`flex items-center justify-between gap-1.5 px-1 ${selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` ? "text-custom-text-100"
? "text-custom-text-100" : "text-custom-text-200 hover:text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100" }`}
}`}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" /> <ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
@ -84,11 +84,10 @@ export const SpreadsheetHeaderColumn = (props: Props) => {
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}> <CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
<div <div
className={`flex items-center justify-between gap-1.5 px-1 ${ className={`flex items-center justify-between gap-1.5 px-1 ${selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` ? "text-custom-text-100"
? "text-custom-text-100" : "text-custom-text-200 hover:text-custom-text-100"
: "text-custom-text-200 hover:text-custom-text-100" }`}
}`}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" /> <ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />

View File

@ -1,4 +1,4 @@
import React, { ReactElement } from "react"; import React, { ReactElement, useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
@ -12,7 +12,7 @@ import { Loader } from "@plane/ui";
// types // types
import { NextPageWithLayout } from "lib/types"; import { NextPageWithLayout } from "lib/types";
// fetch-keys // fetch-keys
import { useIssueDetail } from "hooks/store"; import { useApplication, useIssueDetail } from "hooks/store";
const IssueDetailsPage: NextPageWithLayout = observer(() => { const IssueDetailsPage: NextPageWithLayout = observer(() => {
// router // router
@ -23,6 +23,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
fetchIssue, fetchIssue,
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
const { theme: themeStore } = useApplication();
const { isLoading } = useSWR( const { isLoading } = useSWR(
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null, workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
@ -34,6 +35,21 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
const issue = getIssueById(issueId?.toString() || "") || undefined; const issue = getIssueById(issueId?.toString() || "") || undefined;
const issueLoader = !issue || isLoading ? true : false; const issueLoader = !issue || isLoading ? true : false;
useEffect(() => {
const handleToggleIssueDetailSidebar = () => {
if (window && window.innerWidth < 768) {
themeStore.toggleIssueDetailSidebar(true);
}
if (window && themeStore.issueDetailSidebarCollapsed && window.innerWidth >= 768) {
themeStore.toggleIssueDetailSidebar(false);
}
};
window.addEventListener("resize", handleToggleIssueDetailSidebar);
handleToggleIssueDetailSidebar();
return () => window.removeEventListener("resize", handleToggleIssueDetailSidebar);
}, [themeStore]);
return ( return (
<> <>
{issueLoader ? ( {issueLoader ? (

View File

@ -9,11 +9,13 @@ export interface IThemeStore {
sidebarCollapsed: boolean | undefined; sidebarCollapsed: boolean | undefined;
profileSidebarCollapsed: boolean | undefined; profileSidebarCollapsed: boolean | undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined;
issueDetailSidebarCollapsed: boolean | undefined;
// actions // actions
toggleSidebar: (collapsed?: boolean) => void; toggleSidebar: (collapsed?: boolean) => void;
setTheme: (theme: any) => void; setTheme: (theme: any) => void;
toggleProfileSidebar: (collapsed?: boolean) => void; toggleProfileSidebar: (collapsed?: boolean) => void;
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void; toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
toggleIssueDetailSidebar: (collapsed?: boolean) => void;
} }
export class ThemeStore implements IThemeStore { export class ThemeStore implements IThemeStore {
@ -22,6 +24,7 @@ export class ThemeStore implements IThemeStore {
theme: string | null = null; theme: string | null = null;
profileSidebarCollapsed: boolean | undefined = undefined; profileSidebarCollapsed: boolean | undefined = undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
issueDetailSidebarCollapsed: boolean | undefined = undefined;
// root store // root store
rootStore; rootStore;
@ -32,11 +35,13 @@ export class ThemeStore implements IThemeStore {
theme: observable.ref, theme: observable.ref,
profileSidebarCollapsed: observable.ref, profileSidebarCollapsed: observable.ref,
workspaceAnalyticsSidebarCollapsed: observable.ref, workspaceAnalyticsSidebarCollapsed: observable.ref,
issueDetailSidebarCollapsed: observable.ref,
// action // action
toggleSidebar: action, toggleSidebar: action,
setTheme: action, setTheme: action,
toggleProfileSidebar: action, toggleProfileSidebar: action,
toggleWorkspaceAnalyticsSidebar: action toggleWorkspaceAnalyticsSidebar: action,
toggleIssueDetailSidebar: action,
// computed // computed
}); });
// root store // root store
@ -82,6 +87,15 @@ export class ThemeStore implements IThemeStore {
localStorage.setItem("workspace_analytics_sidebar_collapsed", this.workspaceAnalyticsSidebarCollapsed.toString()); localStorage.setItem("workspace_analytics_sidebar_collapsed", this.workspaceAnalyticsSidebarCollapsed.toString());
}; };
toggleIssueDetailSidebar = (collapsed?: boolean) => {
if(collapsed === undefined) {
this.issueDetailSidebarCollapsed = !this.issueDetailSidebarCollapsed;
} else {
this.issueDetailSidebarCollapsed = collapsed;
}
localStorage.setItem("issue_detail_sidebar_collapsed", this.issueDetailSidebarCollapsed.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