code refactor and improvement (#6203)

* chore: package code refactoring

* chore: component restructuring and refactor

* chore: comment create improvement
This commit is contained in:
Anmol Singh Bhatia 2024-12-16 17:24:50 +05:30 committed by GitHub
parent 442b0fd7e5
commit 438cc33046
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
134 changed files with 1336 additions and 506 deletions

View file

@ -39,3 +39,8 @@ export enum EServerGroupByToFilterOptions {
"project_id" = "project", "project_id" = "project",
"created_by" = "created_by", "created_by" = "created_by",
} }
export enum EIssueServiceType {
ISSUES = "issues",
EPICS = "epics",
}

View file

@ -1,3 +1,4 @@
import { EIssueServiceType } from "@plane/constants";
import { TIssuePriorities } from "../issues"; import { TIssuePriorities } from "../issues";
import { TIssueAttachment } from "./issue_attachment"; import { TIssueAttachment } from "./issue_attachment";
import { TIssueLink } from "./issue_link"; import { TIssueLink } from "./issue_link";
@ -39,6 +40,7 @@ export type TBaseIssue = {
updated_by: string; updated_by: string;
is_draft: boolean; is_draft: boolean;
is_epic?: boolean;
}; };
export type IssueRelation = { export type IssueRelation = {
@ -121,3 +123,7 @@ export type TIssueDetailWidget =
| "relations" | "relations"
| "links" | "links"
| "attachments"; | "attachments";
export type TIssueServiceType =
| EIssueServiceType.ISSUES
| EIssueServiceType.EPICS;

View file

@ -136,6 +136,7 @@ export type TProjectIssuesSearchParams = {
issue_id?: string; issue_id?: string;
workspace_search: boolean; workspace_search: boolean;
target_date?: string; target_date?: string;
epic?: boolean;
}; };
export interface ISearchIssueResponse { export interface ISearchIssueResponse {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,12 @@
import { TIssueServiceType } from "@plane/types";
export type TIssueAdditionalPropertyValuesUpdateProps = { export type TIssueAdditionalPropertyValuesUpdateProps = {
issueId: string; issueId: string;
issueTypeId: string; issueTypeId: string;
projectId: string; projectId: string;
workspaceSlug: string; workspaceSlug: string;
isDisabled: boolean; isDisabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPropertyValuesUpdateProps> = () => <></>; export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPropertyValuesUpdateProps> = () => <></>;

View file

@ -1,6 +1,7 @@
import { TDeDupeIssue } from "@plane/types"; import { TDeDupeIssue } from "@plane/types";
export const useDebouncedDuplicateIssues = ( export const useDebouncedDuplicateIssues = (
workspaceSlug: string | undefined,
workspaceId: string | undefined, workspaceId: string | undefined,
projectId: string | undefined, projectId: string | undefined,
formData: { name: string | undefined; description_html?: string | undefined; issueId?: string | undefined } formData: { name: string | undefined; description_html?: string | undefined; issueId?: string | undefined }

View file

@ -0,0 +1,15 @@
import { IProjectIssuesFilter, ProjectIssuesFilter } from "@/store/issue/project";
import { IIssueRootStore } from "@/store/issue/root.store";
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export type IProjectEpicsFilter = IProjectIssuesFilter;
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export class ProjectEpicsFilter extends ProjectIssuesFilter implements IProjectEpicsFilter {
constructor(_rootStore: IIssueRootStore) {
super(_rootStore);
// root store
this.rootIssueStore = _rootStore;
}
}

View file

@ -0,0 +1,2 @@
export * from "./filter.store";
export * from "./issue.store";

View file

@ -0,0 +1,14 @@
import { IProjectIssues, ProjectIssues } from "@/store/issue/project";
import { IIssueRootStore } from "@/store/issue/root.store";
import { IProjectEpicsFilter } from "./filter.store";
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export type IProjectEpics = IProjectIssues;
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export class ProjectEpics extends ProjectIssues implements IProjectEpics {
constructor(_rootStore: IIssueRootStore, issueFilterStore: IProjectEpicsFilter) {
super(_rootStore, issueFilterStore);
}
}

View file

@ -7,7 +7,14 @@ import uniq from "lodash/uniq";
import update from "lodash/update"; import update from "lodash/update";
import { action, makeObservable, observable, runInAction } from "mobx"; import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
import { TIssueActivityComment, TIssueActivity, TIssueActivityMap, TIssueActivityIdMap } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import {
TIssueActivityComment,
TIssueActivity,
TIssueActivityMap,
TIssueActivityIdMap,
TIssueServiceType,
} from "@plane/types";
// plane web constants // plane web constants
import { EActivityFilterType } from "@/plane-web/constants/issues"; import { EActivityFilterType } from "@/plane-web/constants/issues";
// services // services
@ -29,7 +36,7 @@ export interface IIssueActivityStoreActions {
export interface IIssueActivityStore extends IIssueActivityStoreActions { export interface IIssueActivityStore extends IIssueActivityStoreActions {
// observables // observables
sortOrder: 'asc' | 'desc' sortOrder: "asc" | "desc";
loader: TActivityLoader; loader: TActivityLoader;
activities: TIssueActivityIdMap; activities: TIssueActivityIdMap;
activityMap: TIssueActivityMap; activityMap: TIssueActivityMap;
@ -37,20 +44,24 @@ export interface IIssueActivityStore extends IIssueActivityStoreActions {
getActivitiesByIssueId: (issueId: string) => string[] | undefined; getActivitiesByIssueId: (issueId: string) => string[] | undefined;
getActivityById: (activityId: string) => TIssueActivity | undefined; getActivityById: (activityId: string) => TIssueActivity | undefined;
getActivityCommentByIssueId: (issueId: string) => TIssueActivityComment[] | undefined; getActivityCommentByIssueId: (issueId: string) => TIssueActivityComment[] | undefined;
toggleSortOrder: ()=>void; toggleSortOrder: () => void;
} }
export class IssueActivityStore implements IIssueActivityStore { export class IssueActivityStore implements IIssueActivityStore {
// observables // observables
sortOrder: "asc" | "desc" = 'asc'; sortOrder: "asc" | "desc" = "asc";
loader: TActivityLoader = "fetch"; loader: TActivityLoader = "fetch";
activities: TIssueActivityIdMap = {}; activities: TIssueActivityIdMap = {};
activityMap: TIssueActivityMap = {}; activityMap: TIssueActivityMap = {};
// services // services
serviceType;
issueActivityService; issueActivityService;
constructor(protected store: CoreRootStore) { constructor(
protected store: CoreRootStore,
serviceType: TIssueServiceType = EIssueServiceType.ISSUES
) {
makeObservable(this, { makeObservable(this, {
// observables // observables
sortOrder: observable.ref, sortOrder: observable.ref,
@ -59,10 +70,11 @@ export class IssueActivityStore implements IIssueActivityStore {
activityMap: observable, activityMap: observable,
// actions // actions
fetchActivities: action, fetchActivities: action,
toggleSortOrder: action toggleSortOrder: action,
}); });
this.serviceType = serviceType;
// services // services
this.issueActivityService = new IssueActivityService(); this.issueActivityService = new IssueActivityService(this.serviceType);
} }
// helper methods // helper methods
@ -81,8 +93,10 @@ export class IssueActivityStore implements IIssueActivityStore {
let activityComments: TIssueActivityComment[] = []; let activityComments: TIssueActivityComment[] = [];
const currentStore = this.serviceType === EIssueServiceType.EPICS ? this.store.epic : this.store.issue;
const activities = this.getActivitiesByIssueId(issueId) || []; const activities = this.getActivitiesByIssueId(issueId) || [];
const comments = this.store.issue.issueDetail.comment.getCommentsByIssueId(issueId) || []; const comments = currentStore.issueDetail.comment.getCommentsByIssueId(issueId) || [];
activities.forEach((activityId) => { activities.forEach((activityId) => {
const activity = this.getActivityById(activityId); const activity = this.getActivityById(activityId);
@ -95,7 +109,7 @@ export class IssueActivityStore implements IIssueActivityStore {
}); });
comments.forEach((commentId) => { comments.forEach((commentId) => {
const comment = this.store.issue.issueDetail.comment.getCommentById(commentId); const comment = currentStore.issueDetail.comment.getCommentById(commentId);
if (!comment) return; if (!comment) return;
activityComments.push({ activityComments.push({
id: comment.id, id: comment.id,
@ -104,14 +118,14 @@ export class IssueActivityStore implements IIssueActivityStore {
}); });
}); });
activityComments = orderBy(activityComments, (e)=>new Date(e.created_at || 0), this.sortOrder); activityComments = orderBy(activityComments, (e) => new Date(e.created_at || 0), this.sortOrder);
return activityComments; return activityComments;
}); });
toggleSortOrder = ()=>{ toggleSortOrder = () => {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'; this.sortOrder = this.sortOrder === "asc" ? "desc" : "asc";
} };
// actions // actions
public async fetchActivities( public async fetchActivities(

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
// editor // editor
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor";
// types // types
@ -27,6 +27,7 @@ interface LiteTextEditorWrapperProps
showAccessSpecifier?: boolean; showAccessSpecifier?: boolean;
showSubmitButton?: boolean; showSubmitButton?: boolean;
isSubmitting?: boolean; isSubmitting?: boolean;
showToolbarInitially?: boolean;
uploadFile: (file: File) => Promise<string>; uploadFile: (file: File) => Promise<string>;
} }
@ -41,10 +42,13 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
showAccessSpecifier = false, showAccessSpecifier = false,
showSubmitButton = true, showSubmitButton = true,
isSubmitting = false, isSubmitting = false,
showToolbarInitially = true,
placeholder = "Add comment...", placeholder = "Add comment...",
uploadFile, uploadFile,
...rest ...rest
} = props; } = props;
// states
const [isFocused, setIsFocused] = useState(showToolbarInitially);
// store hooks // store hooks
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
const { const {
@ -73,7 +77,11 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null; const editorRef = isMutableRefObject<EditorRefApi>(ref) ? ref.current : null;
return ( return (
<div className="border border-custom-border-200 rounded p-3 space-y-3"> <div
className={cn("relative border border-custom-border-200 rounded p-3")}
onFocus={() => !showToolbarInitially && setIsFocused(true)}
onBlur={() => !showToolbarInitially && setIsFocused(false)}
>
<LiteTextEditorWithRef <LiteTextEditorWithRef
ref={ref} ref={ref}
disabledExtensions={disabledExtensions} disabledExtensions={disabledExtensions}
@ -92,24 +100,31 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
containerClassName={cn(containerClassName, "relative")} containerClassName={cn(containerClassName, "relative")}
{...rest} {...rest}
/> />
<IssueCommentToolbar <div
accessSpecifier={accessSpecifier} className={cn(
executeCommand={(item) => { "transition-all duration-300 ease-out origin-top overflow-hidden",
// TODO: update this while toolbar homogenization isFocused ? "max-h-[200px] opacity-100 scale-y-100 mt-3" : "max-h-0 opacity-0 scale-y-0 invisible"
// @ts-expect-error type mismatch here )}
editorRef?.executeMenuItemCommand({ >
itemKey: item.itemKey, <IssueCommentToolbar
...item.extraProps, accessSpecifier={accessSpecifier}
}); executeCommand={(item) => {
}} // TODO: update this while toolbar homogenization
handleAccessChange={handleAccessChange} // @ts-expect-error type mismatch here
handleSubmit={(e) => rest.onEnterKeyPress?.(e)} editorRef?.executeMenuItemCommand({
isCommentEmpty={isEmpty} itemKey: item.itemKey,
isSubmitting={isSubmitting} ...item.extraProps,
showAccessSpecifier={showAccessSpecifier} });
editorRef={editorRef} }}
showSubmitButton={showSubmitButton} handleAccessChange={handleAccessChange}
/> handleSubmit={(e) => rest.onEnterKeyPress?.(e)}
isCommentEmpty={isEmpty}
isSubmitting={isSubmitting}
showAccessSpecifier={showAccessSpecifier}
editorRef={editorRef}
showSubmitButton={showSubmitButton}
/>
</div>
</div> </div>
); );
}); });

View file

@ -56,6 +56,7 @@ type Props = {
targetDate?: Date targetDate?: Date
) => ChartDataType | undefined; ) => ChartDataType | undefined;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
isEpic?: boolean;
}; };
export const GanttChartMainContent: React.FC<Props> = observer((props) => { export const GanttChartMainContent: React.FC<Props> = observer((props) => {
@ -79,6 +80,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
updateCurrentViewRenderPayload, updateCurrentViewRenderPayload,
quickAdd, quickAdd,
updateBlockDates, updateBlockDates,
isEpic = false,
} = props; } = props;
// refs // refs
const ganttContainerRef = useRef<HTMLDivElement>(null); const ganttContainerRef = useRef<HTMLDivElement>(null);
@ -159,7 +161,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
entities={{ entities={{
[GANTT_SELECT_GROUP]: blockIds ?? [], [GANTT_SELECT_GROUP]: blockIds ?? [],
}} }}
disabled={!isBulkOperationsEnabled} disabled={!isBulkOperationsEnabled || isEpic}
> >
{(helpers) => ( {(helpers) => (
<> <>
@ -187,6 +189,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
title={title} title={title}
quickAdd={quickAdd} quickAdd={quickAdd}
selectionHelpers={helpers} selectionHelpers={helpers}
isEpic={isEpic}
/> />
<div className="relative min-h-full h-max flex-shrink-0 flex-grow"> <div className="relative min-h-full h-max flex-shrink-0 flex-grow">
<ActiveChartView /> <ActiveChartView />
@ -208,7 +211,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
selectionHelpers={helpers} selectionHelpers={helpers}
ganttContainerRef={ganttContainerRef} ganttContainerRef={ganttContainerRef}
/> />
<TimelineDependencyPaths /> <TimelineDependencyPaths isEpic={isEpic} />
<TimelineDraggablePath /> <TimelineDraggablePath />
<GanttChartBlocksList <GanttChartBlocksList
blockIds={blockIds} blockIds={blockIds}

View file

@ -41,6 +41,7 @@ type ChartViewRootProps = {
canLoadMoreBlocks?: boolean; canLoadMoreBlocks?: boolean;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
showToday: boolean; showToday: boolean;
isEpic?: boolean;
}; };
const timelineViewHelpers = { const timelineViewHelpers = {
@ -71,6 +72,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
quickAdd, quickAdd,
showToday, showToday,
updateBlockDates, updateBlockDates,
isEpic = false,
} = props; } = props;
// states // states
const [itemsContainerWidth, setItemsContainerWidth] = useState(0); const [itemsContainerWidth, setItemsContainerWidth] = useState(0);
@ -204,6 +206,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
updateCurrentViewRenderPayload={updateCurrentViewRenderPayload} updateCurrentViewRenderPayload={updateCurrentViewRenderPayload}
quickAdd={quickAdd} quickAdd={quickAdd}
updateBlockDates={updateBlockDates} updateBlockDates={updateBlockDates}
isEpic={isEpic}
/> />
</div> </div>
); );

View file

@ -26,6 +26,7 @@ type GanttChartRootProps = {
bottomSpacing?: boolean; bottomSpacing?: boolean;
showAllBlocks?: boolean; showAllBlocks?: boolean;
showToday?: boolean; showToday?: boolean;
isEpic?: boolean;
}; };
export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => { export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => {
@ -50,6 +51,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => {
showToday = true, showToday = true,
quickAdd, quickAdd,
updateBlockDates, updateBlockDates,
isEpic = false,
} = props; } = props;
const { setBlockIds } = useTimeLineChartStore(); const { setBlockIds } = useTimeLineChartStore();
@ -81,6 +83,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = observer((props) => {
quickAdd={quickAdd} quickAdd={quickAdd}
showToday={showToday} showToday={showToday}
updateBlockDates={updateBlockDates} updateBlockDates={updateBlockDates}
isEpic={isEpic}
/> />
); );
}); });

View file

@ -19,10 +19,11 @@ type Props = {
enableSelection: boolean; enableSelection: boolean;
isDragging: boolean; isDragging: boolean;
selectionHelpers?: TSelectionHelper; selectionHelpers?: TSelectionHelper;
isEpic?: boolean;
}; };
export const IssuesSidebarBlock = observer((props: Props) => { export const IssuesSidebarBlock = observer((props: Props) => {
const { block, enableSelection, isDragging, selectionHelpers } = props; const { block, enableSelection, isDragging, selectionHelpers, isEpic = false } = props;
// store hooks // store hooks
const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore(); const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
const { getIsIssuePeeked } = useIssueDetail(); const { getIsIssuePeeked } = useIssueDetail();
@ -73,7 +74,7 @@ export const IssuesSidebarBlock = observer((props: Props) => {
)} )}
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate"> <div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
<div className="flex-grow truncate"> <div className="flex-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} /> <IssueGanttSidebarBlock issueId={block.data.id} isEpic={isEpic} />
</div> </div>
{duration && ( {duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200"> <div className="flex-shrink-0 text-sm text-custom-text-200">

View file

@ -29,6 +29,7 @@ type Props = {
enableSelection: boolean; enableSelection: boolean;
showAllBlocks?: boolean; showAllBlocks?: boolean;
selectionHelpers?: TSelectionHelper; selectionHelpers?: TSelectionHelper;
isEpic?: boolean;
}; };
export const IssueGanttSidebar: React.FC<Props> = observer((props) => { export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
@ -42,6 +43,7 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
ganttContainerRef, ganttContainerRef,
showAllBlocks = false, showAllBlocks = false,
selectionHelpers, selectionHelpers,
isEpic = false,
} = props; } = props;
const { getBlockById } = useTimeLineChart(ETimeLineTypeType.ISSUE); const { getBlockById } = useTimeLineChart(ETimeLineTypeType.ISSUE);
@ -101,6 +103,7 @@ export const IssueGanttSidebar: React.FC<Props> = observer((props) => {
enableSelection={enableSelection} enableSelection={enableSelection}
isDragging={isDragging} isDragging={isDragging}
selectionHelpers={selectionHelpers} selectionHelpers={selectionHelpers}
isEpic={isEpic}
/> />
)} )}
</GanttDnDHOC> </GanttDnDHOC>

View file

@ -23,6 +23,7 @@ type Props = {
title: string; title: string;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
isEpic?: boolean;
}; };
export const GanttChartSidebar: React.FC<Props> = observer((props) => { export const GanttChartSidebar: React.FC<Props> = observer((props) => {
@ -38,6 +39,7 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
title, title,
quickAdd, quickAdd,
selectionHelpers, selectionHelpers,
isEpic = false,
} = props; } = props;
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(GANTT_SELECT_GROUP) === "empty"; const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(GANTT_SELECT_GROUP) === "empty";
@ -90,6 +92,7 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
ganttContainerRef, ganttContainerRef,
loadMoreBlocks, loadMoreBlocks,
selectionHelpers, selectionHelpers,
isEpic,
})} })}
</Row> </Row>
{quickAdd ? quickAdd : null} {quickAdd ? quickAdd : null}

View file

@ -65,11 +65,16 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined; const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
// debounced duplicate issues swr // debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectId, { const { duplicateIssues } = useDebouncedDuplicateIssues(
name: issue?.name, workspaceSlug,
description_html: getTextContent(issue?.description_html), projectDetails?.workspace.toString(),
issueId: issue?.id, projectId,
}); {
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
}
);
if (!issue) return <></>; if (!issue) return <></>;

View file

@ -87,10 +87,15 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile); const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
// debounced duplicate issues swr // debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectId, { const { duplicateIssues } = useDebouncedDuplicateIssues(
name: formData?.name, workspaceSlug,
description_html: formData?.description_html, projectDetails?.workspace.toString(),
}); projectId,
{
name: formData?.name,
description_html: formData?.description_html,
}
);
const handleEscKeyDown = (event: KeyboardEvent) => { const handleEscKeyDown = (event: KeyboardEvent) => {
if (descriptionEditorRef.current?.isEditorReadyToDiscard()) { if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {

View file

@ -2,6 +2,8 @@ import { FC, useCallback, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { FileRejection, useDropzone } from "react-dropzone"; import { FileRejection, useDropzone } from "react-dropzone";
import { UploadCloud } from "lucide-react"; import { UploadCloud } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// hooks // hooks
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
@ -21,10 +23,18 @@ type TIssueAttachmentItemList = {
issueId: string; issueId: string;
attachmentHelpers: TAttachmentHelpers; attachmentHelpers: TAttachmentHelpers;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((props) => { export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((props) => {
const { workspaceSlug, projectId, issueId, attachmentHelpers, disabled } = props; const {
workspaceSlug,
projectId,
issueId,
attachmentHelpers,
disabled,
issueServiceType = EIssueServiceType.ISSUES,
} = props;
// states // states
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
// store hooks // store hooks
@ -33,7 +43,7 @@ export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((p
attachmentDeleteModalId, attachmentDeleteModalId,
toggleDeleteAttachmentModal, toggleDeleteAttachmentModal,
fetchActivities, fetchActivities,
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
const { operations: attachmentOperations, snapshot: attachmentSnapshot } = attachmentHelpers; const { operations: attachmentOperations, snapshot: attachmentSnapshot } = attachmentHelpers;
const { create: createAttachment } = attachmentOperations; const { create: createAttachment } = attachmentOperations;
const { uploadStatus } = attachmentSnapshot; const { uploadStatus } = attachmentSnapshot;
@ -104,6 +114,7 @@ export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((p
onClose={() => toggleDeleteAttachmentModal(null)} onClose={() => toggleDeleteAttachmentModal(null)}
attachmentOperations={attachmentOperations} attachmentOperations={attachmentOperations}
attachmentId={attachmentDeleteModalId} attachmentId={attachmentDeleteModalId}
issueServiceType={issueServiceType}
/> />
)} )}
<div <div
@ -122,7 +133,12 @@ export const IssueAttachmentItemList: FC<TIssueAttachmentItemList> = observer((p
</div> </div>
)} )}
{issueAttachments?.map((attachmentId) => ( {issueAttachments?.map((attachmentId) => (
<IssueAttachmentsListItem key={attachmentId} attachmentId={attachmentId} disabled={disabled} /> <IssueAttachmentsListItem
key={attachmentId}
attachmentId={attachmentId}
disabled={disabled}
issueServiceType={issueServiceType}
/>
))} ))}
</div> </div>
</> </>

View file

@ -3,6 +3,8 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Trash } from "lucide-react"; import { Trash } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// ui // ui
import { CustomMenu, Tooltip } from "@plane/ui"; import { CustomMenu, Tooltip } from "@plane/ui";
// components // components
@ -19,17 +21,18 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
type TIssueAttachmentsListItem = { type TIssueAttachmentsListItem = {
attachmentId: string; attachmentId: string;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer((props) => { export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer((props) => {
// props // props
const { attachmentId, disabled } = props; const { attachmentId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
const { const {
attachment: { getAttachmentById }, attachment: { getAttachmentById },
toggleDeleteAttachmentModal, toggleDeleteAttachmentModal,
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
// derived values // derived values
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined; const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
const fileName = getFileName(attachment?.attributes.name ?? ""); const fileName = getFileName(attachment?.attributes.name ?? "");

View file

@ -1,6 +1,9 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// constants
import { EIssueServiceType } from "@plane/constants";
// types // types
import { TIssueServiceType } from "@plane/types";
// ui // ui
import { AlertModalCore } from "@plane/ui"; import { AlertModalCore } from "@plane/ui";
// helper // helper
@ -17,17 +20,18 @@ type Props = {
onClose: () => void; onClose: () => void;
attachmentId: string; attachmentId: string;
attachmentOperations: TAttachmentOperationsRemoveModal; attachmentOperations: TAttachmentOperationsRemoveModal;
issueServiceType?: TIssueServiceType;
}; };
export const IssueAttachmentDeleteModal: FC<Props> = observer((props) => { export const IssueAttachmentDeleteModal: FC<Props> = observer((props) => {
const { isOpen, onClose, attachmentId, attachmentOperations } = props; const { isOpen, onClose, attachmentId, attachmentOperations, issueServiceType = EIssueServiceType.ISSUES } = props;
// states // states
const [loader, setLoader] = useState(false); const [loader, setLoader] = useState(false);
// store hooks // store hooks
const { const {
attachment: { getAttachmentById }, attachment: { getAttachmentById },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
// derived values // derived values
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined; const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;

View file

@ -4,24 +4,17 @@ import { useCallback, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// types // types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
// constants // constants
import { import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType, ISSUE_STORE_TO_FILTERS_MAP } from "@/constants/issue";
EIssueFilterType,
EIssuesStoreType,
EIssueLayoutTypes,
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
} from "@/constants/issue";
// helpers // helpers
import { isIssueFilterActive } from "@/helpers/filter.helper"; import { isIssueFilterActive } from "@/helpers/filter.helper";
// hooks // hooks
import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store"; import { useLabel, useProjectState, useMember, useIssues } from "@/hooks/store";
// plane web types // plane web types
import { TProject } from "@/plane-web/types"; import { TProject } from "@/plane-web/types";
// local components
import { ProjectAnalyticsModal } from "../analytics"; import { ProjectAnalyticsModal } from "../analytics";
type Props = { type Props = {
@ -29,8 +22,16 @@ type Props = {
projectId: string; projectId: string;
workspaceSlug: string; workspaceSlug: string;
canUserCreateIssue: boolean | undefined; canUserCreateIssue: boolean | undefined;
storeType?: EIssuesStoreType.PROJECT | EIssuesStoreType.EPIC;
}; };
const HeaderFilters = observer(({ currentProjectDetails, projectId, workspaceSlug, canUserCreateIssue }: Props) => { const HeaderFilters = observer((props: Props) => {
const {
currentProjectDetails,
projectId,
workspaceSlug,
canUserCreateIssue,
storeType = EIssuesStoreType.PROJECT,
} = props;
// states // states
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
// store hooks // store hooks
@ -39,11 +40,12 @@ const HeaderFilters = observer(({ currentProjectDetails, projectId, workspaceSlu
} = useMember(); } = useMember();
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT); } = useIssues(storeType);
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
// derived values
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;
const layoutDisplayFiltersOptions = ISSUE_STORE_TO_FILTERS_MAP[storeType]?.[activeLayout];
const handleFiltersUpdate = useCallback( const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof IIssueFilterOptions, value: string | string[]) => {
@ -113,7 +115,7 @@ const HeaderFilters = observer(({ currentProjectDetails, projectId, workspaceSlu
handleFiltersUpdate={handleFiltersUpdate} handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined} layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
labels={projectLabels} labels={projectLabels}
memberIds={projectMemberIds ?? undefined} memberIds={projectMemberIds ?? undefined}
states={projectStates} states={projectStates}
@ -123,7 +125,7 @@ const HeaderFilters = observer(({ currentProjectDetails, projectId, workspaceSlu
</FiltersDropdown> </FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end"> <FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection <DisplayFiltersSelection
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined} layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
displayFilters={issueFilters?.displayFilters ?? {}} displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters} handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}} displayProperties={issueFilters?.displayProperties ?? {}}

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// components // components
import { IssueAttachmentItemList } from "@/components/issues/attachment"; import { IssueAttachmentItemList } from "@/components/issues/attachment";
// helper // helper
@ -11,12 +13,13 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled: boolean; disabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueAttachmentsCollapsibleContent: FC<Props> = observer((props) => { export const IssueAttachmentsCollapsibleContent: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled } = props; const { workspaceSlug, projectId, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// helper // helper
const attachmentHelpers = useAttachmentOperations(workspaceSlug, projectId, issueId); const attachmentHelpers = useAttachmentOperations(workspaceSlug, projectId, issueId, issueServiceType);
return ( return (
<IssueAttachmentItemList <IssueAttachmentItemList
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
@ -24,6 +27,7 @@ export const IssueAttachmentsCollapsibleContent: FC<Props> = observer((props) =>
issueId={issueId} issueId={issueId}
disabled={disabled} disabled={disabled}
attachmentHelpers={attachmentHelpers} attachmentHelpers={attachmentHelpers}
issueServiceType={issueServiceType}
/> />
); );
}); });

View file

@ -1,5 +1,7 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// plane ui // plane ui
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
// hooks // hooks
@ -24,11 +26,12 @@ export type TAttachmentHelpers = {
export const useAttachmentOperations = ( export const useAttachmentOperations = (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
issueId: string issueId: string,
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
): TAttachmentHelpers => { ): TAttachmentHelpers => {
const { const {
attachment: { createAttachment, removeAttachment, getAttachmentsUploadStatusByIssueId }, attachment: { createAttachment, removeAttachment, getAttachmentsUploadStatusByIssueId },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const attachmentOperations: TAttachmentOperations = useMemo( const attachmentOperations: TAttachmentOperations = useMemo(

View file

@ -4,6 +4,8 @@ import React, { FC, useCallback, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { FileRejection, useDropzone } from "react-dropzone"; import { FileRejection, useDropzone } from "react-dropzone";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// plane ui // plane ui
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// hooks // hooks
@ -19,18 +21,31 @@ type Props = {
issueId: string; issueId: string;
customButton?: React.ReactNode; customButton?: React.ReactNode;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueAttachmentActionButton: FC<Props> = observer((props) => { export const IssueAttachmentActionButton: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, customButton, disabled = false } = props; const {
workspaceSlug,
projectId,
issueId,
customButton,
disabled = false,
issueServiceType = EIssueServiceType.ISSUES,
} = props;
// state // state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// store hooks // store hooks
const { setLastWidgetAction, fetchActivities } = useIssueDetail(); const { setLastWidgetAction, fetchActivities } = useIssueDetail(issueServiceType);
// file size // file size
const { maxFileSize } = useFileSize(); const { maxFileSize } = useFileSize();
// operations // operations
const { operations: attachmentOperations } = useAttachmentOperations(workspaceSlug, projectId, issueId); const { operations: attachmentOperations } = useAttachmentOperations(
workspaceSlug,
projectId,
issueId,
issueServiceType
);
// handlers // handlers
const handleFetchPropertyActivities = useCallback(() => { const handleFetchPropertyActivities = useCallback(() => {
fetchActivities(workspaceSlug, projectId, issueId); fetchActivities(workspaceSlug, projectId, issueId);

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { Collapsible } from "@plane/ui"; import { Collapsible } from "@plane/ui";
// components // components
import { import {
@ -15,12 +17,13 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const AttachmentsCollapsible: FC<Props> = observer((props) => { export const AttachmentsCollapsible: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props; const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { openWidgets, toggleOpenWidget } = useIssueDetail(); const { openWidgets, toggleOpenWidget } = useIssueDetail(issueServiceType);
// derived values // derived values
const isCollapsibleOpen = openWidgets.includes("attachments"); const isCollapsibleOpen = openWidgets.includes("attachments");
@ -36,6 +39,7 @@ export const AttachmentsCollapsible: FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={disabled} disabled={disabled}
issueServiceType={issueServiceType}
/> />
} }
buttonClassName="w-full" buttonClassName="w-full"
@ -45,6 +49,7 @@ export const AttachmentsCollapsible: FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={disabled} disabled={disabled}
issueServiceType={issueServiceType}
/> />
</Collapsible> </Collapsible>
); );

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC, useMemo } from "react"; import React, { FC, useMemo } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { CollapsibleButton } from "@plane/ui"; import { CollapsibleButton } from "@plane/ui";
// components // components
import { IssueAttachmentActionButton } from "@/components/issues/issue-detail-widgets"; import { IssueAttachmentActionButton } from "@/components/issues/issue-detail-widgets";
@ -13,14 +15,15 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled: boolean; disabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueAttachmentsCollapsibleTitle: FC<Props> = observer((props) => { export const IssueAttachmentsCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, workspaceSlug, projectId, issueId, disabled } = props; const { isOpen, workspaceSlug, projectId, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
// derived values // derived values
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
@ -48,6 +51,7 @@ export const IssueAttachmentsCollapsibleTitle: FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={disabled} disabled={disabled}
issueServiceType={issueServiceType}
/> />
) )
} }

View file

@ -1,5 +1,7 @@
"use client"; "use client";
import React, { FC } from "react"; import React, { FC } from "react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// components // components
import { LinkList } from "../../issue-detail/links"; import { LinkList } from "../../issue-detail/links";
// helper // helper
@ -10,13 +12,21 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled: boolean; disabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueLinksCollapsibleContent: FC<Props> = (props) => { export const IssueLinksCollapsibleContent: FC<Props> = (props) => {
const { workspaceSlug, projectId, issueId, disabled } = props; const { workspaceSlug, projectId, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// helper // helper
const handleLinkOperations = useLinkOperations(workspaceSlug, projectId, issueId); const handleLinkOperations = useLinkOperations(workspaceSlug, projectId, issueId, issueServiceType);
return <LinkList issueId={issueId} linkOperations={handleLinkOperations} disabled={disabled} />; return (
<LinkList
issueId={issueId}
linkOperations={handleLinkOperations}
disabled={disabled}
issueServiceType={issueServiceType}
/>
);
}; };

View file

@ -1,14 +1,20 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { TIssueLink } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { TIssueLink, TIssueServiceType } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// types // types
import { TLinkOperations } from "../../issue-detail/links"; import { TLinkOperations } from "../../issue-detail/links";
export const useLinkOperations = (workspaceSlug: string, projectId: string, issueId: string): TLinkOperations => { export const useLinkOperations = (
const { createLink, updateLink, removeLink } = useIssueDetail(); workspaceSlug: string,
projectId: string,
issueId: string,
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
): TLinkOperations => {
const { createLink, updateLink, removeLink } = useIssueDetail(issueServiceType);
const handleLinkOperations: TLinkOperations = useMemo( const handleLinkOperations: TLinkOperations = useMemo(
() => ({ () => ({

View file

@ -2,18 +2,21 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
type Props = { type Props = {
customButton?: React.ReactNode; customButton?: React.ReactNode;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueLinksActionButton: FC<Props> = observer((props) => { export const IssueLinksActionButton: FC<Props> = observer((props) => {
const { customButton, disabled = false } = props; const { customButton, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { toggleIssueLinkModal } = useIssueDetail(); const { toggleIssueLinkModal } = useIssueDetail(issueServiceType);
// handlers // handlers
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { Collapsible } from "@plane/ui"; import { Collapsible } from "@plane/ui";
// components // components
import { IssueLinksCollapsibleContent, IssueLinksCollapsibleTitle } from "@/components/issues/issue-detail-widgets"; import { IssueLinksCollapsibleContent, IssueLinksCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
@ -12,12 +14,13 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const LinksCollapsible: FC<Props> = observer((props) => { export const LinksCollapsible: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props; const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { openWidgets, toggleOpenWidget } = useIssueDetail(); const { openWidgets, toggleOpenWidget } = useIssueDetail(issueServiceType);
// derived values // derived values
const isCollapsibleOpen = openWidgets.includes("links"); const isCollapsibleOpen = openWidgets.includes("links");
@ -26,7 +29,14 @@ export const LinksCollapsible: FC<Props> = observer((props) => {
<Collapsible <Collapsible
isOpen={isCollapsibleOpen} isOpen={isCollapsibleOpen}
onToggle={() => toggleOpenWidget("links")} onToggle={() => toggleOpenWidget("links")}
title={<IssueLinksCollapsibleTitle isOpen={isCollapsibleOpen} issueId={issueId} disabled={disabled} />} title={
<IssueLinksCollapsibleTitle
isOpen={isCollapsibleOpen}
issueId={issueId}
disabled={disabled}
issueServiceType={issueServiceType}
/>
}
buttonClassName="w-full" buttonClassName="w-full"
> >
<IssueLinksCollapsibleContent <IssueLinksCollapsibleContent
@ -34,6 +44,7 @@ export const LinksCollapsible: FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={disabled} disabled={disabled}
issueServiceType={issueServiceType}
/> />
</Collapsible> </Collapsible>
); );

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC, useMemo } from "react"; import React, { FC, useMemo } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { CollapsibleButton } from "@plane/ui"; import { CollapsibleButton } from "@plane/ui";
// components // components
import { IssueLinksActionButton } from "@/components/issues/issue-detail-widgets"; import { IssueLinksActionButton } from "@/components/issues/issue-detail-widgets";
@ -11,14 +13,15 @@ type Props = {
isOpen: boolean; isOpen: boolean;
issueId: string; issueId: string;
disabled: boolean; disabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueLinksCollapsibleTitle: FC<Props> = observer((props) => { export const IssueLinksCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, issueId, disabled } = props; const { isOpen, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
// derived values // derived values
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
@ -40,7 +43,9 @@ export const IssueLinksCollapsibleTitle: FC<Props> = observer((props) => {
isOpen={isOpen} isOpen={isOpen}
title="Links" title="Links"
indicatorElement={indicatorElement} indicatorElement={indicatorElement}
actionItemElement={!disabled && <IssueLinksActionButton disabled={disabled} />} actionItemElement={
!disabled && <IssueLinksActionButton issueServiceType={issueServiceType} disabled={disabled} />
}
/> />
); );
}); });

View file

@ -1,7 +1,8 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TIssue, TIssueRelationIdMap } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { TIssue, TIssueRelationIdMap, TIssueServiceType } from "@plane/types";
import { Collapsible } from "@plane/ui"; import { Collapsible } from "@plane/ui";
// components // components
import { RelationIssueList } from "@/components/issues"; import { RelationIssueList } from "@/components/issues";
@ -20,6 +21,7 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled: boolean; disabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
type TIssueCrudState = { toggle: boolean; issueId: string | undefined; issue: TIssue | undefined }; type TIssueCrudState = { toggle: boolean; issueId: string | undefined; issue: TIssue | undefined };
@ -33,7 +35,7 @@ export type TRelationObject = {
}; };
export const RelationsCollapsibleContent: FC<Props> = observer((props) => { export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props; const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// state // state
const [issueCrudState, setIssueCrudState] = useState<{ const [issueCrudState, setIssueCrudState] = useState<{
update: TIssueCrudState; update: TIssueCrudState;
@ -56,7 +58,7 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
relation: { getRelationsByIssueId }, relation: { getRelationsByIssueId },
toggleDeleteIssueModal, toggleDeleteIssueModal,
toggleCreateIssueModal, toggleCreateIssueModal,
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
// helper // helper
const issueOperations = useRelationOperations(); const issueOperations = useRelationOperations();
@ -129,6 +131,7 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
disabled={disabled} disabled={disabled}
issueOperations={issueOperations} issueOperations={issueOperations}
handleIssueCrudState={handleIssueCrudState} handleIssueCrudState={handleIssueCrudState}
issueServiceType={issueServiceType}
/> />
</Collapsible> </Collapsible>
</div> </div>

View file

@ -1,7 +1,8 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { TIssue } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { TIssue, TIssueServiceType } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// constants // constants
import { ISSUE_DELETED, ISSUE_UPDATED } from "@/constants/event-tracker"; import { ISSUE_DELETED, ISSUE_UPDATED } from "@/constants/event-tracker";
@ -16,8 +17,10 @@ export type TRelationIssueOperations = {
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
}; };
export const useRelationOperations = (): TRelationIssueOperations => { export const useRelationOperations = (
const { updateIssue, removeIssue } = useIssueDetail(); issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
): TRelationIssueOperations => {
const { updateIssue, removeIssue } = useIssueDetail(issueServiceType);
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const pathname = usePathname(); const pathname = usePathname();

View file

@ -2,6 +2,8 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
@ -13,12 +15,13 @@ type Props = {
issueId: string; issueId: string;
customButton?: React.ReactNode; customButton?: React.ReactNode;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const RelationActionButton: FC<Props> = observer((props) => { export const RelationActionButton: FC<Props> = observer((props) => {
const { customButton, issueId, disabled = false } = props; const { customButton, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { toggleRelationModal, setRelationKey } = useIssueDetail(); const { toggleRelationModal, setRelationKey } = useIssueDetail(issueServiceType);
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions(); const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { Collapsible } from "@plane/ui"; import { Collapsible } from "@plane/ui";
// components // components
import { RelationsCollapsibleContent, RelationsCollapsibleTitle } from "@/components/issues/issue-detail-widgets"; import { RelationsCollapsibleContent, RelationsCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
@ -12,12 +14,13 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const RelationsCollapsible: FC<Props> = observer((props) => { export const RelationsCollapsible: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props; const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { openWidgets, toggleOpenWidget } = useIssueDetail(); const { openWidgets, toggleOpenWidget } = useIssueDetail(issueServiceType);
// derived values // derived values
const isCollapsibleOpen = openWidgets.includes("relations"); const isCollapsibleOpen = openWidgets.includes("relations");
@ -26,7 +29,14 @@ export const RelationsCollapsible: FC<Props> = observer((props) => {
<Collapsible <Collapsible
isOpen={isCollapsibleOpen} isOpen={isCollapsibleOpen}
onToggle={() => toggleOpenWidget("relations")} onToggle={() => toggleOpenWidget("relations")}
title={<RelationsCollapsibleTitle isOpen={isCollapsibleOpen} issueId={issueId} disabled={disabled} />} title={
<RelationsCollapsibleTitle
isOpen={isCollapsibleOpen}
issueId={issueId}
disabled={disabled}
issueServiceType={issueServiceType}
/>
}
buttonClassName="w-full" buttonClassName="w-full"
> >
<RelationsCollapsibleContent <RelationsCollapsibleContent
@ -34,6 +44,7 @@ export const RelationsCollapsible: FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
disabled={disabled} disabled={disabled}
issueServiceType={issueServiceType}
/> />
</Collapsible> </Collapsible>
); );

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC, useMemo } from "react"; import React, { FC, useMemo } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { CollapsibleButton } from "@plane/ui"; import { CollapsibleButton } from "@plane/ui";
// components // components
import { RelationActionButton } from "@/components/issues/issue-detail-widgets"; import { RelationActionButton } from "@/components/issues/issue-detail-widgets";
@ -13,14 +15,15 @@ type Props = {
isOpen: boolean; isOpen: boolean;
issueId: string; issueId: string;
disabled: boolean; disabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const RelationsCollapsibleTitle: FC<Props> = observer((props) => { export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, issueId, disabled } = props; const { isOpen, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hook // store hook
const { const {
relation: { getRelationCountByIssueId }, relation: { getRelationCountByIssueId },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions(); const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
// derived values // derived values
@ -41,7 +44,9 @@ export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
isOpen={isOpen} isOpen={isOpen}
title="Relations" title="Relations"
indicatorElement={indicatorElement} indicatorElement={indicatorElement}
actionItemElement={!disabled && <RelationActionButton issueId={issueId} disabled={disabled} />} actionItemElement={
!disabled && <RelationActionButton issueId={issueId} disabled={disabled} issueServiceType={issueServiceType} />
}
/> />
); );
}); });

View file

@ -1,7 +1,8 @@
"use client"; "use client";
import React, { FC, useCallback, useEffect, useState } from "react"; import React, { FC, useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TIssue } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { TIssue, TIssueServiceType } from "@plane/types";
// components // components
import { DeleteIssueModal } from "@/components/issues/delete-issue-modal"; import { DeleteIssueModal } from "@/components/issues/delete-issue-modal";
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal"; import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
@ -16,12 +17,13 @@ type Props = {
projectId: string; projectId: string;
parentIssueId: string; parentIssueId: string;
disabled: boolean; disabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined }; type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined };
export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => { export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, parentIssueId, disabled } = props; const { workspaceSlug, projectId, parentIssueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// state // state
const [issueCrudState, setIssueCrudState] = useState<{ const [issueCrudState, setIssueCrudState] = useState<{
create: TIssueCrudState; create: TIssueCrudState;
@ -58,7 +60,7 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
} = useIssueDetail(); } = useIssueDetail();
// helpers // helpers
const subIssueOperations = useSubIssueOperations(); const subIssueOperations = useSubIssueOperations(issueServiceType);
const subIssueHelpers = subIssueHelpersByIssueId(`${parentIssueId}_root`); const subIssueHelpers = subIssueHelpersByIssueId(`${parentIssueId}_root`);
// handler // handler
@ -95,7 +97,6 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
useEffect(() => { useEffect(() => {
handleFetchSubIssues(); handleFetchSubIssues();
return () => { return () => {
handleFetchSubIssues(); handleFetchSubIssues();
}; };
@ -123,6 +124,7 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
disabled={!disabled} disabled={!disabled}
handleIssueCrudState={handleIssueCrudState} handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations} subIssueOperations={subIssueOperations}
issueServiceType={issueServiceType}
/> />
)} )}

View file

@ -1,7 +1,8 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { TIssue } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { TIssue, TIssueServiceType } from "@plane/types";
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// helper // helper
import { copyTextToClipboard } from "@/helpers/string.helper"; import { copyTextToClipboard } from "@/helpers/string.helper";
@ -16,15 +17,17 @@ export type TRelationIssueOperations = {
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
}; };
export const useSubIssueOperations = (): TSubIssueOperations => { export const useSubIssueOperations = (
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
): TSubIssueOperations => {
const { const {
subIssues: { setSubIssueHelpers }, subIssues: { setSubIssueHelpers },
fetchSubIssues, fetchSubIssues,
createSubIssues, createSubIssues,
updateSubIssue, updateSubIssue,
removeSubIssue,
deleteSubIssue, deleteSubIssue,
} = useIssueDetail(); } = useIssueDetail();
const { removeSubIssue } = useIssueDetail(issueServiceType);
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
const pathname = usePathname(); const pathname = usePathname();

View file

@ -2,7 +2,8 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { LayersIcon, Plus } from "lucide-react"; import { LayersIcon, Plus } from "lucide-react";
import { TIssue } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { TIssue, TIssueServiceType } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
// hooks // hooks
import { useEventTracker, useIssueDetail } from "@/hooks/store"; import { useEventTracker, useIssueDetail } from "@/hooks/store";
@ -11,10 +12,11 @@ type Props = {
issueId: string; issueId: string;
customButton?: React.ReactNode; customButton?: React.ReactNode;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const SubIssuesActionButton: FC<Props> = observer((props) => { export const SubIssuesActionButton: FC<Props> = observer((props) => {
const { issueId, customButton, disabled = false } = props; const { issueId, customButton, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { const {
issue: { getIssueById }, issue: { getIssueById },
@ -22,7 +24,7 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
toggleSubIssuesModal, toggleSubIssuesModal,
setIssueCrudOperationState, setIssueCrudOperationState,
issueCrudOperationState, issueCrudOperationState,
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
const { setTrackElement } = useEventTracker(); const { setTrackElement } = useEventTracker();
// derived values // derived values

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { Collapsible } from "@plane/ui"; import { Collapsible } from "@plane/ui";
// components // components
import { SubIssuesCollapsibleContent, SubIssuesCollapsibleTitle } from "@/components/issues/issue-detail-widgets"; import { SubIssuesCollapsibleContent, SubIssuesCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
@ -12,13 +14,14 @@ type Props = {
projectId: string; projectId: string;
issueId: string; issueId: string;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const SubIssuesCollapsible: FC<Props> = observer((props) => { export const SubIssuesCollapsible: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props; const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { openWidgets, toggleOpenWidget } = useIssueDetail(); const { openWidgets, toggleOpenWidget } = useIssueDetail(issueServiceType);
// derived state // derived state
const isCollapsibleOpen = openWidgets.includes("sub-issues"); const isCollapsibleOpen = openWidgets.includes("sub-issues");
@ -27,7 +30,14 @@ export const SubIssuesCollapsible: FC<Props> = observer((props) => {
<Collapsible <Collapsible
isOpen={isCollapsibleOpen} isOpen={isCollapsibleOpen}
onToggle={() => toggleOpenWidget("sub-issues")} onToggle={() => toggleOpenWidget("sub-issues")}
title={<SubIssuesCollapsibleTitle isOpen={isCollapsibleOpen} parentIssueId={issueId} disabled={disabled} />} title={
<SubIssuesCollapsibleTitle
isOpen={isCollapsibleOpen}
parentIssueId={issueId}
disabled={disabled}
issueServiceType={issueServiceType}
/>
}
buttonClassName="w-full" buttonClassName="w-full"
> >
<SubIssuesCollapsibleContent <SubIssuesCollapsibleContent
@ -35,6 +45,7 @@ export const SubIssuesCollapsible: FC<Props> = observer((props) => {
projectId={projectId} projectId={projectId}
parentIssueId={issueId} parentIssueId={issueId}
disabled={disabled} disabled={disabled}
issueServiceType={issueServiceType}
/> />
</Collapsible> </Collapsible>
); );

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import React, { FC, useMemo } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { CircularProgressIndicator, CollapsibleButton } from "@plane/ui"; import { CircularProgressIndicator, CollapsibleButton } from "@plane/ui";
// components // components
import { SubIssuesActionButton } from "@/components/issues/issue-detail-widgets"; import { SubIssuesActionButton } from "@/components/issues/issue-detail-widgets";
@ -11,14 +13,15 @@ type Props = {
isOpen: boolean; isOpen: boolean;
parentIssueId: string; parentIssueId: string;
disabled: boolean; disabled: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => { export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, parentIssueId, disabled } = props; const { isOpen, parentIssueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks // store hooks
const { const {
subIssues: { subIssuesByIssueId, stateDistributionByIssueId }, subIssues: { subIssuesByIssueId, stateDistributionByIssueId },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
// derived data // derived data
const subIssuesDistribution = stateDistributionByIssueId(parentIssueId); const subIssuesDistribution = stateDistributionByIssueId(parentIssueId);
@ -32,25 +35,23 @@ export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
const totalCount = subIssues.length; const totalCount = subIssues.length;
const percentage = completedCount && totalCount ? (completedCount / totalCount) * 100 : 0; const percentage = completedCount && totalCount ? (completedCount / totalCount) * 100 : 0;
// indicator element
const indicatorElement = useMemo(
() => (
<div className="flex items-center gap-1.5 text-custom-text-300 text-sm">
<CircularProgressIndicator size={18} percentage={percentage} strokeWidth={3} />
<span>
{completedCount}/{totalCount} Done
</span>
</div>
),
[completedCount, totalCount, percentage]
);
return ( return (
<CollapsibleButton <CollapsibleButton
isOpen={isOpen} isOpen={isOpen}
title="Sub-issues" title="Sub-issues"
indicatorElement={indicatorElement} indicatorElement={
actionItemElement={!disabled && <SubIssuesActionButton issueId={parentIssueId} disabled={disabled} />} <div className="flex items-center gap-1.5 text-custom-text-300 text-sm">
<CircularProgressIndicator size={18} percentage={percentage} strokeWidth={3} />
<span>
{completedCount}/{totalCount} Done
</span>
</div>
}
actionItemElement={
!disabled && (
<SubIssuesActionButton issueId={parentIssueId} disabled={disabled} issueServiceType={issueServiceType} />
)
}
/> />
); );
}); });

View file

@ -2,7 +2,8 @@
import { FC, useMemo } from "react"; import { FC, useMemo } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { IIssueLabel, TIssue } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { IIssueLabel, TIssue, TIssueServiceType } from "@plane/types";
// components // components
import { TOAST_TYPE, setToast } from "@plane/ui"; import { TOAST_TYPE, setToast } from "@plane/ui";
// hooks // hooks
@ -21,6 +22,7 @@ export type TIssueLabel = {
disabled: boolean; disabled: boolean;
isInboxIssue?: boolean; isInboxIssue?: boolean;
onLabelUpdate?: (labelIds: string[]) => void; onLabelUpdate?: (labelIds: string[]) => void;
issueServiceType?: TIssueServiceType;
}; };
export type TLabelOperations = { export type TLabelOperations = {
@ -29,13 +31,21 @@ export type TLabelOperations = {
}; };
export const IssueLabel: FC<TIssueLabel> = observer((props) => { export const IssueLabel: FC<TIssueLabel> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false, isInboxIssue = false, onLabelUpdate } = props; const {
workspaceSlug,
projectId,
issueId,
disabled = false,
isInboxIssue = false,
onLabelUpdate,
issueServiceType = EIssueServiceType.ISSUES,
} = props;
// hooks // hooks
const { updateIssue } = useIssueDetail(); const { updateIssue } = useIssueDetail(issueServiceType);
const { createLabel } = useLabel(); const { createLabel } = useLabel();
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
const { getIssueInboxByIssueId } = useProjectInbox(); const { getIssueInboxByIssueId } = useProjectInbox();
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();

View file

@ -3,8 +3,9 @@
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { EIssueServiceType } from "@plane/constants";
// plane types // plane types
import type { TIssueLinkEditableFields } from "@plane/types"; import type { TIssueLinkEditableFields, TIssueServiceType } from "@plane/types";
// plane ui // plane ui
import { Button, Input, ModalCore } from "@plane/ui"; import { Button, Input, ModalCore } from "@plane/ui";
// hooks // hooks
@ -22,6 +23,7 @@ export type TIssueLinkCreateEditModal = {
isModalOpen: boolean; isModalOpen: boolean;
handleOnClose?: () => void; handleOnClose?: () => void;
linkOperations: TLinkOperationsModal; linkOperations: TLinkOperationsModal;
issueServiceType?: TIssueServiceType;
}; };
const defaultValues: TIssueLinkCreateFormFieldOptions = { const defaultValues: TIssueLinkCreateFormFieldOptions = {
@ -31,7 +33,7 @@ const defaultValues: TIssueLinkCreateFormFieldOptions = {
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observer((props) => { export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observer((props) => {
// props // props
const { isModalOpen, handleOnClose, linkOperations } = props; const { isModalOpen, handleOnClose, linkOperations, issueServiceType = EIssueServiceType.ISSUES } = props;
// react hook form // react hook form
const { const {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
@ -42,7 +44,7 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
defaultValues, defaultValues,
}); });
// store hooks // store hooks
const { issueLinkData: preloadedData, setIssueLinkData } = useIssueDetail(); const { issueLinkData: preloadedData, setIssueLinkData } = useIssueDetail(issueServiceType);
const onClose = () => { const onClose = () => {
setIssueLinkData(null); setIssueLinkData(null);

View file

@ -3,6 +3,8 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react"; import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// ui // ui
import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui"; import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
// helpers // helpers
@ -17,17 +19,18 @@ type TIssueLinkItem = {
linkId: string; linkId: string;
linkOperations: TLinkOperationsModal; linkOperations: TLinkOperationsModal;
isNotAllowed: boolean; isNotAllowed: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => { export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
// props // props
const { linkId, linkOperations, isNotAllowed } = props; const { linkId, linkOperations, isNotAllowed, issueServiceType = EIssueServiceType.ISSUES } = props;
// hooks // hooks
const { const {
toggleIssueLinkModal: toggleIssueLinkModalStore, toggleIssueLinkModal: toggleIssueLinkModalStore,
setIssueLinkData, setIssueLinkData,
link: { getLinkById }, link: { getLinkById },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const linkDetail = getLinkById(linkId); const linkDetail = getLinkById(linkId);
if (!linkDetail) return <></>; if (!linkDetail) return <></>;

View file

@ -1,5 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
// computed // computed
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
import { IssueLinkItem } from "./link-item"; import { IssueLinkItem } from "./link-item";
@ -12,15 +14,16 @@ type TLinkList = {
issueId: string; issueId: string;
linkOperations: TLinkOperationsModal; linkOperations: TLinkOperationsModal;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const LinkList: FC<TLinkList> = observer((props) => { export const LinkList: FC<TLinkList> = observer((props) => {
// props // props
const { issueId, linkOperations, disabled = false } = props; const { issueId, linkOperations, disabled = false, issueServiceType = EIssueServiceType.ISSUES } = props;
// hooks // hooks
const { const {
link: { getLinksByIssueId }, link: { getLinksByIssueId },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
const issueLinks = getLinksByIssueId(issueId); const issueLinks = getLinksByIssueId(issueId);
@ -29,7 +32,13 @@ export const LinkList: FC<TLinkList> = observer((props) => {
return ( return (
<div className="flex flex-col gap-2 py-4"> <div className="flex flex-col gap-2 py-4">
{issueLinks.map((linkId) => ( {issueLinks.map((linkId) => (
<IssueLinkItem key={linkId} linkId={linkId} linkOperations={linkOperations} isNotAllowed={disabled} /> <IssueLinkItem
key={linkId}
linkId={linkId}
linkOperations={linkOperations}
isNotAllowed={disabled}
issueServiceType={issueServiceType}
/>
))} ))}
</div> </div>
); );

View file

@ -55,11 +55,16 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
const issue = issueId ? getIssueById(issueId) : undefined; const issue = issueId ? getIssueById(issueId) : undefined;
// debounced duplicate issues swr // debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectDetails?.id, { const { duplicateIssues } = useDebouncedDuplicateIssues(
name: issue?.name, workspaceSlug,
description_html: getTextContent(issue?.description_html), projectDetails?.workspace.toString(),
issueId: issue?.id, projectDetails?.id,
}); {
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
}
);
useEffect(() => { useEffect(() => {
if (isSubmitting === "submitted") { if (isSubmitting === "submitted") {

View file

@ -88,6 +88,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
isOpen={isParentIssueModalOpen === issueId} isOpen={isParentIssueModalOpen === issueId}
handleClose={() => toggleParentIssueModal(null)} handleClose={() => toggleParentIssueModal(null)}
onChange={(issue: any) => handleParentIssue(issue?.id)} onChange={(issue: any) => handleParentIssue(issue?.id)}
searchEpic
/> />
<button <button
type="button" type="button"

View file

@ -9,7 +9,7 @@ import { TIssue } from "@plane/types";
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui"; import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
// components // components
import { EmptyState } from "@/components/common"; import { EmptyState } from "@/components/common";
import { IssuePeekOverview } from "@/components/issues"; import { IssueDetailsSidebar, IssuePeekOverview } from "@/components/issues";
// constants // constants
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "@/constants/event-tracker"; import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "@/constants/event-tracker";
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
@ -21,7 +21,6 @@ import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/u
import emptyIssue from "@/public/empty-state/issue.svg"; import emptyIssue from "@/public/empty-state/issue.svg";
// local components // local components
import { IssueMainContent } from "./main-content"; import { IssueMainContent } from "./main-content";
import { IssueDetailsSidebar } from "./sidebar";
export type TIssueOperations = { export type TIssueOperations = {
fetch: (workspaceSlug: string, projectId: string, issueId: string, loader?: boolean) => Promise<void>; fetch: (workspaceSlug: string, projectId: string, issueId: string, loader?: boolean) => Promise<void>;

View file

@ -25,18 +25,20 @@ export type CalendarStoreType =
| EIssuesStoreType.CYCLE | EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW | EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.TEAM | EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW; | EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.EPIC;
interface IBaseCalendarRoot { interface IBaseCalendarRoot {
QuickActions: FC<IQuickActionProps>; QuickActions: FC<IQuickActionProps>;
addIssuesToView?: (issueIds: string[]) => Promise<any>; addIssuesToView?: (issueIds: string[]) => Promise<any>;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
viewId?: string | undefined; viewId?: string | undefined;
isEpic?: boolean;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean; canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
} }
export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
const { QuickActions, addIssuesToView, isCompletedCycle = false, viewId, canEditPropertiesBasedOnProject } = props; const { QuickActions, addIssuesToView, isCompletedCycle = false, viewId, isEpic = false, canEditPropertiesBasedOnProject } = props;
// router // router
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
@ -173,6 +175,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
updateFilters={updateFilters} updateFilters={updateFilters}
handleDragAndDrop={handleDragAndDrop} handleDragAndDrop={handleDragAndDrop}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
isEpic={isEpic}
/> />
</div> </div>
</> </>

View file

@ -29,6 +29,7 @@ import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { useIssues } from "@/hooks/store"; import { useIssues } from "@/hooks/store";
import useSize from "@/hooks/use-window-size"; import useSize from "@/hooks/use-window-size";
// store // store
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
import { ICycleIssuesFilter } from "@/store/issue/cycle"; import { ICycleIssuesFilter } from "@/store/issue/cycle";
import { ICalendarStore } from "@/store/issue/issue_calendar_view.store"; import { ICalendarStore } from "@/store/issue/issue_calendar_view.store";
import { IModuleIssuesFilter } from "@/store/issue/module"; import { IModuleIssuesFilter } from "@/store/issue/module";
@ -39,7 +40,12 @@ import { TRenderQuickActions } from "../list/list-view-types";
import type { ICalendarWeek } from "./types"; import type { ICalendarWeek } from "./types";
type Props = { type Props = {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; issuesFilterStore:
| IProjectIssuesFilter
| IModuleIssuesFilter
| ICycleIssuesFilter
| IProjectViewIssuesFilter
| IProjectEpicsFilter;
issues: TIssueMap | undefined; issues: TIssueMap | undefined;
groupedIssueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
layout: "month" | "week" | undefined; layout: "month" | "week" | undefined;
@ -64,6 +70,7 @@ type Props = {
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
) => Promise<void>; ) => Promise<void>;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
isEpic?: boolean;
}; };
export const CalendarChart: React.FC<Props> = observer((props) => { export const CalendarChart: React.FC<Props> = observer((props) => {
@ -84,6 +91,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
updateFilters, updateFilters,
canEditProperties, canEditProperties,
readOnly = false, readOnly = false,
isEpic = false,
} = props; } = props;
// states // states
const [selectedDate, setSelectedDate] = useState<Date>(new Date()); const [selectedDate, setSelectedDate] = useState<Date>(new Date());
@ -167,6 +175,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
readOnly={readOnly} readOnly={readOnly}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
isEpic={isEpic}
/> />
))} ))}
</div> </div>
@ -190,6 +199,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
readOnly={readOnly} readOnly={readOnly}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
isEpic={isEpic}
/> />
)} )}
</div> </div>
@ -216,6 +226,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
isDragDisabled isDragDisabled
isMobileView isMobileView
isEpic={isEpic}
/> />
</div> </div>
</div> </div>
@ -243,6 +254,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
isDragDisabled isDragDisabled
isMobileView isMobileView
isEpic={isEpic}
/> />
</div> </div>
</div> </div>

View file

@ -18,6 +18,7 @@ import { MONTHS_LIST } from "@/constants/calendar";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// types // types
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
import { ICycleIssuesFilter } from "@/store/issue/cycle"; import { ICycleIssuesFilter } from "@/store/issue/cycle";
import { IModuleIssuesFilter } from "@/store/issue/module"; import { IModuleIssuesFilter } from "@/store/issue/module";
import { IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectIssuesFilter } from "@/store/issue/project";
@ -25,7 +26,12 @@ import { IProjectViewIssuesFilter } from "@/store/issue/project-views";
import { TRenderQuickActions } from "../list/list-view-types"; import { TRenderQuickActions } from "../list/list-view-types";
type Props = { type Props = {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; issuesFilterStore:
| IProjectIssuesFilter
| IModuleIssuesFilter
| ICycleIssuesFilter
| IProjectViewIssuesFilter
| IProjectEpicsFilter;
date: ICalendarDate; date: ICalendarDate;
issues: TIssueMap | undefined; issues: TIssueMap | undefined;
groupedIssueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
@ -47,6 +53,7 @@ type Props = {
selectedDate: Date; selectedDate: Date;
setSelectedDate: (date: Date) => void; setSelectedDate: (date: Date) => void;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
isEpic?: boolean;
}; };
export const CalendarDayTile: React.FC<Props> = observer((props) => { export const CalendarDayTile: React.FC<Props> = observer((props) => {
@ -68,6 +75,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
handleDragAndDrop, handleDragAndDrop,
setSelectedDate, setSelectedDate,
canEditProperties, canEditProperties,
isEpic = false,
} = props; } = props;
const [isDraggingOver, setIsDraggingOver] = useState(false); const [isDraggingOver, setIsDraggingOver] = useState(false);
@ -185,6 +193,7 @@ export const CalendarDayTile: React.FC<Props> = observer((props) => {
quickAddCallback={quickAddCallback} quickAddCallback={quickAddCallback}
readOnly={readOnly} readOnly={readOnly}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
isEpic={isEpic}
/> />
</div> </div>
</div> </div>

View file

@ -9,6 +9,7 @@ import { Popover, Transition } from "@headlessui/react";
import { MONTHS_LIST } from "@/constants/calendar"; import { MONTHS_LIST } from "@/constants/calendar";
import { getDate } from "@/helpers/date-time.helper"; import { getDate } from "@/helpers/date-time.helper";
import { useCalendarView } from "@/hooks/store"; import { useCalendarView } from "@/hooks/store";
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
import { ICycleIssuesFilter } from "@/store/issue/cycle"; import { ICycleIssuesFilter } from "@/store/issue/cycle";
import { IModuleIssuesFilter } from "@/store/issue/module"; import { IModuleIssuesFilter } from "@/store/issue/module";
import { IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectIssuesFilter } from "@/store/issue/project";
@ -16,7 +17,12 @@ import { IProjectViewIssuesFilter } from "@/store/issue/project-views";
// helpers // helpers
interface Props { interface Props {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; issuesFilterStore:
| IProjectIssuesFilter
| IModuleIssuesFilter
| ICycleIssuesFilter
| IProjectViewIssuesFilter
| IProjectEpicsFilter;
} }
export const CalendarMonthsDropdown: React.FC<Props> = observer((props: Props) => { export const CalendarMonthsDropdown: React.FC<Props> = observer((props: Props) => {
const { issuesFilterStore } = props; const { issuesFilterStore } = props;

View file

@ -23,13 +23,19 @@ import { CALENDAR_LAYOUTS } from "@/constants/calendar";
import { EIssueFilterType } from "@/constants/issue"; import { EIssueFilterType } from "@/constants/issue";
import { useCalendarView } from "@/hooks/store"; import { useCalendarView } from "@/hooks/store";
import useSize from "@/hooks/use-window-size"; import useSize from "@/hooks/use-window-size";
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
import { ICycleIssuesFilter } from "@/store/issue/cycle"; import { ICycleIssuesFilter } from "@/store/issue/cycle";
import { IModuleIssuesFilter } from "@/store/issue/module"; import { IModuleIssuesFilter } from "@/store/issue/module";
import { IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectIssuesFilter } from "@/store/issue/project";
import { IProjectViewIssuesFilter } from "@/store/issue/project-views"; import { IProjectViewIssuesFilter } from "@/store/issue/project-views";
interface ICalendarHeader { interface ICalendarHeader {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; issuesFilterStore:
| IProjectIssuesFilter
| IModuleIssuesFilter
| ICycleIssuesFilter
| IProjectViewIssuesFilter
| IProjectEpicsFilter;
updateFilters?: ( updateFilters?: (
projectId: string, projectId: string,
filterType: EIssueFilterType, filterType: EIssueFilterType,

View file

@ -13,13 +13,19 @@ import { CalendarMonthsDropdown, CalendarOptionsDropdown } from "@/components/is
// icons // icons
import { EIssueFilterType } from "@/constants/issue"; import { EIssueFilterType } from "@/constants/issue";
import { useCalendarView } from "@/hooks/store/use-calendar-view"; import { useCalendarView } from "@/hooks/store/use-calendar-view";
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
import { ICycleIssuesFilter } from "@/store/issue/cycle"; import { ICycleIssuesFilter } from "@/store/issue/cycle";
import { IModuleIssuesFilter } from "@/store/issue/module"; import { IModuleIssuesFilter } from "@/store/issue/module";
import { IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectIssuesFilter } from "@/store/issue/project";
import { IProjectViewIssuesFilter } from "@/store/issue/project-views"; import { IProjectViewIssuesFilter } from "@/store/issue/project-views";
interface ICalendarHeader { interface ICalendarHeader {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; issuesFilterStore:
| IProjectIssuesFilter
| IModuleIssuesFilter
| ICycleIssuesFilter
| IProjectViewIssuesFilter
| IProjectEpicsFilter;
updateFilters?: ( updateFilters?: (
projectId: string, projectId: string,
filterType: EIssueFilterType, filterType: EIssueFilterType,

View file

@ -15,11 +15,12 @@ type Props = {
issueId: string; issueId: string;
quickActions: TRenderQuickActions; quickActions: TRenderQuickActions;
isDragDisabled: boolean; isDragDisabled: boolean;
isEpic?: boolean;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
}; };
export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => { export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => {
const { issueId, quickActions, isDragDisabled, canEditProperties } = props; const { issueId, quickActions, isDragDisabled, isEpic = false, canEditProperties } = props;
const issueRef = useRef<HTMLAnchorElement | null>(null); const issueRef = useRef<HTMLAnchorElement | null>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@ -58,5 +59,13 @@ export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => {
if (!issue) return null; if (!issue) return null;
return <CalendarIssueBlock isDragging={isDragging} issue={issue} quickActions={quickActions} ref={issueRef} />; return (
<CalendarIssueBlock
isDragging={isDragging}
issue={issue}
quickActions={quickActions}
ref={issueRef}
isEpic={isEpic}
/>
);
}); });

View file

@ -28,11 +28,12 @@ type Props = {
issue: TIssue; issue: TIssue;
quickActions: TRenderQuickActions; quickActions: TRenderQuickActions;
isDragging?: boolean; isDragging?: boolean;
isEpic?: boolean;
}; };
export const CalendarIssueBlock = observer( export const CalendarIssueBlock = observer(
forwardRef<HTMLAnchorElement, Props>((props, ref) => { forwardRef<HTMLAnchorElement, Props>((props, ref) => {
const { issue, quickActions, isDragging = false } = props; const { issue, quickActions, isDragging = false, isEpic = false } = props;
// states // states
const [isMenuActive, setIsMenuActive] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false);
// refs // refs
@ -42,7 +43,7 @@ export const CalendarIssueBlock = observer(
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
const { getProjectStates } = useProjectState(); const { getProjectStates } = useProjectState();
const { getIsIssuePeeked } = useIssueDetail(); const { getIsIssuePeeked } = useIssueDetail();
const { handleRedirection } = useIssuePeekOverviewRedirection(); const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const storeType = useIssueStoreType() as CalendarStoreType; const storeType = useIssueStoreType() as CalendarStoreType;
const { issuesFilter } = useIssues(storeType); const { issuesFilter } = useIssues(storeType);

View file

@ -23,6 +23,7 @@ type Props = {
readOnly?: boolean; readOnly?: boolean;
isMobileView?: boolean; isMobileView?: boolean;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
isEpic?: boolean;
}; };
export const CalendarIssueBlocks: React.FC<Props> = observer((props) => { export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
@ -39,6 +40,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
readOnly, readOnly,
isMobileView = false, isMobileView = false,
canEditProperties, canEditProperties,
isEpic = false,
} = props; } = props;
const formattedDatePayload = renderFormattedPayloadDate(date); const formattedDatePayload = renderFormattedPayloadDate(date);
@ -66,6 +68,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
quickActions={quickActions} quickActions={quickActions}
isDragDisabled={isDragDisabled || isMobileView} isDragDisabled={isDragDisabled || isMobileView}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
isEpic={isEpic}
/> />
</div> </div>
))} ))}

View file

@ -5,6 +5,7 @@ import { CalendarDayTile } from "@/components/issues";
// helpers // helpers
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// types // types
import { IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
import { ICycleIssuesFilter } from "@/store/issue/cycle"; import { ICycleIssuesFilter } from "@/store/issue/cycle";
import { IModuleIssuesFilter } from "@/store/issue/module"; import { IModuleIssuesFilter } from "@/store/issue/module";
import { IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectIssuesFilter } from "@/store/issue/project";
@ -13,7 +14,12 @@ import { TRenderQuickActions } from "../list/list-view-types";
import { ICalendarDate, ICalendarWeek } from "./types"; import { ICalendarDate, ICalendarWeek } from "./types";
type Props = { type Props = {
issuesFilterStore: IProjectIssuesFilter | IModuleIssuesFilter | ICycleIssuesFilter | IProjectViewIssuesFilter; issuesFilterStore:
| IProjectIssuesFilter
| IModuleIssuesFilter
| ICycleIssuesFilter
| IProjectViewIssuesFilter
| IProjectEpicsFilter;
issues: TIssueMap | undefined; issues: TIssueMap | undefined;
groupedIssueIds: TGroupedIssues; groupedIssueIds: TGroupedIssues;
week: ICalendarWeek | undefined; week: ICalendarWeek | undefined;
@ -35,6 +41,7 @@ type Props = {
selectedDate: Date; selectedDate: Date;
setSelectedDate: (date: Date) => void; setSelectedDate: (date: Date) => void;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
isEpic?: boolean;
}; };
export const CalendarWeekDays: React.FC<Props> = observer((props) => { export const CalendarWeekDays: React.FC<Props> = observer((props) => {
@ -56,6 +63,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
selectedDate, selectedDate,
setSelectedDate, setSelectedDate,
canEditProperties, canEditProperties,
isEpic = false,
} = props; } = props;
const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month"; const calendarLayout = issuesFilterStore?.issueFilters?.displayFilters?.calendar?.layout ?? "month";
@ -92,6 +100,7 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
readOnly={readOnly} readOnly={readOnly}
handleDragAndDrop={handleDragAndDrop} handleDragAndDrop={handleDragAndDrop}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
isEpic={isEpic}
/> />
); );
})} })}

View file

@ -6,6 +6,7 @@ import { ProjectDraftEmptyState } from "./draft-issues";
import { GlobalViewEmptyState } from "./global-view"; import { GlobalViewEmptyState } from "./global-view";
import { ModuleEmptyState } from "./module"; import { ModuleEmptyState } from "./module";
import { ProfileViewEmptyState } from "./profile-view"; import { ProfileViewEmptyState } from "./profile-view";
import { ProjectEpicsEmptyState } from "./project-epic";
import { ProjectEmptyState } from "./project-issues"; import { ProjectEmptyState } from "./project-issues";
import { ProjectViewEmptyState } from "./project-view"; import { ProjectViewEmptyState } from "./project-view";
@ -31,6 +32,8 @@ export const IssueLayoutEmptyState = (props: Props) => {
return <GlobalViewEmptyState />; return <GlobalViewEmptyState />;
case EIssuesStoreType.PROFILE: case EIssuesStoreType.PROFILE:
return <ProfileViewEmptyState />; return <ProfileViewEmptyState />;
case EIssuesStoreType.EPIC:
return <ProjectEpicsEmptyState />;
default: default:
return null; return null;
} }

View file

@ -0,0 +1,12 @@
// types
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
// hooks
export const ProjectEpicsEmptyState: React.FC = () => (
<div className="relative h-full w-full overflow-y-auto">
<EmptyState type={EmptyStateType.PROJECT_NO_EPICS} primaryButtonOnClick={() => {}} />
</div>
);

View file

@ -1,29 +1,32 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// types
import { IIssueFilterOptions } from "@plane/types"; import { IIssueFilterOptions } from "@plane/types";
// hooks // ui
// components
import { Header, EHeaderVariant } from "@plane/ui"; import { Header, EHeaderVariant } from "@plane/ui";
// components
import { AppliedFiltersList, SaveFilterView } from "@/components/issues"; import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
// constants // constants
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
// hooks
import { useLabel, useProjectState, useUserPermissions } from "@/hooks/store"; import { useLabel, useProjectState, useUserPermissions } from "@/hooks/store";
import { useIssues } from "@/hooks/store/use-issues"; import { useIssues } from "@/hooks/store/use-issues";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// types type TProjectAppliedFiltersRootProps = {
storeType?: EIssuesStoreType.PROJECT | EIssuesStoreType.EPIC;
};
export const ProjectAppliedFiltersRoot: React.FC = observer(() => { export const ProjectAppliedFiltersRoot: React.FC<TProjectAppliedFiltersRootProps> = observer((props) => {
const { storeType = EIssuesStoreType.PROJECT } = props;
// router // router
const { workspaceSlug, projectId } = useParams() as { const { workspaceSlug, projectId } = useParams();
workspaceSlug: string;
projectId: string;
};
// store hooks // store hooks
const { projectLabels } = useLabel(); const { projectLabels } = useLabel();
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT); } = useIssues(storeType);
const { allowPermissions } = useUserPermissions(); const { allowPermissions } = useUserPermissions();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
@ -84,8 +87,8 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
<Header.RightItem> <Header.RightItem>
{isEditingAllowed && ( {isEditingAllowed && (
<SaveFilterView <SaveFilterView
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug?.toString()}
projectId={projectId} projectId={projectId?.toString()}
filterParams={{ filterParams={{
filters: appliedFilters, filters: appliedFilters,
display_filters: issueFilters?.displayFilters, display_filters: issueFilters?.displayFilters,

View file

@ -27,16 +27,18 @@ import { IssueLayoutHOC } from "../issue-layout-HOC";
interface IBaseGanttRoot { interface IBaseGanttRoot {
viewId?: string | undefined; viewId?: string | undefined;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
isEpic?: boolean;
} }
export type GanttStoreType = export type GanttStoreType =
| EIssuesStoreType.PROJECT | EIssuesStoreType.PROJECT
| EIssuesStoreType.MODULE | EIssuesStoreType.MODULE
| EIssuesStoreType.CYCLE | EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW; | EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.EPIC;
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => { export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
const { viewId, isCompletedCycle = false } = props; const { viewId, isCompletedCycle = false, isEpic = false } = props;
// router // router
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
@ -123,8 +125,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
loaderTitle="Issues" loaderTitle="Issues"
blockIds={issuesIds} blockIds={issuesIds}
blockUpdateHandler={updateIssueBlockStructure} blockUpdateHandler={updateIssueBlockStructure}
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} />} blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} isEpic={isEpic} />}
sidebarToRender={(props) => <IssueGanttSidebar {...props} showAllBlocks />} sidebarToRender={(props) => <IssueGanttSidebar {...props} showAllBlocks isEpic={isEpic} />}
enableBlockLeftResize={isAllowed} enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed} enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed} enableBlockMove={isAllowed}
@ -136,6 +138,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
canLoadMoreBlocks={nextPageResults} canLoadMoreBlocks={nextPageResults}
updateBlockDates={updateBlockDates} updateBlockDates={updateBlockDates}
showAllBlocks showAllBlocks
isEpic={isEpic}
/> />
</div> </div>
</TimeLineTypeContext.Provider> </TimeLineTypeContext.Provider>

View file

@ -21,10 +21,11 @@ import { GanttStoreType } from "./base-gantt-root";
type Props = { type Props = {
issueId: string; issueId: string;
isEpic?: boolean;
}; };
export const IssueGanttBlock: React.FC<Props> = observer((props) => { export const IssueGanttBlock: React.FC<Props> = observer((props) => {
const { issueId } = props; const { issueId, isEpic } = props;
// router // router
const { workspaceSlug: routerWorkspaceSlug } = useParams(); const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString(); const workspaceSlug = routerWorkspaceSlug?.toString();
@ -35,7 +36,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
} = useIssueDetail(); } = useIssueDetail();
// hooks // hooks
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const { handleRedirection } = useIssuePeekOverviewRedirection(); const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
// derived values // derived values
const issueDetails = getIssueById(issueId); const issueDetails = getIssueById(issueId);
@ -78,7 +79,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
// rendering issues on gantt sidebar // rendering issues on gantt sidebar
export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => { export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { issueId } = props; const { issueId, isEpic = false } = props;
// router // router
const { workspaceSlug: routerWorkspaceSlug } = useParams(); const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString(); const workspaceSlug = routerWorkspaceSlug?.toString();
@ -91,7 +92,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { issuesFilter } = useIssues(storeType); const { issuesFilter } = useIssues(storeType);
// handlers // handlers
const { handleRedirection } = useIssuePeekOverviewRedirection(); const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
// derived values // derived values
const issueDetails = getIssueById(issueId); const issueDetails = getIssueById(issueId);
@ -105,7 +106,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
return ( return (
<ControlLink <ControlLink
id={`issue-${issueId}`} id={`issue-${issueId}`}
href={`/${workspaceSlug}/projects/${issueDetails?.project_id}/issues/${issueDetails?.id}`} href={`/${workspaceSlug}/projects/${issueDetails?.project_id}/${isEpic ? "epics" : "issues"}/${issueDetails?.id}`}
onClick={handleIssuePeekOverview} onClick={handleIssuePeekOverview}
className="line-clamp-1 w-full cursor-pointer text-sm text-custom-text-100" className="line-clamp-1 w-full cursor-pointer text-sm text-custom-text-100"
disabled={!!issueDetails?.tempId} disabled={!!issueDetails?.tempId}

View file

@ -6,6 +6,7 @@ import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
import { EIssueServiceType } from "@plane/constants";
import { DeleteIssueModal } from "@/components/issues"; import { DeleteIssueModal } from "@/components/issues";
//constants //constants
import { ISSUE_DELETED } from "@/constants/event-tracker"; import { ISSUE_DELETED } from "@/constants/event-tracker";
@ -34,7 +35,8 @@ export type KanbanStoreType =
| EIssuesStoreType.DRAFT | EIssuesStoreType.DRAFT
| EIssuesStoreType.PROFILE | EIssuesStoreType.PROFILE
| EIssuesStoreType.TEAM | EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW; | EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.EPIC;
export interface IBaseKanBanLayout { export interface IBaseKanBanLayout {
QuickActions: FC<IQuickActionProps>; QuickActions: FC<IQuickActionProps>;
@ -42,10 +44,18 @@ export interface IBaseKanBanLayout {
canEditPropertiesBasedOnProject?: (projectId: string) => boolean; canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
viewId?: string | undefined; viewId?: string | undefined;
isEpic?: boolean;
} }
export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => { export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBaseKanBanLayout) => {
const { QuickActions, addIssuesToView, canEditPropertiesBasedOnProject, isCompletedCycle = false, viewId } = props; const {
QuickActions,
addIssuesToView,
canEditPropertiesBasedOnProject,
isCompletedCycle = false,
viewId,
isEpic = false,
} = props;
// router // router
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
const pathname = usePathname(); const pathname = usePathname();
@ -56,7 +66,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const { issueMap, issuesFilter, issues } = useIssues(storeType); const { issueMap, issuesFilter, issues } = useIssues(storeType);
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const { const {
fetchIssues, fetchIssues,
fetchNextIssues, fetchNextIssues,
@ -275,6 +285,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
handleOnDrop={handleOnDrop} handleOnDrop={handleOnDrop}
loadMoreIssues={fetchMoreIssues} loadMoreIssues={fetchMoreIssues}
isEpic={isEpic}
/> />
</div> </div>
</div> </div>

View file

@ -26,6 +26,7 @@ import { IssueIdentifier } from "@/plane-web/components/issues";
import { TRenderQuickActions } from "../list/list-view-types"; import { TRenderQuickActions } from "../list/list-view-types";
import { IssueProperties } from "../properties/all-properties"; import { IssueProperties } from "../properties/all-properties";
import { getIssueBlockId } from "../utils"; import { getIssueBlockId } from "../utils";
import { EIssueServiceType } from "@plane/constants";
interface IssueBlockProps { interface IssueBlockProps {
issueId: string; issueId: string;
@ -41,6 +42,7 @@ interface IssueBlockProps {
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
shouldRenderByDefault?: boolean; shouldRenderByDefault?: boolean;
isEpic?: boolean;
} }
interface IssueDetailsBlockProps { interface IssueDetailsBlockProps {
@ -50,10 +52,11 @@ interface IssueDetailsBlockProps {
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined; updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: TRenderQuickActions; quickActions: TRenderQuickActions;
isReadOnly: boolean; isReadOnly: boolean;
isEpic?: boolean;
} }
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props) => { const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props) => {
const { cardRef, issue, updateIssue, quickActions, isReadOnly, displayProperties } = props; const { cardRef, issue, updateIssue, quickActions, isReadOnly, displayProperties, isEpic = false } = props;
// hooks // hooks
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
@ -99,6 +102,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
activeLayout="Kanban" activeLayout="Kanban"
updateIssue={updateIssue} updateIssue={updateIssue}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
isEpic={isEpic}
/> />
</> </>
); );
@ -118,6 +122,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
canEditProperties, canEditProperties,
scrollableContainerRef, scrollableContainerRef,
shouldRenderByDefault, shouldRenderByDefault,
isEpic = false,
} = props; } = props;
const cardRef = useRef<HTMLAnchorElement | null>(null); const cardRef = useRef<HTMLAnchorElement | null>(null);
@ -125,8 +130,8 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
const { workspaceSlug: routerWorkspaceSlug } = useParams(); const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString(); const workspaceSlug = routerWorkspaceSlug?.toString();
// hooks // hooks
const { getIsIssuePeeked } = useIssueDetail(); const { getIsIssuePeeked } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const { handleRedirection } = useIssuePeekOverviewRedirection(); const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
// handlers // handlers
@ -210,7 +215,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
> >
<ControlLink <ControlLink
id={getIssueBlockId(issueId, groupId, subGroupId)} id={getIssueBlockId(issueId, groupId, subGroupId)}
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${ href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${isEpic ? "epics" : "issues"}/${
issue.id issue.id
}`} }`}
ref={cardRef} ref={cardRef}
@ -238,6 +243,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
updateIssue={updateIssue} updateIssue={updateIssue}
quickActions={quickActions} quickActions={quickActions}
isReadOnly={!canEditIssueProperties} isReadOnly={!canEditIssueProperties}
isEpic={isEpic}
/> />
</RenderIfVisible> </RenderIfVisible>
</ControlLink> </ControlLink>

View file

@ -18,6 +18,7 @@ interface IssueBlocksListProps {
canDropOverIssue: boolean; canDropOverIssue: boolean;
canDragIssuesInCurrentGrouping: boolean; canDragIssuesInCurrentGrouping: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isEpic?: boolean;
} }
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = observer((props) => { export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = observer((props) => {
@ -33,6 +34,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = observer((p
quickActions, quickActions,
canEditProperties, canEditProperties,
scrollableContainerRef, scrollableContainerRef,
isEpic = false,
} = props; } = props;
return ( return (
@ -62,6 +64,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = observer((p
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping} canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isEpic={isEpic}
/> />
); );
})} })}

View file

@ -58,6 +58,7 @@ export interface IKanBan {
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
showEmptyGroup?: boolean; showEmptyGroup?: boolean;
subGroupIndex?: number; subGroupIndex?: number;
isEpic?: boolean;
} }
export const KanBan: React.FC<IKanBan> = observer((props) => { export const KanBan: React.FC<IKanBan> = observer((props) => {
@ -86,6 +87,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
isDropDisabled, isDropDisabled,
dropErrorMessage, dropErrorMessage,
subGroupIndex = 0, subGroupIndex = 0,
isEpic = false,
} = props; } = props;
// store hooks // store hooks
const storeType = useIssueStoreType(); const storeType = useIssueStoreType();
@ -164,6 +166,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
collapsedGroups={collapsedGroups} collapsedGroups={collapsedGroups}
handleCollapsedGroups={handleCollapsedGroups} handleCollapsedGroups={handleCollapsedGroups}
isEpic={isEpic}
/> />
</div> </div>
)} )}
@ -207,6 +210,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
handleOnDrop={handleOnDrop} handleOnDrop={handleOnDrop}
isEpic={isEpic}
/> />
</RenderIfVisible> </RenderIfVisible>
)} )}

View file

@ -15,6 +15,8 @@ import { CreateUpdateIssueModal } from "@/components/issues";
// hooks // hooks
import { useEventTracker } from "@/hooks/store"; import { useEventTracker } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { CreateUpdateEpicModal } from "@/plane-web/components/epics/epic-modal";
// types
// Plane-web // Plane-web
import { WorkFlowGroupTree } from "@/plane-web/components/workflow"; import { WorkFlowGroupTree } from "@/plane-web/components/workflow";
@ -30,6 +32,7 @@ interface IHeaderGroupByCard {
issuePayload: Partial<TIssue>; issuePayload: Partial<TIssue>;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
isEpic?: boolean;
} }
export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => { export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
@ -45,6 +48,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
issuePayload, issuePayload,
disableIssueCreation, disableIssueCreation,
addIssuesToView, addIssuesToView,
isEpic = false,
} = props; } = props;
const verticalAlignPosition = sub_group_by ? false : collapsedGroups?.group_by.includes(column_id); const verticalAlignPosition = sub_group_by ? false : collapsedGroups?.group_by.includes(column_id);
// states // states
@ -86,13 +90,17 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
return ( return (
<> <>
<CreateUpdateIssueModal {isEpic ? (
isOpen={isOpen} <CreateUpdateEpicModal isOpen={isOpen} onClose={() => setIsOpen(false)} data={issuePayload} />
onClose={() => setIsOpen(false)} ) : (
data={issuePayload} <CreateUpdateIssueModal
storeType={storeType} isOpen={isOpen}
isDraft={isDraftIssue} onClose={() => setIsOpen(false)}
/> data={issuePayload}
storeType={storeType}
isDraft={isDraftIssue}
/>
)}
{renderExistingIssueModal && ( {renderExistingIssueModal && (
<ExistingIssuesListModal <ExistingIssuesListModal

View file

@ -56,6 +56,7 @@ interface IKanbanGroup {
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
orderBy: TIssueOrderByOptions | undefined; orderBy: TIssueOrderByOptions | undefined;
isEpic?: boolean;
} }
export const KanbanGroup = observer((props: IKanbanGroup) => { export const KanbanGroup = observer((props: IKanbanGroup) => {
@ -79,6 +80,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
quickAddCallback, quickAddCallback,
scrollableContainerRef, scrollableContainerRef,
handleOnDrop, handleOnDrop,
isEpic =false
} = props; } = props;
// hooks // hooks
const projectState = useProjectState(); const projectState = useProjectState();
@ -294,6 +296,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
canDropOverIssue={!canOverlayBeVisible} canDropOverIssue={!canOverlayBeVisible}
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping} canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
isEpic={isEpic}
/> />
{shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)} {shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)}

View file

@ -28,7 +28,8 @@ type ListStoreType =
| EIssuesStoreType.ARCHIVED | EIssuesStoreType.ARCHIVED
| EIssuesStoreType.WORKSPACE_DRAFT | EIssuesStoreType.WORKSPACE_DRAFT
| EIssuesStoreType.TEAM | EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW; | EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.EPIC;
interface IBaseListRoot { interface IBaseListRoot {
QuickActions: FC<IQuickActionProps>; QuickActions: FC<IQuickActionProps>;
@ -36,9 +37,17 @@ interface IBaseListRoot {
canEditPropertiesBasedOnProject?: (projectId: string) => boolean; canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
viewId?: string | undefined; viewId?: string | undefined;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
isEpic?: boolean;
} }
export const BaseListRoot = observer((props: IBaseListRoot) => { export const BaseListRoot = observer((props: IBaseListRoot) => {
const { QuickActions, viewId, addIssuesToView, canEditPropertiesBasedOnProject, isCompletedCycle = false } = props; const {
QuickActions,
viewId,
addIssuesToView,
canEditPropertiesBasedOnProject,
isCompletedCycle = false,
isEpic = false,
} = props;
// router // router
const storeType = useIssueStoreType() as ListStoreType; const storeType = useIssueStoreType() as ListStoreType;
//stores //stores
@ -157,6 +166,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
handleOnDrop={handleOnDrop} handleOnDrop={handleOnDrop}
handleCollapsedGroups={handleCollapsedGroups} handleCollapsedGroups={handleCollapsedGroups}
collapsedGroups={collapsedGroups} collapsedGroups={collapsedGroups}
isEpic={isEpic}
/> />
</div> </div>
</IssueLayoutHOC> </IssueLayoutHOC>

View file

@ -5,6 +5,7 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item"; import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
// plane helpers // plane helpers
import { useOutsideClickDetector } from "@plane/hooks"; import { useOutsideClickDetector } from "@plane/hooks";
// types // types
@ -39,6 +40,7 @@ type Props = {
isParentIssueBeingDragged?: boolean; isParentIssueBeingDragged?: boolean;
isLastChild?: boolean; isLastChild?: boolean;
shouldRenderByDefault?: boolean; shouldRenderByDefault?: boolean;
isEpic?: boolean;
}; };
export const IssueBlockRoot: FC<Props> = observer((props) => { export const IssueBlockRoot: FC<Props> = observer((props) => {
@ -59,6 +61,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
isLastChild = false, isLastChild = false,
selectionHelpers, selectionHelpers,
shouldRenderByDefault, shouldRenderByDefault,
isEpic = false,
} = props; } = props;
// states // states
const [isExpanded, setExpanded] = useState<boolean>(false); const [isExpanded, setExpanded] = useState<boolean>(false);
@ -69,7 +72,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
// hooks // hooks
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
// store hooks // store hooks
const { subIssues: subIssuesStore } = useIssueDetail(); const { subIssues: subIssuesStore } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const isSubIssue = nestingLevel !== 0; const isSubIssue = nestingLevel !== 0;
@ -150,10 +153,12 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
canDrag={!isSubIssue && isDragAllowed} canDrag={!isSubIssue && isDragAllowed}
isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging} isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging}
setIsCurrentBlockDragging={setIsCurrentBlockDragging} setIsCurrentBlockDragging={setIsCurrentBlockDragging}
isEpic={isEpic}
/> />
</RenderIfVisible> </RenderIfVisible>
{isExpanded && {isExpanded &&
!isEpic &&
subIssues?.map((subIssueId) => ( subIssues?.map((subIssueId) => (
<IssueBlockRoot <IssueBlockRoot
key={`${subIssueId}`} key={`${subIssueId}`}

View file

@ -6,6 +6,7 @@ import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
// types // types
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types"; import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
// ui // ui
@ -40,6 +41,7 @@ interface IssueBlockProps {
isCurrentBlockDragging: boolean; isCurrentBlockDragging: boolean;
setIsCurrentBlockDragging: React.Dispatch<React.SetStateAction<boolean>>; setIsCurrentBlockDragging: React.Dispatch<React.SetStateAction<boolean>>;
canDrag: boolean; canDrag: boolean;
isEpic?: boolean;
} }
export const IssueBlock = observer((props: IssueBlockProps) => { export const IssueBlock = observer((props: IssueBlockProps) => {
@ -59,6 +61,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
isCurrentBlockDragging, isCurrentBlockDragging,
setIsCurrentBlockDragging, setIsCurrentBlockDragging,
canDrag, canDrag,
isEpic = false,
} = props; } = props;
// ref // ref
const issueRef = useRef<HTMLDivElement | null>(null); const issueRef = useRef<HTMLDivElement | null>(null);
@ -69,7 +72,12 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
// hooks // hooks
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
const { getProjectIdentifierById } = useProject(); const { getProjectIdentifierById } = useProject();
const { getIsIssuePeeked, peekIssue, setPeekIssue, subIssues: subIssuesStore } = useIssueDetail(); const {
getIsIssuePeeked,
peekIssue,
setPeekIssue,
subIssues: subIssuesStore,
} = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const handleIssuePeekOverview = (issue: TIssue) => const handleIssuePeekOverview = (issue: TIssue) =>
workspaceSlug && workspaceSlug &&
@ -143,7 +151,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
return ( return (
<ControlLink <ControlLink
id={`issue-${issue.id}`} id={`issue-${issue.id}`}
href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${issue.id}`} href={`/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${isEpic ? "epics" : "issues"}/${issue.id}`}
onClick={() => handleIssuePeekOverview(issue)} onClick={() => handleIssuePeekOverview(issue)}
className="w-full cursor-pointer" className="w-full cursor-pointer"
disabled={!!issue?.tempId || issue?.is_draft} disabled={!!issue?.tempId || issue?.is_draft}
@ -178,7 +186,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
<div className="flex flex-grow items-center gap-0.5 truncate"> <div className="flex flex-grow items-center gap-0.5 truncate">
<div className="flex items-center gap-1" style={isSubIssue ? { marginLeft } : {}}> <div className="flex items-center gap-1" style={isSubIssue ? { marginLeft } : {}}>
{/* select checkbox */} {/* select checkbox */}
{projectId && canSelectIssues && ( {projectId && canSelectIssues && !isEpic && (
<Tooltip <Tooltip
tooltipContent={ tooltipContent={
<> <>
@ -220,7 +228,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
{/* sub-issues chevron */} {/* sub-issues chevron */}
<div className="size-4 grid place-items-center flex-shrink-0"> <div className="size-4 grid place-items-center flex-shrink-0">
{subIssuesCount > 0 && ( {subIssuesCount > 0 && !isEpic && (
<button <button
type="button" type="button"
className="size-4 grid place-items-center rounded-sm text-custom-text-400 hover:text-custom-text-300" className="size-4 grid place-items-center rounded-sm text-custom-text-400 hover:text-custom-text-300"
@ -275,6 +283,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
updateIssue={updateIssue} updateIssue={updateIssue}
displayProperties={displayProperties} displayProperties={displayProperties}
activeLayout="List" activeLayout="List"
isEpic={isEpic}
/> />
<div <div
className={cn("hidden", { className={cn("hidden", {

View file

@ -19,6 +19,7 @@ interface Props {
isDragAllowed: boolean; isDragAllowed: boolean;
canDropOverIssue: boolean; canDropOverIssue: boolean;
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
isEpic?: boolean;
} }
export const IssueBlocksList: FC<Props> = (props) => { export const IssueBlocksList: FC<Props> = (props) => {
@ -34,6 +35,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
selectionHelpers, selectionHelpers,
isDragAllowed, isDragAllowed,
canDropOverIssue, canDropOverIssue,
isEpic = false,
} = props; } = props;
return ( return (
@ -57,6 +59,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
isLastChild={index === issueIds.length - 1} isLastChild={index === issueIds.length - 1}
isDragAllowed={isDragAllowed} isDragAllowed={isDragAllowed}
canDropOverIssue={canDropOverIssue} canDropOverIssue={canDropOverIssue}
isEpic={isEpic}
/> />
))} ))}
</div> </div>

View file

@ -48,6 +48,7 @@ export interface IList {
loadMoreIssues: (groupId?: string) => void; loadMoreIssues: (groupId?: string) => void;
handleCollapsedGroups: (value: string) => void; handleCollapsedGroups: (value: string) => void;
collapsedGroups: TIssueKanbanFilters; collapsedGroups: TIssueKanbanFilters;
isEpic?: boolean;
} }
export const List: React.FC<IList> = observer((props) => { export const List: React.FC<IList> = observer((props) => {
@ -70,6 +71,7 @@ export const List: React.FC<IList> = observer((props) => {
loadMoreIssues, loadMoreIssues,
handleCollapsedGroups, handleCollapsedGroups,
collapsedGroups, collapsedGroups,
isEpic = false,
} = props; } = props;
const storeType = useIssueStoreType(); const storeType = useIssueStoreType();
@ -121,7 +123,11 @@ export const List: React.FC<IList> = observer((props) => {
return ( return (
<div className="relative size-full flex flex-col"> <div className="relative size-full flex flex-col">
{groups && ( {groups && (
<MultipleSelectGroup containerRef={containerRef} entities={entities} disabled={!isBulkOperationsEnabled}> <MultipleSelectGroup
containerRef={containerRef}
entities={entities}
disabled={!isBulkOperationsEnabled || isEpic}
>
{(helpers) => ( {(helpers) => (
<> <>
<div <div
@ -153,6 +159,7 @@ export const List: React.FC<IList> = observer((props) => {
selectionHelpers={helpers} selectionHelpers={helpers}
handleCollapsedGroups={handleCollapsedGroups} handleCollapsedGroups={handleCollapsedGroups}
collapsedGroups={collapsedGroups} collapsedGroups={collapsedGroups}
isEpic={isEpic}
/> />
))} ))}
</div> </div>

View file

@ -18,6 +18,8 @@ import { cn } from "@/helpers/common.helper";
import { useEventTracker } from "@/hooks/store"; import { useEventTracker } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { TSelectionHelper } from "@/hooks/use-multiple-select";
// plane-web
import { CreateUpdateEpicModal } from "@/plane-web/components/epics/epic-modal";
// Plane-web // Plane-web
import { WorkFlowGroupTree } from "@/plane-web/components/workflow"; import { WorkFlowGroupTree } from "@/plane-web/components/workflow";
@ -33,6 +35,7 @@ interface IHeaderGroupByCard {
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>; addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
handleCollapsedGroups: (value: string) => void; handleCollapsedGroups: (value: string) => void;
isEpic?: boolean;
} }
export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => { export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
@ -48,6 +51,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
addIssuesToView, addIssuesToView,
selectionHelpers, selectionHelpers,
handleCollapsedGroups, handleCollapsedGroups,
isEpic = false,
} = props; } = props;
// states // states
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -157,13 +161,17 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
</div> </div>
))} ))}
<CreateUpdateIssueModal {isEpic ? (
isOpen={isOpen} <CreateUpdateEpicModal isOpen={isOpen} onClose={() => setIsOpen(false)} data={issuePayload} />
onClose={() => setIsOpen(false)} ) : (
data={issuePayload} <CreateUpdateIssueModal
storeType={storeType} isOpen={isOpen}
isDraft={isDraftIssue} onClose={() => setIsOpen(false)}
/> data={issuePayload}
storeType={storeType}
isDraft={isDraftIssue}
/>
)}
{renderExistingIssueModal && ( {renderExistingIssueModal && (
<ExistingIssuesListModal <ExistingIssuesListModal

View file

@ -64,6 +64,7 @@ interface Props {
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
handleCollapsedGroups: (value: string) => void; handleCollapsedGroups: (value: string) => void;
collapsedGroups: TIssueKanbanFilters; collapsedGroups: TIssueKanbanFilters;
isEpic?: boolean;
} }
export const ListGroup = observer((props: Props) => { export const ListGroup = observer((props: Props) => {
@ -90,6 +91,7 @@ export const ListGroup = observer((props: Props) => {
selectionHelpers, selectionHelpers,
handleCollapsedGroups, handleCollapsedGroups,
collapsedGroups, collapsedGroups,
isEpic = false,
} = props; } = props;
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false); const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
@ -266,6 +268,7 @@ export const ListGroup = observer((props: Props) => {
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
selectionHelpers={selectionHelpers} selectionHelpers={selectionHelpers}
handleCollapsedGroups={handleCollapsedGroups} handleCollapsedGroups={handleCollapsedGroups}
isEpic={isEpic}
/> />
</Row> </Row>
{shouldExpand && ( {shouldExpand && (
@ -292,6 +295,7 @@ export const ListGroup = observer((props: Props) => {
isDragAllowed={isDragAllowed} isDragAllowed={isDragAllowed}
canDropOverIssue={!canOverlayBeVisible} canDropOverIssue={!canOverlayBeVisible}
selectionHelpers={selectionHelpers} selectionHelpers={selectionHelpers}
isEpic={isEpic}
/> />
)} )}

View file

@ -42,10 +42,11 @@ export interface IIssueProperties {
isReadOnly: boolean; isReadOnly: boolean;
className: string; className: string;
activeLayout: string; activeLayout: string;
isEpic?: boolean;
} }
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => { export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className } = props; const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className, isEpic = false } = props;
// store hooks // store hooks
const { getProjectById } = useProject(); const { getProjectById } = useProject();
const { labelMap } = useLabel(); const { labelMap } = useLabel();
@ -376,42 +377,46 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
</div> </div>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>
{/* modules */} {!isEpic && (
{projectDetails?.module_view && ( <>
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules"> {/* modules */}
<div className="h-5" onClick={handleEventPropagation}> {projectDetails?.module_view && (
<ModuleDropdown <WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
buttonContainerClassName="truncate max-w-40" <div className="h-5" onClick={handleEventPropagation}>
projectId={issue?.project_id} <ModuleDropdown
value={issue?.module_ids ?? []} buttonContainerClassName="truncate max-w-40"
onChange={handleModule} projectId={issue?.project_id}
disabled={isReadOnly} value={issue?.module_ids ?? []}
renderByDefault={isMobile} onChange={handleModule}
multiple disabled={isReadOnly}
buttonVariant="border-with-text" renderByDefault={isMobile}
showCount multiple
showTooltip buttonVariant="border-with-text"
/> showCount
</div> showTooltip
</WithDisplayPropertiesHOC> />
)} </div>
</WithDisplayPropertiesHOC>
)}
{/* cycles */} {/* cycles */}
{projectDetails?.cycle_view && ( {projectDetails?.cycle_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle"> <WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
<div className="h-5" onClick={handleEventPropagation}> <div className="h-5" onClick={handleEventPropagation}>
<CycleDropdown <CycleDropdown
buttonContainerClassName="truncate max-w-40" buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id} projectId={issue?.project_id}
value={issue?.cycle_id} value={issue?.cycle_id}
onChange={handleCycle} onChange={handleCycle}
disabled={isReadOnly} disabled={isReadOnly}
buttonVariant="border-with-text" buttonVariant="border-with-text"
renderByDefault={isMobile} renderByDefault={isMobile}
showTooltip showTooltip
/> />
</div> </div>
</WithDisplayPropertiesHOC> </WithDisplayPropertiesHOC>
)}
</>
)} )}
{/* estimates */} {/* estimates */}

View file

@ -26,17 +26,19 @@ export type SpreadsheetStoreType =
| EIssuesStoreType.CYCLE | EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW | EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.TEAM | EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW; | EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.EPIC;
interface IBaseSpreadsheetRoot { interface IBaseSpreadsheetRoot {
QuickActions: FC<IQuickActionProps>; QuickActions: FC<IQuickActionProps>;
canEditPropertiesBasedOnProject?: (projectId: string) => boolean; canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
isCompletedCycle?: boolean; isCompletedCycle?: boolean;
viewId?: string | undefined; viewId?: string | undefined;
isEpic?: boolean;
} }
export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
const { QuickActions, canEditPropertiesBasedOnProject, isCompletedCycle = false, viewId } = props; const { QuickActions, canEditPropertiesBasedOnProject, isCompletedCycle = false, viewId, isEpic = false } = props;
// router // router
const { projectId } = useParams(); const { projectId } = useParams();
// store hooks // store hooks
@ -126,6 +128,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle} disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
canLoadMoreIssues={!!nextPageResults} canLoadMoreIssues={!!nextPageResults}
loadMoreIssues={fetchNextIssues} loadMoreIssues={fetchNextIssues}
isEpic={isEpic}
/> />
</IssueLayoutHOC> </IssueLayoutHOC>
); );

View file

@ -4,6 +4,7 @@ import { Dispatch, MouseEvent, MutableRefObject, SetStateAction, useRef, useStat
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { ChevronRight, MoreHorizontal } from "lucide-react"; import { ChevronRight, MoreHorizontal } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
// plane helpers // plane helpers
import { useOutsideClickDetector } from "@plane/hooks"; import { useOutsideClickDetector } from "@plane/hooks";
// types // types
@ -44,6 +45,7 @@ interface Props {
spacingLeft?: number; spacingLeft?: number;
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
shouldRenderByDefault?: boolean; shouldRenderByDefault?: boolean;
isEpic?: boolean;
} }
export const SpreadsheetIssueRow = observer((props: Props) => { export const SpreadsheetIssueRow = observer((props: Props) => {
@ -62,11 +64,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
spacingLeft = 6, spacingLeft = 6,
selectionHelpers, selectionHelpers,
shouldRenderByDefault, shouldRenderByDefault,
isEpic = false,
} = props; } = props;
// states // states
const [isExpanded, setExpanded] = useState<boolean>(false); const [isExpanded, setExpanded] = useState<boolean>(false);
// store hooks // store hooks
const { subIssues: subIssuesStore } = useIssueDetail(); const { subIssues: subIssuesStore } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const { issueMap } = useIssues(); const { issueMap } = useIssues();
// derived values // derived values
@ -110,10 +113,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
setExpanded={setExpanded} setExpanded={setExpanded}
spreadsheetColumnsList={spreadsheetColumnsList} spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers} selectionHelpers={selectionHelpers}
isEpic={isEpic}
/> />
</RenderIfVisible> </RenderIfVisible>
{isExpanded && {isExpanded &&
!isEpic &&
subIssues?.map((subIssueId: string) => ( subIssues?.map((subIssueId: string) => (
<SpreadsheetIssueRow <SpreadsheetIssueRow
key={subIssueId} key={subIssueId}
@ -152,6 +157,7 @@ interface IssueRowDetailsProps {
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
spacingLeft?: number; spacingLeft?: number;
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
isEpic?: boolean;
} }
const IssueRowDetails = observer((props: IssueRowDetailsProps) => { const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
@ -170,6 +176,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
spreadsheetColumnsList, spreadsheetColumnsList,
spacingLeft = 6, spacingLeft = 6,
selectionHelpers, selectionHelpers,
isEpic = false,
} = props; } = props;
// states // states
const [isMenuActive, setIsMenuActive] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false);
@ -180,8 +187,8 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// hooks // hooks
const { getProjectIdentifierById } = useProject(); const { getProjectIdentifierById } = useProject();
const { getIsIssuePeeked, peekIssue } = useIssueDetail(); const { getIsIssuePeeked, peekIssue } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const { handleRedirection } = useIssuePeekOverviewRedirection(); const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
// handlers // handlers
@ -243,7 +250,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
className="relative md:sticky left-0 z-10 group/list-block bg-custom-background-100" className="relative md:sticky left-0 z-10 group/list-block bg-custom-background-100"
> >
<ControlLink <ControlLink
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`} href={`/${workspaceSlug}/projects/${issueDetail.project_id}/${isEpic ? "epics" : "issues"}/${issueId}`}
onClick={() => handleIssuePeekOverview(issueDetail)} onClick={() => handleIssuePeekOverview(issueDetail)}
className={cn( className={cn(
"group clickable cursor-pointer h-11 w-[28rem] flex items-center text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200 bg-transparent group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10", "group clickable cursor-pointer h-11 w-[28rem] flex items-center text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200 bg-transparent group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10",
@ -307,7 +314,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
{/* sub-issues chevron */} {/* sub-issues chevron */}
<div className="grid place-items-center size-4"> <div className="grid place-items-center size-4">
{subIssuesCount > 0 && ( {subIssuesCount > 0 && !isEpic && (
<button <button
type="button" type="button"
className="grid place-items-center size-4 rounded-sm text-custom-text-400 hover:text-custom-text-300" className="grid place-items-center size-4 rounded-sm text-custom-text-400 hover:text-custom-text-300"

View file

@ -29,6 +29,7 @@ type Props = {
loadMoreIssues: () => void; loadMoreIssues: () => void;
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
isEpic?: boolean;
}; };
export const SpreadsheetTable = observer((props: Props) => { export const SpreadsheetTable = observer((props: Props) => {
@ -47,6 +48,7 @@ export const SpreadsheetTable = observer((props: Props) => {
loadMoreIssues, loadMoreIssues,
spreadsheetColumnsList, spreadsheetColumnsList,
selectionHelpers, selectionHelpers,
isEpic = false,
} = props; } = props;
// states // states
@ -127,6 +129,7 @@ export const SpreadsheetTable = observer((props: Props) => {
isScrolled={isScrolled} isScrolled={isScrolled}
spreadsheetColumnsList={spreadsheetColumnsList} spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers} selectionHelpers={selectionHelpers}
isEpic={isEpic}
/> />
))} ))}
</tbody> </tbody>

View file

@ -34,6 +34,7 @@ type Props = {
enableQuickCreateIssue?: boolean; enableQuickCreateIssue?: boolean;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
isWorkspaceLevel?: boolean; isWorkspaceLevel?: boolean;
isEpic?: boolean;
}; };
export const SpreadsheetView: React.FC<Props> = observer((props) => { export const SpreadsheetView: React.FC<Props> = observer((props) => {
@ -51,6 +52,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
canLoadMoreIssues, canLoadMoreIssues,
loadMoreIssues, loadMoreIssues,
isWorkspaceLevel = false, isWorkspaceLevel = false,
isEpic = false,
} = props; } = props;
// refs // refs
const containerRef = useRef<HTMLTableElement | null>(null); const containerRef = useRef<HTMLTableElement | null>(null);
@ -85,7 +87,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
entities={{ entities={{
[SPREADSHEET_SELECT_GROUP]: issueIds, [SPREADSHEET_SELECT_GROUP]: issueIds,
}} }}
disabled={!isBulkOperationsEnabled} disabled={!isBulkOperationsEnabled || isEpic}
> >
{(helpers) => ( {(helpers) => (
<> <>
@ -105,6 +107,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
loadMoreIssues={loadMoreIssues} loadMoreIssues={loadMoreIssues}
spreadsheetColumnsList={spreadsheetColumnsList} spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={helpers} selectionHelpers={helpers}
isEpic={isEpic}
/> />
</div> </div>
<div className="border-t border-custom-border-100"> <div className="border-t border-custom-border-100">

View file

@ -21,7 +21,7 @@ import { FileService } from "@/services/file.service";
const fileService = new FileService(); const fileService = new FileService();
// local components // local components
import { DraftIssueLayout } from "./draft-issue-layout"; import { DraftIssueLayout } from "./draft-issue-layout";
import { IssueFormRoot } from "./form"; import { type IssueFormProps, IssueFormRoot } from "./form";
export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((props) => { export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((props) => {
const { const {
@ -41,7 +41,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
} = props; } = props;
const issueStoreType = useIssueStoreType(); const issueStoreType = useIssueStoreType();
const storeType = issueStoreFromProps ?? issueStoreType; const storeType = (issueStoreFromProps ? issueStoreFromProps : issueStoreType === EIssuesStoreType.EPIC)
? EIssuesStoreType.PROJECT
: issueStoreType;
// ref // ref
const issueTitleRef = useRef<HTMLInputElement>(null); const issueTitleRef = useRef<HTMLInputElement>(null);
// states // states
@ -333,6 +335,30 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
// don't open the modal if there are no projects // don't open the modal if there are no projects
if (!projectIdsWithCreatePermissions || projectIdsWithCreatePermissions.length === 0 || !activeProjectId) return null; if (!projectIdsWithCreatePermissions || projectIdsWithCreatePermissions.length === 0 || !activeProjectId) return null;
const commonIssueModalProps: IssueFormProps = {
issueTitleRef: issueTitleRef,
data: {
...data,
description_html: description,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId.toString() : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId.toString()] : null,
},
onAssetUpload: handleUpdateUploadedAssetIds,
onClose: handleClose,
onSubmit: (payload) => handleFormSubmit(payload, isDraft),
projectId: activeProjectId,
isCreateMoreToggleEnabled: createMore,
onCreateMoreToggleChange: handleCreateMoreToggleChange,
isDraft: isDraft,
moveToIssue: moveToIssue,
modalTitle: modalTitle,
primaryButtonText: primaryButtonText,
isDuplicateModalOpen: isDuplicateModalOpen,
handleDuplicateIssueModal: handleDuplicateIssueModal,
isProjectSelectionDisabled: isProjectSelectionDisabled,
storeType: storeType,
};
return ( return (
<ModalCore <ModalCore
isOpen={isOpen} isOpen={isOpen}
@ -342,51 +368,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear" className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear"
> >
{withDraftIssueWrapper ? ( {withDraftIssueWrapper ? (
<DraftIssueLayout <DraftIssueLayout {...commonIssueModalProps} changesMade={changesMade} onChange={handleFormChange} />
changesMade={changesMade}
data={{
...data,
description_html: description,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId.toString() : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId.toString()] : null,
}}
issueTitleRef={issueTitleRef}
onAssetUpload={handleUpdateUploadedAssetIds}
onChange={handleFormChange}
onClose={handleClose}
onSubmit={(payload) => handleFormSubmit(payload, isDraft)}
projectId={activeProjectId}
isCreateMoreToggleEnabled={createMore}
onCreateMoreToggleChange={handleCreateMoreToggleChange}
isDraft={isDraft}
moveToIssue={moveToIssue}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
) : ( ) : (
<IssueFormRoot <IssueFormRoot {...commonIssueModalProps} />
issueTitleRef={issueTitleRef}
data={{
...data,
description_html: description,
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId.toString() : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId.toString()] : null,
}}
onAssetUpload={handleUpdateUploadedAssetIds}
onClose={handleClose}
isCreateMoreToggleEnabled={createMore}
onCreateMoreToggleChange={handleCreateMoreToggleChange}
onSubmit={(payload) => handleFormSubmit(payload, isDraft)}
projectId={activeProjectId}
isDraft={isDraft}
moveToIssue={moveToIssue}
modalTitle={modalTitle}
primaryButtonText={primaryButtonText}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
)} )}
</ModalCore> </ModalCore>
); );

View file

@ -332,6 +332,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
}} }}
projectId={projectId ?? undefined} projectId={projectId ?? undefined}
issueId={isDraft ? undefined : id} issueId={isDraft ? undefined : id}
searchEpic
/> />
)} )}
/> />

View file

@ -1 +1 @@
export * from "./issue-modal"; export * from "./issue-modal-context";

View file

@ -16,51 +16,15 @@ import { isEmptyHtmlString } from "@/helpers/string.helper";
import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useEventTracker, useWorkspaceDraftIssues } from "@/hooks/store"; import { useEventTracker, useWorkspaceDraftIssues } from "@/hooks/store";
// local components // local components
import { IssueFormRoot } from "./form"; import { IssueFormRoot, type IssueFormProps } from "./form";
export interface DraftIssueProps { export interface DraftIssueProps extends IssueFormProps {
changesMade: Partial<TIssue> | null; changesMade: Partial<TIssue> | null;
data?: Partial<TIssue>;
issueTitleRef: React.MutableRefObject<HTMLInputElement | null>;
isCreateMoreToggleEnabled: boolean;
onAssetUpload: (assetId: string) => void;
onCreateMoreToggleChange: (value: boolean) => void;
onChange: (formData: Partial<TIssue> | null) => void; onChange: (formData: Partial<TIssue> | null) => void;
onClose: (saveDraftIssueInLocalStorage?: boolean) => void;
onSubmit: (formData: Partial<TIssue>, is_draft_issue?: boolean) => Promise<void>;
projectId: string;
isDraft: boolean;
moveToIssue?: boolean;
modalTitle?: string;
primaryButtonText?: {
default: string;
loading: string;
};
isDuplicateModalOpen: boolean;
handleDuplicateIssueModal: (isOpen: boolean) => void;
isProjectSelectionDisabled?: boolean;
} }
export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => { export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
const { const { changesMade, data, onChange, onClose, projectId } = props;
changesMade,
data,
issueTitleRef,
onAssetUpload,
onChange,
onClose,
onSubmit,
projectId,
isCreateMoreToggleEnabled,
onCreateMoreToggleChange,
isDraft,
moveToIssue = false,
modalTitle,
primaryButtonText,
isDuplicateModalOpen,
handleDuplicateIssueModal,
isProjectSelectionDisabled = false,
} = props;
// states // states
const [issueDiscardModal, setIssueDiscardModal] = useState(false); const [issueDiscardModal, setIssueDiscardModal] = useState(false);
// router params // router params
@ -74,7 +38,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
const handleClose = () => { const handleClose = () => {
if (data?.id) { if (data?.id) {
onClose(false); onClose();
setIssueDiscardModal(false); setIssueDiscardModal(false);
} else { } else {
if (changesMade) { if (changesMade) {
@ -93,11 +57,11 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
delete changesMade.description_html; delete changesMade.description_html;
}); });
if (isEmpty(changesMade)) { if (isEmpty(changesMade)) {
onClose(false); onClose();
setIssueDiscardModal(false); setIssueDiscardModal(false);
} else setIssueDiscardModal(true); } else setIssueDiscardModal(true);
} else { } else {
onClose(false); onClose();
setIssueDiscardModal(false); setIssueDiscardModal(false);
} }
} }
@ -126,7 +90,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
}); });
onChange(null); onChange(null);
setIssueDiscardModal(false); setIssueDiscardModal(false);
onClose(false); onClose();
return res; return res;
}) })
.catch(() => { .catch(() => {
@ -162,27 +126,10 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
onDiscard={() => { onDiscard={() => {
onChange(null); onChange(null);
setIssueDiscardModal(false); setIssueDiscardModal(false);
onClose(false); onClose();
}} }}
/> />
<IssueFormRoot <IssueFormRoot {...props} onClose={handleClose} />
isCreateMoreToggleEnabled={isCreateMoreToggleEnabled}
onCreateMoreToggleChange={onCreateMoreToggleChange}
data={data}
issueTitleRef={issueTitleRef}
onAssetUpload={onAssetUpload}
onChange={onChange}
onClose={handleClose}
onSubmit={onSubmit}
projectId={projectId}
isDraft={isDraft}
moveToIssue={moveToIssue}
modalTitle={modalTitle}
primaryButtonText={primaryButtonText}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
</> </>
); );
}); });

View file

@ -19,6 +19,7 @@ import {
IssueTitleInput, IssueTitleInput,
} from "@/components/issues/issue-modal/components"; } from "@/components/issues/issue-modal/components";
import { CreateLabelModal } from "@/components/labels"; import { CreateLabelModal } from "@/components/labels";
import { EIssuesStoreType } from "@/constants/issue";
import { ETabIndices } from "@/constants/tab-indices"; import { ETabIndices } from "@/constants/tab-indices";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
@ -72,6 +73,7 @@ export interface IssueFormProps {
isDuplicateModalOpen: boolean; isDuplicateModalOpen: boolean;
handleDuplicateIssueModal: (isOpen: boolean) => void; handleDuplicateIssueModal: (isOpen: boolean) => void;
isProjectSelectionDisabled?: boolean; isProjectSelectionDisabled?: boolean;
storeType: EIssuesStoreType;
} }
export const IssueFormRoot: FC<IssueFormProps> = observer((props) => { export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
@ -86,8 +88,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
isCreateMoreToggleEnabled, isCreateMoreToggleEnabled,
onCreateMoreToggleChange, onCreateMoreToggleChange,
isDraft, isDraft,
moveToIssue, moveToIssue = false,
modalTitle = `${data?.id ? "Update" : isDraft ? "Create a draft" : "Create new issue"}`, modalTitle,
primaryButtonText = { primaryButtonText = {
default: `${data?.id ? "Update" : isDraft ? "Save to Drafts" : "Save"}`, default: `${data?.id ? "Update" : isDraft ? "Save to Drafts" : "Save"}`,
loading: `${data?.id ? "Updating" : "Saving"}`, loading: `${data?.id ? "Updating" : "Saving"}`,
@ -95,6 +97,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
isDuplicateModalOpen, isDuplicateModalOpen,
handleDuplicateIssueModal, handleDuplicateIssueModal,
isProjectSelectionDisabled = false, isProjectSelectionDisabled = false,
storeType,
} = props; } = props;
// states // states
@ -280,6 +283,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
// debounced duplicate issues swr // debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues( const { duplicateIssues } = useDebouncedDuplicateIssues(
workspaceSlug?.toString(),
projectDetails?.workspace.toString(), projectDetails?.workspace.toString(),
projectId ?? undefined, projectId ?? undefined,
{ {
@ -373,7 +377,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
disabled={!!data?.id || !!data?.sourceIssueId || isProjectSelectionDisabled} disabled={!!data?.id || !!data?.sourceIssueId || isProjectSelectionDisabled}
handleFormChange={handleFormChange} handleFormChange={handleFormChange}
/> />
{projectId && ( {projectId && storeType !== EIssuesStoreType.EPIC && (
<IssueTypeSelect <IssueTypeSelect
control={control} control={control}
projectId={projectId} projectId={projectId}

View file

@ -29,6 +29,7 @@ type Props = {
onChange: (issue: ISearchIssueResponse) => void; onChange: (issue: ISearchIssueResponse) => void;
projectId: string | undefined; projectId: string | undefined;
issueId?: string; issueId?: string;
searchEpic?: boolean;
}; };
// services // services
@ -41,6 +42,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
onChange, onChange,
projectId, projectId,
issueId, issueId,
searchEpic = false,
}) => { }) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@ -72,6 +74,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
parent: true, parent: true,
issue_id: issueId, issue_id: issueId,
workspace_search: isWorkspaceLevel, workspace_search: isWorkspaceLevel,
epic: searchEpic ? true : undefined,
}) })
.then((res) => setIssues(res)) .then((res) => setIssues(res))
.finally(() => { .finally(() => {

View file

@ -57,11 +57,16 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
const issue = issueId ? getIssueById(issueId) : undefined; const issue = issueId ? getIssueById(issueId) : undefined;
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined; const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
// debounced duplicate issues swr // debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectDetails?.id, { const { duplicateIssues } = useDebouncedDuplicateIssues(
name: issue?.name, workspaceSlug,
description_html: getTextContent(issue?.description_html), projectDetails?.workspace.toString(),
issueId: issue?.id, projectDetails?.id,
}); {
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
}
);
if (!issue || !issue.project_id) return <></>; if (!issue || !issue.project_id) return <></>;

View file

@ -14,7 +14,7 @@ import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "@/
import { EIssuesStoreType } from "@/constants/issue"; import { EIssuesStoreType } from "@/constants/issue";
// hooks // hooks
import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store"; import { useEventTracker, useIssueDetail, useIssues, useUserPermissions } from "@/hooks/store";
import { useIssuesStore } from "@/hooks/use-issue-layout-store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
// plane web constants // plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
@ -22,10 +22,16 @@ interface IIssuePeekOverview {
embedIssue?: boolean; embedIssue?: boolean;
embedRemoveCurrentNotification?: () => void; embedRemoveCurrentNotification?: () => void;
is_draft?: boolean; is_draft?: boolean;
storeType?: EIssuesStoreType;
} }
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => { export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { embedIssue = false, embedRemoveCurrentNotification, is_draft = false } = props; const {
embedIssue = false,
embedRemoveCurrentNotification,
is_draft = false,
storeType: issueStoreFromProps,
} = props;
// router // router
const pathname = usePathname(); const pathname = usePathname();
// store hook // store hook
@ -40,8 +46,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
issue: { fetchIssue, getIsFetchingIssueDetails }, issue: { fetchIssue, getIsFetchingIssueDetails },
fetchActivities, fetchActivities,
} = useIssueDetail(); } = useIssueDetail();
const issueStoreType = useIssueStoreType();
const { issues } = useIssuesStore(); const storeType = issueStoreFromProps ?? issueStoreType;
const { issues } = useIssues(storeType);
const { captureIssueEvent } = useEventTracker(); const { captureIssueEvent } = useEventTracker();
// state // state
const [error, setError] = useState(false); const [error, setError] = useState(false);

View file

@ -1,5 +1,7 @@
import { FC, useRef, useState } from "react"; import { FC, useRef, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// constants
import { EIssueServiceType } from "@plane/constants";
// types // types
import { TNameDescriptionLoader } from "@plane/types"; import { TNameDescriptionLoader } from "@plane/types";
// components // components
@ -65,6 +67,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
toggleArchiveIssueModal, toggleArchiveIssueModal,
issue: { getIssueById, getIsLocalDBIssueDescription }, issue: { getIssueById, getIsLocalDBIssueDescription },
} = useIssueDetail(); } = useIssueDetail();
const { isAnyModalOpen: isAnyEpicModalOpen } = useIssueDetail(EIssueServiceType.EPICS);
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
// remove peek id // remove peek id
const removeRoutePeekId = () => { const removeRoutePeekId = () => {
@ -78,7 +81,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issuePeekOverviewRef, issuePeekOverviewRef,
() => { () => {
if (!embedIssue) { if (!embedIssue) {
if (!isAnyModalOpen) { if (!isAnyModalOpen && !isAnyEpicModalOpen) {
removeRoutePeekId(); removeRoutePeekId();
} }
} }

View file

@ -4,7 +4,8 @@ import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { X, Pencil, Trash, Link as LinkIcon } from "lucide-react"; import { X, Pencil, Trash, Link as LinkIcon } from "lucide-react";
// Plane // Plane
import { TIssue } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { TIssue, TIssueServiceType } from "@plane/types";
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
// components // components
import { RelationIssueProperty } from "@/components/issues/relations"; import { RelationIssueProperty } from "@/components/issues/relations";
@ -27,6 +28,7 @@ type Props = {
disabled: boolean; disabled: boolean;
issueOperations: TRelationIssueOperations; issueOperations: TRelationIssueOperations;
handleIssueCrudState: (key: "update" | "delete", issueId: string, issue?: TIssue | null) => void; handleIssueCrudState: (key: "update" | "delete", issueId: string, issue?: TIssue | null) => void;
issueServiceType?: TIssueServiceType;
}; };
export const RelationIssueListItem: FC<Props> = observer((props) => { export const RelationIssueListItem: FC<Props> = observer((props) => {
@ -39,6 +41,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
disabled = false, disabled = false,
issueOperations, issueOperations,
handleIssueCrudState, handleIssueCrudState,
issueServiceType = EIssueServiceType.ISSUES,
} = props; } = props;
// store hooks // store hooks
@ -47,7 +50,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
removeRelation, removeRelation,
toggleCreateIssueModal, toggleCreateIssueModal,
toggleDeleteIssueModal, toggleDeleteIssueModal,
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
const project = useProject(); const project = useProject();
const { getProjectStates } = useProjectState(); const { getProjectStates } = useProjectState();
const { handleRedirection } = useIssuePeekOverviewRedirection(); const { handleRedirection } = useIssuePeekOverviewRedirection();
@ -137,6 +140,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
issueId={relationIssueId} issueId={relationIssueId}
disabled={disabled} disabled={disabled}
issueOperations={issueOperations} issueOperations={issueOperations}
issueServiceType={issueServiceType}
/> />
</div> </div>
<div className="flex-shrink-0 text-sm"> <div className="flex-shrink-0 text-sm">

View file

@ -2,7 +2,8 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// Plane // Plane
import { TIssue } from "@plane/types"; import { EIssueServiceType } from "@plane/constants";
import { TIssue, TIssueServiceType } from "@plane/types";
// components // components
import { RelationIssueListItem } from "@/components/issues/relations"; import { RelationIssueListItem } from "@/components/issues/relations";
// Plane-web // Plane-web
@ -19,6 +20,7 @@ type Props = {
issueOperations: TRelationIssueOperations; issueOperations: TRelationIssueOperations;
handleIssueCrudState: (key: "update" | "delete", issueId: string, issue?: TIssue | null) => void; handleIssueCrudState: (key: "update" | "delete", issueId: string, issue?: TIssue | null) => void;
disabled?: boolean; disabled?: boolean;
issueServiceType?: TIssueServiceType;
}; };
export const RelationIssueList: FC<Props> = observer((props) => { export const RelationIssueList: FC<Props> = observer((props) => {
@ -31,6 +33,7 @@ export const RelationIssueList: FC<Props> = observer((props) => {
disabled = false, disabled = false,
issueOperations, issueOperations,
handleIssueCrudState, handleIssueCrudState,
issueServiceType = EIssueServiceType.ISSUES,
} = props; } = props;
return ( return (
@ -48,6 +51,7 @@ export const RelationIssueList: FC<Props> = observer((props) => {
disabled={disabled} disabled={disabled}
handleIssueCrudState={handleIssueCrudState} handleIssueCrudState={handleIssueCrudState}
issueOperations={issueOperations} issueOperations={issueOperations}
issueServiceType={issueServiceType}
/> />
))} ))}
</div> </div>

View file

@ -1,8 +1,9 @@
"use client"; "use client";
import React, { FC } from "react"; import React, { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
// components // components
import { TIssuePriorities } from "@plane/types"; import { TIssuePriorities, TIssueServiceType } from "@plane/types";
import { PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns"; import { PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
@ -14,14 +15,15 @@ type Props = {
issueId: string; issueId: string;
disabled: boolean; disabled: boolean;
issueOperations: TRelationIssueOperations; issueOperations: TRelationIssueOperations;
issueServiceType?: TIssueServiceType;
}; };
export const RelationIssueProperty: FC<Props> = observer((props) => { export const RelationIssueProperty: FC<Props> = observer((props) => {
const { workspaceSlug, issueId, disabled, issueOperations } = props; const { workspaceSlug, issueId, disabled, issueOperations, issueServiceType = EIssueServiceType.ISSUES } = props;
// hooks // hooks
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail(issueServiceType);
// derived value // derived value
const issue = getIssueById(issueId); const issue = getIssueById(issueId);

View file

@ -3,7 +3,7 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
import { TIssue } from "@plane/types"; import { TIssue, TIssueServiceType } from "@plane/types";
// ui // ui
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
// helpers // helpers
@ -36,6 +36,7 @@ export interface ISubIssues {
) => void; ) => void;
subIssueOperations: TSubIssueOperations; subIssueOperations: TSubIssueOperations;
issueId: string; issueId: string;
issueServiceType?: TIssueServiceType;
} }
export const IssueListItem: React.FC<ISubIssues> = observer((props) => { export const IssueListItem: React.FC<ISubIssues> = observer((props) => {

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