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

View file

@ -136,6 +136,7 @@ export type TProjectIssuesSearchParams = {
issue_id?: string;
workspace_search: boolean;
target_date?: string;
epic?: boolean;
};
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 = {
issueId: string;
issueTypeId: string;
projectId: string;
workspaceSlug: string;
isDisabled: boolean;
issueServiceType?: TIssueServiceType;
};
export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPropertyValuesUpdateProps> = () => <></>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,10 +19,11 @@ type Props = {
enableSelection: boolean;
isDragging: boolean;
selectionHelpers?: TSelectionHelper;
isEpic?: boolean;
};
export const IssuesSidebarBlock = observer((props: Props) => {
const { block, enableSelection, isDragging, selectionHelpers } = props;
const { block, enableSelection, isDragging, selectionHelpers, isEpic = false } = props;
// store hooks
const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
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-grow truncate">
<IssueGanttSidebarBlock issueId={block.data.id} />
<IssueGanttSidebarBlock issueId={block.data.id} isEpic={isEpic} />
</div>
{duration && (
<div className="flex-shrink-0 text-sm text-custom-text-200">

View file

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

View file

@ -23,6 +23,7 @@ type Props = {
title: string;
quickAdd?: React.JSX.Element | undefined;
selectionHelpers: TSelectionHelper;
isEpic?: boolean;
};
export const GanttChartSidebar: React.FC<Props> = observer((props) => {
@ -38,6 +39,7 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
title,
quickAdd,
selectionHelpers,
isEpic = false,
} = props;
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(GANTT_SELECT_GROUP) === "empty";
@ -90,6 +92,7 @@ export const GanttChartSidebar: React.FC<Props> = observer((props) => {
ganttContainerRef,
loadMoreBlocks,
selectionHelpers,
isEpic,
})}
</Row>
{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;
// debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectId, {
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
});
const { duplicateIssues } = useDebouncedDuplicateIssues(
workspaceSlug,
projectDetails?.workspace.toString(),
projectId,
{
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
}
);
if (!issue) return <></>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
"use client";
import { useMemo } from "react";
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";
// constants
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>;
};
export const useRelationOperations = (): TRelationIssueOperations => {
const { updateIssue, removeIssue } = useIssueDetail();
export const useRelationOperations = (
issueServiceType: TIssueServiceType = EIssueServiceType.ISSUES
): TRelationIssueOperations => {
const { updateIssue, removeIssue } = useIssueDetail(issueServiceType);
const { captureIssueEvent } = useEventTracker();
const pathname = usePathname();

View file

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

View file

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

View file

@ -1,6 +1,8 @@
"use client";
import React, { FC, useMemo } from "react";
import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { CollapsibleButton } from "@plane/ui";
// components
import { RelationActionButton } from "@/components/issues/issue-detail-widgets";
@ -13,14 +15,15 @@ type Props = {
isOpen: boolean;
issueId: string;
disabled: boolean;
issueServiceType?: TIssueServiceType;
};
export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, issueId, disabled } = props;
const { isOpen, issueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hook
const {
relation: { getRelationCountByIssueId },
} = useIssueDetail();
} = useIssueDetail(issueServiceType);
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
// derived values
@ -41,7 +44,9 @@ export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
isOpen={isOpen}
title="Relations"
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";
import React, { FC, useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { TIssue } from "@plane/types";
import { EIssueServiceType } from "@plane/constants";
import { TIssue, TIssueServiceType } from "@plane/types";
// components
import { DeleteIssueModal } from "@/components/issues/delete-issue-modal";
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
@ -16,12 +17,13 @@ type Props = {
projectId: string;
parentIssueId: string;
disabled: boolean;
issueServiceType?: TIssueServiceType;
};
type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined };
export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, parentIssueId, disabled } = props;
const { workspaceSlug, projectId, parentIssueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// state
const [issueCrudState, setIssueCrudState] = useState<{
create: TIssueCrudState;
@ -58,7 +60,7 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
} = useIssueDetail();
// helpers
const subIssueOperations = useSubIssueOperations();
const subIssueOperations = useSubIssueOperations(issueServiceType);
const subIssueHelpers = subIssueHelpersByIssueId(`${parentIssueId}_root`);
// handler
@ -95,7 +97,6 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
useEffect(() => {
handleFetchSubIssues();
return () => {
handleFetchSubIssues();
};
@ -123,6 +124,7 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
disabled={!disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
issueServiceType={issueServiceType}
/>
)}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,8 @@
"use client";
import React, { FC, useMemo } from "react";
import React, { FC } from "react";
import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
import { TIssueServiceType } from "@plane/types";
import { CircularProgressIndicator, CollapsibleButton } from "@plane/ui";
// components
import { SubIssuesActionButton } from "@/components/issues/issue-detail-widgets";
@ -11,14 +13,15 @@ type Props = {
isOpen: boolean;
parentIssueId: string;
disabled: boolean;
issueServiceType?: TIssueServiceType;
};
export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
const { isOpen, parentIssueId, disabled } = props;
const { isOpen, parentIssueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
// store hooks
const {
subIssues: { subIssuesByIssueId, stateDistributionByIssueId },
} = useIssueDetail();
} = useIssueDetail(issueServiceType);
// derived data
const subIssuesDistribution = stateDistributionByIssueId(parentIssueId);
@ -32,25 +35,23 @@ export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
const totalCount = subIssues.length;
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 (
<CollapsibleButton
isOpen={isOpen}
title="Sub-issues"
indicatorElement={indicatorElement}
actionItemElement={!disabled && <SubIssuesActionButton issueId={parentIssueId} disabled={disabled} />}
indicatorElement={
<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 { observer } from "mobx-react";
import { IIssueLabel, TIssue } from "@plane/types";
import { EIssueServiceType } from "@plane/constants";
import { IIssueLabel, TIssue, TIssueServiceType } from "@plane/types";
// components
import { TOAST_TYPE, setToast } from "@plane/ui";
// hooks
@ -21,6 +22,7 @@ export type TIssueLabel = {
disabled: boolean;
isInboxIssue?: boolean;
onLabelUpdate?: (labelIds: string[]) => void;
issueServiceType?: TIssueServiceType;
};
export type TLabelOperations = {
@ -29,13 +31,21 @@ export type TLabelOperations = {
};
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
const { updateIssue } = useIssueDetail();
const { updateIssue } = useIssueDetail(issueServiceType);
const { createLabel } = useLabel();
const {
issue: { getIssueById },
} = useIssueDetail();
} = useIssueDetail(issueServiceType);
const { getIssueInboxByIssueId } = useProjectInbox();
const { allowPermissions } = useUserPermissions();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import { TIssue } from "@plane/types";
import { TOAST_TYPE, setPromiseToast, setToast } from "@plane/ui";
// components
import { EmptyState } from "@/components/common";
import { IssuePeekOverview } from "@/components/issues";
import { IssueDetailsSidebar, IssuePeekOverview } from "@/components/issues";
// constants
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "@/constants/event-tracker";
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";
// local components
import { IssueMainContent } from "./main-content";
import { IssueDetailsSidebar } from "./sidebar";
export type TIssueOperations = {
fetch: (workspaceSlug: string, projectId: string, issueId: string, loader?: boolean) => Promise<void>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,11 +15,12 @@ type Props = {
issueId: string;
quickActions: TRenderQuickActions;
isDragDisabled: boolean;
isEpic?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
};
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 [isDragging, setIsDragging] = useState(false);
@ -58,5 +59,13 @@ export const CalendarIssueBlockRoot: React.FC<Props> = observer((props) => {
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;
quickActions: TRenderQuickActions;
isDragging?: boolean;
isEpic?: boolean;
};
export const CalendarIssueBlock = observer(
forwardRef<HTMLAnchorElement, Props>((props, ref) => {
const { issue, quickActions, isDragging = false } = props;
const { issue, quickActions, isDragging = false, isEpic = false } = props;
// states
const [isMenuActive, setIsMenuActive] = useState(false);
// refs
@ -42,7 +43,7 @@ export const CalendarIssueBlock = observer(
const { workspaceSlug, projectId } = useParams();
const { getProjectStates } = useProjectState();
const { getIsIssuePeeked } = useIssueDetail();
const { handleRedirection } = useIssuePeekOverviewRedirection();
const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
const { isMobile } = usePlatformOS();
const storeType = useIssueStoreType() as CalendarStoreType;
const { issuesFilter } = useIssues(storeType);

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import { ProjectDraftEmptyState } from "./draft-issues";
import { GlobalViewEmptyState } from "./global-view";
import { ModuleEmptyState } from "./module";
import { ProfileViewEmptyState } from "./profile-view";
import { ProjectEpicsEmptyState } from "./project-epic";
import { ProjectEmptyState } from "./project-issues";
import { ProjectViewEmptyState } from "./project-view";
@ -31,6 +32,8 @@ export const IssueLayoutEmptyState = (props: Props) => {
return <GlobalViewEmptyState />;
case EIssuesStoreType.PROFILE:
return <ProfileViewEmptyState />;
case EIssuesStoreType.EPIC:
return <ProjectEpicsEmptyState />;
default:
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 { useParams } from "next/navigation";
// types
import { IIssueFilterOptions } from "@plane/types";
// hooks
// components
// ui
import { Header, EHeaderVariant } from "@plane/ui";
// components
import { AppliedFiltersList, SaveFilterView } from "@/components/issues";
// constants
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
// hooks
import { useLabel, useProjectState, useUserPermissions } from "@/hooks/store";
import { useIssues } from "@/hooks/store/use-issues";
// plane web constants
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
const { workspaceSlug, projectId } = useParams() as {
workspaceSlug: string;
projectId: string;
};
const { workspaceSlug, projectId } = useParams();
// store hooks
const { projectLabels } = useLabel();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT);
} = useIssues(storeType);
const { allowPermissions } = useUserPermissions();
const { projectStates } = useProjectState();
@ -84,8 +87,8 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
<Header.RightItem>
{isEditingAllowed && (
<SaveFilterView
workspaceSlug={workspaceSlug}
projectId={projectId}
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
filterParams={{
filters: appliedFilters,
display_filters: issueFilters?.displayFilters,

View file

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

View file

@ -21,10 +21,11 @@ import { GanttStoreType } from "./base-gantt-root";
type Props = {
issueId: string;
isEpic?: boolean;
};
export const IssueGanttBlock: React.FC<Props> = observer((props) => {
const { issueId } = props;
const { issueId, isEpic } = props;
// router
const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString();
@ -35,7 +36,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
} = useIssueDetail();
// hooks
const { isMobile } = usePlatformOS();
const { handleRedirection } = useIssuePeekOverviewRedirection();
const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
// derived values
const issueDetails = getIssueById(issueId);
@ -78,7 +79,7 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
// rendering issues on gantt sidebar
export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { issueId } = props;
const { issueId, isEpic = false } = props;
// router
const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString();
@ -91,7 +92,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
const { issuesFilter } = useIssues(storeType);
// handlers
const { handleRedirection } = useIssuePeekOverviewRedirection();
const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
// derived values
const issueDetails = getIssueById(issueId);
@ -105,7 +106,7 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
return (
<ControlLink
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}
className="line-clamp-1 w-full cursor-pointer text-sm text-custom-text-100"
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 { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { EIssueServiceType } from "@plane/constants";
import { DeleteIssueModal } from "@/components/issues";
//constants
import { ISSUE_DELETED } from "@/constants/event-tracker";
@ -34,7 +35,8 @@ export type KanbanStoreType =
| EIssuesStoreType.DRAFT
| EIssuesStoreType.PROFILE
| EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW;
| EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.EPIC;
export interface IBaseKanBanLayout {
QuickActions: FC<IQuickActionProps>;
@ -42,10 +44,18 @@ export interface IBaseKanBanLayout {
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
isCompletedCycle?: boolean;
viewId?: string | undefined;
isEpic?: boolean;
}
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
const { workspaceSlug, projectId } = useParams();
const pathname = usePathname();
@ -56,7 +66,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const { issueMap, issuesFilter, issues } = useIssues(storeType);
const {
issue: { getIssueById },
} = useIssueDetail();
} = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const {
fetchIssues,
fetchNextIssues,
@ -275,6 +285,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
scrollableContainerRef={scrollableContainerRef}
handleOnDrop={handleOnDrop}
loadMoreIssues={fetchMoreIssues}
isEpic={isEpic}
/>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,8 @@ type ListStoreType =
| EIssuesStoreType.ARCHIVED
| EIssuesStoreType.WORKSPACE_DRAFT
| EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW;
| EIssuesStoreType.TEAM_VIEW
| EIssuesStoreType.EPIC;
interface IBaseListRoot {
QuickActions: FC<IQuickActionProps>;
@ -36,9 +37,17 @@ interface IBaseListRoot {
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
viewId?: string | undefined;
isCompletedCycle?: boolean;
isEpic?: boolean;
}
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
const storeType = useIssueStoreType() as ListStoreType;
//stores
@ -157,6 +166,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
handleOnDrop={handleOnDrop}
handleCollapsedGroups={handleCollapsedGroups}
collapsedGroups={collapsedGroups}
isEpic={isEpic}
/>
</div>
</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 { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import { observer } from "mobx-react";
import { EIssueServiceType } from "@plane/constants";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
// types
@ -39,6 +40,7 @@ type Props = {
isParentIssueBeingDragged?: boolean;
isLastChild?: boolean;
shouldRenderByDefault?: boolean;
isEpic?: boolean;
};
export const IssueBlockRoot: FC<Props> = observer((props) => {
@ -59,6 +61,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
isLastChild = false,
selectionHelpers,
shouldRenderByDefault,
isEpic = false,
} = props;
// states
const [isExpanded, setExpanded] = useState<boolean>(false);
@ -69,7 +72,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
// hooks
const { isMobile } = usePlatformOS();
// store hooks
const { subIssues: subIssuesStore } = useIssueDetail();
const { subIssues: subIssuesStore } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const isSubIssue = nestingLevel !== 0;
@ -150,10 +153,12 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
canDrag={!isSubIssue && isDragAllowed}
isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging}
setIsCurrentBlockDragging={setIsCurrentBlockDragging}
isEpic={isEpic}
/>
</RenderIfVisible>
{isExpanded &&
!isEpic &&
subIssues?.map((subIssueId) => (
<IssueBlockRoot
key={`${subIssueId}`}

View file

@ -6,6 +6,7 @@ import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ChevronRight } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
// types
import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
// ui
@ -40,6 +41,7 @@ interface IssueBlockProps {
isCurrentBlockDragging: boolean;
setIsCurrentBlockDragging: React.Dispatch<React.SetStateAction<boolean>>;
canDrag: boolean;
isEpic?: boolean;
}
export const IssueBlock = observer((props: IssueBlockProps) => {
@ -59,6 +61,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
isCurrentBlockDragging,
setIsCurrentBlockDragging,
canDrag,
isEpic = false,
} = props;
// ref
const issueRef = useRef<HTMLDivElement | null>(null);
@ -69,7 +72,12 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
// hooks
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
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) =>
workspaceSlug &&
@ -143,7 +151,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
return (
<ControlLink
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)}
className="w-full cursor-pointer"
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 items-center gap-1" style={isSubIssue ? { marginLeft } : {}}>
{/* select checkbox */}
{projectId && canSelectIssues && (
{projectId && canSelectIssues && !isEpic && (
<Tooltip
tooltipContent={
<>
@ -220,7 +228,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
{/* sub-issues chevron */}
<div className="size-4 grid place-items-center flex-shrink-0">
{subIssuesCount > 0 && (
{subIssuesCount > 0 && !isEpic && (
<button
type="button"
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}
displayProperties={displayProperties}
activeLayout="List"
isEpic={isEpic}
/>
<div
className={cn("hidden", {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import { Dispatch, MouseEvent, MutableRefObject, SetStateAction, useRef, useStat
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { EIssueServiceType } from "@plane/constants";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
// types
@ -44,6 +45,7 @@ interface Props {
spacingLeft?: number;
selectionHelpers: TSelectionHelper;
shouldRenderByDefault?: boolean;
isEpic?: boolean;
}
export const SpreadsheetIssueRow = observer((props: Props) => {
@ -62,11 +64,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
spacingLeft = 6,
selectionHelpers,
shouldRenderByDefault,
isEpic = false,
} = props;
// states
const [isExpanded, setExpanded] = useState<boolean>(false);
// store hooks
const { subIssues: subIssuesStore } = useIssueDetail();
const { subIssues: subIssuesStore } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const { issueMap } = useIssues();
// derived values
@ -110,10 +113,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
setExpanded={setExpanded}
spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers}
isEpic={isEpic}
/>
</RenderIfVisible>
{isExpanded &&
!isEpic &&
subIssues?.map((subIssueId: string) => (
<SpreadsheetIssueRow
key={subIssueId}
@ -152,6 +157,7 @@ interface IssueRowDetailsProps {
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
spacingLeft?: number;
selectionHelpers: TSelectionHelper;
isEpic?: boolean;
}
const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
@ -170,6 +176,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
spreadsheetColumnsList,
spacingLeft = 6,
selectionHelpers,
isEpic = false,
} = props;
// states
const [isMenuActive, setIsMenuActive] = useState(false);
@ -180,8 +187,8 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
const { workspaceSlug, projectId } = useParams();
// hooks
const { getProjectIdentifierById } = useProject();
const { getIsIssuePeeked, peekIssue } = useIssueDetail();
const { handleRedirection } = useIssuePeekOverviewRedirection();
const { getIsIssuePeeked, peekIssue } = useIssueDetail(isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const { handleRedirection } = useIssuePeekOverviewRedirection(isEpic);
const { isMobile } = usePlatformOS();
// 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"
>
<ControlLink
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/${isEpic ? "epics" : "issues"}/${issueId}`}
onClick={() => handleIssuePeekOverview(issueDetail)}
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",
@ -307,7 +314,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
{/* sub-issues chevron */}
<div className="grid place-items-center size-4">
{subIssuesCount > 0 && (
{subIssuesCount > 0 && !isEpic && (
<button
type="button"
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;
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
selectionHelpers: TSelectionHelper;
isEpic?: boolean;
};
export const SpreadsheetTable = observer((props: Props) => {
@ -47,6 +48,7 @@ export const SpreadsheetTable = observer((props: Props) => {
loadMoreIssues,
spreadsheetColumnsList,
selectionHelpers,
isEpic = false,
} = props;
// states
@ -127,6 +129,7 @@ export const SpreadsheetTable = observer((props: Props) => {
isScrolled={isScrolled}
spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers}
isEpic={isEpic}
/>
))}
</tbody>

View file

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

View file

@ -21,7 +21,7 @@ import { FileService } from "@/services/file.service";
const fileService = new FileService();
// local components
import { DraftIssueLayout } from "./draft-issue-layout";
import { IssueFormRoot } from "./form";
import { type IssueFormProps, IssueFormRoot } from "./form";
export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((props) => {
const {
@ -41,7 +41,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
} = props;
const issueStoreType = useIssueStoreType();
const storeType = issueStoreFromProps ?? issueStoreType;
const storeType = (issueStoreFromProps ? issueStoreFromProps : issueStoreType === EIssuesStoreType.EPIC)
? EIssuesStoreType.PROJECT
: issueStoreType;
// ref
const issueTitleRef = useRef<HTMLInputElement>(null);
// states
@ -333,6 +335,30 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
// don't open the modal if there are no projects
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 (
<ModalCore
isOpen={isOpen}
@ -342,51 +368,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear"
>
{withDraftIssueWrapper ? (
<DraftIssueLayout
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}
/>
<DraftIssueLayout {...commonIssueModalProps} changesMade={changesMade} onChange={handleFormChange} />
) : (
<IssueFormRoot
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}
/>
<IssueFormRoot {...commonIssueModalProps} />
)}
</ModalCore>
);

View file

@ -332,6 +332,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
}}
projectId={projectId ?? undefined}
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 { useEventTracker, useWorkspaceDraftIssues } from "@/hooks/store";
// local components
import { IssueFormRoot } from "./form";
import { IssueFormRoot, type IssueFormProps } from "./form";
export interface DraftIssueProps {
export interface DraftIssueProps extends IssueFormProps {
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;
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) => {
const {
changesMade,
data,
issueTitleRef,
onAssetUpload,
onChange,
onClose,
onSubmit,
projectId,
isCreateMoreToggleEnabled,
onCreateMoreToggleChange,
isDraft,
moveToIssue = false,
modalTitle,
primaryButtonText,
isDuplicateModalOpen,
handleDuplicateIssueModal,
isProjectSelectionDisabled = false,
} = props;
const { changesMade, data, onChange, onClose, projectId } = props;
// states
const [issueDiscardModal, setIssueDiscardModal] = useState(false);
// router params
@ -74,7 +38,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
const handleClose = () => {
if (data?.id) {
onClose(false);
onClose();
setIssueDiscardModal(false);
} else {
if (changesMade) {
@ -93,11 +57,11 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
delete changesMade.description_html;
});
if (isEmpty(changesMade)) {
onClose(false);
onClose();
setIssueDiscardModal(false);
} else setIssueDiscardModal(true);
} else {
onClose(false);
onClose();
setIssueDiscardModal(false);
}
}
@ -126,7 +90,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
});
onChange(null);
setIssueDiscardModal(false);
onClose(false);
onClose();
return res;
})
.catch(() => {
@ -162,27 +126,10 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
onDiscard={() => {
onChange(null);
setIssueDiscardModal(false);
onClose(false);
onClose();
}}
/>
<IssueFormRoot
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}
/>
<IssueFormRoot {...props} onClose={handleClose} />
</>
);
});

View file

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

View file

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

View file

@ -57,11 +57,16 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
const issue = issueId ? getIssueById(issueId) : undefined;
const projectDetails = issue?.project_id ? getProjectById(issue?.project_id) : undefined;
// debounced duplicate issues swr
const { duplicateIssues } = useDebouncedDuplicateIssues(projectDetails?.workspace.toString(), projectDetails?.id, {
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
});
const { duplicateIssues } = useDebouncedDuplicateIssues(
workspaceSlug,
projectDetails?.workspace.toString(),
projectDetails?.id,
{
name: issue?.name,
description_html: getTextContent(issue?.description_html),
issueId: issue?.id,
}
);
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";
// hooks
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
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
@ -22,10 +22,16 @@ interface IIssuePeekOverview {
embedIssue?: boolean;
embedRemoveCurrentNotification?: () => void;
is_draft?: boolean;
storeType?: EIssuesStoreType;
}
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
const pathname = usePathname();
// store hook
@ -40,8 +46,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
issue: { fetchIssue, getIsFetchingIssueDetails },
fetchActivities,
} = useIssueDetail();
const { issues } = useIssuesStore();
const issueStoreType = useIssueStoreType();
const storeType = issueStoreFromProps ?? issueStoreType;
const { issues } = useIssues(storeType);
const { captureIssueEvent } = useEventTracker();
// state
const [error, setError] = useState(false);

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
import React from "react";
import { observer } from "mobx-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
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
// helpers
@ -36,6 +36,7 @@ export interface ISubIssues {
) => void;
subIssueOperations: TSubIssueOperations;
issueId: string;
issueServiceType?: TIssueServiceType;
}
export const IssueListItem: React.FC<ISubIssues> = observer((props) => {

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