From 438cc33046be409d3819eabb000f0582d6e5d888 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:24:50 +0530 Subject: [PATCH] code refactor and improvement (#6203) * chore: package code refactoring * chore: component restructuring and refactor * chore: comment create improvement --- packages/constants/src/issue.ts | 5 + packages/types/src/issues/issue.d.ts | 6 ++ packages/types/src/project/projects.d.ts | 1 + web/ce/components/epics/epic-modal/index.ts | 1 + web/ce/components/epics/epic-modal/modal.tsx | 19 ++++ web/ce/components/epics/index.ts | 1 + .../dependency/dependency-paths.tsx | 10 +- .../components/issue-types/values/update.tsx | 3 + .../hooks/use-debounced-duplicate-issues.tsx | 1 + web/ce/store/issue/epic/filter.store.ts | 15 +++ web/ce/store/issue/epic/index.ts | 2 + web/ce/store/issue/epic/issue.store.ts | 14 +++ .../issue/issue-details/activity.store.ts | 40 +++++--- .../lite-text-editor/lite-text-editor.tsx | 55 +++++++---- .../gantt-chart/chart/main-content.tsx | 7 +- .../components/gantt-chart/chart/root.tsx | 3 + web/core/components/gantt-chart/root.tsx | 3 + .../gantt-chart/sidebar/issues/block.tsx | 5 +- .../gantt-chart/sidebar/issues/sidebar.tsx | 3 + .../components/gantt-chart/sidebar/root.tsx | 3 + .../components/inbox/content/issue-root.tsx | 15 ++- .../inbox/modals/create-modal/create-root.tsx | 13 ++- .../attachment/attachment-item-list.tsx | 22 ++++- .../attachment/attachment-list-item.tsx | 7 +- .../attachment/delete-attachment-modal.tsx | 8 +- web/core/components/issues/filters.tsx | 30 +++--- .../attachments/content.tsx | 8 +- .../attachments/helper.tsx | 7 +- .../attachments/quick-action-button.tsx | 21 ++++- .../issue-detail-widgets/attachments/root.tsx | 9 +- .../attachments/title.tsx | 8 +- .../issue-detail-widgets/links/content.tsx | 16 +++- .../issue-detail-widgets/links/helper.tsx | 12 ++- .../links/quick-action-button.tsx | 7 +- .../issue-detail-widgets/links/root.tsx | 17 +++- .../issue-detail-widgets/links/title.tsx | 11 ++- .../relations/content.tsx | 9 +- .../issue-detail-widgets/relations/helper.tsx | 9 +- .../relations/quick-action-button.tsx | 7 +- .../issue-detail-widgets/relations/root.tsx | 17 +++- .../issue-detail-widgets/relations/title.tsx | 11 ++- .../sub-issues/content.tsx | 10 +- .../sub-issues/helper.tsx | 9 +- .../sub-issues/quick-action-button.tsx | 8 +- .../issue-detail-widgets/sub-issues/root.tsx | 17 +++- .../issue-detail-widgets/sub-issues/title.tsx | 37 ++++---- .../issues/issue-detail/label/root.tsx | 18 +++- .../links/create-update-link-modal.tsx | 8 +- .../issues/issue-detail/links/link-item.tsx | 7 +- .../issues/issue-detail/links/link-list.tsx | 15 ++- .../issues/issue-detail/main-content.tsx | 15 ++- .../issues/issue-detail/parent-select.tsx | 1 + .../components/issues/issue-detail/root.tsx | 3 +- .../calendar/base-calendar-root.tsx | 7 +- .../issue-layouts/calendar/calendar.tsx | 14 ++- .../issue-layouts/calendar/day-tile.tsx | 11 ++- .../calendar/dropdowns/months-dropdown.tsx | 8 +- .../calendar/dropdowns/options-dropdown.tsx | 8 +- .../issues/issue-layouts/calendar/header.tsx | 8 +- .../calendar/issue-block-root.tsx | 13 ++- .../issue-layouts/calendar/issue-block.tsx | 5 +- .../issue-layouts/calendar/issue-blocks.tsx | 3 + .../issue-layouts/calendar/week-days.tsx | 11 ++- .../issue-layouts/empty-states/index.tsx | 3 + .../empty-states/project-epic.tsx | 12 +++ .../applied-filters/roots/project-root.tsx | 25 ++--- .../issue-layouts/gantt/base-gantt-root.tsx | 11 ++- .../issues/issue-layouts/gantt/blocks.tsx | 11 ++- .../issue-layouts/kanban/base-kanban-root.tsx | 17 +++- .../issues/issue-layouts/kanban/block.tsx | 14 ++- .../issue-layouts/kanban/blocks-list.tsx | 3 + .../issues/issue-layouts/kanban/default.tsx | 4 + .../kanban/headers/group-by-card.tsx | 22 +++-- .../issue-layouts/kanban/kanban-group.tsx | 3 + .../issue-layouts/list/base-list-root.tsx | 14 ++- .../issues/issue-layouts/list/block-root.tsx | 7 +- .../issues/issue-layouts/list/block.tsx | 17 +++- .../issues/issue-layouts/list/blocks-list.tsx | 3 + .../issues/issue-layouts/list/default.tsx | 9 +- .../list/headers/group-by-card.tsx | 22 +++-- .../issues/issue-layouts/list/list-group.tsx | 4 + .../properties/all-properties.tsx | 77 ++++++++------- .../spreadsheet/base-spreadsheet-root.tsx | 7 +- .../issue-layouts/spreadsheet/issue-row.tsx | 17 +++- .../spreadsheet/spreadsheet-table.tsx | 3 + .../spreadsheet/spreadsheet-view.tsx | 5 +- .../components/issues/issue-modal/base.tsx | 76 ++++++--------- .../components/default-properties.tsx | 1 + .../issues/issue-modal/context/index.ts | 2 +- ...ssue-modal.tsx => issue-modal-context.tsx} | 0 .../issues/issue-modal/draft-issue-layout.tsx | 71 ++------------ .../components/issues/issue-modal/form.tsx | 10 +- .../issues/parent-issues-list-modal.tsx | 3 + .../issues/peek-overview/issue-detail.tsx | 15 ++- .../components/issues/peek-overview/root.tsx | 15 ++- .../components/issues/peek-overview/view.tsx | 5 +- .../issues/relations/issue-list-item.tsx | 8 +- .../issues/relations/issue-list.tsx | 6 +- .../issues/relations/properties.tsx | 8 +- .../issues/sub-issues/issue-list-item.tsx | 3 +- .../issues/sub-issues/issues-list.tsx | 7 +- .../issues/sub-issues/properties.tsx | 2 + web/core/constants/empty-state.ts | 11 +++ web/core/constants/issue.ts | 89 +++++++++++++++++- web/core/hooks/store/use-issue-detail.ts | 9 +- web/core/hooks/store/use-issues.ts | 11 +++ web/core/hooks/use-group-dragndrop.ts | 3 +- web/core/hooks/use-issue-layout-store.ts | 5 +- .../use-issue-peek-overview-redirection.tsx | 10 +- web/core/hooks/use-issues-actions.tsx | 89 ++++++++++++++++++ web/core/services/issue/issue.service.ts | 82 ++++++++++------ .../services/issue/issue_activity.service.ts | 12 ++- .../services/issue/issue_archive.service.ts | 14 ++- .../issue/issue_attachment.service.ts | 17 ++-- .../services/issue/issue_comment.service.ts | 20 ++-- .../services/issue/issue_reaction.service.ts | 17 +++- web/core/services/issue_filter.service.ts | 20 ++++ web/core/services/project/project.service.ts | 14 ++- .../issue/issue-details/attachment.store.ts | 6 +- .../issue/issue-details/comment.store.ts | 8 +- .../store/issue/issue-details/issue.store.ts | 40 ++++++-- .../store/issue/issue-details/link.store.ts | 8 +- .../issue/issue-details/reaction.store.ts | 8 +- .../store/issue/issue-details/root.store.ts | 20 ++-- .../issue/issue-details/sub_issues.store.ts | 5 +- web/core/store/issue/issue.store.ts | 6 +- web/core/store/issue/root.store.ts | 23 ++++- web/core/store/project/project.store.ts | 27 ++++++ web/core/store/root.store.ts | 4 + web/core/store/router.store.ts | 10 ++ web/core/store/theme.store.ts | 14 +++ web/ee/components/epics/index.ts | 1 + web/public/empty-state/epics/epics-dark.webp | Bin 0 -> 48054 bytes web/public/empty-state/epics/epics-light.webp | Bin 0 -> 50384 bytes 134 files changed, 1336 insertions(+), 506 deletions(-) create mode 100644 web/ce/components/epics/epic-modal/index.ts create mode 100644 web/ce/components/epics/epic-modal/modal.tsx create mode 100644 web/ce/components/epics/index.ts create mode 100644 web/ce/store/issue/epic/filter.store.ts create mode 100644 web/ce/store/issue/epic/index.ts create mode 100644 web/ce/store/issue/epic/issue.store.ts create mode 100644 web/core/components/issues/issue-layouts/empty-states/project-epic.tsx rename web/core/components/issues/issue-modal/context/{issue-modal.tsx => issue-modal-context.tsx} (100%) create mode 100644 web/ee/components/epics/index.ts create mode 100644 web/public/empty-state/epics/epics-dark.webp create mode 100644 web/public/empty-state/epics/epics-light.webp diff --git a/packages/constants/src/issue.ts b/packages/constants/src/issue.ts index 9f6a1a2e2..9aa65d13e 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 b9366cccb..6fd160e77 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 d48342ceb..f878266b7 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 000000000..031608e25 --- /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 000000000..9c76b7bda --- /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 000000000..29da0cc8a --- /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 f049875f1..6feb208a8 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 cff391d9e..2fd629042 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 f0325bc12..8028a6191 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 000000000..a4733c60a --- /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 000000000..0fe6c946b --- /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 000000000..90ccee84d --- /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 6c0029c4f..de84fb87d 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 0fe75904d..b77106a19 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 b9582d21c..63e01c54e 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 81f064e2f..3ed21d144 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 bb286c280..c0218aceb 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 b2f6b8792..d2e5557ff 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 31c9137cc..7202efc55 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 b154bd205..258a1c50b 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 4f7bbd4b4..34c17e15b 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 d377bd90a..ca3c0ef9f 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 e3adc5f82..dfdbde074 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 925ff21c0..32c9d961c 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 4b80ba12f..54b6f5015 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 43b4812e6..28684ac99 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 c2d88a954..fb0b5b2c6 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 1f8027590..7b4788a1a 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 ce83f6826..f2d0cd670 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 2d85270b0..fefc7938f 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 4669528cd..ae915beb8 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 f9a59dd3a..775e2f9d7 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 5b8c11b02..655f23e08 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 1e01ee198..19929df25 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 79be48e4f..10b3b6585 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 ac8b0f663..4267e9e1a 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 dff072e7d..b1ff260f6 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 d6a8edc3f..78c6ff397 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 2c3854bed..3f91b712f 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 5432ee777..2bd9c90bf 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 7df432d5d..cc8abd82f 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 bf1ece310..73770bb13 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 99dbcacb5..5ead3bc5f 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 b3d9bf1fc..ad88c112e 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 daa5169b1..f71e9ba3c 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 99e350c61..2da5d0f79 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 c7629b5dc..68e8acf4d 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 7f20d5367..7ca128b98 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 08d5c0663..f48476164 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 af9b40df1..d83bc635b 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 87ed66445..0377f19c4 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 2392425ca..405cbf2ac 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 9f9f5b5bd..05e5a1631 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 822f42488..b28959c5b 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 c7e6232a0..3c55acf8e 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 35bea6386..2ca6a8f8e 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 734ce25cf..8a03ef4c8 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 a7811a406..674819bbf 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 1c413db35..9aaa13229 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 752e00bfd..e776d29ba 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 000000000..213e5ac40 --- /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 57175ae1d..721bad769 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 efb2b6bd1..bd25dc916 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 314c103ef..d8f4307e8 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 4a4ae9a2c..2fb4da801 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 f1107c8fb..3f20646d1 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 4303946dc..853a25e9b 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 7bff28cd9..ff49ec668 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 4d13e3ef9..bcb2ef189 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 && ( - -
- -
-
- )} + {!isEpic && ( + <> + {/* modules */} + {projectDetails?.module_view && ( + +
+ +
+
+ )} - {/* cycles */} - {projectDetails?.cycle_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 4ba268ded..b7bd6d9f1 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 5c4e9e927..0ce646551 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 798af2577..2a9d4abe1 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 c1978260a..91425e71a 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 61ad8c43a..e396ff2c1 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 d74cb7606..e2c693acc 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 5f182d24e..48e12a377 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 38c98238b..421aa6521 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 70eb51d81..e2e907ef8 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 fc8a4df42..c80212556 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 9ac1253ae..fa5cb886a 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 1b89788a4..efd8786d9 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 543a88c4d..b301c8b1f 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 c4486c2ae..64a5c7a02 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 2178f086f..2ac8f7394 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 948f97721..c4ea3bbd2 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 b2477eead..70df416a4 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 3780567b3..c56828439 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 52e18a905..786173fd3 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 8c9bc905a..6e359dcab 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 bef3bd1f1..3e5bff3e3 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 122e97fe5..aab69402f 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 1433e6ffb..f1ad94c59 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 a0778a47d..2315b4385 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 d74ce0c88..f140eb49f 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 b99d07d88..103cf6e21 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 9012fce5b..b86886ca9 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 b550217cf..322e8c2c6 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 b0c9821a3..ccfce8a86 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 9e2d355e1..39fc8406a 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 d051c685c..3e288bb1a 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 dcc9d11b5..f9c2af8b6 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 ef2a356fc..f9e47a560 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 211a71f48..b0fba6d3c 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 a7c439bee..3fe189842 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 de33706a0..85c6fa5b8 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 f3ae77fa3..5fd0e2245 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 e6e0ca8d0..c72ef1a77 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 9d5fd25c4..df87df67c 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 7fcb0eef4..a6ff334b9 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 944a32f71..002ede3ba 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 d8b5f02b7..25e6d500d 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 621f5f808..ebd118da8 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 051ed49b4..94b775414 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 185f47f17..0102a0936 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 000000000..6cef4035f --- /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 0000000000000000000000000000000000000000..ee8da30145f4b624cf6431cdf44015aee9df3b89 GIT binary patch literal 48054 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 U6~a`weme=D+0d@+Z~y=R0H3kI&j0`b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..179acf0add7ac2f2e0f255821c8cb061d870d336 GIT binary patch literal 50384 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 b7@~@CnR`|Tu{}ZuM=VCF2mk;8000002Yo2F literal 0 HcmV?d00001