mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
4 Commits
release-v-
...
chore-box-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb43542a0c | ||
|
|
2674413d0c | ||
|
|
c224f493d4 | ||
|
|
f099f7f961 |
@@ -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']);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
));
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
17
web/core/components/containers/box-container.tsx
Normal file
17
web/core/components/containers/box-container.tsx
Normal 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>
|
||||
);
|
||||
19
web/core/components/containers/header-container.tsx
Normal file
19
web/core/components/containers/header-container.tsx
Normal 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 };
|
||||
9
web/core/components/containers/hugging-row.tsx
Normal file
9
web/core/components/containers/hugging-row.tsx
Normal 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>
|
||||
);
|
||||
5
web/core/components/containers/index.ts
Normal file
5
web/core/components/containers/index.ts
Normal 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";
|
||||
16
web/core/components/containers/page-container.tsx
Normal file
16
web/core/components/containers/page-container.tsx
Normal 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>
|
||||
);
|
||||
9
web/core/components/containers/regular-row.tsx
Normal file
9
web/core/components/containers/regular-row.tsx
Normal 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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
143
web/core/components/issues/filters.tsx
Normal file
143
web/core/components/issues/filters.tsx
Normal 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;
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
93
web/core/components/project/filters.tsx
Normal file
93
web/core/components/project/filters.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user