Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
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,9 @@
|
||||
import type { TIssue } from "@plane/types";
|
||||
|
||||
export type TDateAlertProps = {
|
||||
date: string;
|
||||
workItem: TIssue;
|
||||
projectId: string;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const DateAlert = (props: TDateAlertProps) => <></>;
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { TIssue } from "@plane/types";
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const TransferHopInfo = ({ workItem }: { workItem: TIssue }) => <></>;
|
||||
@@ -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";
|
||||
115
apps/web/ce/components/issues/issue-layouts/utils.tsx
Normal file
115
apps/web/ce/components/issues/issue-layouts/utils.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { FC } from "react";
|
||||
import { CalendarDays, LayersIcon, Link2, Paperclip } from "lucide-react";
|
||||
// types
|
||||
import { ISSUE_GROUP_BY_OPTIONS } from "@plane/constants";
|
||||
import type { ISvgIcons } from "@plane/propel/icons";
|
||||
import {
|
||||
CycleIcon,
|
||||
StatePropertyIcon,
|
||||
ModuleIcon,
|
||||
MembersPropertyIcon,
|
||||
DueDatePropertyIcon,
|
||||
EstimatePropertyIcon,
|
||||
LabelPropertyIcon,
|
||||
PriorityPropertyIcon,
|
||||
StartDatePropertyIcon,
|
||||
} from "@plane/propel/icons";
|
||||
import type {
|
||||
IGroupByColumn,
|
||||
IIssueDisplayProperties,
|
||||
TGetColumns,
|
||||
TIssueGroupByOptions,
|
||||
TSpreadsheetColumn,
|
||||
} from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
SpreadsheetAssigneeColumn,
|
||||
SpreadsheetAttachmentColumn,
|
||||
SpreadsheetCreatedOnColumn,
|
||||
SpreadsheetDueDateColumn,
|
||||
SpreadsheetEstimateColumn,
|
||||
SpreadsheetLabelColumn,
|
||||
SpreadsheetModuleColumn,
|
||||
SpreadsheetCycleColumn,
|
||||
SpreadsheetLinkColumn,
|
||||
SpreadsheetPriorityColumn,
|
||||
SpreadsheetStartDateColumn,
|
||||
SpreadsheetStateColumn,
|
||||
SpreadsheetSubIssueColumn,
|
||||
SpreadsheetUpdatedOnColumn,
|
||||
} from "@/components/issues/issue-layouts/spreadsheet/columns";
|
||||
// store
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export type TGetScopeMemberIdsResult = {
|
||||
memberIds: string[];
|
||||
includeNone: boolean;
|
||||
};
|
||||
|
||||
export const getScopeMemberIds = ({ isWorkspaceLevel, projectId }: TGetColumns): TGetScopeMemberIdsResult => {
|
||||
// store values
|
||||
const { workspaceMemberIds } = store.memberRoot.workspace;
|
||||
const { projectMemberIds } = store.memberRoot.project;
|
||||
// derived values
|
||||
const memberIds = workspaceMemberIds;
|
||||
|
||||
if (isWorkspaceLevel) {
|
||||
return { memberIds: memberIds ?? [], includeNone: true };
|
||||
}
|
||||
|
||||
if (projectId || (projectMemberIds && projectMemberIds.length > 0)) {
|
||||
const { getProjectMemberIds } = store.memberRoot.project;
|
||||
const _projectMemberIds = projectId ? getProjectMemberIds(projectId, false) : projectMemberIds;
|
||||
return {
|
||||
memberIds: _projectMemberIds ?? [],
|
||||
includeNone: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { memberIds: [], includeNone: true };
|
||||
};
|
||||
|
||||
export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined;
|
||||
|
||||
export const SpreadSheetPropertyIconMap: Record<string, FC<ISvgIcons>> = {
|
||||
MembersPropertyIcon: MembersPropertyIcon,
|
||||
CalenderDays: CalendarDays,
|
||||
DueDatePropertyIcon: DueDatePropertyIcon,
|
||||
EstimatePropertyIcon: EstimatePropertyIcon,
|
||||
LabelPropertyIcon: LabelPropertyIcon,
|
||||
ModuleIcon: ModuleIcon,
|
||||
ContrastIcon: CycleIcon,
|
||||
PriorityPropertyIcon: PriorityPropertyIcon,
|
||||
StartDatePropertyIcon: StartDatePropertyIcon,
|
||||
StatePropertyIcon: StatePropertyIcon,
|
||||
Link2: Link2,
|
||||
Paperclip: Paperclip,
|
||||
LayersIcon: LayersIcon,
|
||||
};
|
||||
|
||||
export const SPREADSHEET_COLUMNS: { [key in keyof IIssueDisplayProperties]: TSpreadsheetColumn } = {
|
||||
assignee: SpreadsheetAssigneeColumn,
|
||||
created_on: SpreadsheetCreatedOnColumn,
|
||||
due_date: SpreadsheetDueDateColumn,
|
||||
estimate: SpreadsheetEstimateColumn,
|
||||
labels: SpreadsheetLabelColumn,
|
||||
modules: SpreadsheetModuleColumn,
|
||||
cycle: SpreadsheetCycleColumn,
|
||||
link: SpreadsheetLinkColumn,
|
||||
priority: SpreadsheetPriorityColumn,
|
||||
start_date: SpreadsheetStartDateColumn,
|
||||
state: SpreadsheetStateColumn,
|
||||
sub_issue_count: SpreadsheetSubIssueColumn,
|
||||
updated_on: SpreadsheetUpdatedOnColumn,
|
||||
attachment_count: SpreadsheetAttachmentColumn,
|
||||
};
|
||||
|
||||
export const useGroupByOptions = (
|
||||
options: TIssueGroupByOptions[]
|
||||
): {
|
||||
key: TIssueGroupByOptions;
|
||||
titleTranslationKey: string;
|
||||
}[] => {
|
||||
const groupByOptions = ISSUE_GROUP_BY_OPTIONS.filter((option) => options.includes(option.key));
|
||||
return groupByOptions;
|
||||
};
|
||||
3
apps/web/ce/components/issues/issue-modal/index.ts
Normal file
3
apps/web/ce/components/issues/issue-modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./provider";
|
||||
export * from "./issue-type-select";
|
||||
export * from "./template-select";
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Control } from "react-hook-form";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
// types
|
||||
import type { TBulkIssueProperties, TIssue } from "@plane/types";
|
||||
|
||||
export type TIssueFields = TIssue & TBulkIssueProperties;
|
||||
|
||||
export type TIssueTypeDropdownVariant = "xs" | "sm";
|
||||
|
||||
export type TIssueTypeSelectProps<T extends Partial<TIssueFields>> = {
|
||||
control: Control<T>;
|
||||
projectId: string | null;
|
||||
editorRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
disabled?: boolean;
|
||||
variant?: TIssueTypeDropdownVariant;
|
||||
placeholder?: string;
|
||||
isRequired?: boolean;
|
||||
renderChevron?: boolean;
|
||||
dropDownContainerClassName?: string;
|
||||
showMandatoryFieldInfo?: boolean; // Show info about mandatory fields
|
||||
handleFormChange?: () => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const IssueTypeSelect = <T extends Partial<TIssueFields>>(props: TIssueTypeSelectProps<T>) => <></>;
|
||||
@@ -0,0 +1,10 @@
|
||||
import type React from "react";
|
||||
|
||||
export type TWorkItemModalAdditionalPropertiesProps = {
|
||||
isDraft?: boolean;
|
||||
projectId: string | null;
|
||||
workItemId: string | undefined;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const WorkItemModalAdditionalProperties: React.FC<TWorkItemModalAdditionalPropertiesProps> = () => null;
|
||||
53
apps/web/ce/components/issues/issue-modal/provider.tsx
Normal file
53
apps/web/ce/components/issues/issue-modal/provider.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import type { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
// components
|
||||
import { IssueModalContext } from "@/components/issues/issue-modal/context";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store/user/user-user";
|
||||
|
||||
export type TIssueModalProviderProps = {
|
||||
templateId?: string;
|
||||
dataForPreload?: Partial<TIssue>;
|
||||
allowedProjectIds?: string[];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const IssueModalProvider = observer((props: TIssueModalProviderProps) => {
|
||||
const { children, allowedProjectIds } = props;
|
||||
// states
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||
// store hooks
|
||||
const { projectsWithCreatePermissions } = useUser();
|
||||
// derived values
|
||||
const projectIdsWithCreatePermissions = Object.keys(projectsWithCreatePermissions ?? {});
|
||||
|
||||
return (
|
||||
<IssueModalContext.Provider
|
||||
value={{
|
||||
allowedProjectIds: allowedProjectIds ?? projectIdsWithCreatePermissions,
|
||||
workItemTemplateId: null,
|
||||
setWorkItemTemplateId: () => {},
|
||||
isApplyingTemplate: false,
|
||||
setIsApplyingTemplate: () => {},
|
||||
selectedParentIssue,
|
||||
setSelectedParentIssue,
|
||||
issuePropertyValues: {},
|
||||
setIssuePropertyValues: () => {},
|
||||
issuePropertyValueErrors: {},
|
||||
setIssuePropertyValueErrors: () => {},
|
||||
getIssueTypeIdOnProjectChange: () => null,
|
||||
getActiveAdditionalPropertiesLength: () => 0,
|
||||
handlePropertyValuesValidation: () => true,
|
||||
handleCreateUpdatePropertyValues: () => Promise.resolve(),
|
||||
handleProjectEntitiesFetch: () => Promise.resolve(),
|
||||
handleTemplateChange: () => Promise.resolve(),
|
||||
handleConvert: () => Promise.resolve(),
|
||||
handleCreateSubWorkItem: () => Promise.resolve(),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</IssueModalContext.Provider>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
export type TWorkItemTemplateDropdownSize = "xs" | "sm";
|
||||
|
||||
export type TWorkItemTemplateSelect = {
|
||||
projectId: string | null;
|
||||
typeId: string | null;
|
||||
disabled?: boolean;
|
||||
size?: TWorkItemTemplateDropdownSize;
|
||||
placeholder?: string;
|
||||
renderChevron?: boolean;
|
||||
dropDownContainerClassName?: string;
|
||||
handleModalClose: () => void;
|
||||
handleFormChange?: () => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const WorkItemTemplateSelect = (props: TWorkItemTemplateSelect) => <></>;
|
||||
1
apps/web/ce/components/issues/quick-add/index.ts
Normal file
1
apps/web/ce/components/issues/quick-add/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
78
apps/web/ce/components/issues/quick-add/root.tsx
Normal file
78
apps/web/ce/components/issues/quick-add/root.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { UseFormRegister, UseFormSetFocus } from "react-hook-form";
|
||||
// plane constants
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// types
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import type { TQuickAddIssueForm } from "@/components/issues/issue-layouts/quick-add";
|
||||
import {
|
||||
CalendarQuickAddIssueForm,
|
||||
GanttQuickAddIssueForm,
|
||||
KanbanQuickAddIssueForm,
|
||||
ListQuickAddIssueForm,
|
||||
SpreadsheetQuickAddIssueForm,
|
||||
} from "@/components/issues/issue-layouts/quick-add";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
|
||||
export type TQuickAddIssueFormRoot = {
|
||||
isOpen: boolean;
|
||||
layout: EIssueLayoutTypes;
|
||||
prePopulatedData?: Partial<TIssue>;
|
||||
projectId: string;
|
||||
hasError?: boolean;
|
||||
setFocus: UseFormSetFocus<TIssue>;
|
||||
register: UseFormRegister<TIssue>;
|
||||
onSubmit: () => void;
|
||||
onClose: () => void;
|
||||
isEpic: boolean;
|
||||
};
|
||||
|
||||
export const QuickAddIssueFormRoot: FC<TQuickAddIssueFormRoot> = observer((props) => {
|
||||
const { isOpen, layout, projectId, hasError = false, setFocus, register, onSubmit, onClose, isEpic } = props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const projectDetail = getProjectById(projectId);
|
||||
// refs
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
// click detection
|
||||
useKeypress("Escape", onClose);
|
||||
useOutsideClickDetector(ref, onClose);
|
||||
// set focus on name input
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
if (!projectDetail) return <></>;
|
||||
|
||||
const QUICK_ADD_ISSUE_FORMS: Record<EIssueLayoutTypes, FC<TQuickAddIssueForm>> = {
|
||||
[EIssueLayoutTypes.LIST]: ListQuickAddIssueForm,
|
||||
[EIssueLayoutTypes.KANBAN]: KanbanQuickAddIssueForm,
|
||||
[EIssueLayoutTypes.CALENDAR]: CalendarQuickAddIssueForm,
|
||||
[EIssueLayoutTypes.GANTT]: GanttQuickAddIssueForm,
|
||||
[EIssueLayoutTypes.SPREADSHEET]: SpreadsheetQuickAddIssueForm,
|
||||
};
|
||||
|
||||
const CurrentLayoutQuickAddIssueForm = QUICK_ADD_ISSUE_FORMS[layout] ?? null;
|
||||
|
||||
if (!CurrentLayoutQuickAddIssueForm) return <></>;
|
||||
|
||||
return (
|
||||
<CurrentLayoutQuickAddIssueForm
|
||||
ref={ref}
|
||||
isOpen={isOpen}
|
||||
projectDetail={projectDetail}
|
||||
hasError={hasError}
|
||||
register={register}
|
||||
onSubmit={onSubmit}
|
||||
isEpic={isEpic}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
// plane imports
|
||||
import type { TActivityFilters, TActivityFilterOption } from "@plane/constants";
|
||||
import { ACTIVITY_FILTER_TYPE_OPTIONS } from "@plane/constants";
|
||||
// components
|
||||
import { ActivityFilter } from "@/components/issues/issue-detail/issue-activity";
|
||||
|
||||
export type TActivityFilterRoot = {
|
||||
selectedFilters: TActivityFilters[];
|
||||
toggleFilter: (filter: TActivityFilters) => void;
|
||||
projectId: string;
|
||||
isIntakeIssue?: boolean;
|
||||
};
|
||||
|
||||
export const ActivityFilterRoot: FC<TActivityFilterRoot> = (props) => {
|
||||
const { selectedFilters, toggleFilter } = props;
|
||||
|
||||
const filters: TActivityFilterOption[] = Object.entries(ACTIVITY_FILTER_TYPE_OPTIONS).map(([key, value]) => {
|
||||
const filterKey = key as TActivityFilters;
|
||||
return {
|
||||
key: filterKey,
|
||||
labelTranslationKey: value.labelTranslationKey,
|
||||
isSelected: selectedFilters.includes(filterKey),
|
||||
onClick: () => toggleFilter(filterKey),
|
||||
};
|
||||
});
|
||||
|
||||
return <ActivityFilter selectedFilters={selectedFilters} filterOptions={filters} />;
|
||||
};
|
||||
1
apps/web/ce/components/issues/worklog/activity/index.ts
Normal file
1
apps/web/ce/components/issues/worklog/activity/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
14
apps/web/ce/components/issues/worklog/activity/root.tsx
Normal file
14
apps/web/ce/components/issues/worklog/activity/root.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import type { TIssueActivityComment } from "@plane/types";
|
||||
|
||||
type TIssueActivityWorklog = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
activityComment: TIssueActivityComment;
|
||||
ends?: "top" | "bottom";
|
||||
};
|
||||
|
||||
export const IssueActivityWorklog: FC<TIssueActivityWorklog> = () => <></>;
|
||||
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
|
||||
type TIssueActivityWorklogCreateButton = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const IssueActivityWorklogCreateButton: FC<TIssueActivityWorklogCreateButton> = () => <></>;
|
||||
1
apps/web/ce/components/issues/worklog/property/index.ts
Normal file
1
apps/web/ce/components/issues/worklog/property/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
12
apps/web/ce/components/issues/worklog/property/root.tsx
Normal file
12
apps/web/ce/components/issues/worklog/property/root.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
|
||||
type TIssueWorklogProperty = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const IssueWorklogProperty: FC<TIssueWorklogProperty> = () => <></>;
|
||||
Reference in New Issue
Block a user