diff --git a/packages/constants/src/issue.ts b/packages/constants/src/issue.ts index 9f6a1a2e2e9..9aa65d13e38 100644 --- a/packages/constants/src/issue.ts +++ b/packages/constants/src/issue.ts @@ -39,3 +39,8 @@ export enum EServerGroupByToFilterOptions { "project_id" = "project", "created_by" = "created_by", } + +export enum EIssueServiceType { + ISSUES = "issues", + EPICS = "epics", +} diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index b9366cccba1..6fd160e77f7 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -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; diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index d48342cebb9..f878266b75d 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -136,6 +136,7 @@ export type TProjectIssuesSearchParams = { issue_id?: string; workspace_search: boolean; target_date?: string; + epic?: boolean; }; export interface ISearchIssueResponse { diff --git a/web/ce/components/epics/epic-modal/index.ts b/web/ce/components/epics/epic-modal/index.ts new file mode 100644 index 00000000000..031608e25ff --- /dev/null +++ b/web/ce/components/epics/epic-modal/index.ts @@ -0,0 +1 @@ +export * from "./modal"; diff --git a/web/ce/components/epics/epic-modal/modal.tsx b/web/ce/components/epics/epic-modal/modal.tsx new file mode 100644 index 00000000000..9c76b7bdab8 --- /dev/null +++ b/web/ce/components/epics/epic-modal/modal.tsx @@ -0,0 +1,19 @@ +"use client"; +import React, { FC } from "react"; +import { TIssue } from "@plane/types"; + +export interface EpicModalProps { + data?: Partial; + isOpen: boolean; + onClose: () => void; + beforeFormSubmit?: () => Promise; + onSubmit?: (res: TIssue) => Promise; + fetchIssueDetails?: boolean; + primaryButtonText?: { + default: string; + loading: string; + }; + isProjectSelectionDisabled?: boolean; +} + +export const CreateUpdateEpicModal: FC = (props) => <>; diff --git a/web/ce/components/epics/index.ts b/web/ce/components/epics/index.ts new file mode 100644 index 00000000000..29da0cc8acc --- /dev/null +++ b/web/ce/components/epics/index.ts @@ -0,0 +1 @@ +export * from "./epic-modal"; diff --git a/web/ce/components/gantt-chart/dependency/dependency-paths.tsx b/web/ce/components/gantt-chart/dependency/dependency-paths.tsx index f049875f113..6feb208a8b7 100644 --- a/web/ce/components/gantt-chart/dependency/dependency-paths.tsx +++ b/web/ce/components/gantt-chart/dependency/dependency-paths.tsx @@ -1 +1,9 @@ -export const TimelineDependencyPaths = () => <>; +import { FC } from "react"; + +type Props = { + isEpic?: boolean; +}; +export const TimelineDependencyPaths: FC = (props) => { + const { isEpic = false } = props; + return <>; +}; diff --git a/web/ce/components/issue-types/values/update.tsx b/web/ce/components/issue-types/values/update.tsx index cff391d9ea9..2fd62904266 100644 --- a/web/ce/components/issue-types/values/update.tsx +++ b/web/ce/components/issue-types/values/update.tsx @@ -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 = () => <>; diff --git a/web/ce/hooks/use-debounced-duplicate-issues.tsx b/web/ce/hooks/use-debounced-duplicate-issues.tsx index f0325bc1284..8028a619104 100644 --- a/web/ce/hooks/use-debounced-duplicate-issues.tsx +++ b/web/ce/hooks/use-debounced-duplicate-issues.tsx @@ -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 } diff --git a/web/ce/store/issue/epic/filter.store.ts b/web/ce/store/issue/epic/filter.store.ts new file mode 100644 index 00000000000..a4733c60a55 --- /dev/null +++ b/web/ce/store/issue/epic/filter.store.ts @@ -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; + } +} diff --git a/web/ce/store/issue/epic/index.ts b/web/ce/store/issue/epic/index.ts new file mode 100644 index 00000000000..0fe6c946b0c --- /dev/null +++ b/web/ce/store/issue/epic/index.ts @@ -0,0 +1,2 @@ +export * from "./filter.store"; +export * from "./issue.store"; diff --git a/web/ce/store/issue/epic/issue.store.ts b/web/ce/store/issue/epic/issue.store.ts new file mode 100644 index 00000000000..90ccee84da0 --- /dev/null +++ b/web/ce/store/issue/epic/issue.store.ts @@ -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); + } +} diff --git a/web/ce/store/issue/issue-details/activity.store.ts b/web/ce/store/issue/issue-details/activity.store.ts index 6c0029c4f8c..de84fb87d9a 100644 --- a/web/ce/store/issue/issue-details/activity.store.ts +++ b/web/ce/store/issue/issue-details/activity.store.ts @@ -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( diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index 0fe75904d8b..b77106a1955 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -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; } @@ -41,10 +42,13 @@ export const LiteTextEditor = React.forwardRef(ref) ? ref.current : null; return ( -
+
!showToolbarInitially && setIsFocused(true)} + onBlur={() => !showToolbarInitially && setIsFocused(false)} + > - { - // 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} - /> +
+ { + // 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} + /> +
); }); diff --git a/web/core/components/gantt-chart/chart/main-content.tsx b/web/core/components/gantt-chart/chart/main-content.tsx index b9582d21cee..63e01c54ea0 100644 --- a/web/core/components/gantt-chart/chart/main-content.tsx +++ b/web/core/components/gantt-chart/chart/main-content.tsx @@ -56,6 +56,7 @@ type Props = { targetDate?: Date ) => ChartDataType | undefined; quickAdd?: React.JSX.Element | undefined; + isEpic?: boolean; }; export const GanttChartMainContent: React.FC = observer((props) => { @@ -79,6 +80,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { updateCurrentViewRenderPayload, quickAdd, updateBlockDates, + isEpic = false, } = props; // refs const ganttContainerRef = useRef(null); @@ -159,7 +161,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { entities={{ [GANTT_SELECT_GROUP]: blockIds ?? [], }} - disabled={!isBulkOperationsEnabled} + disabled={!isBulkOperationsEnabled || isEpic} > {(helpers) => ( <> @@ -187,6 +189,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { title={title} quickAdd={quickAdd} selectionHelpers={helpers} + isEpic={isEpic} />
@@ -208,7 +211,7 @@ export const GanttChartMainContent: React.FC = observer((props) => { selectionHelpers={helpers} ganttContainerRef={ganttContainerRef} /> - + = observer((props) => { quickAdd, showToday, updateBlockDates, + isEpic = false, } = props; // states const [itemsContainerWidth, setItemsContainerWidth] = useState(0); @@ -204,6 +206,7 @@ export const ChartViewRoot: FC = observer((props) => { updateCurrentViewRenderPayload={updateCurrentViewRenderPayload} quickAdd={quickAdd} updateBlockDates={updateBlockDates} + isEpic={isEpic} />
); diff --git a/web/core/components/gantt-chart/root.tsx b/web/core/components/gantt-chart/root.tsx index 81f064e2fb4..3ed21d1440a 100644 --- a/web/core/components/gantt-chart/root.tsx +++ b/web/core/components/gantt-chart/root.tsx @@ -26,6 +26,7 @@ type GanttChartRootProps = { bottomSpacing?: boolean; showAllBlocks?: boolean; showToday?: boolean; + isEpic?: boolean; }; export const GanttChartRoot: FC = observer((props) => { @@ -50,6 +51,7 @@ export const GanttChartRoot: FC = observer((props) => { showToday = true, quickAdd, updateBlockDates, + isEpic = false, } = props; const { setBlockIds } = useTimeLineChartStore(); @@ -81,6 +83,7 @@ export const GanttChartRoot: FC = observer((props) => { quickAdd={quickAdd} showToday={showToday} updateBlockDates={updateBlockDates} + isEpic={isEpic} /> ); }); diff --git a/web/core/components/gantt-chart/sidebar/issues/block.tsx b/web/core/components/gantt-chart/sidebar/issues/block.tsx index bb286c28006..c0218aceb16 100644 --- a/web/core/components/gantt-chart/sidebar/issues/block.tsx +++ b/web/core/components/gantt-chart/sidebar/issues/block.tsx @@ -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) => { )}
- +
{duration && (
diff --git a/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx b/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx index b2f6b879284..d2e5557ff84 100644 --- a/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx +++ b/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx @@ -29,6 +29,7 @@ type Props = { enableSelection: boolean; showAllBlocks?: boolean; selectionHelpers?: TSelectionHelper; + isEpic?: boolean; }; export const IssueGanttSidebar: React.FC = observer((props) => { @@ -42,6 +43,7 @@ export const IssueGanttSidebar: React.FC = observer((props) => { ganttContainerRef, showAllBlocks = false, selectionHelpers, + isEpic = false, } = props; const { getBlockById } = useTimeLineChart(ETimeLineTypeType.ISSUE); @@ -101,6 +103,7 @@ export const IssueGanttSidebar: React.FC = observer((props) => { enableSelection={enableSelection} isDragging={isDragging} selectionHelpers={selectionHelpers} + isEpic={isEpic} /> )} diff --git a/web/core/components/gantt-chart/sidebar/root.tsx b/web/core/components/gantt-chart/sidebar/root.tsx index 31c9137cc5a..7202efc55d9 100644 --- a/web/core/components/gantt-chart/sidebar/root.tsx +++ b/web/core/components/gantt-chart/sidebar/root.tsx @@ -23,6 +23,7 @@ type Props = { title: string; quickAdd?: React.JSX.Element | undefined; selectionHelpers: TSelectionHelper; + isEpic?: boolean; }; export const GanttChartSidebar: React.FC = observer((props) => { @@ -38,6 +39,7 @@ export const GanttChartSidebar: React.FC = 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 = observer((props) => { ganttContainerRef, loadMoreBlocks, selectionHelpers, + isEpic, })} {quickAdd ? quickAdd : null} diff --git a/web/core/components/inbox/content/issue-root.tsx b/web/core/components/inbox/content/issue-root.tsx index b154bd2056a..258a1c50b00 100644 --- a/web/core/components/inbox/content/issue-root.tsx +++ b/web/core/components/inbox/content/issue-root.tsx @@ -65,11 +65,16 @@ export const InboxIssueMainContent: React.FC = 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 <>; diff --git a/web/core/components/inbox/modals/create-modal/create-root.tsx b/web/core/components/inbox/modals/create-modal/create-root.tsx index 4f7bbd4b45b..34c17e15b38 100644 --- a/web/core/components/inbox/modals/create-modal/create-root.tsx +++ b/web/core/components/inbox/modals/create-modal/create-root.tsx @@ -87,10 +87,15 @@ export const InboxIssueCreateRoot: FC = 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()) { diff --git a/web/core/components/issues/attachment/attachment-item-list.tsx b/web/core/components/issues/attachment/attachment-item-list.tsx index d377bd90a16..ca3c0ef9f11 100644 --- a/web/core/components/issues/attachment/attachment-item-list.tsx +++ b/web/core/components/issues/attachment/attachment-item-list.tsx @@ -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 = 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 = 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 = observer((p onClose={() => toggleDeleteAttachmentModal(null)} attachmentOperations={attachmentOperations} attachmentId={attachmentDeleteModalId} + issueServiceType={issueServiceType} /> )}
= observer((p
)} {issueAttachments?.map((attachmentId) => ( - + ))}
diff --git a/web/core/components/issues/attachment/attachment-list-item.tsx b/web/core/components/issues/attachment/attachment-list-item.tsx index e3adc5f828b..dfdbde0741e 100644 --- a/web/core/components/issues/attachment/attachment-list-item.tsx +++ b/web/core/components/issues/attachment/attachment-list-item.tsx @@ -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 = 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 ?? ""); diff --git a/web/core/components/issues/attachment/delete-attachment-modal.tsx b/web/core/components/issues/attachment/delete-attachment-modal.tsx index 925ff21c0c9..32c9d961c7f 100644 --- a/web/core/components/issues/attachment/delete-attachment-modal.tsx +++ b/web/core/components/issues/attachment/delete-attachment-modal.tsx @@ -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 = 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; diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index 4b80ba12f16..54b6f501572 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -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 = 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 ( = observer((props) => issueId={issueId} disabled={disabled} attachmentHelpers={attachmentHelpers} + issueServiceType={issueServiceType} /> ); }); diff --git a/web/core/components/issues/issue-detail-widgets/attachments/helper.tsx b/web/core/components/issues/issue-detail-widgets/attachments/helper.tsx index 43b4812e6a0..28684ac9986 100644 --- a/web/core/components/issues/issue-detail-widgets/attachments/helper.tsx +++ b/web/core/components/issues/issue-detail-widgets/attachments/helper.tsx @@ -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( diff --git a/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx b/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx index c2d88a9541c..fb0b5b2c6fd 100644 --- a/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx +++ b/web/core/components/issues/issue-detail-widgets/attachments/quick-action-button.tsx @@ -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 = 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); diff --git a/web/core/components/issues/issue-detail-widgets/attachments/root.tsx b/web/core/components/issues/issue-detail-widgets/attachments/root.tsx index 1f802759050..7b4788a1a6a 100644 --- a/web/core/components/issues/issue-detail-widgets/attachments/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/attachments/root.tsx @@ -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 = 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 = observer((props) => { projectId={projectId} issueId={issueId} disabled={disabled} + issueServiceType={issueServiceType} /> } buttonClassName="w-full" @@ -45,6 +49,7 @@ export const AttachmentsCollapsible: FC = observer((props) => { projectId={projectId} issueId={issueId} disabled={disabled} + issueServiceType={issueServiceType} /> ); diff --git a/web/core/components/issues/issue-detail-widgets/attachments/title.tsx b/web/core/components/issues/issue-detail-widgets/attachments/title.tsx index ce83f6826df..f2d0cd670c7 100644 --- a/web/core/components/issues/issue-detail-widgets/attachments/title.tsx +++ b/web/core/components/issues/issue-detail-widgets/attachments/title.tsx @@ -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 = 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 = observer((props) => { projectId={projectId} issueId={issueId} disabled={disabled} + issueServiceType={issueServiceType} /> ) } diff --git a/web/core/components/issues/issue-detail-widgets/links/content.tsx b/web/core/components/issues/issue-detail-widgets/links/content.tsx index 2d85270b022..fefc7938fdf 100644 --- a/web/core/components/issues/issue-detail-widgets/links/content.tsx +++ b/web/core/components/issues/issue-detail-widgets/links/content.tsx @@ -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) => { - 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 ; + return ( + + ); }; diff --git a/web/core/components/issues/issue-detail-widgets/links/helper.tsx b/web/core/components/issues/issue-detail-widgets/links/helper.tsx index 4669528cd3a..ae915beb898 100644 --- a/web/core/components/issues/issue-detail-widgets/links/helper.tsx +++ b/web/core/components/issues/issue-detail-widgets/links/helper.tsx @@ -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( () => ({ diff --git a/web/core/components/issues/issue-detail-widgets/links/quick-action-button.tsx b/web/core/components/issues/issue-detail-widgets/links/quick-action-button.tsx index f9a59dd3a71..775e2f9d779 100644 --- a/web/core/components/issues/issue-detail-widgets/links/quick-action-button.tsx +++ b/web/core/components/issues/issue-detail-widgets/links/quick-action-button.tsx @@ -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 = 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) => { diff --git a/web/core/components/issues/issue-detail-widgets/links/root.tsx b/web/core/components/issues/issue-detail-widgets/links/root.tsx index 5b8c11b028d..655f23e0857 100644 --- a/web/core/components/issues/issue-detail-widgets/links/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/links/root.tsx @@ -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 = 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 = observer((props) => { toggleOpenWidget("links")} - title={} + title={ + + } buttonClassName="w-full" > = observer((props) => { projectId={projectId} issueId={issueId} disabled={disabled} + issueServiceType={issueServiceType} /> ); diff --git a/web/core/components/issues/issue-detail-widgets/links/title.tsx b/web/core/components/issues/issue-detail-widgets/links/title.tsx index 1e01ee19887..19929df255d 100644 --- a/web/core/components/issues/issue-detail-widgets/links/title.tsx +++ b/web/core/components/issues/issue-detail-widgets/links/title.tsx @@ -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 = 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 = observer((props) => { isOpen={isOpen} title="Links" indicatorElement={indicatorElement} - actionItemElement={!disabled && } + actionItemElement={ + !disabled && + } /> ); }); diff --git a/web/core/components/issues/issue-detail-widgets/relations/content.tsx b/web/core/components/issues/issue-detail-widgets/relations/content.tsx index 79be48e4f00..10b3b6585eb 100644 --- a/web/core/components/issues/issue-detail-widgets/relations/content.tsx +++ b/web/core/components/issues/issue-detail-widgets/relations/content.tsx @@ -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 = 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 = observer((props) => { relation: { getRelationsByIssueId }, toggleDeleteIssueModal, toggleCreateIssueModal, - } = useIssueDetail(); + } = useIssueDetail(issueServiceType); // helper const issueOperations = useRelationOperations(); @@ -129,6 +131,7 @@ export const RelationsCollapsibleContent: FC = observer((props) => { disabled={disabled} issueOperations={issueOperations} handleIssueCrudState={handleIssueCrudState} + issueServiceType={issueServiceType} />
diff --git a/web/core/components/issues/issue-detail-widgets/relations/helper.tsx b/web/core/components/issues/issue-detail-widgets/relations/helper.tsx index ac8b0f66362..4267e9e1a91 100644 --- a/web/core/components/issues/issue-detail-widgets/relations/helper.tsx +++ b/web/core/components/issues/issue-detail-widgets/relations/helper.tsx @@ -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; }; -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(); diff --git a/web/core/components/issues/issue-detail-widgets/relations/quick-action-button.tsx b/web/core/components/issues/issue-detail-widgets/relations/quick-action-button.tsx index dff072e7dca..b1ff260f6f5 100644 --- a/web/core/components/issues/issue-detail-widgets/relations/quick-action-button.tsx +++ b/web/core/components/issues/issue-detail-widgets/relations/quick-action-button.tsx @@ -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 = 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(); diff --git a/web/core/components/issues/issue-detail-widgets/relations/root.tsx b/web/core/components/issues/issue-detail-widgets/relations/root.tsx index d6a8edc3fc5..78c6ff39704 100644 --- a/web/core/components/issues/issue-detail-widgets/relations/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/relations/root.tsx @@ -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 = 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 = observer((props) => { toggleOpenWidget("relations")} - title={} + title={ + + } buttonClassName="w-full" > = observer((props) => { projectId={projectId} issueId={issueId} disabled={disabled} + issueServiceType={issueServiceType} /> ); diff --git a/web/core/components/issues/issue-detail-widgets/relations/title.tsx b/web/core/components/issues/issue-detail-widgets/relations/title.tsx index 2c3854beddd..3f91b712f77 100644 --- a/web/core/components/issues/issue-detail-widgets/relations/title.tsx +++ b/web/core/components/issues/issue-detail-widgets/relations/title.tsx @@ -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 = 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 = observer((props) => { isOpen={isOpen} title="Relations" indicatorElement={indicatorElement} - actionItemElement={!disabled && } + actionItemElement={ + !disabled && + } /> ); }); diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/content.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/content.tsx index 5432ee777e6..2bd9c90bfb5 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/content.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/content.tsx @@ -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 = 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 = 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 = observer((props) => { useEffect(() => { handleFetchSubIssues(); - return () => { handleFetchSubIssues(); }; @@ -123,6 +124,7 @@ export const SubIssuesCollapsibleContent: FC = observer((props) => { disabled={!disabled} handleIssueCrudState={handleIssueCrudState} subIssueOperations={subIssueOperations} + issueServiceType={issueServiceType} /> )} diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx index 7df432d5d21..cc8abd82fd4 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/helper.tsx @@ -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; }; -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(); diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/quick-action-button.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/quick-action-button.tsx index bf1ece31008..73770bb130c 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/quick-action-button.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/quick-action-button.tsx @@ -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 = 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 = observer((props) => { toggleSubIssuesModal, setIssueCrudOperationState, issueCrudOperationState, - } = useIssueDetail(); + } = useIssueDetail(issueServiceType); const { setTrackElement } = useEventTracker(); // derived values diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/root.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/root.tsx index 99dbcacb510..5ead3bc5f4d 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/root.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/root.tsx @@ -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 = 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 = observer((props) => { toggleOpenWidget("sub-issues")} - title={} + title={ + + } buttonClassName="w-full" > = observer((props) => { projectId={projectId} parentIssueId={issueId} disabled={disabled} + issueServiceType={issueServiceType} /> ); diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/title.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/title.tsx index b3d9bf1fc15..ad88c112ede 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/title.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/title.tsx @@ -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 = 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 = observer((props) => { const totalCount = subIssues.length; const percentage = completedCount && totalCount ? (completedCount / totalCount) * 100 : 0; - // indicator element - const indicatorElement = useMemo( - () => ( -
- - - {completedCount}/{totalCount} Done - -
- ), - [completedCount, totalCount, percentage] - ); - return ( } + indicatorElement={ +
+ + + {completedCount}/{totalCount} Done + +
+ } + actionItemElement={ + !disabled && ( + + ) + } /> ); }); diff --git a/web/core/components/issues/issue-detail/label/root.tsx b/web/core/components/issues/issue-detail/label/root.tsx index daa5169b13b..f71e9ba3ce2 100644 --- a/web/core/components/issues/issue-detail/label/root.tsx +++ b/web/core/components/issues/issue-detail/label/root.tsx @@ -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 = 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(); diff --git a/web/core/components/issues/issue-detail/links/create-update-link-modal.tsx b/web/core/components/issues/issue-detail/links/create-update-link-modal.tsx index 99e350c6132..2da5d0f7955 100644 --- a/web/core/components/issues/issue-detail/links/create-update-link-modal.tsx +++ b/web/core/components/issues/issue-detail/links/create-update-link-modal.tsx @@ -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 = 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 = observe defaultValues, }); // store hooks - const { issueLinkData: preloadedData, setIssueLinkData } = useIssueDetail(); + const { issueLinkData: preloadedData, setIssueLinkData } = useIssueDetail(issueServiceType); const onClose = () => { setIssueLinkData(null); diff --git a/web/core/components/issues/issue-detail/links/link-item.tsx b/web/core/components/issues/issue-detail/links/link-item.tsx index c7629b5dce2..68e8acf4ded 100644 --- a/web/core/components/issues/issue-detail/links/link-item.tsx +++ b/web/core/components/issues/issue-detail/links/link-item.tsx @@ -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 = 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 <>; diff --git a/web/core/components/issues/issue-detail/links/link-list.tsx b/web/core/components/issues/issue-detail/links/link-list.tsx index 7f20d53677c..7ca128b988e 100644 --- a/web/core/components/issues/issue-detail/links/link-list.tsx +++ b/web/core/components/issues/issue-detail/links/link-list.tsx @@ -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 = 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 = observer((props) => { return (
{issueLinks.map((linkId) => ( - + ))}
); diff --git a/web/core/components/issues/issue-detail/main-content.tsx b/web/core/components/issues/issue-detail/main-content.tsx index 08d5c06630c..f484761641b 100644 --- a/web/core/components/issues/issue-detail/main-content.tsx +++ b/web/core/components/issues/issue-detail/main-content.tsx @@ -55,11 +55,16 @@ export const IssueMainContent: React.FC = 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") { diff --git a/web/core/components/issues/issue-detail/parent-select.tsx b/web/core/components/issues/issue-detail/parent-select.tsx index af9b40df1ec..d83bc635b47 100644 --- a/web/core/components/issues/issue-detail/parent-select.tsx +++ b/web/core/components/issues/issue-detail/parent-select.tsx @@ -88,6 +88,7 @@ export const IssueParentSelect: React.FC = observer((props) isOpen={isParentIssueModalOpen === issueId} handleClose={() => toggleParentIssueModal(null)} onChange={(issue: any) => handleParentIssue(issue?.id)} + searchEpic />
diff --git a/web/core/components/issues/issue-layouts/calendar/calendar.tsx b/web/core/components/issues/issue-layouts/calendar/calendar.tsx index 87ed664454f..0377f19c463 100644 --- a/web/core/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/core/components/issues/issue-layouts/calendar/calendar.tsx @@ -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; canEditProperties: (projectId: string | undefined) => boolean; + isEpic?: boolean; }; export const CalendarChart: React.FC = observer((props) => { @@ -84,6 +91,7 @@ export const CalendarChart: React.FC = observer((props) => { updateFilters, canEditProperties, readOnly = false, + isEpic = false, } = props; // states const [selectedDate, setSelectedDate] = useState(new Date()); @@ -167,6 +175,7 @@ export const CalendarChart: React.FC = observer((props) => { addIssuesToView={addIssuesToView} readOnly={readOnly} canEditProperties={canEditProperties} + isEpic={isEpic} /> ))} @@ -190,6 +199,7 @@ export const CalendarChart: React.FC = observer((props) => { addIssuesToView={addIssuesToView} readOnly={readOnly} canEditProperties={canEditProperties} + isEpic={isEpic} /> )} @@ -216,6 +226,7 @@ export const CalendarChart: React.FC = observer((props) => { canEditProperties={canEditProperties} isDragDisabled isMobileView + isEpic={isEpic} /> @@ -243,6 +254,7 @@ export const CalendarChart: React.FC = observer((props) => { canEditProperties={canEditProperties} isDragDisabled isMobileView + isEpic={isEpic} /> diff --git a/web/core/components/issues/issue-layouts/calendar/day-tile.tsx b/web/core/components/issues/issue-layouts/calendar/day-tile.tsx index 2392425caca..405cbf2ac21 100644 --- a/web/core/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/core/components/issues/issue-layouts/calendar/day-tile.tsx @@ -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 = observer((props) => { @@ -68,6 +75,7 @@ export const CalendarDayTile: React.FC = observer((props) => { handleDragAndDrop, setSelectedDate, canEditProperties, + isEpic = false, } = props; const [isDraggingOver, setIsDraggingOver] = useState(false); @@ -185,6 +193,7 @@ export const CalendarDayTile: React.FC = observer((props) => { quickAddCallback={quickAddCallback} readOnly={readOnly} canEditProperties={canEditProperties} + isEpic={isEpic} /> diff --git a/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx b/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx index 9f9f5b5bd6d..05e5a163188 100644 --- a/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx +++ b/web/core/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx @@ -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 = observer((props: Props) => { const { issuesFilterStore } = props; diff --git a/web/core/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx b/web/core/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx index 822f4248872..b28959c5b22 100644 --- a/web/core/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx +++ b/web/core/components/issues/issue-layouts/calendar/dropdowns/options-dropdown.tsx @@ -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, diff --git a/web/core/components/issues/issue-layouts/calendar/header.tsx b/web/core/components/issues/issue-layouts/calendar/header.tsx index c7e6232a053..3c55acf8e4e 100644 --- a/web/core/components/issues/issue-layouts/calendar/header.tsx +++ b/web/core/components/issues/issue-layouts/calendar/header.tsx @@ -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, diff --git a/web/core/components/issues/issue-layouts/calendar/issue-block-root.tsx b/web/core/components/issues/issue-layouts/calendar/issue-block-root.tsx index 35bea6386e5..2ca6a8f8ea7 100644 --- a/web/core/components/issues/issue-layouts/calendar/issue-block-root.tsx +++ b/web/core/components/issues/issue-layouts/calendar/issue-block-root.tsx @@ -15,11 +15,12 @@ type Props = { issueId: string; quickActions: TRenderQuickActions; isDragDisabled: boolean; + isEpic?: boolean; canEditProperties: (projectId: string | undefined) => boolean; }; export const CalendarIssueBlockRoot: React.FC = observer((props) => { - const { issueId, quickActions, isDragDisabled, canEditProperties } = props; + const { issueId, quickActions, isDragDisabled, isEpic = false, canEditProperties } = props; const issueRef = useRef(null); const [isDragging, setIsDragging] = useState(false); @@ -58,5 +59,13 @@ export const CalendarIssueBlockRoot: React.FC = observer((props) => { if (!issue) return null; - return ; + return ( + + ); }); diff --git a/web/core/components/issues/issue-layouts/calendar/issue-block.tsx b/web/core/components/issues/issue-layouts/calendar/issue-block.tsx index 734ce25cff6..8a03ef4c840 100644 --- a/web/core/components/issues/issue-layouts/calendar/issue-block.tsx +++ b/web/core/components/issues/issue-layouts/calendar/issue-block.tsx @@ -28,11 +28,12 @@ type Props = { issue: TIssue; quickActions: TRenderQuickActions; isDragging?: boolean; + isEpic?: boolean; }; export const CalendarIssueBlock = observer( forwardRef((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); diff --git a/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx index a7811a40619..674819bbf7a 100644 --- a/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -23,6 +23,7 @@ type Props = { readOnly?: boolean; isMobileView?: boolean; canEditProperties: (projectId: string | undefined) => boolean; + isEpic?: boolean; }; export const CalendarIssueBlocks: React.FC = observer((props) => { @@ -39,6 +40,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { readOnly, isMobileView = false, canEditProperties, + isEpic = false, } = props; const formattedDatePayload = renderFormattedPayloadDate(date); @@ -66,6 +68,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { quickActions={quickActions} isDragDisabled={isDragDisabled || isMobileView} canEditProperties={canEditProperties} + isEpic={isEpic} /> ))} diff --git a/web/core/components/issues/issue-layouts/calendar/week-days.tsx b/web/core/components/issues/issue-layouts/calendar/week-days.tsx index 1c413db35d8..9aaa132294c 100644 --- a/web/core/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/core/components/issues/issue-layouts/calendar/week-days.tsx @@ -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 = observer((props) => { @@ -56,6 +63,7 @@ export const CalendarWeekDays: React.FC = 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 = observer((props) => { readOnly={readOnly} handleDragAndDrop={handleDragAndDrop} canEditProperties={canEditProperties} + isEpic={isEpic} /> ); })} diff --git a/web/core/components/issues/issue-layouts/empty-states/index.tsx b/web/core/components/issues/issue-layouts/empty-states/index.tsx index 752e00bfddb..e776d29bad2 100644 --- a/web/core/components/issues/issue-layouts/empty-states/index.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/index.tsx @@ -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 ; case EIssuesStoreType.PROFILE: return ; + case EIssuesStoreType.EPIC: + return ; default: return null; } diff --git a/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx b/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx new file mode 100644 index 00000000000..213e5ac405c --- /dev/null +++ b/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx @@ -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 = () => ( +
+ {}} /> +
+); diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index 57175ae1d6d..721bad76969 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -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 = 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(() => { {isEditingAllowed && ( = 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 = observer((props: IBaseGan loaderTitle="Issues" blockIds={issuesIds} blockUpdateHandler={updateIssueBlockStructure} - blockToRender={(data: TIssue) => } - sidebarToRender={(props) => } + blockToRender={(data: TIssue) => } + sidebarToRender={(props) => } enableBlockLeftResize={isAllowed} enableBlockRightResize={isAllowed} enableBlockMove={isAllowed} @@ -136,6 +138,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan canLoadMoreBlocks={nextPageResults} updateBlockDates={updateBlockDates} showAllBlocks + isEpic={isEpic} /> diff --git a/web/core/components/issues/issue-layouts/gantt/blocks.tsx b/web/core/components/issues/issue-layouts/gantt/blocks.tsx index efb2b6bd14c..bd25dc916ed 100644 --- a/web/core/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/core/components/issues/issue-layouts/gantt/blocks.tsx @@ -21,10 +21,11 @@ import { GanttStoreType } from "./base-gantt-root"; type Props = { issueId: string; + isEpic?: boolean; }; export const IssueGanttBlock: React.FC = 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 = 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 = observer((props) => { // rendering issues on gantt sidebar export const IssueGanttSidebarBlock: React.FC = 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 = 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 = observer((props) => { return ( ; @@ -42,10 +44,18 @@ export interface IBaseKanBanLayout { canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; viewId?: string | undefined; + isEpic?: boolean; } export const BaseKanBanRoot: React.FC = 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 = 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 = observer((props: IBas scrollableContainerRef={scrollableContainerRef} handleOnDrop={handleOnDrop} loadMoreIssues={fetchMoreIssues} + isEpic={isEpic} /> diff --git a/web/core/components/issues/issue-layouts/kanban/block.tsx b/web/core/components/issues/issue-layouts/kanban/block.tsx index 314c103efaf..d8f4307e855 100644 --- a/web/core/components/issues/issue-layouts/kanban/block.tsx +++ b/web/core/components/issues/issue-layouts/kanban/block.tsx @@ -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; shouldRenderByDefault?: boolean; + isEpic?: boolean; } interface IssueDetailsBlockProps { @@ -50,10 +52,11 @@ interface IssueDetailsBlockProps { updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; quickActions: TRenderQuickActions; isReadOnly: boolean; + isEpic?: boolean; } const KanbanIssueDetailsBlock: React.FC = 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 = observer((prop activeLayout="Kanban" updateIssue={updateIssue} isReadOnly={isReadOnly} + isEpic={isEpic} /> ); @@ -118,6 +122,7 @@ export const KanbanIssueBlock: React.FC = observer((props) => { canEditProperties, scrollableContainerRef, shouldRenderByDefault, + isEpic = false, } = props; const cardRef = useRef(null); @@ -125,8 +130,8 @@ export const KanbanIssueBlock: React.FC = 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 = observer((props) => { > = observer((props) => { updateIssue={updateIssue} quickActions={quickActions} isReadOnly={!canEditIssueProperties} + isEpic={isEpic} /> diff --git a/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx index 4a4ae9a2c58..2fb4da801bd 100644 --- a/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -18,6 +18,7 @@ interface IssueBlocksListProps { canDropOverIssue: boolean; canDragIssuesInCurrentGrouping: boolean; scrollableContainerRef?: MutableRefObject; + isEpic?: boolean; } export const KanbanIssueBlocksList: React.FC = observer((props) => { @@ -33,6 +34,7 @@ export const KanbanIssueBlocksList: React.FC = observer((p quickActions, canEditProperties, scrollableContainerRef, + isEpic = false, } = props; return ( @@ -62,6 +64,7 @@ export const KanbanIssueBlocksList: React.FC = observer((p canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping} canEditProperties={canEditProperties} scrollableContainerRef={scrollableContainerRef} + isEpic={isEpic} /> ); })} diff --git a/web/core/components/issues/issue-layouts/kanban/default.tsx b/web/core/components/issues/issue-layouts/kanban/default.tsx index f1107c8fb7c..3f20646d1c8 100644 --- a/web/core/components/issues/issue-layouts/kanban/default.tsx +++ b/web/core/components/issues/issue-layouts/kanban/default.tsx @@ -58,6 +58,7 @@ export interface IKanBan { handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise; showEmptyGroup?: boolean; subGroupIndex?: number; + isEpic?: boolean; } export const KanBan: React.FC = observer((props) => { @@ -86,6 +87,7 @@ export const KanBan: React.FC = observer((props) => { isDropDisabled, dropErrorMessage, subGroupIndex = 0, + isEpic = false, } = props; // store hooks const storeType = useIssueStoreType(); @@ -164,6 +166,7 @@ export const KanBan: React.FC = observer((props) => { addIssuesToView={addIssuesToView} collapsedGroups={collapsedGroups} handleCollapsedGroups={handleCollapsedGroups} + isEpic={isEpic} /> )} @@ -207,6 +210,7 @@ export const KanBan: React.FC = observer((props) => { scrollableContainerRef={scrollableContainerRef} loadMoreIssues={loadMoreIssues} handleOnDrop={handleOnDrop} + isEpic={isEpic} /> )} diff --git a/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index 4303946dc1d..853a25e9b3a 100644 --- a/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/core/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -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; disableIssueCreation?: boolean; addIssuesToView?: (issueIds: string[]) => Promise; + isEpic?: boolean; } export const HeaderGroupByCard: FC = observer((props) => { @@ -45,6 +48,7 @@ export const HeaderGroupByCard: FC = 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 = observer((props) => { return ( <> - setIsOpen(false)} - data={issuePayload} - storeType={storeType} - isDraft={isDraftIssue} - /> + {isEpic ? ( + setIsOpen(false)} data={issuePayload} /> + ) : ( + setIsOpen(false)} + data={issuePayload} + storeType={storeType} + isDraft={isDraftIssue} + /> + )} {renderExistingIssueModal && ( ; handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise; 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} : )} diff --git a/web/core/components/issues/issue-layouts/list/base-list-root.tsx b/web/core/components/issues/issue-layouts/list/base-list-root.tsx index 7bff28cd920..ff49ec668ee 100644 --- a/web/core/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/core/components/issues/issue-layouts/list/base-list-root.tsx @@ -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; @@ -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} /> diff --git a/web/core/components/issues/issue-layouts/list/block-root.tsx b/web/core/components/issues/issue-layouts/list/block-root.tsx index 4d13e3ef9fb..bcb2ef18956 100644 --- a/web/core/components/issues/issue-layouts/list/block-root.tsx +++ b/web/core/components/issues/issue-layouts/list/block-root.tsx @@ -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 = observer((props) => { @@ -59,6 +61,7 @@ export const IssueBlockRoot: FC = observer((props) => { isLastChild = false, selectionHelpers, shouldRenderByDefault, + isEpic = false, } = props; // states const [isExpanded, setExpanded] = useState(false); @@ -69,7 +72,7 @@ export const IssueBlockRoot: FC = 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 = observer((props) => { canDrag={!isSubIssue && isDragAllowed} isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging} setIsCurrentBlockDragging={setIsCurrentBlockDragging} + isEpic={isEpic} /> {isExpanded && + !isEpic && subIssues?.map((subIssueId) => ( >; 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(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 ( handleIssuePeekOverview(issue)} className="w-full cursor-pointer" disabled={!!issue?.tempId || issue?.is_draft} @@ -178,7 +186,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
{/* select checkbox */} - {projectId && canSelectIssues && ( + {projectId && canSelectIssues && !isEpic && ( @@ -220,7 +228,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => { {/* sub-issues chevron */}
- {subIssuesCount > 0 && ( + {subIssuesCount > 0 && !isEpic && (
- {/* modules */} - {projectDetails?.module_view && ( - -
- -
-
- )} - - {/* cycles */} - {projectDetails?.cycle_view && ( - -
- -
-
+ {!isEpic && ( + <> + {/* modules */} + {projectDetails?.module_view && ( + +
+ +
+
+ )} + + {/* cycles */} + {projectDetails?.cycle_view && ( + +
+ +
+
+ )} + )} {/* estimates */} diff --git a/web/core/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/core/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 4ba268dedb4..b7bd6d9f134 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -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; 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} /> ); diff --git a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx index 5c4e9e9274c..0ce6465512b 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -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(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} /> {isExpanded && + !isEpic && subIssues?.map((subIssueId: string) => ( { @@ -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" > 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 */}
- {subIssuesCount > 0 && ( + {subIssuesCount > 0 && !isEpic && (
diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index 798af257795..2a9d4abe1ce 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -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 = observer((props) => { const { @@ -41,7 +41,9 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( } = props; const issueStoreType = useIssueStoreType(); - const storeType = issueStoreFromProps ?? issueStoreType; + const storeType = (issueStoreFromProps ? issueStoreFromProps : issueStoreType === EIssuesStoreType.EPIC) + ? EIssuesStoreType.PROJECT + : issueStoreType; // ref const issueTitleRef = useRef(null); // states @@ -333,6 +335,30 @@ export const CreateUpdateIssueModalBase: React.FC = 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 ( = observer(( className="!bg-transparent rounded-lg shadow-none transition-[width] ease-linear" > {withDraftIssueWrapper ? ( - handleFormSubmit(payload, isDraft)} - projectId={activeProjectId} - isCreateMoreToggleEnabled={createMore} - onCreateMoreToggleChange={handleCreateMoreToggleChange} - isDraft={isDraft} - moveToIssue={moveToIssue} - isDuplicateModalOpen={isDuplicateModalOpen} - handleDuplicateIssueModal={handleDuplicateIssueModal} - isProjectSelectionDisabled={isProjectSelectionDisabled} - /> + ) : ( - handleFormSubmit(payload, isDraft)} - projectId={activeProjectId} - isDraft={isDraft} - moveToIssue={moveToIssue} - modalTitle={modalTitle} - primaryButtonText={primaryButtonText} - isDuplicateModalOpen={isDuplicateModalOpen} - handleDuplicateIssueModal={handleDuplicateIssueModal} - isProjectSelectionDisabled={isProjectSelectionDisabled} - /> + )} ); diff --git a/web/core/components/issues/issue-modal/components/default-properties.tsx b/web/core/components/issues/issue-modal/components/default-properties.tsx index c1978260a14..91425e71a1e 100644 --- a/web/core/components/issues/issue-modal/components/default-properties.tsx +++ b/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -332,6 +332,7 @@ export const IssueDefaultProperties: React.FC = ob }} projectId={projectId ?? undefined} issueId={isDraft ? undefined : id} + searchEpic /> )} /> diff --git a/web/core/components/issues/issue-modal/context/index.ts b/web/core/components/issues/issue-modal/context/index.ts index 61ad8c43aa5..e396ff2c171 100644 --- a/web/core/components/issues/issue-modal/context/index.ts +++ b/web/core/components/issues/issue-modal/context/index.ts @@ -1 +1 @@ -export * from "./issue-modal"; +export * from "./issue-modal-context"; diff --git a/web/core/components/issues/issue-modal/context/issue-modal.tsx b/web/core/components/issues/issue-modal/context/issue-modal-context.tsx similarity index 100% rename from web/core/components/issues/issue-modal/context/issue-modal.tsx rename to web/core/components/issues/issue-modal/context/issue-modal-context.tsx diff --git a/web/core/components/issues/issue-modal/draft-issue-layout.tsx b/web/core/components/issues/issue-modal/draft-issue-layout.tsx index d74cb7606e4..e2c693acc4b 100644 --- a/web/core/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/core/components/issues/issue-modal/draft-issue-layout.tsx @@ -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 | null; - data?: Partial; - issueTitleRef: React.MutableRefObject; - isCreateMoreToggleEnabled: boolean; - onAssetUpload: (assetId: string) => void; - onCreateMoreToggleChange: (value: boolean) => void; onChange: (formData: Partial | null) => void; - onClose: (saveDraftIssueInLocalStorage?: boolean) => void; - onSubmit: (formData: Partial, is_draft_issue?: boolean) => Promise; - 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 = 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 = observer((props) => { const handleClose = () => { if (data?.id) { - onClose(false); + onClose(); setIssueDiscardModal(false); } else { if (changesMade) { @@ -93,11 +57,11 @@ export const DraftIssueLayout: React.FC = 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 = observer((props) => { }); onChange(null); setIssueDiscardModal(false); - onClose(false); + onClose(); return res; }) .catch(() => { @@ -162,27 +126,10 @@ export const DraftIssueLayout: React.FC = observer((props) => { onDiscard={() => { onChange(null); setIssueDiscardModal(false); - onClose(false); + onClose(); }} /> - + ); }); diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx index 5f182d24ee5..48e12a377be 100644 --- a/web/core/components/issues/issue-modal/form.tsx +++ b/web/core/components/issues/issue-modal/form.tsx @@ -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 = observer((props) => { @@ -86,8 +88,8 @@ export const IssueFormRoot: FC = 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 = observer((props) => { isDuplicateModalOpen, handleDuplicateIssueModal, isProjectSelectionDisabled = false, + storeType, } = props; // states @@ -280,6 +283,7 @@ export const IssueFormRoot: FC = observer((props) => { // debounced duplicate issues swr const { duplicateIssues } = useDebouncedDuplicateIssues( + workspaceSlug?.toString(), projectDetails?.workspace.toString(), projectId ?? undefined, { @@ -373,7 +377,7 @@ export const IssueFormRoot: FC = observer((props) => { disabled={!!data?.id || !!data?.sourceIssueId || isProjectSelectionDisabled} handleFormChange={handleFormChange} /> - {projectId && ( + {projectId && storeType !== EIssuesStoreType.EPIC && ( void; projectId: string | undefined; issueId?: string; + searchEpic?: boolean; }; // services @@ -41,6 +42,7 @@ export const ParentIssuesListModal: React.FC = ({ onChange, projectId, issueId, + searchEpic = false, }) => { const [isLoading, setIsLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); @@ -72,6 +74,7 @@ export const ParentIssuesListModal: React.FC = ({ parent: true, issue_id: issueId, workspace_search: isWorkspaceLevel, + epic: searchEpic ? true : undefined, }) .then((res) => setIssues(res)) .finally(() => { diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx index 38c98238b55..421aa6521e4 100644 --- a/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/web/core/components/issues/peek-overview/issue-detail.tsx @@ -57,11 +57,16 @@ export const PeekOverviewIssueDetails: FC = 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 <>; diff --git a/web/core/components/issues/peek-overview/root.tsx b/web/core/components/issues/peek-overview/root.tsx index 70eb51d81b8..e2e907ef85a 100644 --- a/web/core/components/issues/peek-overview/root.tsx +++ b/web/core/components/issues/peek-overview/root.tsx @@ -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 = 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 = 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); diff --git a/web/core/components/issues/peek-overview/view.tsx b/web/core/components/issues/peek-overview/view.tsx index fc8a4df42aa..c80212556cb 100644 --- a/web/core/components/issues/peek-overview/view.tsx +++ b/web/core/components/issues/peek-overview/view.tsx @@ -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 = 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 = observer((props) => { issuePeekOverviewRef, () => { if (!embedIssue) { - if (!isAnyModalOpen) { + if (!isAnyModalOpen && !isAnyEpicModalOpen) { removeRoutePeekId(); } } diff --git a/web/core/components/issues/relations/issue-list-item.tsx b/web/core/components/issues/relations/issue-list-item.tsx index 9ac1253aea2..fa5cb886a36 100644 --- a/web/core/components/issues/relations/issue-list-item.tsx +++ b/web/core/components/issues/relations/issue-list-item.tsx @@ -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 = observer((props) => { @@ -39,6 +41,7 @@ export const RelationIssueListItem: FC = observer((props) => { disabled = false, issueOperations, handleIssueCrudState, + issueServiceType = EIssueServiceType.ISSUES, } = props; // store hooks @@ -47,7 +50,7 @@ export const RelationIssueListItem: FC = 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 = observer((props) => { issueId={relationIssueId} disabled={disabled} issueOperations={issueOperations} + issueServiceType={issueServiceType} />
diff --git a/web/core/components/issues/relations/issue-list.tsx b/web/core/components/issues/relations/issue-list.tsx index 1b89788a41e..efd8786d986 100644 --- a/web/core/components/issues/relations/issue-list.tsx +++ b/web/core/components/issues/relations/issue-list.tsx @@ -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 = observer((props) => { @@ -31,6 +33,7 @@ export const RelationIssueList: FC = observer((props) => { disabled = false, issueOperations, handleIssueCrudState, + issueServiceType = EIssueServiceType.ISSUES, } = props; return ( @@ -48,6 +51,7 @@ export const RelationIssueList: FC = observer((props) => { disabled={disabled} handleIssueCrudState={handleIssueCrudState} issueOperations={issueOperations} + issueServiceType={issueServiceType} /> ))}
diff --git a/web/core/components/issues/relations/properties.tsx b/web/core/components/issues/relations/properties.tsx index 543a88c4d0a..b301c8b1f57 100644 --- a/web/core/components/issues/relations/properties.tsx +++ b/web/core/components/issues/relations/properties.tsx @@ -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 = 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); diff --git a/web/core/components/issues/sub-issues/issue-list-item.tsx b/web/core/components/issues/sub-issues/issue-list-item.tsx index c4486c2aeb9..64a5c7a02b6 100644 --- a/web/core/components/issues/sub-issues/issue-list-item.tsx +++ b/web/core/components/issues/sub-issues/issue-list-item.tsx @@ -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 = observer((props) => { diff --git a/web/core/components/issues/sub-issues/issues-list.tsx b/web/core/components/issues/sub-issues/issues-list.tsx index 2178f086fd6..2ac8f7394d3 100644 --- a/web/core/components/issues/sub-issues/issues-list.tsx +++ b/web/core/components/issues/sub-issues/issues-list.tsx @@ -1,6 +1,7 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react"; -import { TIssue } from "@plane/types"; +import { EIssueServiceType } from "@plane/constants"; +import { TIssue, TIssueServiceType } from "@plane/types"; // hooks import { useIssueDetail } from "@/hooks/store"; // components @@ -21,6 +22,7 @@ export interface IIssueList { issue?: TIssue | null ) => void; subIssueOperations: TSubIssueOperations; + issueServiceType?: TIssueServiceType; } export const IssueList: FC = observer((props) => { @@ -33,11 +35,12 @@ export const IssueList: FC = observer((props) => { disabled, handleIssueCrudState, subIssueOperations, + issueServiceType = EIssueServiceType.ISSUES, } = props; // hooks const { subIssues: { subIssuesByIssueId }, - } = useIssueDetail(); + } = useIssueDetail(issueServiceType); const subIssueIds = subIssuesByIssueId(parentIssueId); diff --git a/web/core/components/issues/sub-issues/properties.tsx b/web/core/components/issues/sub-issues/properties.tsx index 948f97721e4..c4ea3bbd2d8 100644 --- a/web/core/components/issues/sub-issues/properties.tsx +++ b/web/core/components/issues/sub-issues/properties.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { TIssueServiceType } from "@plane/types"; // hooks import { PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns"; import { useIssueDetail } from "@/hooks/store"; @@ -12,6 +13,7 @@ export interface IIssueProperty { issueId: string; disabled: boolean; subIssueOperations: TSubIssueOperations; + issueServiceType?: TIssueServiceType; } export const IssueProperty: React.FC = (props) => { diff --git a/web/core/constants/empty-state.ts b/web/core/constants/empty-state.ts index b2477eead86..70df416a466 100644 --- a/web/core/constants/empty-state.ts +++ b/web/core/constants/empty-state.ts @@ -108,6 +108,8 @@ export enum EmptyStateType { INBOX_DETAIL_EMPTY_STATE = "inbox-detail-empty-state", WORKSPACE_DRAFT_ISSUES = "workspace-draft-issues", + + PROJECT_NO_EPICS = "project-no-epics", } const emptyStateDetails = { @@ -787,6 +789,15 @@ const emptyStateDetails = { accessType: "workspace", access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, + [EmptyStateType.PROJECT_NO_EPICS]: { + key: EmptyStateType.PROJECT_NO_EPICS, + title: "Create an epic and assign it to someone, even yourself", + description: + "For larger bodies of work that span several cycles and can live across modules, create an epic. Link issues and sub-issues in a project to an epic and jump into an issue from the overview.", + path: "/empty-state/onboarding/issues", + accessType: "project", + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + }, } as const; export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 3780567b35d..c568284393d 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -34,6 +34,7 @@ export enum EIssuesStoreType { DRAFT = "DRAFT", DEFAULT = "DEFAULT", WORKSPACE_DRAFT = "WORKSPACE_DRAFT", + EPIC = "EPIC", } export enum EIssueLayoutTypes { @@ -51,7 +52,8 @@ export type TCreateModalStoreTypes = | EIssuesStoreType.PROJECT_VIEW | EIssuesStoreType.PROFILE | EIssuesStoreType.CYCLE - | EIssuesStoreType.MODULE; + | EIssuesStoreType.MODULE + | EIssuesStoreType.EPIC; export enum EIssueFilterType { FILTERS = "filters", @@ -134,6 +136,10 @@ export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = "issue_type", ]; +export const EPICS_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = ISSUE_DISPLAY_PROPERTIES_KEYS.filter( + (key) => !["cycle", "modules"].includes(key) +); + export const ISSUE_DISPLAY_PROPERTIES: { key: keyof IIssueDisplayProperties; title: string; @@ -206,9 +212,15 @@ export interface ILayoutDisplayFiltersOptions { }; } -export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { - [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; -} = { +export type TFiltersByLayout = { + [layoutType: string]: ILayoutDisplayFiltersOptions; +}; + +export type TIssueFiltersToDisplayByPageType = { + [pageType: string]: TFiltersByLayout; +}; + +export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: TIssueFiltersToDisplayByPageType = { profile_issues: { list: { filters: ["priority", "state_group", "labels", "start_date", "target_date"], @@ -469,9 +481,78 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { }, }, }, + epics: { + list: { + filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + display_properties: EPICS_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: ["state", "priority", "labels", "assignees", "created_by", null], + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups", "sub_issue"], + }, + }, + kanban: { + filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + display_properties: EPICS_DISPLAY_PROPERTIES_KEYS, + display_filters: { + group_by: ["state", "priority", "labels", "assignees", "created_by"], + sub_group_by: ["state", "priority", "labels", "assignees", "created_by", null], + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups", "sub_issue"], + }, + }, + calendar: { + filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date"], + display_properties: ["key", "issue_type"], + display_filters: { + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, + spreadsheet: { + filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + display_properties: EPICS_DISPLAY_PROPERTIES_KEYS, + display_filters: { + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, + gantt_chart: { + filters: ["priority", "state", "assignees", "mentions", "created_by", "labels", "start_date", "target_date"], + display_properties: ["key", "issue_type"], + display_filters: { + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, + }, ...ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT, }; +export const ISSUE_STORE_TO_FILTERS_MAP: Partial> = { + [EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues, + [EIssuesStoreType.EPIC]: ISSUE_DISPLAY_FILTERS_BY_LAYOUT.epics, +}; + export enum EIssueListRow { HEADER = "HEADER", ISSUE = "ISSUE", diff --git a/web/core/hooks/store/use-issue-detail.ts b/web/core/hooks/store/use-issue-detail.ts index 52e18a905f8..786173fd3f4 100644 --- a/web/core/hooks/store/use-issue-detail.ts +++ b/web/core/hooks/store/use-issue-detail.ts @@ -1,11 +1,14 @@ import { useContext } from "react"; +import { EIssueServiceType } from "@plane/constants"; +import { TIssueServiceType } from "@plane/types"; // mobx store import { StoreContext } from "@/lib/store-context"; // types import { IIssueDetail } from "@/store/issue/issue-details/root.store"; -export const useIssueDetail = (): IIssueDetail => { +export const useIssueDetail = (serviceType: TIssueServiceType = EIssueServiceType.ISSUES): IIssueDetail => { const context = useContext(StoreContext); - if (context === undefined) throw new Error("useInbox must be used within StoreProvider"); - return context.issue.issueDetail; + if (context === undefined) throw new Error("useIssueDetail must be used within StoreProvider"); + if (serviceType === EIssueServiceType.EPICS) return context.epic.issueDetail; + else return context.issue.issueDetail; }; diff --git a/web/core/hooks/store/use-issues.ts b/web/core/hooks/store/use-issues.ts index 8c9bc905af4..6e359dcabc3 100644 --- a/web/core/hooks/store/use-issues.ts +++ b/web/core/hooks/store/use-issues.ts @@ -4,6 +4,8 @@ import { TIssueMap } from "@plane/types"; // mobx store import { EIssuesStoreType } from "@/constants/issue"; import { StoreContext } from "@/lib/store-context"; +// plane web types +import { IProjectEpics, IProjectEpicsFilter } from "@/plane-web/store/issue/epic"; // types import { ITeamIssues, ITeamIssuesFilter } from "@/plane-web/store/issue/team"; import { ITeamViewIssues, ITeamViewIssuesFilter } from "@/plane-web/store/issue/team-views"; @@ -71,6 +73,10 @@ export type TStoreIssues = { issues: IProjectIssues; issuesFilter: IProjectIssuesFilter; }; + [EIssuesStoreType.EPIC]: defaultIssueStore & { + issues: IProjectEpics; + issuesFilter: IProjectEpicsFilter; + }; }; export const useIssues = (storeType?: T): TStoreIssues[T] => { @@ -137,6 +143,11 @@ export const useIssues = (storeType?: T): TStoreIssu issues: context.issue.draftIssues, issuesFilter: context.issue.draftIssuesFilter, }) as TStoreIssues[T]; + case EIssuesStoreType.EPIC: + return merge(defaultStore, { + issues: context.issue.projectEpics, + issuesFilter: context.issue.projectEpicsFilter, + }) as TStoreIssues[T]; default: return merge(defaultStore, { issues: context.issue.projectIssues, diff --git a/web/core/hooks/use-group-dragndrop.ts b/web/core/hooks/use-group-dragndrop.ts index bef3bd1f115..3e5bff3e364 100644 --- a/web/core/hooks/use-group-dragndrop.ts +++ b/web/core/hooks/use-group-dragndrop.ts @@ -19,7 +19,8 @@ type DNDStoreType = | EIssuesStoreType.ARCHIVED | EIssuesStoreType.WORKSPACE_DRAFT | EIssuesStoreType.TEAM - | EIssuesStoreType.TEAM_VIEW; + | EIssuesStoreType.TEAM_VIEW + | EIssuesStoreType.EPIC; export const useGroupIssuesDragNDrop = ( storeType: DNDStoreType, diff --git a/web/core/hooks/use-issue-layout-store.ts b/web/core/hooks/use-issue-layout-store.ts index 122e97fe53c..aab69402f0e 100644 --- a/web/core/hooks/use-issue-layout-store.ts +++ b/web/core/hooks/use-issue-layout-store.ts @@ -7,8 +7,7 @@ export const IssuesStoreContext = createContext(un export const useIssueStoreType = () => { const storeType = useContext(IssuesStoreContext); - - const { globalViewId, viewId, projectId, cycleId, moduleId, userId, teamId } = useParams(); + const { globalViewId, viewId, projectId, cycleId, moduleId, userId, epicId, teamId } = useParams(); // If store type exists in context, use that store type if (storeType) return storeType; @@ -24,6 +23,8 @@ export const useIssueStoreType = () => { if (moduleId) return EIssuesStoreType.MODULE; + if (epicId) return EIssuesStoreType.EPIC; + if (projectId) return EIssuesStoreType.PROJECT; if (teamId) return EIssuesStoreType.TEAM; diff --git a/web/core/hooks/use-issue-peek-overview-redirection.tsx b/web/core/hooks/use-issue-peek-overview-redirection.tsx index 1433e6ffb9b..f1ad94c5963 100644 --- a/web/core/hooks/use-issue-peek-overview-redirection.tsx +++ b/web/core/hooks/use-issue-peek-overview-redirection.tsx @@ -1,14 +1,18 @@ import { useRouter } from "next/navigation"; +// constants +import { EIssueServiceType } from "@plane/constants"; // types import { TIssue } from "@plane/types"; // hooks import { useIssueDetail } from "./store"; -const useIssuePeekOverviewRedirection = () => { +const useIssuePeekOverviewRedirection = (isEpic: boolean = false) => { // router const router = useRouter(); // store hooks - const { getIsIssuePeeked, setPeekIssue } = useIssueDetail(); + const { getIsIssuePeeked, setPeekIssue } = useIssueDetail( + isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES + ); const handleRedirection = ( workspaceSlug: string | undefined, @@ -20,7 +24,7 @@ const useIssuePeekOverviewRedirection = () => { const { project_id, id, archived_at, tempId } = issue; if (workspaceSlug && project_id && id && !getIsIssuePeeked(id) && !tempId) { - const issuePath = `/${workspaceSlug}/projects/${project_id}/${archived_at ? "archives/" : ""}issues/${id}`; + const issuePath = `/${workspaceSlug}/projects/${project_id}/${archived_at ? "archives/" : ""}${isEpic ? "epics" : "issues"}/${id}`; if (isMobile) { router.push(issuePath); diff --git a/web/core/hooks/use-issues-actions.tsx b/web/core/hooks/use-issues-actions.tsx index a0778a47d19..2315b43850b 100644 --- a/web/core/hooks/use-issues-actions.tsx +++ b/web/core/hooks/use-issues-actions.tsx @@ -41,6 +41,7 @@ export interface IssueActions { export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => { const teamIssueActions = useTeamIssueActions(); const projectIssueActions = useProjectIssueActions(); + const projectEpicsActions = useProjectEpicsActions(); const cycleIssueActions = useCycleIssueActions(); const moduleIssueActions = useModuleIssueActions(); const teamViewIssueActions = useTeamViewIssueActions(); @@ -73,6 +74,8 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => { case EIssuesStoreType.WORKSPACE_DRAFT: //@ts-expect-error type mismatch return workspaceDraftIssueActions; + case EIssuesStoreType.EPIC: + return projectEpicsActions; case EIssuesStoreType.PROJECT: default: return projectIssueActions; @@ -165,6 +168,92 @@ const useProjectIssueActions = () => { ); }; +const useProjectEpicsActions = () => { + // router + const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams(); + const workspaceSlug = routerWorkspaceSlug?.toString(); + const projectId = routerProjectId?.toString(); + // store hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.EPIC); + + const fetchIssues = useCallback( + async (loadType: TLoader, options: IssuePaginationOptions) => { + if (!workspaceSlug || !projectId) return; + return issues.fetchIssues(workspaceSlug.toString(), projectId.toString(), loadType, options); + }, + [issues.fetchIssues, workspaceSlug, projectId] + ); + const fetchNextIssues = useCallback( + async (groupId?: string, subGroupId?: string) => { + if (!workspaceSlug || !projectId) return; + return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId); + }, + [issues.fetchIssues, workspaceSlug, projectId] + ); + + const createIssue = useCallback( + async (projectId: string | undefined | null, data: Partial) => { + if (!workspaceSlug || !projectId) return; + return await issues.createIssue(workspaceSlug, projectId, data); + }, + [issues.createIssue, workspaceSlug] + ); + const quickAddIssue = useCallback( + async (projectId: string | undefined | null, data: TIssue) => { + if (!workspaceSlug || !projectId) return; + return await issues.quickAddIssue(workspaceSlug, projectId, data); + }, + [issues.quickAddIssue, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string | undefined | null, issueId: string, data: Partial) => { + if (!workspaceSlug || !projectId) return; + return await issues.updateIssue(workspaceSlug, projectId, issueId, data); + }, + [issues.updateIssue, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string | undefined | null, issueId: string) => { + if (!workspaceSlug || !projectId) return; + return await issues.removeIssue(workspaceSlug, projectId, issueId); + }, + [issues.removeIssue, workspaceSlug] + ); + const archiveIssue = useCallback( + async (projectId: string | undefined | null, issueId: string) => { + if (!workspaceSlug || !projectId) return; + return await issues.archiveIssue(workspaceSlug, projectId, issueId); + }, + [issues.archiveIssue, workspaceSlug] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + if (!workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters); + }, + [issuesFilter.updateFilters, workspaceSlug] + ); + + return useMemo( + () => ({ + fetchIssues, + fetchNextIssues, + createIssue, + quickAddIssue, + updateIssue, + removeIssue, + archiveIssue, + updateFilters, + }), + [fetchIssues, fetchNextIssues, createIssue, quickAddIssue, updateIssue, removeIssue, archiveIssue, updateFilters] + ); +}; + const useCycleIssueActions = () => { // router const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, cycleId: routerCycleId } = useParams(); diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts index d74ce0c88ff..f140eb49f08 100644 --- a/web/core/services/issue/issue.service.ts +++ b/web/core/services/issue/issue.service.ts @@ -1,12 +1,14 @@ +import { EIssueServiceType } from "@plane/constants"; // types -import type { - IIssueDisplayProperties, - TBulkOperationsPayload, - TIssue, - TIssueActivity, - TIssueLink, - TIssuesResponse, - TIssueSubIssues, +import { + type IIssueDisplayProperties, + type TBulkOperationsPayload, + type TIssue, + type TIssueActivity, + type TIssueLink, + type TIssueServiceType, + type TIssuesResponse, + type TIssueSubIssues, } from "@plane/types"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; @@ -18,12 +20,15 @@ import { addIssuesBulk, deleteIssueFromLocal, updateIssue } from "@/local-db/uti import { APIService } from "@/services/api.service"; export class IssueService extends APIService { - constructor() { + private serviceType: TIssueServiceType; + + constructor(serviceType: TIssueServiceType = EIssueServiceType.ISSUES) { super(API_BASE_URL); + this.serviceType = serviceType; } async createIssue(workspaceSlug: string, projectId: string, data: Partial): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, data) + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -39,7 +44,7 @@ export class IssueService extends APIService { const path = (queries.expand as string)?.includes("issue_relation") && !queries.group_by ? `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues-detail/` - : `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`; + : `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/`; return this.get( path, { @@ -59,7 +64,11 @@ export class IssueService extends APIService { queries?: any, config = {} ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/v2/issues/`, { params: queries }, config) + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/v2/${this.serviceType}/`, + { params: queries }, + config + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -89,7 +98,7 @@ export class IssueService extends APIService { projectId: string, queries?: any ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/`, { params: queries, }) .then((response) => response?.data) @@ -99,7 +108,7 @@ export class IssueService extends APIService { } async retrieve(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/`, { params: queries, }) .then((response) => { @@ -114,7 +123,7 @@ export class IssueService extends APIService { } async retrieveIssues(workspaceSlug: string, projectId: string, issueIds: string[]): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/list/`, { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/list/`, { params: { issues: issueIds.join(",") }, }) .then((response) => { @@ -129,7 +138,7 @@ export class IssueService extends APIService { } async getIssueActivities(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`) + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/history/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -173,7 +182,10 @@ export class IssueService extends APIService { relation?: "blocking" | null; } ) { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/`, data) + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/issue-relation/`, + data + ) .then((response) => response?.data) .catch((error) => { throw error?.response; @@ -182,7 +194,7 @@ export class IssueService extends APIService { async deleteIssueRelation(workspaceSlug: string, projectId: string, issueId: string, relationId: string) { return this.delete( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-relation/${relationId}/` + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/issue-relation/${relationId}/` ) .then((response) => response?.data) .catch((error) => { @@ -213,7 +225,7 @@ export class IssueService extends APIService { } async patchIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial): Promise { - return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, data) + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -222,7 +234,7 @@ export class IssueService extends APIService { async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise { deleteIssueFromLocal(issuesId); - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issuesId}/`) + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issuesId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -242,7 +254,9 @@ export class IssueService extends APIService { } async subIssues(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`) + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "issues" : "sub-issues"}/` + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -255,7 +269,10 @@ export class IssueService extends APIService { issueId: string, data: { sub_issue_ids: string[] } ): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`, data) + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "issues" : "sub-issues"}/`, + data + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -263,7 +280,9 @@ export class IssueService extends APIService { } async fetchIssueLinks(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`) + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "links" : "issue-links"}/` + ) .then((response) => response?.data) .catch((error) => { throw error?.response; @@ -276,7 +295,10 @@ export class IssueService extends APIService { issueId: string, data: Partial ): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/`, data) + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "links" : "issue-links"}/`, + data + ) .then((response) => response?.data) .catch((error) => { throw error?.response; @@ -291,7 +313,7 @@ export class IssueService extends APIService { data: Partial ): Promise { return this.patch( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/`, + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "links" : "issue-links"}/${linkId}/`, data ) .then((response) => response?.data) @@ -302,7 +324,7 @@ export class IssueService extends APIService { async deleteIssueLink(workspaceSlug: string, projectId: string, issueId: string, linkId: string): Promise { return this.delete( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-links/${linkId}/` + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "links" : "issue-links"}/${linkId}/` ) .then((response) => response?.data) .catch((error) => { @@ -365,7 +387,7 @@ export class IssueService extends APIService { ): Promise<{ subscribed: boolean; }> { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`) + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/subscribe/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -373,7 +395,9 @@ export class IssueService extends APIService { } async unsubscribeFromIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`) + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/subscribe/` + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -381,7 +405,7 @@ export class IssueService extends APIService { } async subscribeToIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`) + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/subscribe/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/core/services/issue/issue_activity.service.ts b/web/core/services/issue/issue_activity.service.ts index b99d07d889d..103cf6e2171 100644 --- a/web/core/services/issue/issue_activity.service.ts +++ b/web/core/services/issue/issue_activity.service.ts @@ -1,12 +1,16 @@ -import { TIssueActivity } from "@plane/types"; +import { EIssueServiceType } from "@plane/constants"; +import { TIssueActivity, TIssueServiceType } from "@plane/types"; import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // types // helper export class IssueActivityService extends APIService { - constructor() { + private serviceType: TIssueServiceType; + + constructor(serviceType: TIssueServiceType = EIssueServiceType.ISSUES) { super(API_BASE_URL); + this.serviceType = serviceType; } async getIssueActivities( @@ -19,9 +23,9 @@ export class IssueActivityService extends APIService { } | object = {} ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`, { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/history/`, { params: { - activity_type: "issue-property", + activity_type: `${this.serviceType === EIssueServiceType.EPICS ? "epic-property" : "issue-property"}`, ...params, }, }) diff --git a/web/core/services/issue/issue_archive.service.ts b/web/core/services/issue/issue_archive.service.ts index 9012fce5b8e..b86886ca935 100644 --- a/web/core/services/issue/issue_archive.service.ts +++ b/web/core/services/issue/issue_archive.service.ts @@ -1,12 +1,16 @@ -import { TIssue } from "@plane/types"; +import { EIssueServiceType } from "@plane/constants"; +import { TIssue, TIssueServiceType } from "@plane/types"; import { API_BASE_URL } from "@/helpers/common.helper"; import { APIService } from "@/services/api.service"; // types // constants export class IssueArchiveService extends APIService { - constructor() { + private serviceType: TIssueServiceType; + + constructor(serviceType: TIssueServiceType = EIssueServiceType.ISSUES) { super(API_BASE_URL); + this.serviceType = serviceType; } async getArchivedIssues(workspaceSlug: string, projectId: string, queries?: any, config = {}): Promise { @@ -30,7 +34,7 @@ export class IssueArchiveService extends APIService { ): Promise<{ archived_at: string; }> { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`) + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/archive/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -38,7 +42,7 @@ export class IssueArchiveService extends APIService { } async restoreIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`) + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/archive/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -51,7 +55,7 @@ export class IssueArchiveService extends APIService { issueId: string, queries?: any ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`, { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/archive/`, { params: queries, }) .then((response) => response?.data) diff --git a/web/core/services/issue/issue_attachment.service.ts b/web/core/services/issue/issue_attachment.service.ts index b550217cf27..322e8c2c68a 100644 --- a/web/core/services/issue/issue_attachment.service.ts +++ b/web/core/services/issue/issue_attachment.service.ts @@ -1,6 +1,7 @@ import { AxiosRequestConfig } from "axios"; +import { EIssueServiceType } from "@plane/constants"; // plane types -import { TIssueAttachment, TIssueAttachmentUploadResponse } from "@plane/types"; +import { TIssueAttachment, TIssueAttachmentUploadResponse, TIssueServiceType } from "@plane/types"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; import { generateFileUploadPayload, getFileMetaDataForUpload } from "@/helpers/file.helper"; @@ -10,11 +11,13 @@ import { FileUploadService } from "@/services/file-upload.service"; export class IssueAttachmentService extends APIService { private fileUploadService: FileUploadService; + private serviceType: TIssueServiceType; - constructor() { + constructor(serviceType: TIssueServiceType = EIssueServiceType.ISSUES) { super(API_BASE_URL); // upload service this.fileUploadService = new FileUploadService(); + this.serviceType = serviceType; } private async updateIssueAttachmentUploadStatus( @@ -24,7 +27,7 @@ export class IssueAttachmentService extends APIService { attachmentId: string ): Promise { return this.patch( - `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/attachments/${attachmentId}/` + `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments/${attachmentId}/` ) .then((response) => response?.data) .catch((error) => { @@ -41,7 +44,7 @@ export class IssueAttachmentService extends APIService { ): Promise { const fileMetaData = getFileMetaDataForUpload(file); return this.post( - `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/attachments/`, + `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments/`, fileMetaData ) .then(async (response) => { @@ -61,7 +64,9 @@ export class IssueAttachmentService extends APIService { } async getIssueAttachments(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/attachments/`) + return this.get( + `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments/` + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -75,7 +80,7 @@ export class IssueAttachmentService extends APIService { assetId: string ): Promise { return this.delete( - `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/attachments/${assetId}/` + `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments/${assetId}/` ) .then((response) => response?.data) .catch((error) => { diff --git a/web/core/services/issue/issue_comment.service.ts b/web/core/services/issue/issue_comment.service.ts index b0c9821a36c..ccfce8a86c1 100644 --- a/web/core/services/issue/issue_comment.service.ts +++ b/web/core/services/issue/issue_comment.service.ts @@ -1,5 +1,6 @@ +import { EIssueServiceType } from "@plane/constants"; // plane types -import { TFileSignedURLResponse, TIssueComment } from "@plane/types"; +import { TFileSignedURLResponse, TIssueComment, TIssueServiceType } from "@plane/types"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; import { generateFileUploadPayload, getFileMetaDataForUpload } from "@/helpers/file.helper"; @@ -9,11 +10,13 @@ import { FileUploadService } from "@/services/file-upload.service"; export class IssueCommentService extends APIService { private fileUploadService: FileUploadService; + private serviceType: TIssueServiceType; - constructor() { + constructor(serviceType: TIssueServiceType = EIssueServiceType.ISSUES) { super(API_BASE_URL); // upload service this.fileUploadService = new FileUploadService(); + this.serviceType = serviceType; } async getIssueComments( @@ -26,9 +29,9 @@ export class IssueCommentService extends APIService { } | object = {} ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/history/`, { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/history/`, { params: { - activity_type: "issue-comment", + activity_type: `${this.serviceType === EIssueServiceType.EPICS ? "epic-comment" : "issue-comment"}`, ...params, }, }) @@ -44,7 +47,10 @@ export class IssueCommentService extends APIService { issueId: string, data: Partial ): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/`, data) + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/comments/`, + data + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -59,7 +65,7 @@ export class IssueCommentService extends APIService { data: Partial ): Promise { return this.patch( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/`, + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/comments/${commentId}/`, data ) .then((response) => response?.data) @@ -75,7 +81,7 @@ export class IssueCommentService extends APIService { commentId: string ): Promise { return this.delete( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/comments/${commentId}/` + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/comments/${commentId}/` ) .then((response) => response?.data) .catch((error) => { diff --git a/web/core/services/issue/issue_reaction.service.ts b/web/core/services/issue/issue_reaction.service.ts index 9e2d355e120..39fc8406ac6 100644 --- a/web/core/services/issue/issue_reaction.service.ts +++ b/web/core/services/issue/issue_reaction.service.ts @@ -1,12 +1,16 @@ -import type { TIssueCommentReaction, TIssueReaction } from "@plane/types"; +import { EIssueServiceType } from "@plane/constants"; +import { type TIssueCommentReaction, type TIssueReaction, type TIssueServiceType } from "@plane/types"; import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; // types export class IssueReactionService extends APIService { - constructor() { + private serviceType: TIssueServiceType; + + constructor(serviceType: TIssueServiceType = EIssueServiceType.ISSUES) { super(API_BASE_URL); + this.serviceType = serviceType; } async createIssueReaction( @@ -15,7 +19,10 @@ export class IssueReactionService extends APIService { issueId: string, data: Partial ): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/`, data) + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/reactions/`, + data + ) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -23,7 +30,7 @@ export class IssueReactionService extends APIService { } async listIssueReactions(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/`) + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/reactions/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -32,7 +39,7 @@ export class IssueReactionService extends APIService { async deleteIssueReaction(workspaceSlug: string, projectId: string, issueId: string, reaction: string): Promise { return this.delete( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/${reaction}/` + `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/reactions/${reaction}/` ) .then((response) => response?.data) .catch((error) => { diff --git a/web/core/services/issue_filter.service.ts b/web/core/services/issue_filter.service.ts index d051c685cd4..3e288bb1a8d 100644 --- a/web/core/services/issue_filter.service.ts +++ b/web/core/services/issue_filter.service.ts @@ -48,6 +48,26 @@ export class IssueFiltersService extends APIService { }); } + // epic issue filters + async fetchProjectEpicFilters(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/epics-user-properties/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async patchProjectEpicFilters( + workspaceSlug: string, + projectId: string, + data: Partial + ): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/epics-user-properties/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + // cycle issue filters async fetchCycleIssueFilters( workspaceSlug: string, diff --git a/web/core/services/project/project.service.ts b/web/core/services/project/project.service.ts index dcc9d11b52b..f9c2af8b66a 100644 --- a/web/core/services/project/project.service.ts +++ b/web/core/services/project/project.service.ts @@ -1,8 +1,4 @@ -import type { - GithubRepositoriesResponse, - ISearchIssueResponse, - TProjectIssuesSearchParams, -} from "@plane/types"; +import type { GithubRepositoriesResponse, ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; // plane web types @@ -67,6 +63,14 @@ export class ProjectService extends APIService { }); } + async fetchProjectEpicProperties(workspaceSlug: string, projectId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/epic-properties/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async setProjectView( workspaceSlug: string, projectId: string, diff --git a/web/core/store/issue/issue-details/attachment.store.ts b/web/core/store/issue/issue-details/attachment.store.ts index ef2a356fcf1..f9e47a56055 100644 --- a/web/core/store/issue/issue-details/attachment.store.ts +++ b/web/core/store/issue/issue-details/attachment.store.ts @@ -8,7 +8,7 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx" import { computedFn } from "mobx-utils"; import { v4 as uuidv4 } from "uuid"; // types -import { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap } from "@plane/types"; +import { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap, TIssueServiceType } from "@plane/types"; // services import { IssueAttachmentService } from "@/services/issue"; import { IIssueRootStore } from "../root.store"; @@ -64,7 +64,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore { // services issueAttachmentService; - constructor(rootStore: IIssueRootStore) { + constructor(rootStore: IIssueRootStore, serviceType: TIssueServiceType) { makeObservable(this, { // observables attachments: observable, @@ -82,7 +82,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore { this.rootIssueStore = rootStore; this.rootIssueDetailStore = rootStore.issueDetail; // services - this.issueAttachmentService = new IssueAttachmentService(); + this.issueAttachmentService = new IssueAttachmentService(serviceType); } // computed diff --git a/web/core/store/issue/issue-details/comment.store.ts b/web/core/store/issue/issue-details/comment.store.ts index 211a71f48e4..b0fba6d3c05 100644 --- a/web/core/store/issue/issue-details/comment.store.ts +++ b/web/core/store/issue/issue-details/comment.store.ts @@ -5,7 +5,7 @@ import uniq from "lodash/uniq"; import update from "lodash/update"; import { action, makeObservable, observable, runInAction } from "mobx"; // services -import { TIssueComment, TIssueCommentMap, TIssueCommentIdMap } from "@plane/types"; +import { TIssueComment, TIssueCommentMap, TIssueCommentIdMap, TIssueServiceType } from "@plane/types"; import { IssueCommentService } from "@/services/issue"; // types import { IIssueDetail } from "./root.store"; @@ -50,12 +50,13 @@ export class IssueCommentStore implements IIssueCommentStore { loader: TCommentLoader = "fetch"; comments: TIssueCommentIdMap = {}; commentMap: TIssueCommentMap = {}; + serviceType; // root store rootIssueDetail: IIssueDetail; // services issueCommentService; - constructor(rootStore: IIssueDetail) { + constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) { makeObservable(this, { // observables loader: observable.ref, @@ -68,9 +69,10 @@ export class IssueCommentStore implements IIssueCommentStore { removeComment: action, }); // root store + this.serviceType = serviceType; this.rootIssueDetail = rootStore; // services - this.issueCommentService = new IssueCommentService(); + this.issueCommentService = new IssueCommentService(serviceType); } // helper methods diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts index a7c439beedd..3fe18984206 100644 --- a/web/core/store/issue/issue-details/issue.store.ts +++ b/web/core/store/issue/issue-details/issue.store.ts @@ -1,7 +1,8 @@ import { makeObservable, observable } from "mobx"; import { computedFn } from "mobx-utils"; +import { EIssueServiceType } from "@plane/constants"; // types -import { TIssue } from "@plane/types"; +import { TIssue, TIssueServiceType } from "@plane/types"; // local import { persistence } from "@/local-db/storage.sqlite"; // services @@ -46,11 +47,12 @@ export class IssueStore implements IIssueStore { // root store rootIssueDetailStore: IIssueDetail; // services + serviceType; issueService; issueArchiveService; issueDraftService; - constructor(rootStore: IIssueDetail) { + constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) { makeObservable(this, { fetchingIssueDetails: observable.ref, localDBIssueDescription: observable.ref, @@ -58,8 +60,9 @@ export class IssueStore implements IIssueStore { // root store this.rootIssueDetailStore = rootStore; // services - this.issueService = new IssueService(); - this.issueArchiveService = new IssueArchiveService(); + this.serviceType = serviceType; + this.issueService = new IssueService(serviceType); + this.issueArchiveService = new IssueArchiveService(serviceType); this.issueDraftService = new IssueDraftService(); } @@ -190,15 +193,32 @@ export class IssueStore implements IIssueStore { }; updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { - await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); - await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); + const currentStore = + this.serviceType === EIssueServiceType.EPICS + ? this.rootIssueDetailStore.rootIssueStore.projectEpics + : this.rootIssueDetailStore.rootIssueStore.projectIssues; + + await Promise.all([ + currentStore.updateIssue(workspaceSlug, projectId, issueId, data), + this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId), + ]); }; - removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => - this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + const currentStore = + this.serviceType === EIssueServiceType.EPICS + ? this.rootIssueDetailStore.rootIssueStore.projectEpics + : this.rootIssueDetailStore.rootIssueStore.projectIssues; + currentStore.removeIssue(workspaceSlug, projectId, issueId); + }; - archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => - this.rootIssueDetailStore.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); + archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + const currentStore = + this.serviceType === EIssueServiceType.EPICS + ? this.rootIssueDetailStore.rootIssueStore.projectEpics + : this.rootIssueDetailStore.rootIssueStore.projectIssues; + currentStore.archiveIssue(workspaceSlug, projectId, issueId); + }; addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => { await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addCycleToIssue( diff --git a/web/core/store/issue/issue-details/link.store.ts b/web/core/store/issue/issue-details/link.store.ts index de33706a00c..85c6fa5b83d 100644 --- a/web/core/store/issue/issue-details/link.store.ts +++ b/web/core/store/issue/issue-details/link.store.ts @@ -1,7 +1,7 @@ import set from "lodash/set"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; // services -import { TIssueLink, TIssueLinkMap, TIssueLinkIdMap } from "@plane/types"; +import { TIssueLink, TIssueLinkMap, TIssueLinkIdMap, TIssueServiceType } from "@plane/types"; import { IssueService } from "@/services/issue"; // types import { IIssueDetail } from "./root.store"; @@ -44,8 +44,9 @@ export class IssueLinkStore implements IIssueLinkStore { rootIssueDetailStore: IIssueDetail; // services issueService; + serviceType; - constructor(rootStore: IIssueDetail) { + constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) { makeObservable(this, { // observables links: observable, @@ -59,10 +60,11 @@ export class IssueLinkStore implements IIssueLinkStore { updateLink: action, removeLink: action, }); + this.serviceType = serviceType; // root store this.rootIssueDetailStore = rootStore; // services - this.issueService = new IssueService(); + this.issueService = new IssueService(serviceType); } // computed diff --git a/web/core/store/issue/issue-details/reaction.store.ts b/web/core/store/issue/issue-details/reaction.store.ts index f3ae77fa3f9..5fd0e224528 100644 --- a/web/core/store/issue/issue-details/reaction.store.ts +++ b/web/core/store/issue/issue-details/reaction.store.ts @@ -7,7 +7,7 @@ import { action, makeObservable, observable, runInAction } from "mobx"; // services // types // helpers -import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap } from "@plane/types"; +import { TIssueReaction, TIssueReactionMap, TIssueReactionIdMap, TIssue, TIssueServiceType } from "@plane/types"; import { groupReactions } from "@/helpers/emoji.helper"; import { IssueReactionService } from "@/services/issue"; import { IIssueDetail } from "./root.store"; @@ -44,8 +44,9 @@ export class IssueReactionStore implements IIssueReactionStore { rootIssueDetailStore: IIssueDetail; // services issueReactionService; + serviceType; - constructor(rootStore: IIssueDetail) { + constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) { makeObservable(this, { // observables reactions: observable, @@ -56,10 +57,11 @@ export class IssueReactionStore implements IIssueReactionStore { createReaction: action, removeReaction: action, }); + this.serviceType = serviceType; // root store this.rootIssueDetailStore = rootStore; // services - this.issueReactionService = new IssueReactionService(); + this.issueReactionService = new IssueReactionService(serviceType); } // helper methods diff --git a/web/core/store/issue/issue-details/root.store.ts b/web/core/store/issue/issue-details/root.store.ts index e6e0ca8d0b2..c72ef1a77d7 100644 --- a/web/core/store/issue/issue-details/root.store.ts +++ b/web/core/store/issue/issue-details/root.store.ts @@ -8,6 +8,7 @@ import { TIssueLink, TIssueReaction, TIssueDetailWidget, + TIssueServiceType, } from "@plane/types"; // plane web store import { @@ -140,6 +141,8 @@ export class IssueDetail implements IIssueDetail { isRelationModalOpen: TIssueRelationModal | null = null; isSubIssuesModalOpen: string | null = null; attachmentDeleteModalId: string | null = null; + // service type + serviceType: TIssueServiceType; // store rootIssueStore: IIssueRootStore; issue: IIssueStore; @@ -153,7 +156,7 @@ export class IssueDetail implements IIssueDetail { comment: IIssueCommentStore; commentReaction: IIssueCommentReactionStore; - constructor(rootStore: IIssueRootStore) { + constructor(rootStore: IIssueRootStore, serviceType: TIssueServiceType) { makeObservable(this, { // observables peekIssue: observable, @@ -191,15 +194,16 @@ export class IssueDetail implements IIssueDetail { }); // store + this.serviceType = serviceType; this.rootIssueStore = rootStore; - this.issue = new IssueStore(this); - this.reaction = new IssueReactionStore(this); - this.attachment = new IssueAttachmentStore(rootStore); - this.activity = new IssueActivityStore(rootStore.rootStore as RootStore); - this.comment = new IssueCommentStore(this); + this.issue = new IssueStore(this, serviceType); + this.reaction = new IssueReactionStore(this, serviceType); + this.attachment = new IssueAttachmentStore(rootStore, serviceType); + this.activity = new IssueActivityStore(rootStore.rootStore as RootStore, serviceType); + this.comment = new IssueCommentStore(this, serviceType); this.commentReaction = new IssueCommentReactionStore(this); - this.subIssues = new IssueSubIssuesStore(this); - this.link = new IssueLinkStore(this); + this.subIssues = new IssueSubIssuesStore(this, serviceType); + this.link = new IssueLinkStore(this, serviceType); this.subscription = new IssueSubscriptionStore(this); this.relation = new IssueRelationStore(this); } diff --git a/web/core/store/issue/issue-details/sub_issues.store.ts b/web/core/store/issue/issue-details/sub_issues.store.ts index 9d5fd25c4f9..df87df67c5f 100644 --- a/web/core/store/issue/issue-details/sub_issues.store.ts +++ b/web/core/store/issue/issue-details/sub_issues.store.ts @@ -11,6 +11,7 @@ import { TIssueSubIssuesStateDistributionMap, TIssueSubIssuesIdMap, TSubIssuesStateDistribution, + TIssueServiceType, } from "@plane/types"; // services import { updatePersistentLayer } from "@/local-db/utils/utils"; @@ -65,7 +66,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { // services issueService; - constructor(rootStore: IIssueDetail) { + constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) { makeObservable(this, { // observables subIssuesStateDistribution: observable, @@ -83,7 +84,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { // root store this.rootIssueDetailStore = rootStore; // services - this.issueService = new IssueService(); + this.issueService = new IssueService(serviceType); } // helper methods diff --git a/web/core/store/issue/issue.store.ts b/web/core/store/issue/issue.store.ts index 7fcb0eef482..a6ff334b9a6 100644 --- a/web/core/store/issue/issue.store.ts +++ b/web/core/store/issue/issue.store.ts @@ -3,7 +3,7 @@ import update from "lodash/update"; import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types -import { TIssue } from "@plane/types"; +import { TIssue, TIssueServiceType } from "@plane/types"; // helpers import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper"; // services @@ -30,7 +30,7 @@ export class IssueStore implements IIssueStore { // service issueService; - constructor() { + constructor(serviceType: TIssueServiceType) { makeObservable(this, { // observable issuesMap: observable, @@ -40,7 +40,7 @@ export class IssueStore implements IIssueStore { removeIssue: action, }); - this.issueService = new IssueService(); + this.issueService = new IssueService(serviceType); } // actions diff --git a/web/core/store/issue/root.store.ts b/web/core/store/issue/root.store.ts index 944a32f7196..002ede3ba4d 100644 --- a/web/core/store/issue/root.store.ts +++ b/web/core/store/issue/root.store.ts @@ -1,7 +1,10 @@ import isEmpty from "lodash/isEmpty"; import { autorun, makeObservable, observable } from "mobx"; -import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types"; +// types +import { EIssueServiceType } from "@plane/constants"; +import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite, TIssueServiceType } from "@plane/types"; // plane web store +import { IProjectEpics, IProjectEpicsFilter, ProjectEpics, ProjectEpicsFilter } from "@/plane-web/store/issue/epic"; import { ITeamIssuesFilter, ITeamIssues, TeamIssues, TeamIssuesFilter } from "@/plane-web/store/issue/team"; import { ITeamViewIssues, @@ -59,6 +62,7 @@ export interface IIssueRootStore { cycleMap: Record | undefined; rootStore: RootStore; + serviceType: TIssueServiceType; issues: IIssueStore; @@ -99,6 +103,9 @@ export interface IIssueRootStore { issueKanBanView: IIssueKanBanViewStore; issueCalendarView: ICalendarStore; + + projectEpicsFilter: IProjectEpicsFilter; + projectEpics: IProjectEpics; } export class IssueRootStore implements IIssueRootStore { @@ -122,6 +129,7 @@ export class IssueRootStore implements IIssueRootStore { cycleMap: Record | undefined = undefined; rootStore: RootStore; + serviceType: TIssueServiceType; issues: IIssueStore; @@ -163,7 +171,10 @@ export class IssueRootStore implements IIssueRootStore { issueKanBanView: IIssueKanBanViewStore; issueCalendarView: ICalendarStore; - constructor(rootStore: RootStore) { + projectEpicsFilter: IProjectEpicsFilter; + projectEpics: IProjectEpics; + + constructor(rootStore: RootStore, serviceType: TIssueServiceType = EIssueServiceType.ISSUES) { makeObservable(this, { workspaceSlug: observable.ref, teamId: observable.ref, @@ -184,6 +195,7 @@ export class IssueRootStore implements IIssueRootStore { cycleMap: observable, }); + this.serviceType = serviceType; this.rootStore = rootStore; autorun(() => { @@ -209,9 +221,9 @@ export class IssueRootStore implements IIssueRootStore { if (!isEmpty(rootStore?.cycle?.cycleMap)) this.cycleMap = rootStore?.cycle?.cycleMap; }); - this.issues = new IssueStore(); + this.issues = new IssueStore(this.serviceType); - this.issueDetail = new IssueDetail(this); + this.issueDetail = new IssueDetail(this, this.serviceType); this.workspaceIssuesFilter = new WorkspaceIssuesFilter(this); this.workspaceIssues = new WorkspaceIssues(this, this.workspaceIssuesFilter); @@ -248,5 +260,8 @@ export class IssueRootStore implements IIssueRootStore { this.issueKanBanView = new IssueKanBanViewStore(this); this.issueCalendarView = new CalendarStore(); + + this.projectEpicsFilter = new ProjectEpicsFilter(this); + this.projectEpics = new ProjectEpics(this, this.projectEpicsFilter); } } diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index d8b5f02b7b8..25e6d500da4 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -18,6 +18,9 @@ export interface IProjectStore { projectMap: { [projectId: string]: TProject; // projectId: project Info }; + projectEpicPropertiesMap: { + [projectId: string]: any; + }; // computed filteredProjectIds: string[] | undefined; workspaceProjectIds: string[] | undefined; @@ -29,7 +32,9 @@ export interface IProjectStore { // actions getProjectById: (projectId: string | undefined | null) => TProject | undefined; getProjectIdentifierById: (projectId: string | undefined | null) => string; + getProjectEpicPropertiesById: (projectId: string | undefined | null) => any; // fetch actions + fetchProjectEpicProperties: (workspaceSlug: string, projectId: string) => Promise; fetchProjects: (workspaceSlug: string) => Promise; fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise; // favorites actions @@ -53,6 +58,9 @@ export class ProjectStore implements IProjectStore { projectMap: { [projectId: string]: TProject; // projectId: project Info } = {}; + projectEpicPropertiesMap: { + [projectId: string]: any; + } = {}; // root store rootStore: CoreRootStore; // service @@ -77,6 +85,7 @@ export class ProjectStore implements IProjectStore { joinedProjectIds: computed, favoriteProjectIds: computed, // fetch actions + fetchProjectEpicProperties: action, fetchProjects: action, fetchProjectDetails: action, // favorites actions @@ -205,6 +214,24 @@ export class ProjectStore implements IProjectStore { return projectIds; } + fetchProjectEpicProperties = async (workspaceSlug: string, projectId: string) => { + try { + const response = await this.projectService.fetchProjectEpicProperties(workspaceSlug, projectId); + runInAction(() => { + set(this.projectEpicPropertiesMap, [projectId], response); + }); + return response; + } catch (error) { + console.log("Failed to fetch epic properties from project store"); + throw error; + } + }; + + getProjectEpicPropertiesById = computedFn((projectId: string | undefined | null) => { + const projectEpicProperties = this.projectEpicPropertiesMap[projectId ?? ""]; + return projectEpicProperties; + }); + /** * get Workspace projects using workspace slug * @param workspaceSlug diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index 621f5f8082f..ebd118da87c 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -1,4 +1,5 @@ import { enableStaticRendering } from "mobx-react"; +import { EIssueServiceType } from "@plane/constants"; // plane web store import { CommandPaletteStore, ICommandPaletteStore } from "@/plane-web/store/command-palette.store"; import { RootStore } from "@/plane-web/store/root.store"; @@ -42,6 +43,7 @@ export class CoreRootStore { projectView: IProjectViewStore; globalView: IGlobalViewStore; issue: IIssueRootStore; + epic: IIssueRootStore; state: IStateStore; label: ILabelStore; dashboard: IDashboardStore; @@ -75,6 +77,7 @@ export class CoreRootStore { this.projectView = new ProjectViewStore(this); this.globalView = new GlobalViewStore(this); this.issue = new IssueRootStore(this as unknown as RootStore); + this.epic = new IssueRootStore(this as unknown as RootStore, EIssueServiceType.EPICS); this.state = new StateStore(this as unknown as RootStore); this.label = new LabelStore(this); this.dashboard = new DashboardStore(this); @@ -106,6 +109,7 @@ export class CoreRootStore { this.projectView = new ProjectViewStore(this); this.globalView = new GlobalViewStore(this); this.issue = new IssueRootStore(this as unknown as RootStore); + this.epic = new IssueRootStore(this as unknown as RootStore, EIssueServiceType.EPICS); this.state = new StateStore(this as unknown as RootStore); this.label = new LabelStore(this); this.dashboard = new DashboardStore(this); diff --git a/web/core/store/router.store.ts b/web/core/store/router.store.ts index 051ed49b4a0..94b77541427 100644 --- a/web/core/store/router.store.ts +++ b/web/core/store/router.store.ts @@ -21,6 +21,7 @@ export interface IRouterStore { issueId: string | undefined; inboxId: string | undefined; webhookId: string | undefined; + epicId: string | undefined; } export class RouterStore implements IRouterStore { @@ -47,6 +48,7 @@ export class RouterStore implements IRouterStore { issueId: computed, inboxId: computed, webhookId: computed, + epicId: computed, }); } @@ -163,4 +165,12 @@ export class RouterStore implements IRouterStore { get webhookId() { return this.query?.webhookId?.toString(); } + + /** + * Returns the epic id from the query + * @returns string|undefined + */ + get epicId() { + return this.query?.epicId?.toString(); + } } diff --git a/web/core/store/theme.store.ts b/web/core/store/theme.store.ts index 185f47f1739..0102a093690 100644 --- a/web/core/store/theme.store.ts +++ b/web/core/store/theme.store.ts @@ -6,11 +6,13 @@ export interface IThemeStore { profileSidebarCollapsed: boolean | undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined; issueDetailSidebarCollapsed: boolean | undefined; + epicDetailSidebarCollapsed: boolean | undefined; // actions toggleSidebar: (collapsed?: boolean) => void; toggleProfileSidebar: (collapsed?: boolean) => void; toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void; toggleIssueDetailSidebar: (collapsed?: boolean) => void; + toggleEpicDetailSidebar: (collapsed?: boolean) => void; } export class ThemeStore implements IThemeStore { @@ -19,6 +21,7 @@ export class ThemeStore implements IThemeStore { profileSidebarCollapsed: boolean | undefined = undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined; issueDetailSidebarCollapsed: boolean | undefined = undefined; + epicDetailSidebarCollapsed: boolean | undefined = undefined; constructor() { makeObservable(this, { @@ -27,11 +30,13 @@ export class ThemeStore implements IThemeStore { profileSidebarCollapsed: observable.ref, workspaceAnalyticsSidebarCollapsed: observable.ref, issueDetailSidebarCollapsed: observable.ref, + epicDetailSidebarCollapsed: observable.ref, // action toggleSidebar: action, toggleProfileSidebar: action, toggleWorkspaceAnalyticsSidebar: action, toggleIssueDetailSidebar: action, + toggleEpicDetailSidebar: action, }); } @@ -82,4 +87,13 @@ export class ThemeStore implements IThemeStore { } localStorage.setItem("issue_detail_sidebar_collapsed", this.issueDetailSidebarCollapsed.toString()); }; + + toggleEpicDetailSidebar = (collapsed?: boolean) => { + if (collapsed === undefined) { + this.epicDetailSidebarCollapsed = !this.epicDetailSidebarCollapsed; + } else { + this.epicDetailSidebarCollapsed = collapsed; + } + localStorage.setItem("epic_detail_sidebar_collapsed", this.epicDetailSidebarCollapsed.toString()); + }; } diff --git a/web/ee/components/epics/index.ts b/web/ee/components/epics/index.ts new file mode 100644 index 00000000000..6cef4035fc7 --- /dev/null +++ b/web/ee/components/epics/index.ts @@ -0,0 +1 @@ +export * from "ce/components/epics"; diff --git a/web/public/empty-state/epics/epics-dark.webp b/web/public/empty-state/epics/epics-dark.webp new file mode 100644 index 00000000000..ee8da30145f Binary files /dev/null and b/web/public/empty-state/epics/epics-dark.webp differ diff --git a/web/public/empty-state/epics/epics-light.webp b/web/public/empty-state/epics/epics-light.webp new file mode 100644 index 00000000000..179acf0add7 Binary files /dev/null and b/web/public/empty-state/epics/epics-light.webp differ