Compare commits

...

4 Commits

Author SHA1 Message Date
gakshita
eb43542a0c fix: revamped box containers 2024-08-28 17:56:20 +05:30
gakshita
2674413d0c fix: home header 2024-08-28 17:51:02 +05:30
gakshita
c224f493d4 fix: filters code splitting 2024-08-28 17:41:25 +05:30
gakshita
f099f7f961 chore: headers + common containers 2024-08-28 17:35:20 +05:30
52 changed files with 1023 additions and 826 deletions

View File

@@ -333,6 +333,8 @@ module.exports = {
72: "16.2rem",
80: "18rem",
96: "21.6rem",
"page-x": "1.35rem",
"page-y": "1.35rem"
},
margin: {
0: "0",
@@ -434,5 +436,23 @@ module.exports = {
custom: ["Inter", "sans-serif"],
},
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography"), function({ addUtilities }) {
const newUtilities = {
// Mobile screens
'.px-page-x': {
paddingLeft: '0.675rem',
paddingRight: '0.675rem',
},
// Medium screens (768px and up)
'@media (min-width: 768px)': {
'.px-page-x': {
paddingLeft: '1.35rem',
paddingRight: '1.35rem',
},
}
};
addUtilities(newUtilities, ['responsive']);
},
],
};

View File

@@ -6,25 +6,24 @@ import { Breadcrumbs, ContrastIcon } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
// plane web components
import { HeaderContainer } from "@/components/containers";
import { UpgradeBadge } from "@/plane-web/components/workspace";
export const WorkspaceActiveCycleHeader = observer(() => (
<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 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 items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label="Active Cycles"
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300 rotate-180" />}
/>
}
/>
</Breadcrumbs>
<UpgradeBadge size="md" />
</div>
</div>
</div>
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label="Active Cycles"
icon={<ContrastIcon className="h-4 w-4 text-custom-text-300 rotate-180" />}
/>
}
/>
</Breadcrumbs>
<UpgradeBadge size="md" />
</HeaderContainer.LeftItem>
</HeaderContainer>
));

View File

@@ -10,6 +10,7 @@ import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
// helpers
import { HeaderContainer } from "@/components/containers";
import { cn } from "@/helpers/common.helper";
// hooks
import { useAppTheme } from "@/hooks/store";
@@ -37,37 +38,33 @@ export const WorkspaceAnalyticsHeader = observer(() => {
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 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 items-center justify-between">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink label="Analytics" icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />} />
}
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Analytics" icon={<BarChart2 className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
{analytics_tab === "custom" ? (
<button
className="block md:hidden"
onClick={() => {
toggleWorkspaceAnalyticsSidebar();
}}
>
<PanelRight
className={cn(
"block h-4 w-4 md:hidden",
!workspaceAnalyticsSidebarCollapsed ? "text-custom-primary-100" : "text-custom-text-200"
)}
/>
</Breadcrumbs>
{analytics_tab === "custom" && (
<button
className="block md:hidden"
onClick={() => {
toggleWorkspaceAnalyticsSidebar();
}}
>
<PanelRight
className={cn(
"block h-4 w-4 md:hidden",
!workspaceAnalyticsSidebarCollapsed ? "text-custom-primary-100" : "text-custom-text-200"
)}
/>
</button>
)}
</div>
</div>
</div>
</button>
) : (
<></>
)}
</HeaderContainer.LeftItem>
</HeaderContainer>
</>
);
});

View File

@@ -11,6 +11,7 @@ import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
// constants
import { HeaderContainer } from "@/components/containers";
import { CHANGELOG_REDIRECTED, GITHUB_REDIRECTED } from "@/constants/event-tracker";
// hooks
import { useEventTracker } from "@/hooks/store";
@@ -22,8 +23,8 @@ export const WorkspaceDashboardHeader = () => {
return (
<>
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
@@ -32,8 +33,8 @@ export const WorkspaceDashboardHeader = () => {
/>
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-3 px-3">
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<a
onClick={() =>
captureEvent(CHANGELOG_REDIRECTED, {
@@ -67,8 +68,8 @@ export const WorkspaceDashboardHeader = () => {
/>
<span className="hidden text-xs font-medium sm:hidden md:block">Star us on GitHub</span>
</a>
</div>
</div>
</HeaderContainer.RightItem>
</HeaderContainer>
</>
);
};

View File

@@ -10,7 +10,7 @@ import { IUserProfileProjectSegregation } from "@plane/types";
import { Breadcrumbs, CustomMenu, UserActivityIcon } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
// components
import { ProfileIssuesFilter } from "@/components/profile";
import { HeaderContainer } from "@/components/containers";
import { PROFILE_ADMINS_TAB, PROFILE_VIEWER_TAB } from "@/constants/profile";
import { EUserWorkspaceRoles } from "@/constants/workspace";
import { cn } from "@/helpers/common.helper";
@@ -23,7 +23,7 @@ type TUserProfileHeader = {
};
export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const { userProjectsData, type = undefined, showProfileIssuesFilter } = props;
const { userProjectsData, type = undefined } = props;
// router
const { workspaceSlug, userId } = useParams();
// store hooks
@@ -46,8 +46,8 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const breadcrumbLabel = `${isCurrentUser ? "Your" : userName} Work`;
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div className="flex w-full justify-between">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
@@ -61,7 +61,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
}
/>
</Breadcrumbs>
<div className="hidden md:flex md:items-center">{showProfileIssuesFilter && <ProfileIssuesFilter />}</div>
{/* <div className="hidden md:flex md:items-center">{showProfileIssuesFilter && <ProfileIssuesFilter />}</div> */}
<div className="flex gap-4 md:hidden">
<CustomMenu
maxHeight={"md"}
@@ -104,7 +104,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
</button>
</div>
</div>
</div>
</div>
</HeaderContainer.LeftItem>
</HeaderContainer>
);
});

View File

@@ -8,6 +8,7 @@ import { ArchiveIcon, Breadcrumbs, Tooltip } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
// constants
import { HeaderContainer } from "@/components/containers";
import { PROJECT_ARCHIVES_BREADCRUMB_LIST } from "@/constants/archives";
import { EIssuesStoreType } from "@/constants/issue";
// hooks
@@ -16,8 +17,8 @@ import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TProps = {
activeTab: 'issues' | 'cycles' | 'modules';
}
activeTab: "issues" | "cycles" | "modules";
};
export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
const { activeTab } = props;
@@ -38,8 +39,8 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
PROJECT_ARCHIVES_BREADCRUMB_LIST[activeTab as keyof typeof PROJECT_ARCHIVES_BREADCRUMB_LIST];
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div className="flex items-center gap-2.5">
<Breadcrumbs onBack={router.back} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
@@ -92,7 +93,7 @@ export const ProjectArchivesHeader: FC<TProps> = observer((props: TProps) => {
</Tooltip>
) : null}
</div>
</div>
</div>
</HeaderContainer.LeftItem>
</HeaderContainer>
);
});

View File

@@ -7,6 +7,7 @@ import useSWR from "swr";
import { ArchiveIcon, Breadcrumbs, LayersIcon } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { IssueDetailQuickActions } from "@/components/issues";
// constants
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
@@ -36,66 +37,66 @@ export const ProjectArchivedIssueDetailsHeader = observer(() => {
);
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Archives"
icon={<ArchiveIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Issues"
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={
currentProjectDetails && issueDetails
? `${currentProjectDetails.identifier}-${issueDetails.sequence_id}`
: ""
}
/>
}
/>
</Breadcrumbs>
</div>
</div>
<IssueDetailQuickActions
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={archivedIssueId.toString()}
/>
</div>
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Archives"
icon={<ArchiveIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/archives/issues`}
label="Issues"
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={
currentProjectDetails && issueDetails
? `${currentProjectDetails.identifier}-${issueDetails.sequence_id}`
: ""
}
/>
}
/>
</Breadcrumbs>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<IssueDetailQuickActions
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={archivedIssueId.toString()}
/>
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -13,6 +13,7 @@ import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/u
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
import { BreadcrumbLink, Logo } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import {
@@ -161,8 +162,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)}
cycleDetails={cycleDetails ?? undefined}
/>
<div className="relative z-[15] w-full items-center gap-x-2 gap-y-4">
<div className="flex justify-between bg-custom-sidebar-background-100 p-4">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div className="flex items-center gap-2">
<Breadcrumbs onBack={router.back} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
@@ -235,6 +236,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
/>
</Breadcrumbs>
</div>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<div className="hidden items-center gap-2 md:flex ">
<LayoutSelection
layouts={[
@@ -315,8 +318,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
>
<PanelRight className={cn("h-4 w-4", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")} />
</button>
</div>
</div>
</HeaderContainer.RightItem>
</HeaderContainer>
</>
);
});

View File

@@ -9,6 +9,7 @@ import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
import { BreadcrumbLink, Logo } from "@/components/common";
import { CyclesViewHeader } from "@/components/cycles";
// constants
import { HeaderContainer } from "@/components/containers";
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
@@ -30,48 +31,50 @@ export const CyclesListHeader: FC = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs onBack={router.back} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
icon={
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Cycles" icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</div>
</div>
{canUserCreateCycle && currentProjectDetails && (
<div className="flex items-center gap-3">
<CyclesViewHeader projectId={currentProjectDetails.id} />
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Cycles page");
toggleCreateCycleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Cycle
</Button>
</div>
)}
</div>
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs onBack={router.back} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
icon={
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Cycles" icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
{canUserCreateCycle && currentProjectDetails ? (
<div className="flex items-center gap-3">
<CyclesViewHeader projectId={currentProjectDetails.id} />
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Cycles page");
toggleCreateCycleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Cycle
</Button>
</div>
) : (
<></>
)}
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -8,6 +8,7 @@ import { RefreshCcw } from "lucide-react";
import { Breadcrumbs, Button, Intake } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { InboxIssueCreateEditModalRoot } from "@/components/inbox";
// constants
import { EUserProjectRoles } from "@/constants/project";
@@ -30,8 +31,8 @@ export const ProjectInboxHeader: FC = observer(() => {
const isViewer = currentProjectRole === EUserProjectRoles.VIEWER;
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div className="flex items-center gap-4">
<Breadcrumbs isLoading={currentProjectDetailsLoader}>
<Breadcrumbs.BreadcrumbItem
@@ -64,23 +65,26 @@ export const ProjectInboxHeader: FC = observer(() => {
</div>
)}
</div>
</div>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
{currentProjectDetails?.inbox_view && workspaceSlug && projectId && !isViewer ? (
<div className="flex items-center gap-2">
<InboxIssueCreateEditModalRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
modalState={createIssueModal}
handleModalClose={() => setCreateIssueModal(false)}
issue={undefined}
/>
{currentProjectDetails?.inbox_view && workspaceSlug && projectId && !isViewer && (
<div className="flex items-center gap-2">
<InboxIssueCreateEditModalRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
modalState={createIssueModal}
handleModalClose={() => setCreateIssueModal(false)}
issue={undefined}
/>
<Button variant="primary" size="sm" onClick={() => setCreateIssueModal(true)}>
Add issue
</Button>
</div>
)}
</div>
<Button variant="primary" size="sm" onClick={() => setCreateIssueModal(true)}>
Add issue
</Button>
</div>
) : (
<></>
)}
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -7,6 +7,7 @@ import { PanelRight } from "lucide-react";
import { Breadcrumbs, LayersIcon } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { IssueDetailQuickActions } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
@@ -29,8 +30,8 @@ export const ProjectIssueDetailsHeader = observer(() => {
const isSidebarCollapsed = issueDetailSidebarCollapsed;
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div>
<Breadcrumbs onBack={router.back} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
@@ -75,17 +76,19 @@ export const ProjectIssueDetailsHeader = observer(() => {
/>
</Breadcrumbs>
</div>
</div>
<IssueDetailQuickActions
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={issueId.toString()}
/>
<button className="block md:hidden" onClick={() => toggleIssueDetailSidebar()}>
<PanelRight
className={cn("h-4 w-4 ", !isSidebarCollapsed ? "text-custom-primary-100" : " text-custom-text-200")}
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<IssueDetailQuickActions
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={issueId.toString()}
/>
</button>
</div>
<button className="block md:hidden" onClick={() => toggleIssueDetailSidebar()}>
<PanelRight
className={cn("h-4 w-4 ", !isSidebarCollapsed ? "text-custom-primary-100" : " text-custom-text-200")}
/>
</button>
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -1,130 +1,55 @@
"use client";
import { useCallback, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { Briefcase, Circle, ExternalLink } from "lucide-react";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
import { BreadcrumbLink, CountChip, Logo } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
import { HeaderContainer } from "@/components/containers";
// constants
import {
EIssueFilterType,
EIssuesStoreType,
EIssueLayoutTypes,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
import HeaderFilters from "@/components/issues/filters";
import { EIssuesStoreType } from "@/constants/issue";
// helpers
import { EUserProjectRoles } from "@/constants/project";
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import {
useEventTracker,
useLabel,
useProject,
useProjectState,
useUser,
useMember,
useCommandPalette,
} from "@/hooks/store";
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
import { useIssues } from "@/hooks/store/use-issues";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
export const ProjectIssuesHeader = observer(() => {
// states
const [analyticsModal, setAnalyticsModal] = useState(false);
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
const {
project: { projectMemberIds },
} = useMember();
const {
issuesFilter: { issueFilters, updateFilters },
issues: { getGroupIssueCount },
} = useIssues(EIssuesStoreType.PROJECT);
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
const {
issues: { getGroupIssueCount },
} = useIssues(EIssuesStoreType.PROJECT);
const { currentProjectDetails, loader } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { isMobile } = usePlatformOS();
const activeLayout = issueFilters?.displayFilters?.layout;
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
},
[workspaceSlug, projectId, issueFilters, updateFilters]
);
const handleLayoutChange = useCallback(
(layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
},
[workspaceSlug, projectId, updateFilters]
);
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
const issuesCount = getGroupIssueCount(undefined, undefined, false);
const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
const issuesCount = getGroupIssueCount(undefined, undefined, false);
return (
<>
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
projectDetails={currentProjectDetails ?? undefined}
/>
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div className="flex items-center gap-2.5">
<Breadcrumbs onBack={() => router.back()} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
@@ -165,7 +90,7 @@ export const ProjectIssuesHeader = observer(() => {
</Tooltip>
) : null}
</div>
{currentProjectDetails?.anchor && (
{currentProjectDetails?.anchor ? (
<a
href={publishedURL}
className="group flex items-center gap-1.5 rounded bg-custom-primary-100/10 px-2.5 py-1 text-xs font-medium text-custom-primary-100"
@@ -176,61 +101,20 @@ export const ProjectIssuesHeader = observer(() => {
Public
<ExternalLink className="hidden h-3 w-3 group-hover:block" strokeWidth={2} />
</a>
) : (
<></>
)}
</div>
<div className="items-center gap-2 hidden md:flex">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<div className="hidden gap-3 md:flex">
<HeaderFilters
projectId={projectId}
currentProjectDetails={currentProjectDetails}
workspaceSlug={workspaceSlug}
canUserCreateIssue={canUserCreateIssue}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>
{canUserCreateIssue && (
<>
<Button
className="hidden md:block"
onClick={() => setAnalyticsModal(true)}
variant="neutral-primary"
size="sm"
>
Analytics
</Button>
</div>
{canUserCreateIssue ? (
<Button
onClick={() => {
setTrackElement("Project issues page");
@@ -240,9 +124,11 @@ export const ProjectIssuesHeader = observer(() => {
>
<div className="hidden sm:block">Add</div> Issue
</Button>
</>
)}
</div>
) : (
<></>
)}
</HeaderContainer.RightItem>
</HeaderContainer>
</>
);
});

View File

@@ -13,6 +13,7 @@ import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui";
// components
import { ProjectAnalyticsModal } from "@/components/analytics";
import { BreadcrumbLink, Logo } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import {
@@ -161,169 +162,164 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)}
moduleDetails={moduleDetails ?? undefined}
/>
<div className="relative z-[15] items-center gap-x-2 gap-y-4">
<div className="flex justify-between bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<Breadcrumbs onBack={router.back} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<span>
<span className="hidden md:block">
<BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
icon={
currentProjectDetails && (
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</span>
)
}
/>
</span>
<Link
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs onBack={router.back} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<span>
<span className="hidden md:block">
<BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
className="block pl-2 text-custom-text-300 md:hidden"
>
...
</Link>
icon={
currentProjectDetails && (
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</span>
)
}
/>
</span>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/modules`}
label="Modules"
icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="component"
component={
<CustomMenu
label={
<>
<DiceIcon className="h-3 w-3" />
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
{issuesCount && issuesCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issuesCount} ${
issuesCount > 1 ? "issues" : "issue"
} in this module`}
position="bottom"
>
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
{issuesCount}
</span>
</Tooltip>
) : null}
</div>
</>
}
className="ml-1.5 flex-shrink-0"
placement="bottom-start"
<Link
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
className="block pl-2 text-custom-text-300 md:hidden"
>
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
</CustomMenu>
}
/>
</Breadcrumbs>
</div>
<div className="flex items-center gap-2">
<div className="hidden gap-2 md:flex">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown
title="Filters"
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
...
</Link>
</span>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${projectId}/modules`}
label="Modules"
icon={<DiceIcon className="h-4 w-4 text-custom-text-300" />}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
/>
<Breadcrumbs.BreadcrumbItem
type="component"
component={
<CustomMenu
label={
<>
<DiceIcon className="h-3 w-3" />
<div className="flex w-auto max-w-[70px] items-center gap-2 truncate sm:max-w-[200px]">
<p className="truncate">{moduleDetails?.name && moduleDetails.name}</p>
{issuesCount && issuesCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issuesCount} ${
issuesCount > 1 ? "issues" : "issue"
} in this module`}
position="bottom"
>
<span className="flex flex-shrink-0 cursor-default items-center justify-center rounded-xl bg-custom-primary-100/20 px-2 text-center text-xs font-semibold text-custom-primary-100">
{issuesCount}
</span>
</Tooltip>
) : null}
</div>
</>
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["module"]}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>
{canUserCreateIssue && (
<>
<Button
className="hidden md:block"
onClick={() => setAnalyticsModal(true)}
variant="neutral-primary"
size="sm"
className="ml-1.5 flex-shrink-0"
placement="bottom-start"
>
Analytics
</Button>
<Button
className="hidden sm:flex"
onClick={() => {
setTrackElement("Module issues page");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
}}
size="sm"
>
Add issue
</Button>
</>
)}
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
onClick={toggleSidebar}
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
</CustomMenu>
}
/>
</Breadcrumbs>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<div className="hidden gap-2 md:flex">
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown
title="Filters"
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<ArrowRight
className={`hidden h-4 w-4 duration-300 md:block ${isSidebarCollapsed ? "-rotate-180" : ""}`}
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
<PanelRight
className={cn(
"block h-4 w-4 md:hidden",
!isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200"
)}
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["module"]}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</button>
</FiltersDropdown>
</div>
</div>
</div>
{canUserCreateIssue ? (
<>
<Button
className="hidden md:block"
onClick={() => setAnalyticsModal(true)}
variant="neutral-primary"
size="sm"
>
Analytics
</Button>
<Button
className="hidden sm:flex"
onClick={() => {
setTrackElement("Module issues page");
toggleCreateIssueModal(true, EIssuesStoreType.MODULE);
}}
size="sm"
>
Add issue
</Button>
</>
) : (
<></>
)}
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80"
onClick={toggleSidebar}
>
<ArrowRight className={`hidden h-4 w-4 duration-300 md:block ${isSidebarCollapsed ? "-rotate-180" : ""}`} />
<PanelRight
className={cn("block h-4 w-4 md:hidden", !isSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")}
/>
</button>
</HeaderContainer.RightItem>
</HeaderContainer>
</>
);
});

View File

@@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
import { Breadcrumbs, Button, DiceIcon } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { ModuleViewHeader } from "@/components/modules";
// constants
import { EUserProjectRoles } from "@/constants/project";
@@ -30,8 +31,8 @@ export const ModulesListHeader: React.FC = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div>
<Breadcrumbs onBack={router.back} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
@@ -56,10 +57,10 @@ export const ModulesListHeader: React.FC = observer(() => {
/>
</Breadcrumbs>
</div>
</div>
<div className="flex items-center gap-2">
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<ModuleViewHeader />
{canUserCreateModule && (
{canUserCreateModule ? (
<Button
variant="primary"
size="sm"
@@ -70,8 +71,10 @@ export const ModulesListHeader: React.FC = observer(() => {
>
<div className="hidden sm:block">Add</div> Module
</Button>
) : (
<></>
)}
</div>
</div>
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -11,6 +11,7 @@ import { Breadcrumbs, Button, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE,
// components
import { BreadcrumbLink, Logo } from "@/components/common";
// helpers
import { HeaderContainer } from "@/components/containers";
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
// hooks
import { usePage, useProject } from "@/hooks/store";
@@ -59,8 +60,8 @@ export const PageDetailsHeader = observer(() => {
const isVersionHistoryOverlayActive = !!searchParams.get("version");
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
@@ -158,27 +159,31 @@ export const PageDetailsHeader = observer(() => {
/>
</Breadcrumbs>
</div>
</div>
<PageDetailsHeaderExtraActions />
{isContentEditable && !isVersionHistoryOverlayActive && (
<Button
variant="primary"
size="sm"
onClick={() => {
// ctrl/cmd + s to save the changes
const event = new KeyboardEvent("keydown", {
key: "s",
ctrlKey: !isMac,
metaKey: isMac,
});
window.dispatchEvent(event);
}}
className="flex-shrink-0 w-24"
loading={isSubmitting === "submitting"}
>
{isSubmitting === "submitting" ? "Saving" : "Save changes"}
</Button>
)}
</div>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<PageDetailsHeaderExtraActions />
{isContentEditable && !isVersionHistoryOverlayActive ? (
<Button
variant="primary"
size="sm"
onClick={() => {
// ctrl/cmd + s to save the changes
const event = new KeyboardEvent("keydown", {
key: "s",
ctrlKey: !isMac,
metaKey: isMac,
});
window.dispatchEvent(event);
}}
className="flex-shrink-0 w-24"
loading={isSubmitting === "submitting"}
>
{isSubmitting === "submitting" ? "Saving" : "Save changes"}
</Button>
) : (
<></>
)}
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -8,6 +8,7 @@ import { Breadcrumbs, Button } from "@plane/ui";
// helpers
import { BreadcrumbLink, Logo } from "@/components/common";
// constants
import { HeaderContainer } from "@/components/containers";
import { EPageAccess } from "@/constants/page";
import { EUserProjectRoles } from "@/constants/project";
// hooks
@@ -30,8 +31,8 @@ export const PagesListHeader = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
@@ -56,24 +57,28 @@ export const PagesListHeader = observer(() => {
/>
</Breadcrumbs>
</div>
</div>
{canUserCreatePage && (
<div className="flex items-center gap-2">
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Project pages page");
toggleCreatePageModal({
isOpen: true,
pageAccess: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
});
}}
>
Add page
</Button>
</div>
)}
</div>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
{canUserCreatePage ? (
<div className="flex items-center gap-2">
<Button
variant="primary"
size="sm"
onClick={() => {
setTrackElement("Project pages page");
toggleCreatePageModal({
isOpen: true,
pageAccess: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
});
}}
>
Add page
</Button>
</div>
) : (
<></>
)}
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -9,6 +9,7 @@ import { Breadcrumbs, CustomMenu } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
// constants
import { HeaderContainer } from "@/components/containers";
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useProject, useUser } from "@/hooks/store";
@@ -29,8 +30,8 @@ export const ProjectSettingHeader: FC = observer(() => {
const projectMemberInfo = currentProjectRole || EUserProjectRoles.GUEST;
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div>
<div className="z-50">
<Breadcrumbs onBack={router.back} isLoading={loader}>
@@ -84,7 +85,7 @@ export const ProjectSettingHeader: FC = observer(() => {
)
)}
</CustomMenu>
</div>
</div>
</HeaderContainer.LeftItem>
</HeaderContainer>
);
});

View File

@@ -11,6 +11,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
import { Breadcrumbs, Button, CustomMenu, Tooltip } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import {
@@ -136,8 +137,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const publishLink = getPublishViewLink(viewDetails?.anchor);
return (
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2">
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
@@ -208,15 +209,17 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
/>
</Breadcrumbs>
{viewDetails?.access === EViewAccess.PRIVATE && (
{viewDetails?.access === EViewAccess.PRIVATE ? (
<div className="cursor-default text-custom-text-300">
<Tooltip tooltipContent={"Private"}>
<Lock className="h-4 w-4" />
</Tooltip>
</div>
) : (
<></>
)}
{viewDetails?.anchor && publishLink && (
{viewDetails?.anchor && publishLink ? (
<a
href={publishLink}
className="px-3 py-1.5 bg-green-500/20 text-green-500 rounded text-xs font-medium flex items-center gap-1.5"
@@ -226,10 +229,12 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
<span className="flex-shrink-0 rounded-full size-1.5 bg-green-500" />
Live
</a>
) : (
<></>
)}
</div>
<div className="flex items-center gap-2">
{!viewDetails?.is_locked && (
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
{!viewDetails?.is_locked ? (
<>
<LayoutSelection
layouts={[
@@ -278,8 +283,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
/>
</FiltersDropdown>
</>
) : (
<></>
)}
{canUserCreateIssue && (
{canUserCreateIssue ? (
<Button
onClick={() => {
setTrackElement("PROJECT_VIEW_PAGE_HEADER");
@@ -289,8 +296,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
>
Add issue
</Button>
) : (
<></>
)}
</div>
</div>
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -9,6 +9,7 @@ import { TViewFilterProps } from "@plane/types";
import { Breadcrumbs, Button } from "@plane/ui";
// components
import { BreadcrumbLink, Logo } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { ViewListHeader } from "@/components/views";
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
// constants
@@ -47,44 +48,42 @@ export const ProjectViewsHeader = observer(() => {
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails && (
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Views" icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</div>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails && (
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Views" icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<ViewListHeader />
<div>
<Button variant="primary" size="sm" onClick={() => toggleCreateViewModal(true)}>
Add view
</Button>
</div>
</div>
</div>
</HeaderContainer.RightItem>
</HeaderContainer>
{isFiltersApplied && (
<div className="border-t border-custom-border-200 px-5 py-3">
<div className="border-t border-custom-border-200 p-page-x py-3">
<ViewAppliedFiltersList
appliedFilters={filters.filters ?? {}}
handleClearAllFilters={clearAllFilters}

View File

@@ -8,30 +8,29 @@ import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
// hooks
import { HeaderContainer } from "@/components/containers";
import { useWorkspace } from "@/hooks/store";
export const WorkspaceSettingHeader: FC = observer(() => {
const { currentWorkspace, loader } = useWorkspace();
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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${currentWorkspace?.slug}/settings`}
label={currentWorkspace?.name ?? "Workspace"}
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Settings" />} />
</Breadcrumbs>
</div>
</div>
</div>
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${currentWorkspace?.slug}/settings`}
label={currentWorkspace?.name ?? "Workspace"}
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Settings" />} />
</Breadcrumbs>
</HeaderContainer.LeftItem>
</HeaderContainer>
);
});

View File

@@ -10,6 +10,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
import { Breadcrumbs, Button } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// constants
@@ -98,17 +99,18 @@ export const GlobalIssuesHeader = observer(() => {
return (
<>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
<div className="relative z-[15] flex h-[3.75rem] w-full items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="relative flex gap-2">
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label={`Views`} icon={<Layers className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
</div>
<div className="flex items-center gap-2">
{!isLocked && (
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
{!isLocked ? (
<>
<FiltersDropdown
title="Filters"
@@ -135,13 +137,15 @@ export const GlobalIssuesHeader = observer(() => {
/>
</FiltersDropdown>
</>
) : (
<></>
)}
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
Add view
</Button>
</div>
</div>
</HeaderContainer.RightItem>
</HeaderContainer>
</>
);
});

View File

@@ -1,6 +1,7 @@
// types
import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types";
// constants
import { BoxContainer } from "@/components/containers";
import { STATE_GROUPS } from "@/constants/state";
type Props = {
@@ -8,7 +9,7 @@ type Props = {
};
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="space-y-3 rounded-[10px] border border-custom-border-200 p-3">
<BoxContainer>
<div>
<h4 className="text-base font-medium text-custom-text-100">Total open tasks</h4>
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
@@ -47,5 +48,5 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
);
})}
</div>
</div>
</BoxContainer>
);

View File

@@ -1,4 +1,5 @@
// ui
import { BoxContainer } from "@/components/containers";
import { ProfileEmptyState } from "@/components/ui";
// image
import emptyUsers from "@/public/empty-state/empty_users.svg";
@@ -18,7 +19,7 @@ type Props = {
};
export const AnalyticsLeaderBoard: React.FC<Props> = ({ users, title, emptyStateMessage, workspaceSlug }) => (
<div className="rounded-[10px] border border-custom-border-200 p-3">
<BoxContainer>
<h6 className="text-base font-medium">{title}</h6>
{users.length > 0 ? (
<div className="mt-3 space-y-3">
@@ -57,5 +58,5 @@ export const AnalyticsLeaderBoard: React.FC<Props> = ({ users, title, emptyState
<ProfileEmptyState title="No Data yet" description={emptyStateMessage} image={emptyUsers} />
</div>
)}
</div>
</BoxContainer>
);

View File

@@ -1,9 +1,9 @@
// ui
import { IDefaultAnalyticsUser } from "@plane/types";
import { BoxContainer } from "@/components/containers";
import { BarGraph, ProfileEmptyState } from "@/components/ui";
// image
import emptyBarGraph from "@/public/empty-state/empty_bar_graph.svg";
// types
type Props = {
pendingUnAssignedIssuesUser: IDefaultAnalyticsUser | undefined;
@@ -11,7 +11,7 @@ type Props = {
};
export const AnalyticsScope: React.FC<Props> = ({ pendingUnAssignedIssuesUser, pendingAssignedIssues }) => (
<div className="rounded-[10px] border border-custom-border-200 p-3">
<BoxContainer>
<div className="divide-y divide-custom-border-200">
<div>
<div className="flex items-center justify-between">
@@ -87,5 +87,5 @@ export const AnalyticsScope: React.FC<Props> = ({ pendingUnAssignedIssuesUser, p
)}
</div>
</div>
</div>
</BoxContainer>
);

View File

@@ -1,5 +1,6 @@
// ui
import { IDefaultAnalyticsResponse } from "@plane/types";
import { BoxContainer } from "@/components/containers";
import { LineGraph, ProfileEmptyState } from "@/components/ui";
// image
import { MONTHS_LIST } from "@/constants/calendar";
@@ -12,8 +13,8 @@ type Props = {
};
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="rounded-[10px] border border-custom-border-200 py-3">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
<BoxContainer>
<h1 className="text-base font-medium">Issues closed in a year</h1>
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
<LineGraph
data={[
@@ -55,5 +56,5 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
/>
</div>
)}
</div>
</BoxContainer>
);

View File

@@ -0,0 +1,17 @@
import { cn } from "@plane/editor";
type Props = {
children: React.ReactNode;
className?: string;
};
export const BoxContainer = ({ children, className = "" }: Props) => (
<div
className={cn(
`bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 p-6`,
className
)}
>
{children}
</div>
);

View File

@@ -0,0 +1,19 @@
interface Props {
children: string | JSX.Element | JSX.Element[];
}
const HeaderContainer = ({ children }: Props) => (
<div className="relative z-10 flex w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 !bg-blue-200">
{children}
</div>
);
const LeftItem = ({ children }: Props) => (
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">{children}</div>
);
const RightItem = ({ children }: Props) => <div className="w-full flex items-center justify-end gap-3">{children}</div>;
HeaderContainer.LeftItem = LeftItem;
HeaderContainer.RightItem = RightItem;
HeaderContainer.displayName = "core-header-container";
export { HeaderContainer };

View File

@@ -0,0 +1,9 @@
import { cn } from "@plane/editor";
type Props = {
children: string | JSX.Element | JSX.Element[];
className?: string;
};
export const HuggingRow = ({ children, className }: Props) => (
<div className={cn("px-0 py-page-y h-full w-full", className)}>{children}</div>
);

View File

@@ -0,0 +1,5 @@
export * from "./header-container";
export * from "./page-container";
export * from "./box-container";
export * from "./regular-row";
export * from "./hugging-row";

View File

@@ -0,0 +1,16 @@
import { cn } from "@plane/editor";
type Props = {
children: React.ReactNode;
className?: string;
};
export const PageContainer = ({ children, className = "" }: Props) => (
<div
className={cn(
"px-page-x py-page-y h-full w-full flex flex-col space-y-7 overflow-y-auto vertical-scrollbar scrollbar-md",
className
)}
>
{children}
</div>
);

View File

@@ -0,0 +1,9 @@
import { cn } from "@plane/editor";
type Props = {
children: string | JSX.Element | JSX.Element[];
className?: string;
};
export const RegularRow = ({ children, className }: Props) => (
<div className={cn("px-page-x py-page-y w-full", className)}>{children}</div>
);

View File

@@ -3,6 +3,7 @@
import { ReactNode } from "react";
// components
import { SidebarHamburgerToggle } from "@/components/core";
import { RegularRow } from "../containers";
export interface AppHeaderProps {
header: ReactNode;
@@ -13,16 +14,14 @@ export const AppHeader = (props: AppHeaderProps) => {
const { header, mobileHeader } = props;
return (
<>
<div className="z-[15]">
<div className="z-10 flex w-full items-center border-b border-custom-border-200">
<div className="block bg-custom-sidebar-background-100 py-4 pl-5 md:hidden">
<SidebarHamburgerToggle />
</div>
<div className="w-full">{header}</div>
<div className="z-[15] !bg-blue-200">
<RegularRow className="h-[3.75rem] z-10 flex gap-2 w-full items-center border-b border-custom-border-200">
<div className="block bg-custom-sidebar-background-100 md:hidden">
<SidebarHamburgerToggle />
</div>
{mobileHeader && mobileHeader}
</div>
</>
<div className="w-full">{header}</div>
</RegularRow>
{mobileHeader && mobileHeader}
</div>
);
};

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
import { Tab } from "@headlessui/react";
import { TAssignedIssuesWidgetFilters, TAssignedIssuesWidgetResponse } from "@plane/types";
// hooks
import { BoxContainer } from "@/components/containers";
import {
DurationFilterDropdown,
IssuesErrorState,
@@ -79,7 +80,7 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
if ((!widgetDetails || !widgetStats) && !widgetStatsError) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96">
<BoxContainer className="flex flex-col min-h-96">
{widgetStatsError ? (
<IssuesErrorState
isRefreshing={fetching}
@@ -93,7 +94,7 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
) : (
widgetStats && (
<>
<div className="flex items-center justify-between gap-2 p-6 pl-7">
<div className="flex items-center justify-between gap-2 pb-6">
<Link
href={`/${workspaceSlug}/workspace-views/assigned/${filterParams}`}
className="text-lg font-semibold text-custom-text-300 hover:underline"
@@ -137,9 +138,7 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
}}
className="h-full flex flex-col"
>
<div className="px-6">
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
</div>
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
<Tab.Panels as="div" className="h-full">
{tabsList.map((tab) => {
if (tab.key !== selectedTab) return null;
@@ -161,6 +160,6 @@ export const AssignedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
</>
)
)}
</div>
</BoxContainer>
);
});

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
import { Tab } from "@headlessui/react";
import { TCreatedIssuesWidgetFilters, TCreatedIssuesWidgetResponse } from "@plane/types";
// hooks
import { BoxContainer } from "@/components/containers";
import {
DurationFilterDropdown,
IssuesErrorState,
@@ -76,7 +77,7 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
if ((!widgetDetails || !widgetStats) && !widgetStatsError) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full hover:shadow-custom-shadow-4xl duration-300 flex flex-col min-h-96">
<BoxContainer className="flex flex-col min-h-96">
{widgetStatsError ? (
<IssuesErrorState
isRefreshing={fetching}
@@ -90,7 +91,7 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
) : (
widgetStats && (
<>
<div className="flex items-center justify-between gap-2 p-6 pl-7">
<div className="flex items-center justify-between gap-2 pb-6">
<Link
href={`/${workspaceSlug}/workspace-views/created/${filterParams}`}
className="text-lg font-semibold text-custom-text-300 hover:underline"
@@ -134,9 +135,7 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
}}
className="h-full flex flex-col"
>
<div className="px-6">
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
</div>
<TabsList durationFilter={selectedDurationFilter} selectedTab={selectedTab} />
<Tab.Panels as="div" className="h-full">
{tabsList.map((tab) => {
if (tab.key !== selectedTab) return null;
@@ -158,6 +157,6 @@ export const CreatedIssuesWidget: React.FC<WidgetProps> = observer((props) => {
</>
)
)}
</div>
</BoxContainer>
);
});

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
// types
import { TIssuesByPriorityWidgetFilters, TIssuesByPriorityWidgetResponse } from "@plane/types";
// components
import { BoxContainer } from "@/components/containers";
import {
DurationFilterDropdown,
IssuesByPriorityEmptyState,
@@ -69,8 +70,8 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
}));
return (
<div className="flex min-h-96 w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-custom-border-200 bg-custom-background-100 py-6 duration-300 hover:shadow-custom-shadow-4xl">
<div className="flex items-center justify-between gap-2 pl-7 pr-6">
<BoxContainer className="flex min-h-96 w-full flex-col overflow-hidden">
<div className="flex items-center justify-between gap-2">
<Link
href={`/${workspaceSlug}/workspace-views/assigned`}
className="text-lg font-semibold text-custom-text-300 hover:underline"
@@ -106,6 +107,6 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
<IssuesByPriorityEmptyState />
</div>
)}
</div>
</BoxContainer>
);
});

View File

@@ -4,6 +4,7 @@ import Link from "next/link";
// types
import { TIssuesByStateGroupsWidgetFilters, TIssuesByStateGroupsWidgetResponse, TStateGroups } from "@plane/types";
// components
import { BoxContainer } from "@/components/containers";
import {
DurationFilterDropdown,
IssuesByStateGroupEmptyState,
@@ -79,14 +80,14 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
startedCount > 0
? "started"
: unStartedCount > 0
? "unstarted"
: backlogCount > 0
? "backlog"
: completedCount > 0
? "completed"
: canceledCount > 0
? "cancelled"
: null;
? "unstarted"
: backlogCount > 0
? "backlog"
: completedCount > 0
? "completed"
: canceledCount > 0
? "cancelled"
: null;
setActiveStateGroup(stateGroup);
setDefaultStateGroup(stateGroup);
@@ -134,8 +135,8 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
};
return (
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full py-6 hover:shadow-custom-shadow-4xl duration-300 overflow-hidden min-h-96 flex flex-col">
<div className="flex items-center justify-between gap-2 pl-7 pr-6">
<BoxContainer className="overflow-hidden min-h-96 flex flex-col">
<div className="flex items-center justify-between gap-2">
<Link
href={`/${workspaceSlug}/workspace-views/assigned`}
className="text-lg font-semibold text-custom-text-300 hover:underline"
@@ -154,7 +155,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
/>
</div>
{totalCount > 0 ? (
<div className="flex items-center pl-10 md:pl-11 lg:pl-14 pr-11 mt-11">
<div className="flex items-center mt-11">
<div className="flex flex-col sm:flex-row md:flex-row lg:flex-row items-center justify-evenly gap-x-10 gap-y-8 w-full">
<div>
<PieGraph
@@ -215,6 +216,6 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
<IssuesByStateGroupEmptyState />
</div>
)}
</div>
</BoxContainer>
);
});

View File

@@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { TOverviewStatsWidgetResponse } from "@plane/types";
// hooks
import { BoxContainer } from "@/components/containers";
import { WidgetLoader } from "@/components/dashboard/widgets";
import { cn } from "@/helpers/common.helper";
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
@@ -63,8 +64,8 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<div
className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full grid lg:grid-cols-4 md:grid-cols-2 sm:grid-cols-2 grid-cols-2 p-0.5 hover:shadow-custom-shadow-4xl duration-300
<BoxContainer
className="w-full grid lg:grid-cols-4 md:grid-cols-2 sm:grid-cols-2 grid-cols-2 p-0
[&>div>a>div]:border-r
[&>div:last-child>a>div]:border-0
[&>div>a>div]:border-custom-border-200
@@ -93,6 +94,6 @@ export const OverviewStatsWidget: React.FC<WidgetProps> = observer((props) => {
</Link>
</div>
))}
</div>
</BoxContainer>
);
});

View File

@@ -9,6 +9,7 @@ import { TRecentActivityWidgetResponse } from "@plane/types";
// UI
import { Avatar, getButtonStyling } from "@plane/ui";
// components
import { BoxContainer } from "@/components/containers";
import { ActivityIcon, ActivityMessage, IssueLink } from "@/components/core";
import { RecentActivityEmptyState, WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
// helpers
@@ -38,12 +39,12 @@ export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<div className="min-h-96 w-full rounded-xl border-[0.5px] border-custom-border-200 bg-custom-background-100 py-6 duration-300 hover:shadow-custom-shadow-4xl">
<Link href={redirectionLink} className="mx-7 text-lg font-semibold text-custom-text-300 hover:underline">
<BoxContainer className="min-h-96 w-full">
<Link href={redirectionLink} className="text-lg font-semibold text-custom-text-300 hover:underline">
Your issue activities
</Link>
{widgetStats.length > 0 ? (
<div className="mx-7 mt-4 space-y-6">
<div className="mt-4 space-y-6">
{widgetStats.map((activity) => (
<div key={activity.id} className="flex gap-5">
<div className="flex-shrink-0">
@@ -104,6 +105,6 @@ export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
<RecentActivityEmptyState />
</div>
)}
</div>
</BoxContainer>
);
});

View File

@@ -1,6 +1,7 @@
import { useState } from "react";
import { Search } from "lucide-react";
// types
import { BoxContainer } from "@/components/containers";
import { WidgetProps } from "@/components/dashboard/widgets";
// components
import { DefaultCollaboratorsList } from "./default-list";
@@ -14,8 +15,8 @@ export const RecentCollaboratorsWidget: React.FC<WidgetProps> = (props) => {
const [searchQuery, setSearchQuery] = useState("");
return (
<div className="w-full rounded-xl border-[0.5px] border-custom-border-200 bg-custom-background-100 duration-300 hover:shadow-custom-shadow-4xl">
<div className="flex flex-col sm:flex-row items-start justify-between px-7 pt-6">
<BoxContainer className="w-full">
<div className="flex flex-col sm:flex-row items-start justify-between">
<div>
<h4 className="text-lg font-semibold text-custom-text-300">Collaborators</h4>
<p className="mt-2 text-xs font-medium text-custom-text-300">
@@ -42,6 +43,6 @@ export const RecentCollaboratorsWidget: React.FC<WidgetProps> = (props) => {
) : (
<DefaultCollaboratorsList dashboardId={dashboardId} perPage={PER_PAGE} workspaceSlug={workspaceSlug} />
)}
</div>
</BoxContainer>
);
};

View File

@@ -10,6 +10,7 @@ import { TRecentProjectsWidgetResponse } from "@plane/types";
import { Avatar, AvatarGroup } from "@plane/ui";
// components
import { Logo } from "@/components/common";
import { BoxContainer } from "@/components/containers";
import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
// constants
import { PROJECT_BACKGROUND_COLORS } from "@/constants/dashboard";
@@ -82,14 +83,11 @@ export const RecentProjectsWidget: React.FC<WidgetProps> = observer((props) => {
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
return (
<div className="min-h-96 w-full rounded-xl border-[0.5px] border-custom-border-200 bg-custom-background-100 py-6 duration-300 hover:shadow-custom-shadow-4xl">
<Link
href={`/${workspaceSlug}/projects`}
className="mx-7 text-lg font-semibold text-custom-text-300 hover:underline"
>
<BoxContainer className="min-h-96 w-full">
<Link href={`/${workspaceSlug}/projects`} className="text-lg font-semibold text-custom-text-300 hover:underline">
Recent projects
</Link>
<div className="mx-7 mt-4 space-y-8">
<div className="mt-4 space-y-8">
{canCreateProject && (
<button
type="button"
@@ -113,6 +111,6 @@ export const RecentProjectsWidget: React.FC<WidgetProps> = observer((props) => {
<ProjectListItem key={projectId} projectId={projectId} workspaceSlug={workspaceSlug} />
))}
</div>
</div>
</BoxContainer>
);
});

View File

@@ -0,0 +1,143 @@
"use client";
import { useCallback, useState } from "react";
import { TProject } from "ee/types";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
import { Button } from "@plane/ui";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants
import {
EIssueFilterType,
EIssuesStoreType,
EIssueLayoutTypes,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "@/constants/issue";
// helpers
import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks
import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store";
import { ProjectAnalyticsModal } from "../analytics";
type Props = {
currentProjectDetails: TProject | undefined;
projectId: string;
workspaceSlug: string;
canUserCreateIssue: boolean | undefined;
};
const HeaderFilters = ({ currentProjectDetails, projectId, workspaceSlug, canUserCreateIssue }: Props) => {
// states
const [analyticsModal, setAnalyticsModal] = useState(false);
// store hooks
const {
project: { projectMemberIds },
} = useMember();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT);
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const activeLayout = issueFilters?.displayFilters?.layout;
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
},
[workspaceSlug, projectId, issueFilters, updateFilters]
);
const handleLayoutChange = useCallback(
(layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
},
[workspaceSlug, projectId, updateFilters]
);
return (
<>
<ProjectAnalyticsModal
isOpen={analyticsModal}
onClose={() => setAnalyticsModal(false)}
projectDetails={currentProjectDetails ?? undefined}
/>
<LayoutSelection
layouts={[
EIssueLayoutTypes.LIST,
EIssueLayoutTypes.KANBAN,
EIssueLayoutTypes.CALENDAR,
EIssueLayoutTypes.SPREADSHEET,
EIssueLayoutTypes.GANTT,
]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isIssueFilterActive(issueFilters)}>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
{canUserCreateIssue ? (
<Button className="hidden md:block" onClick={() => setAnalyticsModal(true)} variant="neutral-primary" size="sm">
Analytics
</Button>
) : (
<></>
)}
</>
);
};
export default HeaderFilters;

View File

@@ -25,6 +25,7 @@ import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web constants
import { EEstimateSystem } from "@/plane-web/constants/estimates";
import { BoxContainer } from "../containers";
type Props = {
moduleId: string;
@@ -176,7 +177,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
return (
<div className="relative">
<Link ref={parentRef} href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}>
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<BoxContainer className="flex h-44 flex-col justify-between rounded">
<div>
<div className="flex items-center justify-between gap-2">
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
@@ -228,11 +229,11 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
<span className="flex-grow truncate">{renderFormattedDate(endDate)}</span>
</div>
) : (
<span className="text-xs text-custom-text-400">No due date</span>
<span className="text-xs text-custom-text-400">No due ddate</span>
)}
</div>
</div>
</div>
</BoxContainer>
</Link>
<div className="absolute right-4 bottom-[18px] flex items-center gap-1.5">
{isEditingAllowed && (

View File

@@ -6,6 +6,7 @@ import useSWR from "swr";
// ui
import { Loader } from "@plane/ui";
// components
import { BoxContainer } from "@/components/containers";
import { ActivityMessage, IssueLink } from "@/components/core";
import { ProfileEmptyState } from "@/components/ui";
// constants
@@ -39,7 +40,7 @@ export const ProfileActivity = observer(() => {
return (
<div className="space-y-2">
<h3 className="text-lg font-medium">Recent activity</h3>
<div className="rounded border border-custom-border-100 p-6">
<BoxContainer>
{userProfileActivity ? (
userProfileActivity.results.length > 0 ? (
<div className="space-y-5">
@@ -94,7 +95,7 @@ export const ProfileActivity = observer(() => {
<Loader.Item height="40px" />
</Loader>
)}
</div>
</BoxContainer>
</div>
);
});

View File

@@ -3,6 +3,7 @@
// ui
import { IUserProfileData } from "@plane/types";
import { Loader } from "@plane/ui";
import { BoxContainer } from "@/components/containers";
import { BarGraph, ProfileEmptyState } from "@/components/ui";
// image
import { capitalizeFirstLetter } from "@/helpers/string.helper";
@@ -18,7 +19,7 @@ export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) =>
<div className="flex flex-col space-y-2">
<h3 className="text-lg font-medium">Issues by Priority</h3>
{userProfile ? (
<div className="flex-grow rounded border border-custom-border-100">
<BoxContainer className="flex-grow">
{userProfile.priority_distribution.length > 0 ? (
<BarGraph
data={userProfile.priority_distribution.map((priority) => ({
@@ -74,7 +75,7 @@ export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) =>
/>
</div>
)}
</div>
</BoxContainer>
) : (
<div className="grid place-items-center p-7">
<Loader className="flex items-end gap-12">

View File

@@ -1,5 +1,6 @@
// ui
import { IUserProfileData, IUserStateDistribution } from "@plane/types";
import { BoxContainer } from "@/components/containers";
import { ProfileEmptyState, PieGraph } from "@/components/ui";
// image
import { STATE_GROUPS } from "@/constants/state";
@@ -18,7 +19,7 @@ export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, u
return (
<div className="flex flex-col space-y-2">
<h3 className="text-lg font-medium">Issues by state</h3>
<div className="flex-grow rounded border border-custom-border-100 p-7">
<BoxContainer className="flex-grow">
{userProfile.state_distribution.length > 0 ? (
<div className="grid grid-cols-1 gap-x-6 md:grid-cols-2">
<div>
@@ -80,7 +81,7 @@ export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, u
image={stateGraph}
/>
)}
</div>
</BoxContainer>
</div>
);
};

View File

@@ -7,6 +7,7 @@ import { useParams } from "next/navigation";
import { UserCircle2 } from "lucide-react";
import { IUserProfileData } from "@plane/types";
import { CreateIcon, LayerStackIcon, Loader } from "@plane/ui";
import { BoxContainer } from "@/components/containers";
// types
type Props = {
@@ -44,7 +45,7 @@ export const ProfileStats: React.FC<Props> = ({ userProfile }) => {
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{overviewCards.map((card) => (
<Link key={card.route} href={`/${workspaceSlug}/profile/${userId}/${card.route}`}>
<span className="flex items-center gap-3 whitespace-nowrap rounded border border-custom-border-100 p-4">
<BoxContainer className="flex gap-2 p-4">
<div className="grid h-11 w-11 place-items-center rounded bg-custom-background-90">
<card.icon className="h-5 w-5" />
</div>
@@ -52,7 +53,7 @@ export const ProfileStats: React.FC<Props> = ({ userProfile }) => {
<p className="text-sm text-custom-text-400">{card.title}</p>
<p className="text-xl font-semibold">{card.value}</p>
</div>
</span>
</BoxContainer>
</Link>
))}
</div>

View File

@@ -1,5 +1,6 @@
// types
import { IUserStateDistribution } from "@plane/types";
import { BoxContainer } from "@/components/containers";
import { STATE_GROUPS } from "@/constants/state";
// constants
@@ -12,26 +13,24 @@ export const ProfileWorkload: React.FC<Props> = ({ stateDistribution }) => (
<h3 className="text-lg font-medium">Workload</h3>
<div className="grid grid-cols-1 justify-stretch gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
{stateDistribution.map((group) => (
<div key={group.state_group}>
<a className="flex gap-2 whitespace-nowrap rounded border border-custom-border-100 p-4">
<div
className="h-3 w-3 rounded-sm"
style={{
backgroundColor: STATE_GROUPS[group.state_group].color,
}}
/>
<div className="-mt-1 space-y-1">
<p className="text-sm text-custom-text-400">
{group.state_group === "unstarted"
? "Not started"
: group.state_group === "started"
? "Working on"
: STATE_GROUPS[group.state_group].label}
</p>
<p className="text-xl font-semibold">{group.state_count}</p>
</div>
</a>
</div>
<BoxContainer key={group.state_group} className="flex gap-2 p-4">
<div
className="h-3 w-3 rounded-sm"
style={{
backgroundColor: STATE_GROUPS[group.state_group].color,
}}
/>
<div className="-mt-1 space-y-1">
<p className="text-sm text-custom-text-400">
{group.state_group === "unstarted"
? "Not started"
: group.state_group === "started"
? "Working on"
: STATE_GROUPS[group.state_group].label}
</p>
<p className="text-xl font-semibold">{group.state_count}</p>
</div>
</BoxContainer>
))}
</div>
</div>

View File

@@ -0,0 +1,93 @@
import { useCallback } from "react";
import { useParams } from "next/navigation";
import { ListFilter } from "lucide-react";
// types
import { cn } from "@plane/editor";
import { TProjectFilters } from "@plane/types";
// components
import { FiltersDropdown } from "@/components/issues";
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useMember, useProjectFilter } from "@/hooks/store";
type Props = {
filterMenuButton?: React.ReactNode;
classname?: string;
filterClassname?: string;
isMobile?: boolean;
};
const HeaderFilters = ({ filterMenuButton, isMobile, classname = "", filterClassname = "" }: Props) => {
// router
const { workspaceSlug } = useParams();
const {
currentWorkspaceDisplayFilters: displayFilters,
currentWorkspaceFilters: filters,
updateFilters,
updateDisplayFilters,
} = useProjectFilter();
const {
workspace: { workspaceMemberIds },
} = useMember();
const handleFilters = useCallback(
(key: keyof TProjectFilters, value: string | string[]) => {
if (!workspaceSlug) return;
let newValues = filters?.[key] ?? [];
if (Array.isArray(value)) {
if (key === "created_at" && newValues.find((v) => v.includes("custom"))) newValues = [];
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else {
if (key === "created_at") newValues = [value];
else newValues.push(value);
}
}
updateFilters(workspaceSlug.toString(), { [key]: newValues });
},
[filters, updateFilters, workspaceSlug]
);
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
return (
<div className={cn("flex gap-3", classname)}>
<ProjectOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!workspaceSlug || val === displayFilters?.order_by) return;
updateDisplayFilters(workspaceSlug.toString(), {
order_by: val,
});
}}
isMobile={isMobile}
/>
<div className={cn(filterClassname)}>
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title="Filters"
placement="bottom-end"
isFiltersApplied={isFiltersApplied}
menuButton={filterMenuButton || null}
>
<ProjectFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleFiltersUpdate={handleFilters}
handleDisplayFiltersUpdate={(val) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug.toString(), val);
}}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
</div>
</div>
);
};
export default HeaderFilters;

View File

@@ -1,29 +1,24 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { Search, Briefcase, X, ListFilter } from "lucide-react";
// types
import { TProjectFilters } from "@plane/types";
import { usePathname } from "next/navigation";
import { Search, Briefcase, X } from "lucide-react";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { FiltersDropdown } from "@/components/issues";
import { ProjectFiltersSelection, ProjectOrderByDropdown } from "@/components/project";
import { HeaderContainer } from "@/components/containers";
// constants
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
import { useCommandPalette, useEventTracker, useMember, useProjectFilter, useUser } from "@/hooks/store";
import { useCommandPalette, useEventTracker, useProjectFilter, useUser } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import HeaderFilters from "./filters";
export const ProjectsBaseHeader = observer(() => {
// router
const { workspaceSlug } = useParams();
// states
const [isSearchOpen, setIsSearchOpen] = useState(false);
// refs
@@ -36,17 +31,8 @@ export const ProjectsBaseHeader = observer(() => {
} = useUser();
const pathname = usePathname();
const {
currentWorkspaceDisplayFilters: displayFilters,
currentWorkspaceFilters: filters,
updateFilters,
updateDisplayFilters,
searchQuery,
updateSearchQuery,
} = useProjectFilter();
const {
workspace: { workspaceMemberIds },
} = useMember();
const { searchQuery, updateSearchQuery } = useProjectFilter();
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
@@ -55,29 +41,6 @@ export const ProjectsBaseHeader = observer(() => {
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const isArchived = pathname.includes("/archives");
const handleFilters = useCallback(
(key: keyof TProjectFilters, value: string | string[]) => {
if (!workspaceSlug) return;
let newValues = filters?.[key] ?? [];
if (Array.isArray(value)) {
if (key === "created_at" && newValues.find((v) => v.includes("custom"))) newValues = [];
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else {
if (key === "created_at") newValues = [value];
else newValues.push(value);
}
}
updateFilters(workspaceSlug.toString(), { [key]: newValues });
},
[filters, updateFilters, workspaceSlug]
);
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
@@ -89,22 +52,18 @@ export const ProjectsBaseHeader = observer(() => {
if (searchQuery.trim() !== "") setIsSearchOpen(true);
}, [searchQuery]);
const isFiltersApplied = calculateTotalFilters(filters ?? {}) !== 0;
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 bg-custom-sidebar-background-100 p-4">
<div className="flex flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
/>
{isArchived && <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Archived" />} />}
</Breadcrumbs>
</div>
</div>
<div className="w-full flex items-center justify-end gap-3">
<HeaderContainer>
<HeaderContainer.LeftItem>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Projects" icon={<Briefcase className="h-4 w-4 text-custom-text-300" />} />}
/>
{isArchived && <Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label="Archived" />} />}
</Breadcrumbs>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<div className="flex items-center">
{!isSearchOpen && (
<button
@@ -149,35 +108,11 @@ export const ProjectsBaseHeader = observer(() => {
)}
</div>
</div>
<div className="hidden md:flex gap-3">
<ProjectOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!workspaceSlug || val === displayFilters?.order_by) return;
updateDisplayFilters(workspaceSlug.toString(), {
order_by: val,
});
}}
/>
<FiltersDropdown
icon={<ListFilter className="h-3 w-3" />}
title="Filters"
placement="bottom-end"
isFiltersApplied={isFiltersApplied}
>
<ProjectFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleFiltersUpdate={handleFilters}
handleDisplayFiltersUpdate={(val) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug.toString(), val);
}}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
<div className="hidden md:flex">
<HeaderFilters />
</div>
{isAuthorizedUser && (
{isAuthorizedUser ? (
<Button
size="sm"
onClick={() => {
@@ -188,8 +123,10 @@ export const ProjectsBaseHeader = observer(() => {
>
<span className="hidden sm:inline-block">Add</span> Project
</Button>
) : (
<></>
)}
</div>
</div>
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -6,6 +6,7 @@ import { Inbox } from "lucide-react";
import { Breadcrumbs } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { HeaderContainer } from "@/components/containers";
import { SidebarHamburgerToggle } from "@/components/core";
import { NotificationSidebarHeaderOptions } from "@/components/workspace-notifications";
@@ -18,8 +19,8 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
if (!workspaceSlug) 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 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HeaderContainer>
<HeaderContainer.LeftItem>
<div className="block bg-custom-sidebar-background-100 md:hidden">
<SidebarHamburgerToggle />
</div>
@@ -31,9 +32,10 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
}
/>
</Breadcrumbs>
</div>
<NotificationSidebarHeaderOptions workspaceSlug={workspaceSlug} />
</div>
</HeaderContainer.LeftItem>
<HeaderContainer.RightItem>
<NotificationSidebarHeaderOptions workspaceSlug={workspaceSlug} />
</HeaderContainer.RightItem>
</HeaderContainer>
);
});

View File

@@ -5,6 +5,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import { CountChip } from "@/components/common";
import { RegularRow } from "@/components/containers";
import {
NotificationsLoader,
NotificationEmptyState,
@@ -46,11 +47,11 @@ export const NotificationsSidebar: FC = observer(() => {
)}
>
<div className="relative w-full h-full overflow-hidden flex flex-col">
<div className="border-b border-custom-border-200">
<RegularRow className="h-[3.75rem] border-b border-custom-border-200">
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
</div>
</RegularRow>
<div className="flex-shrink-0 w-full h-[46px] border-b border-custom-border-200 px-5 relative flex items-center gap-2">
<div className="flex-shrink-0 w-full h-[46px] border-b border-custom-border-200 px-page-x relative flex items-center gap-2">
{NOTIFICATION_TABS.map((tab) => (
<div
key={tab.value}

View File

@@ -108,7 +108,7 @@ export const GlobalViewsHeader: React.FC = observer(() => {
<div className="group relative flex border-b border-custom-border-200">
<div
ref={containerRef}
className="flex w-full items-center overflow-x-auto px-4 horizontal-scrollbar scrollbar-sm"
className="flex w-full items-center overflow-x-auto px-page-x horizontal-scrollbar scrollbar-sm"
>
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab, index) => (
<DefaultViewTab key={`${tab.key}-${index}`} tab={tab} />