feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,4 @@
// local imports
import { WorkspaceActiveCyclesUpgrade } from "./workspace-active-cycles-upgrade";
export const WorkspaceActiveCyclesRoot = () => <WorkspaceActiveCyclesUpgrade />;

View File

@@ -0,0 +1,136 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope, Search } from "lucide-react";
// plane imports
import { MARKETING_PRICING_PAGE_LINK } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/propel/button";
import { ContentWrapper } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { ProIcon } from "@/components/common/pro-icon";
// hooks
import { useUser } from "@/hooks/store/user";
export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
{
key: "10000_feet_view",
title: "10,000-feet view of all active cycles.",
description:
"Zoom out to see running cycles across all your projects at once instead of going from Cycle to Cycle in each project.",
icon: Folder,
},
{
key: "get_snapshot_of_each_active_cycle",
title: "Get a snapshot of each active cycle.",
description:
"Track high-level metrics for all active cycles, see their state of progress, and get a sense of scope against deadlines.",
icon: CircleDashed,
},
{
key: "compare_burndowns",
title: "Compare burndowns.",
description: "Monitor how each of your teams are performing with a peek into each cycles burndown report.",
icon: BarChart4,
},
{
key: "quickly_see_make_or_break_issues",
title: "Quickly see make-or-break work items. ",
description:
"Preview high-priority work items for each cycle against due dates. See all of them per cycle in one click.",
icon: AlertOctagon,
},
{
key: "zoom_into_cycles_that_need_attention",
title: "Zoom into cycles that need attention. ",
description: "Investigate the state of any cycle that doesnt conform to expectations in one click.",
icon: Search,
},
{
key: "stay_ahead_of_blockers",
title: "Stay ahead of blockers.",
description:
"Spot challenges from one project to another and see inter-cycle dependencies that arent obvious from any other view.",
icon: Microscope,
},
];
export const WorkspaceActiveCyclesUpgrade = observer(() => {
const { t } = useTranslation();
// store hooks
const {
userProfile: { data: userProfile },
} = useUser();
const isDarkMode = userProfile?.theme.theme === "dark";
return (
<ContentWrapper className="gap-10">
<div
className={cn("item-center flex min-h-[25rem] justify-between rounded-xl", {
"bg-gradient-to-l from-[#CFCFCF] to-[#212121]": userProfile?.theme.theme === "dark",
"bg-gradient-to-l from-[#3b5ec6] to-[#f5f7fe]": userProfile?.theme.theme === "light",
})}
>
<div className="relative flex flex-col justify-center gap-7 px-14 lg:w-1/2">
<div className="flex max-w-64 flex-col gap-2">
<h2 className="text-2xl font-semibold">{t("on_demand_snapshots_of_all_your_cycles")}</h2>
<p className="text-base font-medium text-custom-text-300">{t("active_cycles_description")}</p>
</div>
<div className="flex items-center gap-3">
<a
className={`${getButtonStyling("primary", "md")} cursor-pointer`}
href={MARKETING_PRICING_PAGE_LINK}
target="_blank"
rel="noreferrer"
>
<ProIcon className="h-3.5 w-3.5 text-white" />
{t("upgrade")}
</a>
</div>
<span className="absolute left-0 top-0">
<Image
src={`/workspace-active-cycles/cta-l-1-${isDarkMode ? "dark" : "light"}.webp`}
height={125}
width={125}
className="rounded-xl"
alt="l-1"
/>
</span>
</div>
<div className="relative hidden w-1/2 lg:block">
<span className="absolute bottom-0 right-0">
<Image
src={`/workspace-active-cycles/cta-r-1-${isDarkMode ? "dark" : "light"}.webp`}
height={420}
width={500}
alt="r-1"
/>
</span>
<span className="absolute -bottom-16 right-1/2 rounded-xl">
<Image
src={`/workspace-active-cycles/cta-r-2-${isDarkMode ? "dark" : "light"}.webp`}
height={210}
width={280}
alt="r-2"
/>
</span>
</div>
</div>
<div className="grid h-full grid-cols-1 gap-5 pb-8 lg:grid-cols-2 xl:grid-cols-3">
{WORKSPACE_ACTIVE_CYCLES_DETAILS.map((item) => (
<div key={item.title} className="flex min-h-32 w-full flex-col gap-2 rounded-md bg-custom-background-80 p-4">
<div className="flex gap-2 justify-between">
<h3 className="font-medium">{t(item.key)}</h3>
<item.icon className="mt-1 h-4 w-4 text-blue-500" />
</div>
<span className="text-sm text-custom-text-300">{t(`${item.key}_description`)}</span>
</div>
))}
</div>
</ContentWrapper>
);
});

View File

@@ -0,0 +1,8 @@
import type { AnalyticsTab } from "@plane/types";
import { Overview } from "@/components/analytics/overview";
import { WorkItems } from "@/components/analytics/work-items";
export const getAnalyticsTabs = (t: (key: string, params?: Record<string, any>) => string): AnalyticsTab[] => [
{ key: "overview", label: t("common.overview"), content: Overview, isDisabled: false },
{ key: "work-items", label: t("sidebar.work_items"), content: WorkItems, isDisabled: false },
];

View File

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

View File

@@ -0,0 +1,4 @@
"use client";
import React from "react";
export const AppRailRoot = () => <></>;

View File

@@ -0,0 +1,11 @@
"use client";
import type { FC } from "react";
import React from "react";
export type TCustomAutomationsRootProps = {
projectId: string;
workspaceSlug: string;
};
export const CustomAutomationsRoot: FC<TCustomAutomationsRootProps> = () => <></>;

View File

@@ -0,0 +1,32 @@
"use client";
import type { FC } from "react";
// plane imports
import type { EProjectFeatureKey } from "@plane/constants";
// local components
import { ProjectBreadcrumb } from "./project";
import { ProjectFeatureBreadcrumb } from "./project-feature";
type TCommonProjectBreadcrumbProps = {
workspaceSlug: string;
projectId: string;
featureKey?: EProjectFeatureKey;
isLast?: boolean;
};
export const CommonProjectBreadcrumbs: FC<TCommonProjectBreadcrumbProps> = (props) => {
const { workspaceSlug, projectId, featureKey, isLast = false } = props;
return (
<>
<ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />
{featureKey && (
<ProjectFeatureBreadcrumb
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
featureKey={featureKey}
isLast={isLast}
/>
)}
</>
);
};

View File

@@ -0,0 +1,70 @@
"use client";
import type { FC } from "react";
import { observer } from "mobx-react";
// plane imports
import { EProjectFeatureKey } from "@plane/constants";
import type { ISvgIcons } from "@plane/propel/icons";
import { BreadcrumbNavigationDropdown, Breadcrumbs } from "@plane/ui";
// components
import { SwitcherLabel } from "@/components/common/switcher-label";
import type { TNavigationItem } from "@/components/workspace/sidebar/project-navigation";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useAppRouter } from "@/hooks/use-app-router";
// local imports
import { getProjectFeatureNavigation } from "../projects/navigation/helper";
type TProjectFeatureBreadcrumbProps = {
workspaceSlug: string;
projectId: string;
featureKey: EProjectFeatureKey;
isLast?: boolean;
additionalNavigationItems?: TNavigationItem[];
};
export const ProjectFeatureBreadcrumb = observer((props: TProjectFeatureBreadcrumbProps) => {
const { workspaceSlug, projectId, featureKey, isLast = false, additionalNavigationItems } = props;
// router
const router = useAppRouter();
// store hooks
const { getPartialProjectById } = useProject();
// derived values
const project = getPartialProjectById(projectId);
if (!project) return null;
const navigationItems = getProjectFeatureNavigation(workspaceSlug, projectId, project);
// if additional navigation items are provided, add them to the navigation items
const allNavigationItems = [...(additionalNavigationItems || []), ...navigationItems];
return (
<>
<Breadcrumbs.Item
component={
<BreadcrumbNavigationDropdown
selectedItemKey={featureKey}
navigationItems={allNavigationItems
.filter((item) => item.shouldRender)
.map((item) => ({
key: item.key,
title: item.name,
customContent: <SwitcherLabel name={item.name} LabelIcon={item.icon as FC<ISvgIcons>} />,
action: () => router.push(item.href),
icon: item.icon as FC<ISvgIcons>,
}))}
handleOnClick={() => {
router.push(
`/${workspaceSlug}/projects/${projectId}/${featureKey === EProjectFeatureKey.WORK_ITEMS ? "issues" : featureKey}/`
);
}}
isLast={isLast}
/>
}
showSeparator={false}
isLast={isLast}
/>
</>
);
});

View File

@@ -0,0 +1,83 @@
"use client";
import { observer } from "mobx-react";
import { ProjectIcon } from "@plane/propel/icons";
// plane imports
import type { ICustomSearchSelectOption } from "@plane/types";
import { BreadcrumbNavigationSearchDropdown, Breadcrumbs } from "@plane/ui";
// components
import { Logo } from "@/components/common/logo";
import { SwitcherLabel } from "@/components/common/switcher-label";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useAppRouter } from "@/hooks/use-app-router";
import type { TProject } from "@/plane-web/types";
type TProjectBreadcrumbProps = {
workspaceSlug: string;
projectId: string;
handleOnClick?: () => void;
};
export const ProjectBreadcrumb = observer((props: TProjectBreadcrumbProps) => {
const { workspaceSlug, projectId, handleOnClick } = props;
// router
const router = useAppRouter();
// store hooks
const { joinedProjectIds, getPartialProjectById } = useProject();
const currentProjectDetails = getPartialProjectById(projectId);
// store hooks
if (!currentProjectDetails) return null;
// derived values
const switcherOptions = joinedProjectIds
.map((projectId) => {
const project = getPartialProjectById(projectId);
return {
value: projectId,
query: project?.name,
content: (
<SwitcherLabel
name={project?.name}
logo_props={project?.logo_props}
LabelIcon={ProjectIcon}
type="material"
/>
),
};
})
.filter((option) => option !== undefined) as ICustomSearchSelectOption[];
// helpers
const renderIcon = (projectDetails: TProject) => (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={projectDetails.logo_props} size={14} />
</span>
);
return (
<>
<Breadcrumbs.Item
component={
<BreadcrumbNavigationSearchDropdown
selectedItem={currentProjectDetails.id}
navigationItems={switcherOptions}
onChange={(value: string) => {
router.push(`/${workspaceSlug}/projects/${value}/issues`);
}}
title={currentProjectDetails?.name}
icon={renderIcon(currentProjectDetails)}
handleOnClick={() => {
if (handleOnClick) handleOnClick();
else router.push(`/${workspaceSlug}/projects/${currentProjectDetails.id}/issues/`);
}}
shouldTruncate
/>
}
showSeparator={false}
/>
</>
);
});

View File

@@ -0,0 +1 @@
export * from "./work-item-actions";

View File

@@ -0,0 +1,50 @@
import { Command } from "cmdk";
import { observer } from "mobx-react";
import { Check } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { StateGroupIcon } from "@plane/propel/icons";
import { Spinner } from "@plane/ui";
// store hooks
import { useProjectState } from "@/hooks/store/use-project-state";
export type TChangeWorkItemStateListProps = {
projectId: string | null;
currentStateId: string | null;
handleStateChange: (stateId: string) => void;
};
export const ChangeWorkItemStateList = observer((props: TChangeWorkItemStateListProps) => {
const { projectId, currentStateId, handleStateChange } = props;
// store hooks
const { getProjectStates } = useProjectState();
// derived values
const projectStates = getProjectStates(projectId);
return (
<>
{projectStates ? (
projectStates.length > 0 ? (
projectStates.map((state) => (
<Command.Item key={state.id} onSelect={() => handleStateChange(state.id)} className="focus:outline-none">
<div className="flex items-center space-x-3">
<StateGroupIcon
stateGroup={state.group}
color={state.color}
size={EIconSize.LG}
percentage={state?.order}
/>
<p>{state.name}</p>
</div>
<div>{state.id === currentStateId && <Check className="h-3 w-3" />}</div>
</Command.Item>
))
) : (
<div className="text-center">No states found</div>
)
) : (
<Spinner />
)}
</>
);
});

View File

@@ -0,0 +1 @@
export * from "./change-state-list";

View File

@@ -0,0 +1,112 @@
"use client";
import { LayoutGrid } from "lucide-react";
// plane imports
import { CycleIcon, ModuleIcon, PageIcon, ProjectIcon, ViewsIcon } from "@plane/propel/icons";
import type {
IWorkspaceDefaultSearchResult,
IWorkspaceIssueSearchResult,
IWorkspacePageSearchResult,
IWorkspaceProjectSearchResult,
IWorkspaceSearchResult,
} from "@plane/types";
import { generateWorkItemLink } from "@plane/utils";
// plane web components
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
export type TCommandGroups = {
[key: string]: {
icon: React.ReactNode | null;
itemName: (item: any) => React.ReactNode;
path: (item: any, projectId: string | undefined) => string;
title: string;
};
};
export const commandGroups: TCommandGroups = {
cycle: {
icon: <CycleIcon className="h-3 w-3" />,
itemName: (cycle: IWorkspaceDefaultSearchResult) => (
<h6>
<span className="text-xs text-custom-text-300">{cycle.project__identifier}</span> {cycle.name}
</h6>
),
path: (cycle: IWorkspaceDefaultSearchResult) =>
`/${cycle?.workspace__slug}/projects/${cycle?.project_id}/cycles/${cycle?.id}`,
title: "Cycles",
},
issue: {
icon: null,
itemName: (issue: IWorkspaceIssueSearchResult) => (
<div className="flex gap-2">
<IssueIdentifier
projectId={issue.project_id}
issueTypeId={issue.type_id}
projectIdentifier={issue.project__identifier}
issueSequenceId={issue.sequence_id}
textContainerClassName="text-xs"
/>{" "}
{issue.name}
</div>
),
path: (issue: IWorkspaceIssueSearchResult) =>
generateWorkItemLink({
workspaceSlug: issue?.workspace__slug,
projectId: issue?.project_id,
issueId: issue?.id,
projectIdentifier: issue.project__identifier,
sequenceId: issue?.sequence_id,
}),
title: "Work items",
},
issue_view: {
icon: <ViewsIcon className="h-3 w-3" />,
itemName: (view: IWorkspaceDefaultSearchResult) => (
<h6>
<span className="text-xs text-custom-text-300">{view.project__identifier}</span> {view.name}
</h6>
),
path: (view: IWorkspaceDefaultSearchResult) =>
`/${view?.workspace__slug}/projects/${view?.project_id}/views/${view?.id}`,
title: "Views",
},
module: {
icon: <ModuleIcon className="h-3 w-3" />,
itemName: (module: IWorkspaceDefaultSearchResult) => (
<h6>
<span className="text-xs text-custom-text-300">{module.project__identifier}</span> {module.name}
</h6>
),
path: (module: IWorkspaceDefaultSearchResult) =>
`/${module?.workspace__slug}/projects/${module?.project_id}/modules/${module?.id}`,
title: "Modules",
},
page: {
icon: <PageIcon className="h-3 w-3" />,
itemName: (page: IWorkspacePageSearchResult) => (
<h6>
<span className="text-xs text-custom-text-300">{page.project__identifiers?.[0]}</span> {page.name}
</h6>
),
path: (page: IWorkspacePageSearchResult, projectId: string | undefined) => {
let redirectProjectId = page?.project_ids?.[0];
if (!!projectId && page?.project_ids?.includes(projectId)) redirectProjectId = projectId;
return redirectProjectId
? `/${page?.workspace__slug}/projects/${redirectProjectId}/pages/${page?.id}`
: `/${page?.workspace__slug}/pages/${page?.id}`;
},
title: "Pages",
},
project: {
icon: <ProjectIcon className="h-3 w-3" />,
itemName: (project: IWorkspaceProjectSearchResult) => project?.name,
path: (project: IWorkspaceProjectSearchResult) => `/${project?.workspace__slug}/projects/${project?.id}/issues/`,
title: "Projects",
},
workspace: {
icon: <LayoutGrid className="h-3 w-3" />,
itemName: (workspace: IWorkspaceSearchResult) => workspace?.name,
path: (workspace: IWorkspaceSearchResult) => `/${workspace?.slug}/`,
title: "Workspaces",
},
};

View File

@@ -0,0 +1,3 @@
export * from "./actions";
export * from "./modals";
export * from "./helpers";

View File

@@ -0,0 +1,3 @@
export * from "./workspace-level";
export * from "./project-level";
export * from "./issue-level";

View File

@@ -0,0 +1,102 @@
import type { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import type { TIssue } from "@plane/types";
import { EIssueServiceType, EIssuesStoreType } from "@plane/types";
// components
import { BulkDeleteIssuesModal } from "@/components/core/modals/bulk-delete-issues-modal";
import { DeleteIssueModal } from "@/components/issues/delete-issue-modal";
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal/modal";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useUser } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
import { useIssuesActions } from "@/hooks/use-issues-actions";
export type TIssueLevelModalsProps = {
projectId: string | undefined;
issueId: string | undefined;
};
export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) => {
const { projectId, issueId } = props;
// router
const { workspaceSlug, cycleId, moduleId } = useParams();
const router = useAppRouter();
// store hooks
const { data: currentUser } = useUser();
const {
issue: { getIssueById },
} = useIssueDetail();
const { removeIssue: removeEpic } = useIssuesActions(EIssuesStoreType.EPIC);
const { removeIssue: removeWorkItem } = useIssuesActions(EIssuesStoreType.PROJECT);
const {
isCreateIssueModalOpen,
toggleCreateIssueModal,
isDeleteIssueModalOpen,
toggleDeleteIssueModal,
isBulkDeleteIssueModalOpen,
toggleBulkDeleteIssueModal,
createWorkItemAllowedProjectIds,
} = useCommandPalette();
// derived values
const issueDetails = issueId ? getIssueById(issueId) : undefined;
const { fetchSubIssues: fetchSubWorkItems } = useIssueDetail();
const { fetchSubIssues: fetchEpicSubWorkItems } = useIssueDetail(EIssueServiceType.EPICS);
const handleDeleteIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const isEpic = issueDetails?.is_epic;
const deleteAction = isEpic ? removeEpic : removeWorkItem;
const redirectPath = `/${workspaceSlug}/projects/${projectId}/${isEpic ? "epics" : "issues"}`;
await deleteAction(projectId, issueId);
router.push(redirectPath);
} catch (error) {
console.error("Failed to delete issue:", error);
}
};
const handleCreateIssueSubmit = async (newIssue: TIssue) => {
if (!workspaceSlug || !newIssue.project_id || !newIssue.id || newIssue.parent_id !== issueDetails?.id) return;
const fetchAction = issueDetails?.is_epic ? fetchEpicSubWorkItems : fetchSubWorkItems;
await fetchAction(workspaceSlug?.toString(), newIssue.project_id, issueDetails.id);
};
const getCreateIssueModalData = () => {
if (cycleId) return { cycle_id: cycleId.toString() };
if (moduleId) return { module_ids: [moduleId.toString()] };
return undefined;
};
return (
<>
<CreateUpdateIssueModal
isOpen={isCreateIssueModalOpen}
onClose={() => toggleCreateIssueModal(false)}
data={getCreateIssueModalData()}
onSubmit={handleCreateIssueSubmit}
allowedProjectIds={createWorkItemAllowedProjectIds}
/>
{workspaceSlug && projectId && issueId && issueDetails && (
<DeleteIssueModal
handleClose={() => toggleDeleteIssueModal(false)}
isOpen={isDeleteIssueModalOpen}
data={issueDetails}
onSubmit={() => handleDeleteIssue(workspaceSlug.toString(), projectId?.toString(), issueId?.toString())}
isEpic={issueDetails?.is_epic}
/>
)}
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssueModalOpen}
onClose={() => toggleBulkDeleteIssueModal(false)}
user={currentUser}
/>
</>
);
});

View File

@@ -0,0 +1,62 @@
import { observer } from "mobx-react";
// components
import { CycleCreateUpdateModal } from "@/components/cycles/modal";
import { CreateUpdateModuleModal } from "@/components/modules";
import { CreatePageModal } from "@/components/pages/modals/create-page-modal";
import { CreateUpdateProjectViewModal } from "@/components/views/modal";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
// plane web hooks
import { EPageStoreType } from "@/plane-web/hooks/store";
export type TProjectLevelModalsProps = {
workspaceSlug: string;
projectId: string;
};
export const ProjectLevelModals = observer((props: TProjectLevelModalsProps) => {
const { workspaceSlug, projectId } = props;
// store hooks
const {
isCreateCycleModalOpen,
toggleCreateCycleModal,
isCreateModuleModalOpen,
toggleCreateModuleModal,
isCreateViewModalOpen,
toggleCreateViewModal,
createPageModal,
toggleCreatePageModal,
} = useCommandPalette();
return (
<>
<CycleCreateUpdateModal
isOpen={isCreateCycleModalOpen}
handleClose={() => toggleCreateCycleModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdateModuleModal
isOpen={isCreateModuleModalOpen}
onClose={() => toggleCreateModuleModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdateProjectViewModal
isOpen={isCreateViewModalOpen}
onClose={() => toggleCreateViewModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreatePageModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isModalOpen={createPageModal.isOpen}
pageAccess={createPageModal.pageAccess}
handleModalClose={() => toggleCreatePageModal({ isOpen: false })}
redirectionEnabled
storeType={EPageStoreType.PROJECT}
/>
</>
);
});

View File

@@ -0,0 +1,25 @@
import { observer } from "mobx-react";
// components
import { CreateProjectModal } from "@/components/project/create-project-modal";
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
export type TWorkspaceLevelModalsProps = {
workspaceSlug: string;
};
export const WorkspaceLevelModals = observer((props: TWorkspaceLevelModalsProps) => {
const { workspaceSlug } = props;
// store hooks
const { isCreateProjectModalOpen, toggleCreateProjectModal } = useCommandPalette();
return (
<>
<CreateProjectModal
isOpen={isCreateProjectModalOpen}
onClose={() => toggleCreateProjectModal(false)}
workspaceSlug={workspaceSlug.toString()}
/>
</>
);
});

View File

@@ -0,0 +1,82 @@
import type { FC, ReactNode } from "react";
import { useRef } from "react";
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import type { TIssueComment } from "@plane/types";
import { EIssueCommentAccessSpecifier } from "@plane/types";
import { Avatar, Tooltip } from "@plane/ui";
import { calculateTimeAgo, cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
type TCommentBlock = {
comment: TIssueComment;
ends: "top" | "bottom" | undefined;
quickActions: ReactNode;
children: ReactNode;
};
export const CommentBlock: FC<TCommentBlock> = observer((props) => {
const { comment, ends, quickActions, children } = props;
// refs
const commentBlockRef = useRef<HTMLDivElement>(null);
// store hooks
const { getUserDetails } = useMember();
// derived values
const userDetails = getUserDetails(comment?.actor);
// translation
const { t } = useTranslation();
const displayName = comment?.actor_detail?.is_bot
? comment?.actor_detail?.first_name + ` ${t("bot")}`
: (userDetails?.display_name ?? comment?.actor_detail?.display_name);
const avatarUrl = userDetails?.avatar_url ?? comment?.actor_detail?.avatar_url;
if (!comment) return null;
return (
<div
className={`relative flex gap-3 ${ends === "top" ? `pb-2` : ends === "bottom" ? `pt-2` : `py-2`}`}
ref={commentBlockRef}
>
<div
className="absolute left-[13px] top-0 bottom-0 w-0.5 transition-border duration-1000 bg-custom-background-80"
aria-hidden
/>
<div
className={cn(
"flex-shrink-0 relative w-7 h-6 rounded-full transition-border duration-1000 flex justify-center items-center z-[3] uppercase font-medium"
)}
>
<Avatar size="base" name={displayName} src={getFileURL(avatarUrl)} className="flex-shrink-0" />
</div>
<div className="flex flex-col gap-3 truncate flex-grow">
<div className="flex w-full gap-2">
<div className="flex-1 flex flex-wrap items-center gap-1">
<div className="flex items-center gap-1">
<span className="text-xs font-medium">
{`${displayName}${comment.access === EIssueCommentAccessSpecifier.EXTERNAL ? " (External User)" : ""}`}
</span>
</div>
<div className="text-xs text-custom-text-300">
commented{" "}
<Tooltip
tooltipContent={`${renderFormattedDate(comment.created_at)} at ${renderFormattedTime(comment.created_at)}`}
position="bottom"
>
<span className="text-custom-text-350">
{calculateTimeAgo(comment.updated_at)}
{comment.edited_at && ` (${t("edited")})`}
</span>
</Tooltip>
</div>
</div>
<div className="flex-shrink-0 ">{quickActions}</div>
</div>
<div className="text-base mb-2">{children}</div>
</div>
</div>
);
});

View File

@@ -0,0 +1 @@
export * from "./comment-block";

View File

@@ -0,0 +1,17 @@
import type { ReactNode } from "react";
import { observer } from "mobx-react";
import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button";
import { useAppTheme } from "@/hooks/store/use-app-theme";
export const ExtendedAppHeader = observer((props: { header: ReactNode }) => {
const { header } = props;
// store hooks
const { sidebarCollapsed } = useAppTheme();
return (
<>
{sidebarCollapsed && <AppSidebarToggleButton />}
<div className="w-full">{header}</div>
</>
);
});

View File

@@ -0,0 +1,7 @@
import type { IWorkspace } from "@plane/types";
type TProps = {
workspace?: IWorkspace;
};
export const SubscriptionPill = (props: TProps) => <></>;

View File

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

View File

@@ -0,0 +1,109 @@
"use client";
import { useMemo } from "react";
import { observer } from "mobx-react";
import { Disclosure } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Row } from "@plane/ui";
// components
import { ActiveCycleStats } from "@/components/cycles/active-cycle/cycle-stats";
import { ActiveCycleProductivity } from "@/components/cycles/active-cycle/productivity";
import { ActiveCycleProgress } from "@/components/cycles/active-cycle/progress";
import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details";
import { CycleListGroupHeader } from "@/components/cycles/list/cycle-list-group-header";
import { CyclesListItem } from "@/components/cycles/list/cycles-list-item";
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
import type { ActiveCycleIssueDetails } from "@/store/issue/cycle";
interface IActiveCycleDetails {
workspaceSlug: string;
projectId: string;
cycleId?: string;
showHeader?: boolean;
}
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props;
// plane hooks
const { t } = useTranslation();
// store hooks
const { currentProjectActiveCycleId } = useCycle();
// derived values
const cycleId = propsCycleId ?? currentProjectActiveCycleId;
const activeCycleResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/cycle/active" });
// fetch cycle details
const {
handleFiltersUpdate,
cycle: activeCycle,
cycleIssueDetails,
} = useCyclesDetails({ workspaceSlug, projectId, cycleId });
const ActiveCyclesComponent = useMemo(
() => (
<>
{!cycleId || !activeCycle ? (
<DetailedEmptyState
title={t("project_cycles.empty_state.active.title")}
description={t("project_cycles.empty_state.active.description")}
assetPath={activeCycleResolvedPath}
/>
) : (
<div className="flex flex-col border-b border-custom-border-200">
{cycleId && (
<CyclesListItem
key={cycleId}
cycleId={cycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
className="!border-b-transparent"
/>
)}
<Row className="bg-custom-background-100 pt-3 pb-6">
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress
handleFiltersUpdate={handleFiltersUpdate}
projectId={projectId}
workspaceSlug={workspaceSlug}
cycle={activeCycle}
/>
<ActiveCycleProductivity workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
<ActiveCycleStats
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
cycleId={cycleId}
handleFiltersUpdate={handleFiltersUpdate}
cycleIssueDetails={cycleIssueDetails as ActiveCycleIssueDetails}
/>
</div>
</Row>
</div>
)}
</>
),
[cycleId, activeCycle, workspaceSlug, projectId, handleFiltersUpdate, cycleIssueDetails]
);
return (
<>
{showHeader ? (
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
<CycleListGroupHeader title={t("project_cycles.active_cycle.label")} type="current" isExpanded={open} />
</Disclosure.Button>
<Disclosure.Panel>{ActiveCyclesComponent}</Disclosure.Panel>
</>
)}
</Disclosure>
) : (
<>{ActiveCyclesComponent}</>
)}
</>
);
});

View File

@@ -0,0 +1,7 @@
import type { FC } from "react";
import { observer } from "mobx-react";
type Props = {
cycleId: string;
projectId: string;
};
export const CycleAdditionalActions: FC<Props> = observer(() => <></>);

View File

@@ -0,0 +1,86 @@
"use client";
import type { FC } from "react";
import { Fragment } from "react";
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import type { TCycleEstimateType } from "@plane/types";
import { Loader } from "@plane/ui";
import { getDate } from "@plane/utils";
// components
import ProgressChart from "@/components/core/sidebar/progress-chart";
import { validateCycleSnapshot } from "@/components/cycles/analytics-sidebar/issue-progress";
import { EstimateTypeDropdown } from "@/components/cycles/dropdowns";
// hooks
import { useCycle } from "@/hooks/store/use-cycle";
type ProgressChartProps = {
workspaceSlug: string;
projectId: string;
cycleId: string;
};
export const SidebarChart: FC<ProgressChartProps> = observer((props) => {
const { workspaceSlug, projectId, cycleId } = props;
// hooks
const { getEstimateTypeByCycleId, getCycleById, fetchCycleDetails, fetchArchivedCycleDetails, setEstimateType } =
useCycle();
const { t } = useTranslation();
// derived data
const cycleDetails = validateCycleSnapshot(getCycleById(cycleId));
const cycleStartDate = getDate(cycleDetails?.start_date);
const cycleEndDate = getDate(cycleDetails?.end_date);
const totalEstimatePoints = cycleDetails?.total_estimate_points || 0;
const totalIssues = cycleDetails?.total_issues || 0;
const estimateType = getEstimateTypeByCycleId(cycleId);
const chartDistributionData =
estimateType === "points" ? cycleDetails?.estimate_distribution : cycleDetails?.distribution || undefined;
const completionChartDistributionData = chartDistributionData?.completion_chart || undefined;
if (!workspaceSlug || !projectId || !cycleId) return null;
const isArchived = !!cycleDetails?.archived_at;
// handlers
const onChange = async (value: TCycleEstimateType) => {
setEstimateType(cycleId, value);
if (!workspaceSlug || !projectId || !cycleId) return;
try {
if (isArchived) {
await fetchArchivedCycleDetails(workspaceSlug, projectId, cycleId);
} else {
await fetchCycleDetails(workspaceSlug, projectId, cycleId);
}
} catch (err) {
console.error(err);
setEstimateType(cycleId, estimateType);
}
};
return (
<div>
<div className="relative flex items-center justify-between gap-2 pt-4">
<EstimateTypeDropdown value={estimateType} onChange={onChange} cycleId={cycleId} projectId={projectId} />
</div>
<div className="py-4">
<div>
{cycleStartDate && cycleEndDate && completionChartDistributionData ? (
<Fragment>
<ProgressChart
distribution={completionChartDistributionData}
totalIssues={estimateType === "points" ? totalEstimatePoints : totalIssues}
plotTitle={estimateType === "points" ? t("points") : t("work_items")}
/>
</Fragment>
) : (
<Loader className="w-full h-[160px] mt-4">
<Loader.Item width="100%" height="100%" />
</Loader>
)}
</div>
</div>
</div>
);
});

View File

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

View File

@@ -0,0 +1,13 @@
"use client";
import type { FC } from "react";
import React from "react";
// components
import { SidebarChart } from "./base";
type Props = {
workspaceSlug: string;
projectId: string;
cycleId: string;
};
export const SidebarChartRoot: FC<Props> = (props) => <SidebarChart {...props} />;

View File

@@ -0,0 +1,2 @@
export * from "./modal";
export * from "./use-end-cycle";

View File

@@ -0,0 +1,13 @@
import React from "react";
interface Props {
isOpen: boolean;
handleClose: () => void;
cycleId: string;
projectId: string;
workspaceSlug: string;
transferrableIssuesCount: number;
cycleName: string;
}
export const EndCycleModal: React.FC<Props> = () => <></>;

View File

@@ -0,0 +1,7 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const useEndCycle = (isCurrentCycle: boolean) => ({
isEndCycleModalOpen: false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setEndCycleModalOpen: (value: boolean) => {},
endCycleContextMenu: undefined,
});

View File

@@ -0,0 +1,4 @@
export * from "./active-cycle";
export * from "./analytics-sidebar";
export * from "./additional-actions";
export * from "./end-cycle";

View File

@@ -0,0 +1,16 @@
"use client";
import type { FC } from "react";
import React from "react";
// local components
type TDeDupeButtonRoot = {
workspaceSlug: string;
isDuplicateModalOpen: boolean;
handleOnClick: () => void;
label: string;
};
export const DeDupeButtonRoot: FC<TDeDupeButtonRoot> = (props) => {
const { workspaceSlug, isDuplicateModalOpen, label, handleOnClick } = props;
return <></>;
};

View File

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

View File

@@ -0,0 +1,16 @@
"use-client";
import type { FC } from "react";
// types
import type { TDeDupeIssue } from "@plane/types";
type TDuplicateModalRootProps = {
workspaceSlug: string;
issues: TDeDupeIssue[];
handleDuplicateIssueModal: (value: boolean) => void;
};
export const DuplicateModalRoot: FC<TDuplicateModalRootProps> = (props) => {
const { workspaceSlug, issues, handleDuplicateIssueModal } = props;
return <></>;
};

View File

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

View File

@@ -0,0 +1,24 @@
"use client";
import type { FC } from "react";
import React from "react";
import { observer } from "mobx-react";
// types
import type { TDeDupeIssue } from "@plane/types";
import type { TIssueOperations } from "@/components/issues/issue-detail";
type TDeDupeIssuePopoverRootProps = {
workspaceSlug: string;
projectId: string;
rootIssueId: string;
issues: TDeDupeIssue[];
issueOperations: TIssueOperations;
disabled?: boolean;
renderDeDupeActionModals?: boolean;
isIntakeIssue?: boolean;
};
export const DeDupeIssuePopoverRoot: FC<TDeDupeIssuePopoverRootProps> = observer((props) => {
const {} = props;
return <></>;
});

View File

@@ -0,0 +1,13 @@
"use client";
import type { FC } from "react";
type TDeDupeIssueButtonLabelProps = {
isOpen: boolean;
buttonLabel: string;
};
export const DeDupeIssueButtonLabel: FC<TDeDupeIssueButtonLabelProps> = (props) => {
const { isOpen, buttonLabel } = props;
return <></>;
};

View File

@@ -0,0 +1 @@
export * from "./mentions";

View File

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

View File

@@ -0,0 +1,4 @@
// plane editor
import type { TMentionComponentProps } from "@plane/editor";
export const EditorAdditionalMentionsRoot: React.FC<TMentionComponentProps> = () => null;

View File

@@ -0,0 +1 @@
export * from "./embeds";

View File

@@ -0,0 +1 @@
export * from "./modal";

View File

@@ -0,0 +1,20 @@
"use client";
import type { FC } from "react";
import React from "react";
import type { TIssue } from "@plane/types";
export interface EpicModalProps {
data?: Partial<TIssue>;
isOpen: boolean;
onClose: () => void;
beforeFormSubmit?: () => Promise<void>;
onSubmit?: (res: TIssue) => Promise<void>;
fetchIssueDetails?: boolean;
primaryButtonText?: {
default: string;
loading: string;
};
isProjectSelectionDisabled?: boolean;
}
export const CreateUpdateEpicModal: FC<EpicModalProps> = (props) => <></>;

View File

@@ -0,0 +1,49 @@
import type { FC } from "react";
import { observer } from "mobx-react";
import { Pen, Trash } from "lucide-react";
import { PROJECT_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
import { Tooltip } from "@plane/propel/tooltip";
// components
import { ProIcon } from "@/components/common/pro-icon";
type TEstimateListItem = {
estimateId: string;
isAdmin: boolean;
isEstimateEnabled: boolean;
isEditable: boolean;
onEditClick?: (estimateId: string) => void;
onDeleteClick?: (estimateId: string) => void;
};
export const EstimateListItemButtons: FC<TEstimateListItem> = observer((props) => {
const { estimateId, isAdmin, isEditable, onDeleteClick } = props;
if (!isAdmin || !isEditable) return <></>;
return (
<div className="relative flex items-center gap-1">
<Tooltip
tooltipContent={
<div className="relative flex items-center gap-2">
<div>Upgrade</div>
<ProIcon className="w-3 h-3" />
</div>
}
position="top"
>
<button
className="relative flex-shrink-0 w-6 h-6 flex justify-center items-center rounded cursor-pointer transition-colors overflow-hidden hover:bg-custom-background-80"
data-ph-element={PROJECT_SETTINGS_TRACKER_ELEMENTS.ESTIMATES_LIST_ITEM}
>
<Pen size={12} />
</button>
</Tooltip>
<button
className="relative flex-shrink-0 w-6 h-6 flex justify-center items-center rounded cursor-pointer transition-colors overflow-hidden hover:bg-custom-background-80"
onClick={() => onDeleteClick && onDeleteClick(estimateId)}
data-ph-element={PROJECT_SETTINGS_TRACKER_ELEMENTS.ESTIMATES_LIST_ITEM}
>
<Trash size={12} />
</button>
</div>
);
});

View File

@@ -0,0 +1,13 @@
import type { TEstimateSystemKeys } from "@plane/types";
import { EEstimateSystem } from "@plane/types";
export const isEstimateSystemEnabled = (key: TEstimateSystemKeys) => {
switch (key) {
case EEstimateSystem.POINTS:
return true;
case EEstimateSystem.CATEGORIES:
return true;
default:
return false;
}
};

View File

@@ -0,0 +1,4 @@
export * from "./estimate-list-item-buttons";
export * from "./update";
export * from "./points";
export * from "./helper";

View File

@@ -0,0 +1 @@
export * from "./time-input";

View File

@@ -0,0 +1,8 @@
import type { FC } from "react";
export type TEstimateTimeInputProps = {
value?: number;
handleEstimateInputValue: (value: string) => void;
};
export const EstimateTimeInput: FC<TEstimateTimeInputProps> = () => <></>;

View File

@@ -0,0 +1,19 @@
"use client";
import type { FC } from "react";
import type { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@plane/types";
export type TEstimatePointDelete = {
workspaceSlug: string;
projectId: string;
estimateId: string;
estimatePointId: string;
estimatePoints: TEstimatePointsObject[];
callback: () => void;
estimatePointError?: TEstimateTypeErrorObject | undefined;
handleEstimatePointError?: (newValue: string, message: string | undefined, mode?: "add" | "delete") => void;
estimateSystem: TEstimateSystemKeys;
};
export const EstimatePointDelete: FC<TEstimatePointDelete> = () => <></>;

View File

@@ -0,0 +1 @@
export * from "./delete";

View File

@@ -0,0 +1 @@
export * from "./modal";

View File

@@ -0,0 +1,14 @@
"use client";
import type { FC } from "react";
import { observer } from "mobx-react";
type TUpdateEstimateModal = {
workspaceSlug: string;
projectId: string;
estimateId: string | undefined;
isOpen: boolean;
handleClose: () => void;
};
export const UpdateEstimateModal: FC<TUpdateEstimateModal> = observer(() => <></>);

View File

@@ -0,0 +1,59 @@
import type { FC } from "react";
// components
import type { IBlockUpdateData, IGanttBlock } from "@plane/types";
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
// hooks
import { BlockRow } from "@/components/gantt-chart/blocks/block-row";
import { BLOCK_HEIGHT } from "@/components/gantt-chart/constants";
import type { TSelectionHelper } from "@/hooks/use-multiple-select";
// types
export type GanttChartBlocksProps = {
blockIds: string[];
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
handleScrollToBlock: (block: IGanttBlock) => void;
enableAddBlock: boolean | ((blockId: string) => boolean);
showAllBlocks: boolean;
selectionHelpers: TSelectionHelper;
ganttContainerRef: React.RefObject<HTMLDivElement>;
};
export const GanttChartRowList: FC<GanttChartBlocksProps> = (props) => {
const {
blockIds,
blockUpdateHandler,
handleScrollToBlock,
enableAddBlock,
showAllBlocks,
selectionHelpers,
ganttContainerRef,
} = props;
return (
<div className="absolute top-0 left-0 min-w-full w-max">
{blockIds?.map((blockId) => (
<>
<RenderIfVisible
root={ganttContainerRef}
horizontalOffset={100}
verticalOffset={200}
classNames="relative min-w-full w-max"
placeholderChildren={<div className="w-full pointer-events-none" style={{ height: `${BLOCK_HEIGHT}px` }} />}
shouldRecordHeights={false}
>
<BlockRow
key={blockId}
blockId={blockId}
showAllBlocks={showAllBlocks}
blockUpdateHandler={blockUpdateHandler}
handleScrollToBlock={handleScrollToBlock}
enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock}
selectionHelpers={selectionHelpers}
ganttContainerRef={ganttContainerRef}
/>
</RenderIfVisible>
</>
))}
</div>
);
};

View File

@@ -0,0 +1,53 @@
import type { FC } from "react";
//
import type { IBlockUpdateDependencyData } from "@plane/types";
import { GanttChartBlock } from "@/components/gantt-chart/blocks/block";
export type GanttChartBlocksProps = {
blockIds: string[];
blockToRender: (data: any) => React.ReactNode;
enableBlockLeftResize: boolean | ((blockId: string) => boolean);
enableBlockRightResize: boolean | ((blockId: string) => boolean);
enableBlockMove: boolean | ((blockId: string) => boolean);
ganttContainerRef: React.RefObject<HTMLDivElement>;
showAllBlocks: boolean;
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
enableDependency: boolean | ((blockId: string) => boolean);
};
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
const {
blockIds,
blockToRender,
enableBlockLeftResize,
enableBlockRightResize,
enableBlockMove,
ganttContainerRef,
showAllBlocks,
updateBlockDates,
enableDependency,
} = props;
return (
<>
{blockIds?.map((blockId) => (
<GanttChartBlock
key={blockId}
blockId={blockId}
showAllBlocks={showAllBlocks}
blockToRender={blockToRender}
enableBlockLeftResize={
typeof enableBlockLeftResize === "function" ? enableBlockLeftResize(blockId) : enableBlockLeftResize
}
enableBlockRightResize={
typeof enableBlockRightResize === "function" ? enableBlockRightResize(blockId) : enableBlockRightResize
}
enableBlockMove={typeof enableBlockMove === "function" ? enableBlockMove(blockId) : enableBlockMove}
enableDependency={typeof enableDependency === "function" ? enableDependency(blockId) : enableDependency}
ganttContainerRef={ganttContainerRef}
updateBlockDates={updateBlockDates}
/>
))}
</>
);
};

View File

@@ -0,0 +1,2 @@
export * from "./left-draggable";
export * from "./right-draggable";

View File

@@ -0,0 +1,9 @@
import type { RefObject } from "react";
import type { IGanttBlock } from "@plane/types";
type LeftDependencyDraggableProps = {
block: IGanttBlock;
ganttContainerRef: RefObject<HTMLDivElement>;
};
export const LeftDependencyDraggable = (props: LeftDependencyDraggableProps) => <></>;

View File

@@ -0,0 +1,8 @@
import type { RefObject } from "react";
import type { IGanttBlock } from "@plane/types";
type RightDependencyDraggableProps = {
block: IGanttBlock;
ganttContainerRef: RefObject<HTMLDivElement>;
};
export const RightDependencyDraggable = (props: RightDependencyDraggableProps) => <></>;

View File

@@ -0,0 +1,9 @@
import type { FC } from "react";
type Props = {
isEpic?: boolean;
};
export const TimelineDependencyPaths: FC<Props> = (props) => {
const { isEpic = false } = props;
return <></>;
};

View File

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

View File

@@ -0,0 +1,3 @@
export * from "./blockDraggables";
export * from "./dependency-paths";
export * from "./draggable-dependency-path";

View File

@@ -0,0 +1 @@
export * from "./dependency";

View File

@@ -0,0 +1,2 @@
export * from "./version-number";
export * from "./product-updates-header";

View File

@@ -0,0 +1,28 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { PlaneLogo } from "@plane/propel/icons";
// helpers
import { cn } from "@plane/utils";
// package.json
import packageJson from "package.json";
export const ProductUpdatesHeader = observer(() => {
const { t } = useTranslation();
return (
<div className="flex gap-2 mx-6 my-4 items-center justify-between flex-shrink-0">
<div className="flex w-full items-center">
<div className="flex gap-2 text-xl font-medium">{t("whats_new")}</div>
<div
className={cn(
"px-2 mx-2 py-0.5 text-center text-xs font-medium rounded-full bg-custom-primary-100/20 text-custom-primary-100"
)}
>
{t("version")}: v{packageJson.version}
</div>
</div>
<div className="flex flex-shrink-0 items-center gap-8">
<PlaneLogo className="h-6 w-auto text-custom-text-100" />
</div>
</div>
);
});

View File

@@ -0,0 +1,12 @@
// assets
import { useTranslation } from "@plane/i18n";
import packageJson from "package.json";
export const PlaneVersionNumber: React.FC = () => {
const { t } = useTranslation();
return (
<span>
{t("version")}: v{packageJson.version}
</span>
);
};

View File

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

View File

@@ -0,0 +1 @@
export * from "./peek-overviews";

View File

@@ -0,0 +1,9 @@
"use client";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
export const HomePeekOverviewsRoot = () => (
<>
<IssuePeekOverview />
</>
);

View File

@@ -0,0 +1,7 @@
import type { EInboxIssueSource } from "@plane/types";
export type TInboxSourcePill = {
source: EInboxIssueSource;
};
export const InboxSourcePill = (props: TInboxSourcePill) => <></>;

View File

@@ -0,0 +1 @@
export * from "./maintenance-message";

View File

@@ -0,0 +1,37 @@
export const MaintenanceMessage = () => {
const linkMap = [
{
key: "mail_to",
label: "Contact Support",
value: "mailto:support@plane.so",
},
];
return (
<>
<div className="flex flex-col gap-2.5">
<h1 className="text-xl font-semibold text-custom-text-100 text-left">
&#x1F6A7; Looks like Plane didn&apos;t start up correctly!
</h1>
<span className="text-base font-medium text-custom-text-200 text-left">
Some services might have failed to start. Please check your container logs to identify and resolve the issue.
If you&apos;re stuck, reach out to our support team for more help.
</span>
</div>
<div className="flex items-center justify-start gap-6 mt-1">
{linkMap.map((link) => (
<div key={link.key}>
<a
href={link.value}
target="_blank"
rel="noopener noreferrer"
className="text-custom-primary-100 hover:underline text-sm"
>
{link.label}
</a>
</div>
))}
</div>
</>
);
};

View File

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

View File

@@ -0,0 +1,21 @@
import { observer } from "mobx-react";
// components
import { BulkOperationsUpgradeBanner } from "@/components/issues/bulk-operations/upgrade-banner";
// hooks
import { useMultipleSelectStore } from "@/hooks/store/use-multiple-select-store";
import type { TSelectionHelper } from "@/hooks/use-multiple-select";
type Props = {
className?: string;
selectionHelpers: TSelectionHelper;
};
export const IssueBulkOperationsRoot: React.FC<Props> = observer((props) => {
const { className, selectionHelpers } = props;
// store hooks
const { isSelectionActive } = useMultipleSelectStore();
if (!isSelectionActive || selectionHelpers.isSelectionDisabled) return null;
return <BulkOperationsUpgradeBanner className={className} />;
});

View File

@@ -0,0 +1,11 @@
"use client";
import { observer } from "mobx-react";
type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};
export const AppliedIssueTypeFilters: React.FC<Props> = observer(() => null);

View File

@@ -0,0 +1,12 @@
"use client";
import type React from "react";
import { observer } from "mobx-react";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
searchQuery: string;
};
export const FilterIssueTypes: React.FC<Props> = observer(() => null);

View File

@@ -0,0 +1,12 @@
"use client";
import type React from "react";
import { observer } from "mobx-react";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
searchQuery: string;
};
export const FilterTeamProjects: React.FC<Props> = observer(() => null);

View File

@@ -0,0 +1,125 @@
"use client";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { Circle, ExternalLink } from "lucide-react";
// plane imports
import {
EUserPermissions,
EUserPermissionsLevel,
SPACE_BASE_PATH,
SPACE_BASE_URL,
WORK_ITEM_TRACKER_ELEMENTS,
EProjectFeatureKey,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { Tooltip } from "@plane/propel/tooltip";
import { EIssuesStoreType } from "@plane/types";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { CountChip } from "@/components/common/count-chip";
// constants
import { HeaderFilters } from "@/components/issues/filters";
// helpers
// hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useIssues } from "@/hooks/store/use-issues";
import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web
import { CommonProjectBreadcrumbs } from "../breadcrumbs/common";
export const IssuesHeader = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
const {
issues: { getGroupIssueCount },
} = useIssues(EIssuesStoreType.PROJECT);
// i18n
const { t } = useTranslation();
const { currentProjectDetails, loader } = useProject();
const { toggleCreateIssueModal } = useCommandPalette();
const { allowPermissions } = useUserPermissions();
const { isMobile } = usePlatformOS();
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 = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
return (
<Header>
<Header.LeftItem>
<div className="flex items-center gap-2.5">
<Breadcrumbs onBack={() => router.back()} isLoading={loader === "init-loader"} className="flex-grow-0">
<CommonProjectBreadcrumbs
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
featureKey={EProjectFeatureKey.WORK_ITEMS}
isLast
/>
</Breadcrumbs>
{issuesCount && issuesCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "work items" : "work item"} in this project`}
position="bottom"
>
<CountChip count={issuesCount} />
</Tooltip>
) : null}
</div>
{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"
target="_blank"
rel="noopener noreferrer"
>
<Circle className="h-1.5 w-1.5 fill-custom-primary-100" strokeWidth={2} />
{t("workspace_projects.network.public.title")}
<ExternalLink className="hidden h-3 w-3 group-hover:block" strokeWidth={2} />
</a>
) : (
<></>
)}
</Header.LeftItem>
<Header.RightItem>
<div className="hidden gap-3 md:flex">
<HeaderFilters
projectId={projectId}
currentProjectDetails={currentProjectDetails}
workspaceSlug={workspaceSlug}
canUserCreateIssue={canUserCreateIssue}
/>
</div>
{canUserCreateIssue ? (
<Button
onClick={() => {
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
data-ph-element={WORK_ITEM_TRACKER_ELEMENTS.HEADER_ADD_BUTTON.WORK_ITEMS}
size="sm"
>
<div className="block sm:hidden">{t("issue.label", { count: 1 })}</div>
<div className="hidden sm:block">{t("issue.add.label")}</div>
</Button>
) : (
<></>
)}
</Header.RightItem>
</Header>
);
});

View File

@@ -0,0 +1,14 @@
import type { FC } from "react";
// plane types
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
export type TWorkItemAdditionalWidgetActionButtonsProps = {
disabled: boolean;
hideWidgets: TWorkItemWidgets[];
issueServiceType: TIssueServiceType;
projectId: string;
workItemId: string;
workspaceSlug: string;
};
export const WorkItemAdditionalWidgetActionButtons: FC<TWorkItemAdditionalWidgetActionButtonsProps> = () => null;

View File

@@ -0,0 +1,14 @@
import type { FC } from "react";
// plane types
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
export type TWorkItemAdditionalWidgetCollapsiblesProps = {
disabled: boolean;
hideWidgets: TWorkItemWidgets[];
issueServiceType: TIssueServiceType;
projectId: string;
workItemId: string;
workspaceSlug: string;
};
export const WorkItemAdditionalWidgetCollapsibles: FC<TWorkItemAdditionalWidgetCollapsiblesProps> = () => null;

View File

@@ -0,0 +1,13 @@
import type { FC } from "react";
// plane types
import type { TIssueServiceType, TWorkItemWidgets } from "@plane/types";
export type TWorkItemAdditionalWidgetModalsProps = {
hideWidgets: TWorkItemWidgets[];
issueServiceType: TIssueServiceType;
projectId: string;
workItemId: string;
workspaceSlug: string;
};
export const WorkItemAdditionalWidgetModals: FC<TWorkItemAdditionalWidgetModalsProps> = () => null;

View File

@@ -0,0 +1,13 @@
"use client";
import type { FC } from "react";
import { observer } from "mobx-react";
export type TAdditionalActivityRoot = {
activityId: string;
showIssue?: boolean;
ends: "top" | "bottom" | undefined;
field: string | undefined;
};
export const AdditionalActivityRoot: FC<TAdditionalActivityRoot> = observer(() => <></>);

View File

@@ -0,0 +1,14 @@
import type { FC } from "react";
import React from "react";
// plane imports
export type TWorkItemAdditionalSidebarProperties = {
workItemId: string;
workItemTypeId: string | null;
projectId: string;
workspaceSlug: string;
isEditable: boolean;
isPeekView?: boolean;
};
export const WorkItemAdditionalSidebarProperties: FC<TWorkItemAdditionalSidebarProperties> = () => <></>;

View File

@@ -0,0 +1,7 @@
export * from "./issue-identifier";
export * from "./issue-properties-activity";
export * from "./issue-type-switcher";
export * from "./issue-type-activity";
export * from "./parent-select-root";
export * from "./issue-creator";
export * from "./additional-activity-root";

View File

@@ -0,0 +1,36 @@
import type { FC } from "react";
import Link from "next/link";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
type TIssueUser = {
activityId: string;
customUserName?: string;
};
export const IssueCreatorDisplay: FC<TIssueUser> = (props) => {
const { activityId, customUserName } = props;
// hooks
const {
activity: { getActivityById },
} = useIssueDetail();
const activity = getActivityById(activityId);
if (!activity) return <></>;
return (
<>
{customUserName ? (
<span className="text-custom-text-100 font-medium">{customUserName || "Plane"}</span>
) : (
<Link
href={`/${activity?.workspace_detail?.slug}/profile/${activity?.actor_detail?.id}`}
className="hover:underline text-custom-text-100 font-medium"
>
{activity.actor_detail?.display_name}
</Link>
)}
</>
);
};

View File

@@ -0,0 +1,105 @@
import type { FC } from "react";
import { observer } from "mobx-react";
// types
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import type { IIssueDisplayProperties } from "@plane/types";
// ui
// helpers
import { cn } from "@plane/utils";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useProject } from "@/hooks/store/use-project";
type TIssueIdentifierBaseProps = {
projectId: string;
size?: "xs" | "sm" | "md" | "lg";
textContainerClassName?: string;
displayProperties?: IIssueDisplayProperties | undefined;
enableClickToCopyIdentifier?: boolean;
};
type TIssueIdentifierFromStore = TIssueIdentifierBaseProps & {
issueId: string;
};
type TIssueIdentifierWithDetails = TIssueIdentifierBaseProps & {
issueTypeId?: string | null;
projectIdentifier: string;
issueSequenceId: string | number;
};
export type TIssueIdentifierProps = TIssueIdentifierFromStore | TIssueIdentifierWithDetails;
type TIssueTypeIdentifier = {
issueTypeId: string;
size?: "xs" | "sm" | "md" | "lg";
};
export const IssueTypeIdentifier: FC<TIssueTypeIdentifier> = observer((props) => <></>);
type TIdentifierTextProps = {
identifier: string;
enableClickToCopyIdentifier?: boolean;
textContainerClassName?: string;
};
export const IdentifierText: React.FC<TIdentifierTextProps> = (props) => {
const { identifier, enableClickToCopyIdentifier = false, textContainerClassName } = props;
// handlers
const handleCopyIssueIdentifier = () => {
if (enableClickToCopyIdentifier) {
navigator.clipboard.writeText(identifier).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Work item ID copied to clipboard",
});
});
}
};
return (
<Tooltip tooltipContent="Click to copy" disabled={!enableClickToCopyIdentifier} position="top">
<span
className={cn(
"text-base font-medium text-custom-text-300",
{
"cursor-pointer": enableClickToCopyIdentifier,
},
textContainerClassName
)}
onClick={handleCopyIssueIdentifier}
>
{identifier}
</span>
</Tooltip>
);
};
export const IssueIdentifier: React.FC<TIssueIdentifierProps> = observer((props) => {
const { projectId, textContainerClassName, displayProperties, enableClickToCopyIdentifier = false } = props;
// store hooks
const { getProjectIdentifierById } = useProject();
const {
issue: { getIssueById },
} = useIssueDetail();
// Determine if the component is using store data or not
const isUsingStoreData = "issueId" in props;
// derived values
const issue = isUsingStoreData ? getIssueById(props.issueId) : null;
const projectIdentifier = isUsingStoreData ? getProjectIdentifierById(projectId) : props.projectIdentifier;
const issueSequenceId = isUsingStoreData ? issue?.sequence_id : props.issueSequenceId;
const shouldRenderIssueID = displayProperties ? displayProperties.key : true;
if (!shouldRenderIssueID) return null;
return (
<div className="flex items-center space-x-2">
<IdentifierText
identifier={`${projectIdentifier}-${issueSequenceId}`}
enableClickToCopyIdentifier={enableClickToCopyIdentifier}
textContainerClassName={textContainerClassName}
/>
</div>
);
});

View File

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

View File

@@ -0,0 +1,8 @@
import type { FC } from "react";
type TIssueAdditionalPropertiesActivity = {
activityId: string;
ends: "top" | "bottom" | undefined;
};
export const IssueAdditionalPropertiesActivity: FC<TIssueAdditionalPropertiesActivity> = () => <></>;

View File

@@ -0,0 +1,8 @@
"use client";
import type { FC } from "react";
import { observer } from "mobx-react";
export type TIssueTypeActivity = { activityId: string; showIssue?: boolean; ends: "top" | "bottom" | undefined };
export const IssueTypeActivity: FC<TIssueTypeActivity> = observer(() => <></>);

View File

@@ -0,0 +1,24 @@
import { observer } from "mobx-react";
// store hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
// plane web components
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
export type TIssueTypeSwitcherProps = {
issueId: string;
disabled: boolean;
};
export const IssueTypeSwitcher: React.FC<TIssueTypeSwitcherProps> = observer((props) => {
const { issueId } = props;
// store hooks
const {
issue: { getIssueById },
} = useIssueDetail();
// derived values
const issue = getIssueById(issueId);
if (!issue || !issue.project_id) return <></>;
return <IssueIdentifier issueId={issueId} projectId={issue.project_id} size="md" enableClickToCopyIdentifier />;
});

View File

@@ -0,0 +1,83 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
// components
import type { TIssueOperations } from "@/components/issues/issue-detail";
import { IssueParentSelect } from "@/components/issues/issue-detail/parent-select";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
type TIssueParentSelect = {
className?: string;
disabled?: boolean;
issueId: string;
issueOperations: TIssueOperations;
projectId: string;
workspaceSlug: string;
};
export const IssueParentSelectRoot: React.FC<TIssueParentSelect> = observer((props) => {
const { issueId, issueOperations, projectId, workspaceSlug } = props;
const { t } = useTranslation();
// store hooks
const {
issue: { getIssueById },
} = useIssueDetail();
const {
toggleParentIssueModal,
removeSubIssue,
subIssues: { setSubIssueHelpers, fetchSubIssues },
} = useIssueDetail();
// derived values
const issue = getIssueById(issueId);
const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined;
const handleParentIssue = async (_issueId: string | null = null) => {
try {
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
await issueOperations.fetch(workspaceSlug, projectId, issueId, false);
if (_issueId) await fetchSubIssues(workspaceSlug, projectId, _issueId);
toggleParentIssueModal(null);
} catch (error) {
console.error("something went wrong while fetching the issue");
}
};
const handleRemoveSubIssue = async (
workspaceSlug: string,
projectId: string,
parentIssueId: string,
issueId: string
) => {
try {
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
await fetchSubIssues(workspaceSlug, projectId, parentIssueId);
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
} catch (error) {
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.error.label"),
message: t("common.something_went_wrong"),
});
}
};
const workItemLink = `/${workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue?.id}`;
if (!issue) return <></>;
return (
<IssueParentSelect
{...props}
handleParentIssue={handleParentIssue}
handleRemoveSubIssue={handleRemoveSubIssue}
workItemLink={workItemLink}
/>
);
});

View File

@@ -0,0 +1,10 @@
import type { FC } from "react";
import React from "react";
import type { IIssueDisplayProperties, TIssue } from "@plane/types";
export type TWorkItemLayoutAdditionalProperties = {
displayProperties: IIssueDisplayProperties;
issue: TIssue;
};
export const WorkItemLayoutAdditionalProperties: FC<TWorkItemLayoutAdditionalProperties> = (props) => <></>;

View File

@@ -0,0 +1,2 @@
export * from "./team-issues";
export * from "./team-view-issues";

View File

@@ -0,0 +1,3 @@
import { observer } from "mobx-react";
export const TeamEmptyState: React.FC = observer(() => <></>);

View File

@@ -0,0 +1,3 @@
import { observer } from "mobx-react";
export const TeamProjectWorkItemEmptyState: React.FC = observer(() => <></>);

View File

@@ -0,0 +1,3 @@
import { observer } from "mobx-react";
export const TeamViewEmptyState: React.FC = observer(() => <></>);

View File

@@ -0,0 +1,14 @@
"use client";
import type { FC } from "react";
import React from "react";
type Props = {
issueId: string;
className?: string;
size?: number;
showProgressText?: boolean;
showLabel?: boolean;
};
export const IssueStats: FC<Props> = (props) => <></>;

View File

@@ -0,0 +1,22 @@
import type { Copy } from "lucide-react";
import type { TContextMenuItem } from "@plane/ui";
export interface CopyMenuHelperProps {
baseItem: {
key: string;
title: string;
icon: typeof Copy;
action: () => void;
shouldRender: boolean;
};
activeLayout: string;
setCreateUpdateIssueModal: (open: boolean) => void;
setDuplicateWorkItemModal?: (open: boolean) => void;
workspaceSlug?: string;
}
export const createCopyMenuWithDuplication = (props: CopyMenuHelperProps): TContextMenuItem => {
const { baseItem } = props;
return baseItem;
};

View File

@@ -0,0 +1,11 @@
import type { FC } from "react";
type TDuplicateWorkItemModalProps = {
workItemId: string;
onClose: () => void;
isOpen: boolean;
workspaceSlug: string;
projectId: string;
};
export const DuplicateWorkItemModal: FC<TDuplicateWorkItemModalProps> = () => <></>;

View File

@@ -0,0 +1,2 @@
export * from "./duplicate-modal";
export * from "./copy-menu-helper";

Some files were not shown because too many files have changed in this diff Show More