feat: init
This commit is contained in:
1
apps/web/ce/components/active-cycles/index.ts
Normal file
1
apps/web/ce/components/active-cycles/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
4
apps/web/ce/components/active-cycles/root.tsx
Normal file
4
apps/web/ce/components/active-cycles/root.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
// local imports
|
||||
import { WorkspaceActiveCyclesUpgrade } from "./workspace-active-cycles-upgrade";
|
||||
|
||||
export const WorkspaceActiveCyclesRoot = () => <WorkspaceActiveCyclesUpgrade />;
|
||||
@@ -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 cycle’s 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 doesn’t 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 aren’t 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>
|
||||
);
|
||||
});
|
||||
8
apps/web/ce/components/analytics/tabs.tsx
Normal file
8
apps/web/ce/components/analytics/tabs.tsx
Normal 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 },
|
||||
];
|
||||
1
apps/web/ce/components/app-rail/index.ts
Normal file
1
apps/web/ce/components/app-rail/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
4
apps/web/ce/components/app-rail/root.tsx
Normal file
4
apps/web/ce/components/app-rail/root.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
|
||||
export const AppRailRoot = () => <></>;
|
||||
11
apps/web/ce/components/automations/root.tsx
Normal file
11
apps/web/ce/components/automations/root.tsx
Normal 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> = () => <></>;
|
||||
32
apps/web/ce/components/breadcrumbs/common.tsx
Normal file
32
apps/web/ce/components/breadcrumbs/common.tsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
70
apps/web/ce/components/breadcrumbs/project-feature.tsx
Normal file
70
apps/web/ce/components/breadcrumbs/project-feature.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
83
apps/web/ce/components/breadcrumbs/project.tsx
Normal file
83
apps/web/ce/components/breadcrumbs/project.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
1
apps/web/ce/components/command-palette/actions/index.ts
Normal file
1
apps/web/ce/components/command-palette/actions/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./work-item-actions";
|
||||
@@ -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 />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./change-state-list";
|
||||
112
apps/web/ce/components/command-palette/helpers.tsx
Normal file
112
apps/web/ce/components/command-palette/helpers.tsx
Normal 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",
|
||||
},
|
||||
};
|
||||
3
apps/web/ce/components/command-palette/index.ts
Normal file
3
apps/web/ce/components/command-palette/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./actions";
|
||||
export * from "./modals";
|
||||
export * from "./helpers";
|
||||
3
apps/web/ce/components/command-palette/modals/index.ts
Normal file
3
apps/web/ce/components/command-palette/modals/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./workspace-level";
|
||||
export * from "./project-level";
|
||||
export * from "./issue-level";
|
||||
102
apps/web/ce/components/command-palette/modals/issue-level.tsx
Normal file
102
apps/web/ce/components/command-palette/modals/issue-level.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
82
apps/web/ce/components/comments/comment-block.tsx
Normal file
82
apps/web/ce/components/comments/comment-block.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
1
apps/web/ce/components/comments/index.ts
Normal file
1
apps/web/ce/components/comments/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./comment-block";
|
||||
17
apps/web/ce/components/common/extended-app-header.tsx
Normal file
17
apps/web/ce/components/common/extended-app-header.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
|
||||
type TProps = {
|
||||
workspace?: IWorkspace;
|
||||
};
|
||||
|
||||
export const SubscriptionPill = (props: TProps) => <></>;
|
||||
1
apps/web/ce/components/cycles/active-cycle/index.ts
Normal file
1
apps/web/ce/components/cycles/active-cycle/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
109
apps/web/ce/components/cycles/active-cycle/root.tsx
Normal file
109
apps/web/ce/components/cycles/active-cycle/root.tsx
Normal 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}</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
7
apps/web/ce/components/cycles/additional-actions.tsx
Normal file
7
apps/web/ce/components/cycles/additional-actions.tsx
Normal 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(() => <></>);
|
||||
86
apps/web/ce/components/cycles/analytics-sidebar/base.tsx
Normal file
86
apps/web/ce/components/cycles/analytics-sidebar/base.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
1
apps/web/ce/components/cycles/analytics-sidebar/index.ts
Normal file
1
apps/web/ce/components/cycles/analytics-sidebar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
13
apps/web/ce/components/cycles/analytics-sidebar/root.tsx
Normal file
13
apps/web/ce/components/cycles/analytics-sidebar/root.tsx
Normal 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} />;
|
||||
2
apps/web/ce/components/cycles/end-cycle/index.ts
Normal file
2
apps/web/ce/components/cycles/end-cycle/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./modal";
|
||||
export * from "./use-end-cycle";
|
||||
13
apps/web/ce/components/cycles/end-cycle/modal.tsx
Normal file
13
apps/web/ce/components/cycles/end-cycle/modal.tsx
Normal 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> = () => <></>;
|
||||
@@ -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,
|
||||
});
|
||||
4
apps/web/ce/components/cycles/index.ts
Normal file
4
apps/web/ce/components/cycles/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./active-cycle";
|
||||
export * from "./analytics-sidebar";
|
||||
export * from "./additional-actions";
|
||||
export * from "./end-cycle";
|
||||
16
apps/web/ce/components/de-dupe/de-dupe-button.tsx
Normal file
16
apps/web/ce/components/de-dupe/de-dupe-button.tsx
Normal 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 <></>;
|
||||
};
|
||||
1
apps/web/ce/components/de-dupe/duplicate-modal/index.ts
Normal file
1
apps/web/ce/components/de-dupe/duplicate-modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
16
apps/web/ce/components/de-dupe/duplicate-modal/root.tsx
Normal file
16
apps/web/ce/components/de-dupe/duplicate-modal/root.tsx
Normal 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 <></>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
24
apps/web/ce/components/de-dupe/duplicate-popover/root.tsx
Normal file
24
apps/web/ce/components/de-dupe/duplicate-popover/root.tsx
Normal 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 <></>;
|
||||
});
|
||||
13
apps/web/ce/components/de-dupe/issue-block/button-label.tsx
Normal file
13
apps/web/ce/components/de-dupe/issue-block/button-label.tsx
Normal 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 <></>;
|
||||
};
|
||||
1
apps/web/ce/components/editor/embeds/index.ts
Normal file
1
apps/web/ce/components/editor/embeds/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./mentions";
|
||||
1
apps/web/ce/components/editor/embeds/mentions/index.ts
Normal file
1
apps/web/ce/components/editor/embeds/mentions/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
4
apps/web/ce/components/editor/embeds/mentions/root.tsx
Normal file
4
apps/web/ce/components/editor/embeds/mentions/root.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
// plane editor
|
||||
import type { TMentionComponentProps } from "@plane/editor";
|
||||
|
||||
export const EditorAdditionalMentionsRoot: React.FC<TMentionComponentProps> = () => null;
|
||||
1
apps/web/ce/components/editor/index.ts
Normal file
1
apps/web/ce/components/editor/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./embeds";
|
||||
1
apps/web/ce/components/epics/epic-modal/index.ts
Normal file
1
apps/web/ce/components/epics/epic-modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./modal";
|
||||
20
apps/web/ce/components/epics/epic-modal/modal.tsx
Normal file
20
apps/web/ce/components/epics/epic-modal/modal.tsx
Normal 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) => <></>;
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
13
apps/web/ce/components/estimates/helper.tsx
Normal file
13
apps/web/ce/components/estimates/helper.tsx
Normal 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;
|
||||
}
|
||||
};
|
||||
4
apps/web/ce/components/estimates/index.ts
Normal file
4
apps/web/ce/components/estimates/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./estimate-list-item-buttons";
|
||||
export * from "./update";
|
||||
export * from "./points";
|
||||
export * from "./helper";
|
||||
1
apps/web/ce/components/estimates/inputs/index.ts
Normal file
1
apps/web/ce/components/estimates/inputs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./time-input";
|
||||
8
apps/web/ce/components/estimates/inputs/time-input.tsx
Normal file
8
apps/web/ce/components/estimates/inputs/time-input.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
export type TEstimateTimeInputProps = {
|
||||
value?: number;
|
||||
handleEstimateInputValue: (value: string) => void;
|
||||
};
|
||||
|
||||
export const EstimateTimeInput: FC<TEstimateTimeInputProps> = () => <></>;
|
||||
19
apps/web/ce/components/estimates/points/delete.tsx
Normal file
19
apps/web/ce/components/estimates/points/delete.tsx
Normal 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> = () => <></>;
|
||||
1
apps/web/ce/components/estimates/points/index.ts
Normal file
1
apps/web/ce/components/estimates/points/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./delete";
|
||||
1
apps/web/ce/components/estimates/update/index.ts
Normal file
1
apps/web/ce/components/estimates/update/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./modal";
|
||||
14
apps/web/ce/components/estimates/update/modal.tsx
Normal file
14
apps/web/ce/components/estimates/update/modal.tsx
Normal 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(() => <></>);
|
||||
59
apps/web/ce/components/gantt-chart/blocks/block-row-list.tsx
Normal file
59
apps/web/ce/components/gantt-chart/blocks/block-row-list.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
53
apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx
Normal file
53
apps/web/ce/components/gantt-chart/blocks/blocks-list.tsx
Normal 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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./left-draggable";
|
||||
export * from "./right-draggable";
|
||||
@@ -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) => <></>;
|
||||
@@ -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) => <></>;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
isEpic?: boolean;
|
||||
};
|
||||
export const TimelineDependencyPaths: FC<Props> = (props) => {
|
||||
const { isEpic = false } = props;
|
||||
return <></>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const TimelineDraggablePath = () => <></>;
|
||||
3
apps/web/ce/components/gantt-chart/dependency/index.ts
Normal file
3
apps/web/ce/components/gantt-chart/dependency/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./blockDraggables";
|
||||
export * from "./dependency-paths";
|
||||
export * from "./draggable-dependency-path";
|
||||
1
apps/web/ce/components/gantt-chart/index.ts
Normal file
1
apps/web/ce/components/gantt-chart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./dependency";
|
||||
2
apps/web/ce/components/global/index.ts
Normal file
2
apps/web/ce/components/global/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./version-number";
|
||||
export * from "./product-updates-header";
|
||||
28
apps/web/ce/components/global/product-updates-header.tsx
Normal file
28
apps/web/ce/components/global/product-updates-header.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
12
apps/web/ce/components/global/version-number.tsx
Normal file
12
apps/web/ce/components/global/version-number.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
apps/web/ce/components/home/header.tsx
Normal file
1
apps/web/ce/components/home/header.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export const HomePageHeader = () => <></>;
|
||||
1
apps/web/ce/components/home/index.ts
Normal file
1
apps/web/ce/components/home/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./peek-overviews";
|
||||
9
apps/web/ce/components/home/peek-overviews.tsx
Normal file
9
apps/web/ce/components/home/peek-overviews.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
|
||||
export const HomePeekOverviewsRoot = () => (
|
||||
<>
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
);
|
||||
7
apps/web/ce/components/inbox/source-pill.tsx
Normal file
7
apps/web/ce/components/inbox/source-pill.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { EInboxIssueSource } from "@plane/types";
|
||||
|
||||
export type TInboxSourcePill = {
|
||||
source: EInboxIssueSource;
|
||||
};
|
||||
|
||||
export const InboxSourcePill = (props: TInboxSourcePill) => <></>;
|
||||
1
apps/web/ce/components/instance/index.ts
Normal file
1
apps/web/ce/components/instance/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./maintenance-message";
|
||||
37
apps/web/ce/components/instance/maintenance-message.tsx
Normal file
37
apps/web/ce/components/instance/maintenance-message.tsx
Normal 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">
|
||||
🚧 Looks like Plane didn'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'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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
apps/web/ce/components/issues/bulk-operations/index.ts
Normal file
1
apps/web/ce/components/issues/bulk-operations/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
21
apps/web/ce/components/issues/bulk-operations/root.tsx
Normal file
21
apps/web/ce/components/issues/bulk-operations/root.tsx
Normal 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} />;
|
||||
});
|
||||
@@ -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);
|
||||
12
apps/web/ce/components/issues/filters/issue-types.tsx
Normal file
12
apps/web/ce/components/issues/filters/issue-types.tsx
Normal 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);
|
||||
12
apps/web/ce/components/issues/filters/team-project.tsx
Normal file
12
apps/web/ce/components/issues/filters/team-project.tsx
Normal 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);
|
||||
125
apps/web/ce/components/issues/header.tsx
Normal file
125
apps/web/ce/components/issues/header.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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(() => <></>);
|
||||
@@ -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> = () => <></>;
|
||||
7
apps/web/ce/components/issues/issue-details/index.ts
Normal file
7
apps/web/ce/components/issues/issue-details/index.ts
Normal 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";
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
105
apps/web/ce/components/issues/issue-details/issue-identifier.tsx
Normal file
105
apps/web/ce/components/issues/issue-details/issue-identifier.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
type TIssueAdditionalPropertiesActivity = {
|
||||
activityId: string;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
};
|
||||
|
||||
export const IssueAdditionalPropertiesActivity: FC<TIssueAdditionalPropertiesActivity> = () => <></>;
|
||||
@@ -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(() => <></>);
|
||||
@@ -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 />;
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -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) => <></>;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./team-issues";
|
||||
export * from "./team-view-issues";
|
||||
@@ -0,0 +1,3 @@
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const TeamEmptyState: React.FC = observer(() => <></>);
|
||||
@@ -0,0 +1,3 @@
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const TeamProjectWorkItemEmptyState: React.FC = observer(() => <></>);
|
||||
@@ -0,0 +1,3 @@
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export const TeamViewEmptyState: React.FC = observer(() => <></>);
|
||||
14
apps/web/ce/components/issues/issue-layouts/issue-stats.tsx
Normal file
14
apps/web/ce/components/issues/issue-layouts/issue-stats.tsx
Normal 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) => <></>;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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> = () => <></>;
|
||||
@@ -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
Reference in New Issue
Block a user