refactor: components folder structure

This commit is contained in:
Aaryan Khandelwal 2024-06-05 00:03:58 +05:30
parent 899d49b1f0
commit 83a1b0cf1f
51 changed files with 218 additions and 642 deletions

View File

@ -6,10 +6,10 @@ type Props = {
}; };
}; };
const ProjectIssuesLayout = async (props: Props) => { const IssuesLayout = async (props: Props) => {
const { children } = props; const { children } = props;
return <>{children}</>; return <>{children}</>;
}; };
export default ProjectIssuesLayout; export default IssuesLayout;

View File

@ -17,15 +17,15 @@ type Props = {
}; };
}; };
const ProjectIssuesPage = (props: Props) => { const IssuesPage = (props: Props) => {
const { params } = props; const { params } = props;
const { workspaceSlug, projectId } = params; const { workspaceSlug, projectId } = params;
// states // states
const [error, setError] = useState(false); const [error, setError] = useState(false);
// params // params
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const board = searchParams.get("board") || undefined; const board = searchParams.get("board");
const peekId = searchParams.get("peekId") || undefined; const peekId = searchParams.get("peekId");
useEffect(() => { useEffect(() => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -50,4 +50,4 @@ const ProjectIssuesPage = (props: Props) => {
return <LogoSpinner />; return <LogoSpinner />;
}; };
export default ProjectIssuesPage; export default IssuesPage;

View File

@ -1,38 +1,47 @@
"use client"; "use client";
import Image from "next/image"; // ui
import { useTheme } from "next-themes";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// assets
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
export default function InstanceError() {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
const ErrorPage = () => {
const handleRetry = () => { const handleRetry = () => {
window.location.reload(); window.location.reload();
}; };
return ( return (
<div className="relative h-screen overflow-x-hidden overflow-y-auto container px-5 mx-auto flex justify-center items-center"> <div className="grid h-screen place-items-center p-4">
<div className="w-auto max-w-2xl relative space-y-8 py-10"> <div className="space-y-8 text-center">
<div className="relative flex flex-col justify-center items-center space-y-4"> <div className="space-y-2">
<Image src={instanceImage} alt="Plane instance failure image" /> <h3 className="text-lg font-semibold">Exception Detected!</h3>
<h3 className="font-medium text-2xl text-white">Unable to fetch instance details.</h3> <p className="mx-auto w-1/2 text-sm text-custom-text-200">
<p className="font-medium text-base text-center"> We{"'"}re Sorry! An exception has been detected, and our engineering team has been notified. We apologize
We were unable to fetch the details of the instance. <br /> for any inconvenience this may have caused. Please reach out to our engineering team at{" "}
Fret not, it might just be a connectivity issue. <a href="mailto:support@plane.so" className="text-custom-primary">
support@plane.so
</a>{" "}
or on our{" "}
<a
href="https://discord.com/invite/A92xrEGCge"
target="_blank"
className="text-custom-primary"
rel="noopener noreferrer"
>
Discord
</a>{" "}
server for further assistance.
</p> </p>
</div> </div>
<div className="flex justify-center"> <div className="flex items-center justify-center gap-2">
<Button size="md" onClick={handleRetry}> <Button variant="primary" size="md" onClick={handleRetry}>
Retry Refresh
</Button> </Button>
{/* <Button variant="neutral-primary" size="md" onClick={() => {}}>
Sign out
</Button> */}
</div> </div>
</div> </div>
</div> </div>
); );
} };
export default ErrorPage;

View File

@ -5,7 +5,7 @@ import Image from "next/image";
import useSWR from "swr"; import useSWR from "swr";
// components // components
import { LogoSpinner } from "@/components/common"; import { LogoSpinner } from "@/components/common";
import IssueNavbar from "@/components/issues/navbar"; import { IssuesNavbarRoot } from "@/components/issues";
// hooks // hooks
import { usePublish, usePublishList } from "@/hooks/store"; import { usePublish, usePublishList } from "@/hooks/store";
// assets // assets
@ -18,7 +18,7 @@ type Props = {
}; };
}; };
const ProjectIssuesLayout = observer((props: Props) => { const IssuesLayout = observer((props: Props) => {
const { children, params } = props; const { children, params } = props;
// params // params
const { anchor } = params; const { anchor } = params;
@ -33,7 +33,7 @@ const ProjectIssuesLayout = observer((props: Props) => {
return ( return (
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden"> <div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100"> <div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
<IssueNavbar publishSettings={publishSettings} /> <IssuesNavbarRoot publishSettings={publishSettings} />
</div> </div>
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div> <div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
<a <a
@ -53,4 +53,4 @@ const ProjectIssuesLayout = observer((props: Props) => {
); );
}); });
export default ProjectIssuesLayout; export default IssuesLayout;

View File

@ -3,7 +3,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
// components // components
import { ProjectDetailsView } from "@/components/views"; import { IssuesLayoutsRoot } from "@/components/issues";
// hooks // hooks
import { usePublish } from "@/hooks/store"; import { usePublish } from "@/hooks/store";
@ -13,7 +13,7 @@ type Props = {
}; };
}; };
const ProjectIssuesPage =observer ((props: Props) => { const IssuesPage = observer((props: Props) => {
const { params } = props; const { params } = props;
const { anchor } = params; const { anchor } = params;
// params // params
@ -24,7 +24,7 @@ const ProjectIssuesPage =observer ((props: Props) => {
if (!publishSettings) return null; if (!publishSettings) return null;
return <ProjectDetailsView peekId={peekId} publishSettings={publishSettings} />; return <IssuesLayoutsRoot peekId={peekId} publishSettings={publishSettings} />;
}); });
export default ProjectIssuesPage; export default IssuesPage;

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// components // components
import { UserAvatar } from "@/components/issues/navbar/user-avatar"; import { UserAvatar } from "@/components/issues";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUser } from "@/hooks/store";
// assets // assets
@ -25,7 +25,7 @@ export const UserLoggedIn = observer(() => {
return ( return (
<div className="flex flex-col h-screen w-screen"> <div className="flex flex-col h-screen w-screen">
<div className="relative flex w-full items-center justify-between gap-4 border-b border-custom-border-200 px-6 py-5"> <div className="relative flex w-full items-center justify-between gap-4 border-b border-custom-border-200 px-6 py-5">
<div> <div className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" /> <Image src={logo} alt="Plane logo" />
</div> </div>
<UserAvatar /> <UserAvatar />

View File

@ -1,3 +1,2 @@
export * from "./latest-feature-block";
export * from "./project-logo"; export * from "./project-logo";
export * from "./logo-spinner"; export * from "./logo-spinner";

View File

@ -1,40 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
// icons
import { Lightbulb } from "lucide-react";
// images
import latestFeatures from "public/onboarding/onboarding-pages.svg";
export const LatestFeatureBlock = () => {
const { resolvedTheme } = useTheme();
return (
<>
<div className="mx-auto mt-16 flex rounded-[3.5px] border border-onboarding-border-200 bg-onboarding-background-100 py-2 sm:w-96">
<Lightbulb className="mx-3 mr-2 h-7 w-7" />
<p className="text-left text-sm text-onboarding-text-100">
Pages gets a facelift! Write anything and use Galileo to help you start.{" "}
<Link href="https://plane.so/changelog" target="_blank" rel="noopener noreferrer">
<span className="text-sm font-medium underline hover:cursor-pointer">Learn more</span>
</Link>
</p>
</div>
<div
className={`mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 object-cover sm:h-52 sm:w-96 ${
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
}`}
>
<div className="h-[90%]">
<Image
src={latestFeatures}
alt="Plane Issues"
className={`-mt-2 ml-10 h-full rounded-md ${
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70"
} `}
/>
</div>
</div>
</>
);
};

View File

@ -1,2 +1 @@
export * from "./not-ready-view";
export * from "./instance-failure-view"; export * from "./instance-failure-view";

View File

@ -1,62 +0,0 @@
"use client";
import { FC } from "react";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
// ui
import { Button } from "@plane/ui";
// helper
import { GOD_MODE_URL, SPACE_BASE_PATH } from "@/helpers/common.helper";
// images
import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png";
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import BlackHorizontalLogo from "public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "public/plane-logos/white-horizontal-with-blue-logo.png";
export const InstanceNotReady: FC = () => {
const { resolvedTheme } = useTheme();
const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern;
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<div className="relative">
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="flex items-center gap-x-2 py-10">
<Link href={`${SPACE_BASE_PATH}/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>
<div className="absolute inset-0 z-0">
<Image src={patternBackground} className="w-screen h-full object-cover" alt="Plane background pattern" />
</div>
<div className="relative z-10 mb-[110px] flex-grow">
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-onboarding-text-400">
Get started by setting up your instance and workspace
</p>
</div>
<div>
<a href={GOD_MODE_URL}>
<Button size="lg" className="w-full">
Get started
</Button>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -1,10 +0,0 @@
"use client";
export const IssueBlockDownVotes = ({ number }: { number: number }) => (
<div className="flex h-6 items-center rounded border-[0.5px] border-custom-border-300 px-1.5 py-1 pl-1 text-xs text-custom-text-300">
<span className="material-symbols-rounded !m-0 rotate-180 !p-0 text-base text-custom-text-300">
arrow_upward_alt
</span>
{number}
</div>
);

View File

@ -1,19 +0,0 @@
"use client";
export const IssueBlockLabels = ({ labels }: any) => (
<div className="relative flex flex-wrap items-center gap-1">
{labels &&
labels.length > 0 &&
labels.map((_label: any) => (
<div
key={_label?.id}
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
>
<div className="flex items-center gap-1.5 text-custom-text-200">
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: `${_label?.color}` }} />
<div className="text-xs">{_label?.name}</div>
</div>
</div>
))}
</div>
);

View File

@ -1,18 +0,0 @@
// ui
import { StateGroupIcon } from "@plane/ui";
// constants
import { issueGroupFilter } from "@/constants/issue";
export const IssueBlockState = ({ state }: any) => {
const stateGroup = issueGroupFilter(state.group);
if (stateGroup === null) return <></>;
return (
<div className="flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs shadow-sm duration-300 focus:outline-none">
<div className="flex w-full items-center gap-1.5 text-custom-text-200">
<StateGroupIcon stateGroup={state.group} color={state.color} />
<div className="text-xs">{state?.name}</div>
</div>
</div>
);
};

View File

@ -1,8 +0,0 @@
"use client";
export const IssueBlockUpVotes = ({ number }: { number: number }) => (
<div className="flex h-6 items-center rounded border-[0.5px] border-custom-border-300 px-1.5 py-1 pl-1 text-xs text-custom-text-300">
<span className="material-symbols-rounded !m-0 !p-0 text-base text-custom-text-300">arrow_upward_alt</span>
{number}
</div>
);

View File

@ -1 +0,0 @@
export const IssueCalendarView = () => <div> </div>;

View File

@ -1 +0,0 @@
export const IssueGanttView = () => <div> </div>;

View File

@ -1 +0,0 @@
export const IssueSpreadsheetView = () => <div> </div>;

View File

@ -0,0 +1,2 @@
export * from "./issue-layouts";
export * from "./navbar";

View File

@ -0,0 +1,4 @@
export * from "./kanban";
export * from "./list";
export * from "./properties";
export * from "./root";

View File

@ -4,9 +4,7 @@ import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
// components // components
import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date"; import { IssueBlockDueDate, IssueBlockPriority, IssueBlockState } from "@/components/issues";
import { IssueBlockPriority } from "@/components/issues/board-views/block-priority";
import { IssueBlockState } from "@/components/issues/board-views/block-state";
// helpers // helpers
import { queryParamGenerator } from "@/helpers/query-param-generator"; import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks // hooks
@ -14,13 +12,13 @@ import { useIssueDetails, usePublish } from "@/hooks/store";
// interfaces // interfaces
import { IIssue } from "@/types/issue"; import { IIssue } from "@/types/issue";
type IssueKanBanBlockProps = { type Props = {
anchor: string; anchor: string;
issue: IIssue; issue: IIssue;
params: any; params: any;
}; };
export const IssueKanBanBlock: FC<IssueKanBanBlockProps> = observer((props) => { export const IssueKanBanBlock: FC<Props> = observer((props) => {
const { anchor, issue } = props; const { anchor, issue } = props;
// router // router
const router = useRouter(); const router = useRouter();

View File

@ -5,14 +5,13 @@ import { observer } from "mobx-react-lite";
import { IStateLite } from "@plane/types"; import { IStateLite } from "@plane/types";
// ui // ui
import { StateGroupIcon } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// constants
import { issueGroupFilter } from "@/constants/issue";
export const IssueKanBanHeader = observer(({ state }: { state: IStateLite }) => { type Props = {
// const { getCountOfIssuesByState } = useIssue(); state: IStateLite;
const stateGroup = issueGroupFilter(state.group); };
if (stateGroup === null) return <></>; export const IssueKanBanHeader: React.FC<Props> = observer((props) => {
const { state } = props;
return ( return (
<div className="flex items-center gap-2 px-2 pb-2"> <div className="flex items-center gap-2 px-2 pb-2">

View File

@ -0,0 +1,3 @@
export * from "./block";
export * from "./header";
export * from "./root";

View File

@ -3,18 +3,17 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
import { IssueKanBanBlock } from "@/components/issues/board-views/kanban/block"; import { IssueKanBanBlock, IssueKanBanHeader } from "@/components/issues";
import { IssueKanBanHeader } from "@/components/issues/board-views/kanban/header";
// ui // ui
import { Icon } from "@/components/ui"; import { Icon } from "@/components/ui";
// mobx hook // mobx hook
import { useIssue } from "@/hooks/store"; import { useIssue } from "@/hooks/store";
type IssueKanbanViewProps = { type Props = {
anchor: string; anchor: string;
}; };
export const IssueKanbanView: FC<IssueKanbanViewProps> = observer((props) => { export const IssueKanbanLayoutRoot: FC<Props> = observer((props) => {
const { anchor } = props; const { anchor } = props;
// store hooks // store hooks
const { states, getFilteredIssuesByState } = useIssue(); const { states, getFilteredIssuesByState } = useIssue();

View File

@ -3,10 +3,7 @@ import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
// components // components
import { IssueBlockDueDate } from "@/components/issues/board-views/block-due-date"; import { IssueBlockDueDate, IssueBlockLabels, IssueBlockPriority, IssueBlockState } from "@/components/issues";
import { IssueBlockLabels } from "@/components/issues/board-views/block-labels";
import { IssueBlockPriority } from "@/components/issues/board-views/block-priority";
import { IssueBlockState } from "@/components/issues/board-views/block-state";
// helpers // helpers
import { queryParamGenerator } from "@/helpers/query-param-generator"; import { queryParamGenerator } from "@/helpers/query-param-generator";
// hook // hook
@ -20,7 +17,7 @@ type IssueListBlockProps = {
issue: IIssue; issue: IIssue;
}; };
export const IssueListBlock: FC<IssueListBlockProps> = observer((props) => { export const IssueListLayoutBlock: FC<IssueListBlockProps> = observer((props) => {
const { anchor, issue } = props; const { anchor, issue } = props;
const searchParams = useSearchParams(); const searchParams = useSearchParams();
// query params // query params

View File

@ -1,19 +1,18 @@
"use client"; "use client";
import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// types // types
import { IStateLite } from "@plane/types"; import { IStateLite } from "@plane/types";
// ui // ui
import { StateGroupIcon } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// constants
import { issueGroupFilter } from "@/constants/issue";
export const IssueListHeader = observer(({ state }: { state: IStateLite }) => { type Props = {
// const { getCountOfIssuesByState } = useIssue(); state: IStateLite;
const stateGroup = issueGroupFilter(state.group); };
// const count = getCountOfIssuesByState(state.id);
if (stateGroup === null) return <></>; export const IssueListLayoutHeader: React.FC<Props> = observer((props) => {
const { state } = props;
return ( return (
<div className="flex items-center gap-2 p-3"> <div className="flex items-center gap-2 p-3">

View File

@ -0,0 +1,3 @@
export * from "./block";
export * from "./header";
export * from "./root";

View File

@ -2,16 +2,15 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components // components
import { IssueListBlock } from "@/components/issues/board-views/list/block"; import { IssueListLayoutBlock, IssueListLayoutHeader } from "@/components/issues";
import { IssueListHeader } from "@/components/issues/board-views/list/header";
// mobx hook // mobx hook
import { useIssue } from "@/hooks/store"; import { useIssue } from "@/hooks/store";
type IssueListViewProps = { type Props = {
anchor: string; anchor: string;
}; };
export const IssueListView: FC<IssueListViewProps> = observer((props) => { export const IssuesListLayoutRoot: FC<Props> = observer((props) => {
const { anchor } = props; const { anchor } = props;
// store hooks // store hooks
const { states, getFilteredIssuesByState } = useIssue(); const { states, getFilteredIssuesByState } = useIssue();
@ -23,11 +22,11 @@ export const IssueListView: FC<IssueListViewProps> = observer((props) => {
return ( return (
<div key={state.id} className="relative w-full"> <div key={state.id} className="relative w-full">
<IssueListHeader state={state} /> <IssueListLayoutHeader state={state} />
{issues && issues.length > 0 ? ( {issues && issues.length > 0 ? (
<div className="divide-y divide-custom-border-200"> <div className="divide-y divide-custom-border-200">
{issues.map((issue) => ( {issues.map((issue) => (
<IssueListBlock key={issue.id} anchor={anchor} issue={issue} /> <IssueListLayoutBlock key={issue.id} anchor={anchor} issue={issue} />
))} ))}
</div> </div>
) : ( ) : (

View File

@ -0,0 +1,4 @@
export * from "./due-date";
export * from "./labels";
export * from "./priority";
export * from "./state";

View File

@ -0,0 +1,17 @@
"use client";
export const IssueBlockLabels = ({ labels }: any) => (
<div className="relative flex flex-wrap items-center gap-1">
{labels?.map((_label: any) => (
<div
key={_label?.id}
className="flex flex-shrink-0 cursor-default items-center rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
>
<div className="flex items-center gap-1.5 text-custom-text-200">
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: `${_label?.color}` }} />
<div className="text-xs">{_label?.name}</div>
</div>
</div>
))}
</div>
);

View File

@ -0,0 +1,11 @@
// ui
import { StateGroupIcon } from "@plane/ui";
export const IssueBlockState = ({ state }: any) => (
<div className="flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs shadow-sm duration-300 focus:outline-none">
<div className="flex w-full items-center gap-1.5 text-custom-text-200">
<StateGroupIcon stateGroup={state.group} color={state.color} />
<div className="text-xs">{state?.name}</div>
</div>
</div>
);

View File

@ -6,11 +6,7 @@ import Image from "next/image";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
// components // components
import { IssueCalendarView } from "@/components/issues/board-views/calendar"; import { IssueKanbanLayoutRoot, IssuesListLayoutRoot } from "@/components/issues";
import { IssueGanttView } from "@/components/issues/board-views/gantt";
import { IssueKanbanView } from "@/components/issues/board-views/kanban";
import { IssueListView } from "@/components/issues/board-views/list";
import { IssueSpreadsheetView } from "@/components/issues/board-views/spreadsheet";
import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root"; import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root";
import { IssuePeekOverview } from "@/components/issues/peek-overview"; import { IssuePeekOverview } from "@/components/issues/peek-overview";
// hooks // hooks
@ -20,12 +16,12 @@ import { PublishStore } from "@/store/publish/publish.store";
// assets // assets
import SomethingWentWrongImage from "public/something-went-wrong.svg"; import SomethingWentWrongImage from "public/something-went-wrong.svg";
type ProjectDetailsViewProps = { type Props = {
peekId: string | undefined; peekId: string | undefined;
publishSettings: PublishStore; publishSettings: PublishStore;
}; };
export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props) => { export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
const { peekId, publishSettings } = props; const { peekId, publishSettings } = props;
// query params // query params
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -84,17 +80,14 @@ export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props)
{activeLayout === "list" && ( {activeLayout === "list" && (
<div className="relative h-full w-full overflow-y-auto"> <div className="relative h-full w-full overflow-y-auto">
<IssueListView anchor={anchor} /> <IssuesListLayoutRoot anchor={anchor} />
</div> </div>
)} )}
{activeLayout === "kanban" && ( {activeLayout === "kanban" && (
<div className="relative mx-auto h-full w-full p-5"> <div className="relative mx-auto h-full w-full p-5">
<IssueKanbanView anchor={anchor} /> <IssueKanbanLayoutRoot anchor={anchor} />
</div> </div>
)} )}
{activeLayout === "calendar" && <IssueCalendarView />}
{activeLayout === "spreadsheet" && <IssueSpreadsheetView />}
{activeLayout === "gantt" && <IssueGanttView />}
</div> </div>
) )
)} )}

View File

@ -4,10 +4,8 @@ import { useEffect, FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
// components // components
import { IssuesLayoutSelection, NavbarTheme, UserAvatar } from "@/components/issues";
import { IssueFiltersDropdown } from "@/components/issues/filters"; import { IssueFiltersDropdown } from "@/components/issues/filters";
import { NavbarIssueBoardView } from "@/components/issues/navbar/issue-board-view";
import { NavbarTheme } from "@/components/issues/navbar/theme";
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
// helpers // helpers
import { queryParamGenerator } from "@/helpers/query-param-generator"; import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks // hooks
@ -105,7 +103,7 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
<> <>
{/* issue views */} {/* issue views */}
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out"> <div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
<NavbarIssueBoardView anchor={anchor} /> <IssuesLayoutSelection anchor={anchor} />
</div> </div>
{/* issue filters */} {/* issue filters */}

View File

@ -0,0 +1,5 @@
export * from "./controls";
export * from "./layout-selection";
export * from "./root";
export * from "./theme";
export * from "./user-avatar";

View File

@ -1,70 +0,0 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useRouter, useSearchParams } from "next/navigation";
// constants
import { issueLayoutViews } from "@/constants/issue";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { useIssueFilter } from "@/hooks/store";
// mobx
import { TIssueLayout } from "@/types/issue";
type NavbarIssueBoardViewProps = {
anchor: string;
};
export const NavbarIssueBoardView: FC<NavbarIssueBoardViewProps> = observer((props) => {
const { anchor } = props;
// router
const router = useRouter();
const searchParams = useSearchParams();
// query params
const labels = searchParams.get("labels") || undefined;
const state = searchParams.get("state") || undefined;
const priority = searchParams.get("priority") || undefined;
const peekId = searchParams.get("peekId") || undefined;
// hooks
const { layoutOptions, getIssueFilters, updateIssueFilters } = useIssueFilter();
// derived values
const issueFilters = getIssueFilters(anchor);
const activeLayout = issueFilters?.display_filters?.layout || undefined;
const handleCurrentBoardView = (boardView: TIssueLayout) => {
updateIssueFilters(anchor, "display_filters", "layout", boardView);
const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels });
router.push(`/issues/${anchor}?${queryParam}`);
};
return (
<>
{Object.keys(issueLayoutViews).map((key: string) => {
const layoutKey = key as TIssueLayout;
if (layoutOptions[layoutKey]) {
return (
<div
key={layoutKey}
className={`flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded-sm ${
layoutKey === activeLayout
? `bg-custom-background-80 text-custom-text-200`
: `text-custom-text-300 hover:bg-custom-background-80`
}`}
onClick={() => handleCurrentBoardView(layoutKey)}
title={layoutKey}
>
<span
className={`material-symbols-rounded text-[18px] ${
issueLayoutViews[layoutKey]?.className ? issueLayoutViews[layoutKey]?.className : ``
}`}
>
{issueLayoutViews[layoutKey]?.icon}
</span>
</div>
);
}
})}
</>
);
});

View File

@ -0,0 +1,67 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useRouter, useSearchParams } from "next/navigation";
// ui
import { Tooltip } from "@plane/ui";
// constants
import { ISSUE_LAYOUTS } from "@/constants/issue";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
import { useIssueFilter } from "@/hooks/store";
// mobx
import { TIssueLayout } from "@/types/issue";
type Props = {
anchor: string;
};
export const IssuesLayoutSelection: FC<Props> = observer((props) => {
const { anchor } = props;
// router
const router = useRouter();
const searchParams = useSearchParams();
// query params
const labels = searchParams.get("labels");
const state = searchParams.get("state");
const priority = searchParams.get("priority");
const peekId = searchParams.get("peekId");
// hooks
const { layoutOptions, getIssueFilters, updateIssueFilters } = useIssueFilter();
// derived values
const issueFilters = getIssueFilters(anchor);
const activeLayout = issueFilters?.display_filters?.layout || undefined;
const handleCurrentBoardView = (boardView: TIssueLayout) => {
updateIssueFilters(anchor, "display_filters", "layout", boardView);
const { queryParam } = queryParamGenerator({ board: boardView, peekId, priority, state, labels });
router.push(`/issues/${anchor}?${queryParam}`);
};
return (
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{ISSUE_LAYOUTS.map((layout) => {
if (!layoutOptions[layout.key]) return;
return (
<Tooltip key={layout.key} tooltipContent={layout.title}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
activeLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => handleCurrentBoardView(layout.key)}
>
<layout.icon
strokeWidth={2}
className={`size-3.5 ${activeLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"}`}
/>
</button>
</Tooltip>
);
})}
</div>
);
});

View File

@ -4,15 +4,15 @@ import { observer } from "mobx-react-lite";
import { Briefcase } from "lucide-react"; import { Briefcase } from "lucide-react";
// components // components
import { ProjectLogo } from "@/components/common"; import { ProjectLogo } from "@/components/common";
import { NavbarControls } from "@/components/issues/navbar/controls"; import { NavbarControls } from "@/components/issues";
// store // store
import { PublishStore } from "@/store/publish/publish.store"; import { PublishStore } from "@/store/publish/publish.store";
type IssueNavbarProps = { type Props = {
publishSettings: PublishStore; publishSettings: PublishStore;
}; };
const IssueNavbar: FC<IssueNavbarProps> = observer((props) => { export const IssuesNavbarRoot: FC<Props> = observer((props) => {
const { publishSettings } = props; const { publishSettings } = props;
// hooks // hooks
const { project_details } = publishSettings; const { project_details } = publishSettings;
@ -41,5 +41,3 @@ const IssueNavbar: FC<IssueNavbarProps> = observer((props) => {
</div> </div>
); );
}); });
export default IssueNavbar;

View File

@ -1,142 +0,0 @@
import { Fragment, useState, useRef } from "react";
import Link from "next/link";
import { Check, ChevronLeft } from "lucide-react";
import { Popover, Transition } from "@headlessui/react";
// hooks
import useOutSideClick from "hooks/use-outside-click";
type ItemOptionType = {
display: React.ReactNode;
as?: "button" | "link" | "div";
href?: string;
isSelected?: boolean;
onClick?: () => void;
children?: ItemOptionType[] | null;
};
type DropdownItemProps = {
item: ItemOptionType;
};
type DropDownListProps = {
open: boolean;
handleClose?: () => void;
items: ItemOptionType[];
};
type DropdownProps = {
button: React.ReactNode | (() => React.ReactNode);
items: ItemOptionType[];
};
const DropdownList: React.FC<DropDownListProps> = (props) => {
const { open, items, handleClose } = props;
const ref = useRef(null);
useOutSideClick(ref, () => {
if (handleClose) handleClose();
});
return (
<Popover className="absolute -left-1">
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel
ref={ref}
className="absolute left-1/2 z-10 mt-1 max-w-[9rem] origin-top-right -translate-x-full select-none rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none"
>
<div className="w-full rounded-md text-sm shadow-lg">
{items.map((item, index) => (
<DropdownItem key={index} item={item} />
))}
</div>
</Popover.Panel>
</Transition>
</Popover>
);
};
const DropdownItem: React.FC<DropdownItemProps> = (props) => {
const { item } = props;
const { display, children, as: itemAs, href, onClick, isSelected } = item;
const [open, setOpen] = useState(false);
return (
<div className="group relative flex w-full gap-x-6 rounded-lg p-1">
{(!itemAs || itemAs === "button" || itemAs === "div") && (
<button
type="button"
onClick={() => {
if (!children) {
if (onClick) onClick();
return;
}
setOpen((prev) => !prev);
}}
className={`flex w-full items-center gap-1 rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80 ${
isSelected ? "bg-custom-background-80" : ""
}`}
>
{children && <ChevronLeft className="h-4 w-4 transform transition-transform" strokeWidth={2} />}
{!children && <span />}
<span className="truncate text-xs">{display}</span>
<Check className={`h-3 w-3 opacity-0 ${isSelected ? "opacity-100" : ""}`} strokeWidth={2} />
</button>
)}
{itemAs === "link" && <Link href={href || "#"}>{display}</Link>}
{children && <DropdownList open={open} handleClose={() => setOpen(false)} items={children} />}
</div>
);
};
const Dropdown: React.FC<DropdownProps> = (props) => {
const { button, items } = props;
return (
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`group flex items-center justify-between gap-2 rounded-md border border-custom-border-200 px-3 py-1.5 text-xs shadow-sm duration-300 hover:bg-custom-background-90 hover:text-custom-text-100 focus:outline-none ${
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
}`}
>
{typeof button === "function" ? button() : button}
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute left-full z-10 mt-1 w-36 origin-top-right -translate-x-full select-none rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
<div className="w-full">
{items.map((item, index) => (
<DropdownItem key={index} item={item} />
))}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
};
export { Dropdown };

View File

@ -1,3 +1,2 @@
export * from "./dropdown";
export * from "./icon"; export * from "./icon";
export * from "./reaction-selector"; export * from "./reaction-selector";

View File

@ -1,2 +1 @@
export * from "./auth"; export * from "./auth";
export * from "./project-details";

View File

@ -1,13 +1,7 @@
import { Calendar, GanttChartSquare, Kanban, List, Sheet } from "lucide-react";
// types // types
import { TIssuePriorities } from "@plane/types"; import { TIssuePriorities } from "@plane/types";
import { import { TIssueLayout, TIssueFilterKeys, TIssueFilterPriorityObject } from "@/types/issue";
TIssueLayout,
TIssueLayoutViews,
TIssueFilterKeys,
TIssueFilterPriorityObject,
TIssueFilterState,
TIssueFilterStateObject,
} from "@/types/issue";
// issue filters // issue filters
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]> } = { export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]> } = {
@ -28,20 +22,18 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"f
}, },
}; };
export const issueLayoutViews: Partial<TIssueLayoutViews> = { export const ISSUE_LAYOUTS: {
list: { key: TIssueLayout;
title: "List View", title: string;
icon: "format_list_bulleted", icon: any;
className: "", }[] = [
}, { key: "list", title: "List", icon: List },
kanban: { { key: "kanban", title: "Kanban", icon: Kanban },
title: "Board View", { key: "calendar", title: "Calendar", icon: Calendar },
icon: "grid_view", { key: "spreadsheet", title: "Spreadsheet", icon: Sheet },
className: "", { key: "gantt", title: "Gantt chart", icon: GanttChartSquare },
}, ];
};
// issue priority filters
export const issuePriorityFilters: TIssueFilterPriorityObject[] = [ export const issuePriorityFilters: TIssueFilterPriorityObject[] = [
{ {
key: "urgent", key: "urgent",
@ -84,55 +76,3 @@ export const issuePriorityFilter = (priorityKey: TIssuePriorities): TIssueFilter
if (currentIssuePriority) return currentIssuePriority; if (currentIssuePriority) return currentIssuePriority;
return undefined; return undefined;
}; };
// issue group filters
export const issueGroupColors: {
[key in TIssueFilterState]: string;
} = {
backlog: "#d9d9d9",
unstarted: "#3f76ff",
started: "#f59e0b",
completed: "#16a34a",
cancelled: "#dc2626",
};
export const issueGroups: TIssueFilterStateObject[] = [
{
key: "backlog",
title: "Backlog",
color: "#d9d9d9",
className: `text-[#d9d9d9] bg-[#d9d9d9]/10`,
},
{
key: "unstarted",
title: "Unstarted",
color: "#3f76ff",
className: `text-[#3f76ff] bg-[#3f76ff]/10`,
},
{
key: "started",
title: "Started",
color: "#f59e0b",
className: `text-[#f59e0b] bg-[#f59e0b]/10`,
},
{
key: "completed",
title: "Completed",
color: "#16a34a",
className: `text-[#16a34a] bg-[#16a34a]/10`,
},
{
key: "cancelled",
title: "Cancelled",
color: "#dc2626",
className: `text-[#dc2626] bg-[#dc2626]/10`,
},
];
export const issueGroupFilter = (issueKey: TIssueFilterState): TIssueFilterStateObject | undefined => {
const currentIssueStateGroup: TIssueFilterStateObject | undefined =
issueGroups && issueGroups.length > 0 ? issueGroups.find((group) => group.key === issueKey) : undefined;
if (currentIssueStateGroup) return currentIssueStateGroup;
return undefined;
};

View File

@ -1,12 +0,0 @@
export const USER_ROLES = [
{ value: "Product / Project Manager", label: "Product / Project Manager" },
{ value: "Development / Engineering", label: "Development / Engineering" },
{ value: "Founder / Executive", label: "Founder / Executive" },
{ value: "Freelancer / Consultant", label: "Freelancer / Consultant" },
{ value: "Marketing / Growth", label: "Marketing / Growth" },
{ value: "Sales / Business Development", label: "Sales / Business Development" },
{ value: "Support / Operations", label: "Support / Operations" },
{ value: "Student / Professor", label: "Student / Professor" },
{ value: "Human Resources", label: "Human Resources" },
{ value: "Other", label: "Other" },
];

View File

@ -1,23 +1,3 @@
export const getRandomEmoji = () => {
const emojis = [
"8986",
"9200",
"128204",
"127773",
"127891",
"127947",
"128076",
"128077",
"128187",
"128188",
"128512",
"128522",
"128578",
];
return emojis[Math.floor(Math.random() * emojis.length)];
};
export const renderEmoji = ( export const renderEmoji = (
emoji: emoji:
| string | string

View File

@ -3,7 +3,7 @@ import DOMPurify from "dompurify";
export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2"); export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2");
const fallbackCopyTextToClipboard = (text: string) => { const fallbackCopyTextToClipboard = (text: string) => {
var textArea = document.createElement("textarea"); const textArea = document.createElement("textarea");
textArea.value = text; textArea.value = text;
// Avoid scrolling to bottom // Avoid scrolling to bottom
@ -18,7 +18,7 @@ const fallbackCopyTextToClipboard = (text: string) => {
try { try {
// FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this. // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this.
// https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
var successful = document.execCommand("copy"); document.execCommand("copy");
} catch (err) {} } catch (err) {}
document.body.removeChild(textArea); document.body.removeChild(textArea);

View File

@ -1,7 +1,9 @@
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import useSWR from "swr"; import useSWR from "swr";
// types
import { IUser } from "@plane/types"; import { IUser } from "@plane/types";
import { UserService } from "services/user.service"; // services
import { UserService } from "@/services/user.service";
export const useMention = () => { export const useMention = () => {
const userService = new UserService(); const userService = new UserService();

14
space/types/app.d.ts vendored
View File

@ -1,14 +0,0 @@
export interface IAppConfig {
email_password_login: boolean;
file_size_limit: number;
google_client_id: string | null;
github_app_name: string | null;
github_client_id: string | null;
magic_login: boolean;
slack_client_id: string | null;
posthog_api_key: string | null;
posthog_host: string | null;
has_openai_configured: boolean;
has_unsplash_configured: boolean;
is_self_managed: boolean;
}

View File

@ -1,16 +1,9 @@
import { IStateLite, IWorkspaceLite, TIssuePriorities } from "@plane/types"; import { IStateLite, IWorkspaceLite, TIssuePriorities, TStateGroups } from "@plane/types";
export type TIssueLayout = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; export type TIssueLayout = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt";
export type TIssueLayoutOptions = { export type TIssueLayoutOptions = {
[key in TIssueLayout]: boolean; [key in TIssueLayout]: boolean;
}; };
export type TIssueLayoutViews = {
[key in TIssueLayout]: {
title: string;
icon: string;
className: string;
};
};
export type TIssueFilterPriorityObject = { export type TIssueFilterPriorityObject = {
key: TIssuePriorities; key: TIssuePriorities;
@ -19,14 +12,6 @@ export type TIssueFilterPriorityObject = {
icon: string; icon: string;
}; };
export type TIssueFilterState = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
export type TIssueFilterStateObject = {
key: TIssueFilterState;
title: string;
color: string;
className: string;
};
export type TIssueFilterKeys = "priority" | "state" | "labels"; export type TIssueFilterKeys = "priority" | "state" | "labels";
export type TDisplayFilters = { export type TDisplayFilters = {
@ -34,7 +19,7 @@ export type TDisplayFilters = {
}; };
export type TFilters = { export type TFilters = {
state: TIssueFilterState[]; state: TStateGroups[];
priority: TIssuePriorities[]; priority: TIssuePriorities[];
labels: string[]; labels: string[];
}; };

View File

@ -1,5 +1,5 @@
import { IWorkspaceLite } from "@plane/types"; import { IWorkspaceLite } from "@plane/types";
import { TProjectDetails, TViewDetails } from "./project"; import { TProjectDetails, TViewDetails } from "@/types/project";
export type TPublishEntityType = "project"; export type TPublishEntityType = "project";

View File

@ -1,4 +0,0 @@
export interface IThemeStore {
theme: string;
setTheme: (theme: "light" | "dark" | string) => void;
}

30
space/types/user.d.ts vendored
View File

@ -1,30 +0,0 @@
export interface IUser {
avatar: string;
cover_image: string | null;
created_at: Date;
created_location: string;
date_joined: Date;
email: string;
display_name: string;
first_name: string;
id: string;
is_email_verified: boolean;
is_onboarded: boolean;
is_tour_completed: boolean;
last_location: string;
last_login: Date;
last_name: string;
mobile_number: string;
role: string;
is_password_autoset: boolean;
onboarding_step: {
workspace_join?: boolean;
profile_complete?: boolean;
workspace_create?: boolean;
workspace_invite?: boolean;
};
token: string;
updated_at: Date;
username: string;
user_timezone: string;
}