From 211d5e1cd0eca259af16571f3799bc0e3a91bfaa Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 27 Dec 2024 18:18:45 +0530 Subject: [PATCH] chore: code refactor and build fix (#6285) * chore: code refactor and build fix * chore: code refactor * chore: code refactor --- .../components/issues/delete-issue-modal.tsx | 16 +-- web/core/components/issues/filters.tsx | 2 + .../sub-issues/content.tsx | 5 +- .../issue-detail-widgets/sub-issues/title.tsx | 2 +- .../issue-detail/issue-activity/sort-root.tsx | 12 +- .../calendar/base-calendar-root.tsx | 2 +- .../issue-layouts/calendar/issue-blocks.tsx | 1 + .../display-filters-selection.tsx | 3 + .../display-filters/display-properties.tsx | 7 ++ .../header/display-filters/issue-grouping.tsx | 7 +- .../header/filters/filters-selection.tsx | 3 + .../issue-layouts/gantt/base-gantt-root.tsx | 5 +- .../issue-layouts/group-drag-overlay.tsx | 4 +- .../issues/issue-layouts/list/list-group.tsx | 2 + .../properties/all-properties.tsx | 105 +++++++++--------- .../issue-layouts/quick-add/button/gantt.tsx | 4 +- .../issue-layouts/quick-add/button/kanban.tsx | 4 +- .../issue-layouts/quick-add/button/list.tsx | 4 +- .../quick-add/button/spreadsheet.tsx | 4 +- .../spreadsheet/columns/header-column.tsx | 5 +- .../spreadsheet/columns/sub-issue-column.tsx | 9 +- .../spreadsheet/spreadsheet-header-column.tsx | 11 +- .../spreadsheet/spreadsheet-header.tsx | 5 +- .../spreadsheet/spreadsheet-table.tsx | 1 + .../spreadsheet/spreadsheet-view.tsx | 1 + .../issues/parent-issues-list-modal.tsx | 2 +- .../issues/sub-issues/issues-list.tsx | 1 + web/core/constants/empty-state.ts | 99 +++++++++++++++++ web/core/constants/issue.ts | 4 +- web/core/services/issue/issue.service.ts | 40 +++++-- .../store/issue/helpers/base-issues.store.ts | 1 + .../store/issue/issue-details/issue.store.ts | 6 +- .../issue/issue-details/sub_issues.store.ts | 16 ++- 33 files changed, 292 insertions(+), 101 deletions(-) diff --git a/web/core/components/issues/delete-issue-modal.tsx b/web/core/components/issues/delete-issue-modal.tsx index f191412347..2129aab118 100644 --- a/web/core/components/issues/delete-issue-modal.tsx +++ b/web/core/components/issues/delete-issue-modal.tsx @@ -11,7 +11,6 @@ import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; import { useIssues, useProject, useUser, useUserPermissions } from "@/hooks/store"; // plane-web import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; - type Props = { isOpen: boolean; handleClose: () => void; @@ -19,10 +18,11 @@ type Props = { data?: TIssue | TDeDupeIssue; isSubIssue?: boolean; onSubmit?: () => Promise; + isEpic?: boolean; }; export const DeleteIssueModal: React.FC = (props) => { - const { dataId, data, isOpen, handleClose, isSubIssue = false, onSubmit } = props; + const { dataId, data, isOpen, handleClose, isSubIssue = false, onSubmit, isEpic = false } = props; // states const [isDeleting, setIsDeleting] = useState(false); // store hooks @@ -70,12 +70,14 @@ export const DeleteIssueModal: React.FC = (props) => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", - message: `${isSubIssue ? "Sub-issue" : "Issue"} deleted successfully`, + message: `${isSubIssue ? "Sub-issue" : isEpic ? "Epic" : "Issue"} deleted successfully`, }); onClose(); }) .catch((errors) => { - const isPermissionError = errors?.error === "Only admin or creator can delete the issue"; + const isPermissionError = + errors?.error === + `Only admin or creator can delete the ${isSubIssue ? "sub-issue" : isEpic ? "epic" : "issue"}`; const currentError = isPermissionError ? PROJECT_ERROR_MESSAGES.permissionError : PROJECT_ERROR_MESSAGES.issueDeleteError; @@ -94,14 +96,14 @@ export const DeleteIssueModal: React.FC = (props) => { handleSubmit={handleIssueDelete} isSubmitting={isDeleting} isOpen={isOpen} - title="Delete issue" + title={`Delete ${isEpic ? "epic" : "issue"}`} content={ <> - Are you sure you want to delete issue{" "} + {`Are you sure you want to delete ${isEpic ? "epic" : "issue"} `} {projectDetails?.identifier}-{issue?.sequence_id} - {""}? All of the data related to the issue will be permanently removed. This action cannot be undone. + {` ? All of the data related to the ${isEpic ? "epic" : "issue"} will be permanently removed. This action cannot be undone.`} } /> diff --git a/web/core/components/issues/filters.tsx b/web/core/components/issues/filters.tsx index f96c582c9a..6cb089465d 100644 --- a/web/core/components/issues/filters.tsx +++ b/web/core/components/issues/filters.tsx @@ -123,6 +123,7 @@ const HeaderFilters = observer((props: Props) => { states={projectStates} cycleViewDisabled={!currentProjectDetails?.cycle_view} moduleViewDisabled={!currentProjectDetails?.module_view} + isEpic={storeType === EIssuesStoreType.EPIC} /> @@ -134,6 +135,7 @@ const HeaderFilters = observer((props: Props) => { handleDisplayPropertiesUpdate={handleDisplayProperties} cycleViewDisabled={!currentProjectDetails?.cycle_view} moduleViewDisabled={!currentProjectDetails?.module_view} + isEpic={storeType === EIssuesStoreType.EPIC} /> {canUserCreateIssue ? ( 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 428cf02f6c..2d197ebcbf 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 @@ -53,11 +53,10 @@ export const SubIssuesCollapsibleContent: FC = observer((props) => { }, }); // store hooks + const { toggleCreateIssueModal, toggleDeleteIssueModal } = useIssueDetail(); const { subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers }, - toggleCreateIssueModal, - toggleDeleteIssueModal, - } = useIssueDetail(); + } = useIssueDetail(issueServiceType); // helpers const subIssueOperations = useSubIssueOperations(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 ad88c112ed..e4ff1ea731 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 @@ -38,7 +38,7 @@ export const SubIssuesCollapsibleTitle: FC = observer((props) => { return ( diff --git a/web/core/components/issues/issue-detail/issue-activity/sort-root.tsx b/web/core/components/issues/issue-detail/issue-activity/sort-root.tsx index ed4d371b52..b1fc2e8f30 100644 --- a/web/core/components/issues/issue-detail/issue-activity/sort-root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/sort-root.tsx @@ -9,18 +9,24 @@ import { cn } from "@/helpers/common.helper"; export type TActivitySortRoot = { sortOrder: "asc" | "desc"; toggleSort: () => void; + className?: string; + iconClassName?: string; }; export const ActivitySortRoot: FC = memo((props) => (
{ props.toggleSort(); }} > {props.sortOrder === "asc" ? ( - + ) : ( - + )}
)); diff --git a/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 07e124bf0b..582d20c369 100644 --- a/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/core/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -50,7 +50,7 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { const { workspaceSlug } = useParams(); // hooks - const storeType = useIssueStoreType() as CalendarStoreType; + const storeType = isEpic ? EIssuesStoreType.EPIC : (useIssueStoreType() as CalendarStoreType); const { allowPermissions } = useUserPermissions(); const { issues, issuesFilter, issueMap } = useIssues(storeType); const { 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 674819bbf7..5eaae6d57d 100644 --- a/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/core/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -87,6 +87,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { }} quickAddCallback={quickAddCallback} addIssuesToView={addIssuesToView} + isEpic={isEpic} /> )} diff --git a/web/core/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/core/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index cd33525c8e..12338658ef 100644 --- a/web/core/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -25,6 +25,7 @@ type Props = { ignoreGroupedFilters?: Partial[]; cycleViewDisabled?: boolean; moduleViewDisabled?: boolean; + isEpic?: boolean; }; export const DisplayFiltersSelection: React.FC = observer((props) => { @@ -37,6 +38,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { ignoreGroupedFilters = [], cycleViewDisabled = false, moduleViewDisabled = false, + isEpic = false, } = props; const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) => @@ -61,6 +63,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { handleUpdate={handleDisplayPropertiesUpdate} cycleViewDisabled={cycleViewDisabled} moduleViewDisabled={moduleViewDisabled} + isEpic={isEpic} /> )} diff --git a/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx b/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx index 1e60783822..9651343492 100644 --- a/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx @@ -16,6 +16,7 @@ type Props = { handleUpdate: (updatedDisplayProperties: Partial) => void; cycleViewDisabled?: boolean; moduleViewDisabled?: boolean; + isEpic?: boolean; }; export const FilterDisplayProperties: React.FC = observer((props) => { @@ -25,6 +26,7 @@ export const FilterDisplayProperties: React.FC = observer((props) => { handleUpdate, cycleViewDisabled = false, moduleViewDisabled = false, + isEpic = false, } = props; // router const { workspaceSlug, projectId: routerProjectId } = useParams(); @@ -45,6 +47,11 @@ export const FilterDisplayProperties: React.FC = observer((props) => { default: return shouldRenderDisplayProperty({ workspaceSlug: workspaceSlug?.toString(), projectId, key: property.key }); } + }).map((property) => { + if (isEpic && property.key === "sub_issue_count") { + return { ...property, title: "Issue count" }; + } + return property; }); return ( diff --git a/web/core/components/issues/issue-layouts/filters/header/display-filters/issue-grouping.tsx b/web/core/components/issues/issue-layouts/filters/header/display-filters/issue-grouping.tsx index f3c0fdf136..0de60f625b 100644 --- a/web/core/components/issues/issue-layouts/filters/header/display-filters/issue-grouping.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/display-filters/issue-grouping.tsx @@ -11,10 +11,11 @@ import { ISSUE_FILTER_OPTIONS } from "@/constants/issue"; type Props = { selectedIssueType: TIssueGroupingFilters | undefined; handleUpdate: (val: TIssueGroupingFilters) => void; + isEpic?: boolean; }; export const FilterIssueGrouping: React.FC = observer((props) => { - const { selectedIssueType, handleUpdate } = props; + const { selectedIssueType, handleUpdate, isEpic = false } = props; const [previewEnabled, setPreviewEnabled] = React.useState(true); @@ -23,7 +24,7 @@ export const FilterIssueGrouping: React.FC = observer((props) => { return ( <> setPreviewEnabled(!previewEnabled)} /> @@ -34,7 +35,7 @@ export const FilterIssueGrouping: React.FC = observer((props) => { key={issueType?.key} isChecked={activeIssueType === issueType?.key ? true : false} onClick={() => handleUpdate(issueType?.key)} - title={issueType.title} + title={`${issueType.title} ${isEpic ? "Epics" : "Issues"}`} multiple={false} /> ))} diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index c45db7f490..f097574081 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -42,6 +42,7 @@ type Props = { states?: IState[] | undefined; cycleViewDisabled?: boolean; moduleViewDisabled?: boolean; + isEpic?: boolean; }; export const FilterSelection: React.FC = observer((props) => { @@ -56,6 +57,7 @@ export const FilterSelection: React.FC = observer((props) => { states, cycleViewDisabled = false, moduleViewDisabled = false, + isEpic = false, } = props; // hooks const { isMobile } = usePlatformOS(); @@ -234,6 +236,7 @@ export const FilterSelection: React.FC = observer((props) => { type: val, }) } + isEpic={isEpic} /> )} diff --git a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx index c0c97a22ac..1b56eee4da 100644 --- a/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/core/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -111,6 +111,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan target_date: renderFormattedPayloadDate(targetDate), }} quickAddCallback={quickAddIssue} + isEpic={isEpic} /> ) : undefined; @@ -120,8 +121,8 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan
} diff --git a/web/core/components/issues/issue-layouts/group-drag-overlay.tsx b/web/core/components/issues/issue-layouts/group-drag-overlay.tsx index 822b2e0df1..db19336e43 100644 --- a/web/core/components/issues/issue-layouts/group-drag-overlay.tsx +++ b/web/core/components/issues/issue-layouts/group-drag-overlay.tsx @@ -16,6 +16,7 @@ type Props = { dropErrorMessage?: string; orderBy: TIssueOrderByOptions | undefined; isDraggingOverColumn: boolean; + isEpic?: boolean; }; export const GroupDragOverlay = (props: Props) => { @@ -27,6 +28,7 @@ export const GroupDragOverlay = (props: Props) => { dropErrorMessage, orderBy, isDraggingOverColumn, + isEpic = false, } = props; const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible; @@ -68,7 +70,7 @@ export const GroupDragOverlay = (props: Props) => { The layout is ordered by {readableOrderBy}. )} - Drop here to move the issue. + {`Drop here to move the ${isEpic ? "epic" : "issue"}.`} )}
diff --git a/web/core/components/issues/issue-layouts/list/list-group.tsx b/web/core/components/issues/issue-layouts/list/list-group.tsx index 1e3413662b..22faef8438 100644 --- a/web/core/components/issues/issue-layouts/list/list-group.tsx +++ b/web/core/components/issues/issue-layouts/list/list-group.tsx @@ -284,6 +284,7 @@ export const ListGroup = observer((props: Props) => { dropErrorMessage={group.dropErrorMessage} orderBy={orderBy} isDraggingOverColumn={isDraggingOverColumn} + isEpic={isEpic} /> {groupIssueIds && ( { prePopulatedData={prePopulateQuickAddData(group_by, group.id)} containerClassName="border-b border-t border-custom-border-200 bg-custom-background-100 " quickAddCallback={quickAddCallback} + isEpic={isEpic} /> )} diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index e27c4d1119..c23d305229 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, SyntheticEvent } from "react"; import xor from "lodash/xor"; import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; @@ -245,7 +245,7 @@ export const IssueProperties: React.FC = observer((props) => { const redirectToIssueDetail = () => { router.push( - `/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${issue.id}#sub-issues` + `/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${isEpic ? "epics" : "issues"}/${issue.id}#sub-issues` ); // router.push({ // pathname: `/${workspaceSlug}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${ @@ -265,7 +265,7 @@ export const IssueProperties: React.FC = observer((props) => { const maxDate = getDate(issue.target_date); maxDate?.setDate(maxDate.getDate()); - const handleEventPropagation = (e: React.MouseEvent) => { + const handleEventPropagation = (e: SyntheticEvent) => { e.stopPropagation(); e.preventDefault(); }; @@ -275,7 +275,7 @@ export const IssueProperties: React.FC = observer((props) => { {/* basic properties */} {/* state */} -
+
= observer((props) => { {/* priority */} -
+
= observer((props) => { {/* start date */} -
+
= observer((props) => { {/* target/due date */} -
+
= observer((props) => { {/* assignee */} -
+
= observer((props) => {
- {!isEpic && ( - <> - {/* modules */} - {projectDetails?.module_view && ( - -
- -
-
- )} + <> + {!isEpic && ( + <> + {/* modules */} + {projectDetails?.module_view && ( + +
+ +
+
+ )} - {/* cycles */} - {projectDetails?.cycle_view && ( - -
- -
-
- )} - - )} + {/* cycles */} + {projectDetails?.cycle_view && ( + +
+ +
+
+ )} + + )} + {/* estimates */} {projectId && areEstimateEnabledByProjectId(projectId?.toString()) && ( -
+
= observer((props) => { shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!subIssueCount} >
{ e.stopPropagation(); e.preventDefault(); @@ -467,6 +470,7 @@ export const IssueProperties: React.FC = observer((props) => { >
@@ -489,6 +493,7 @@ export const IssueProperties: React.FC = observer((props) => { >
diff --git a/web/core/components/issues/issue-layouts/quick-add/button/gantt.tsx b/web/core/components/issues/issue-layouts/quick-add/button/gantt.tsx index eb9dd35a85..e9297abc9c 100644 --- a/web/core/components/issues/issue-layouts/quick-add/button/gantt.tsx +++ b/web/core/components/issues/issue-layouts/quick-add/button/gantt.tsx @@ -5,7 +5,7 @@ import { Row } from "@plane/ui"; import { TQuickAddIssueButton } from "../root"; export const GanttQuickAddIssueButton: FC = observer((props) => { - const { onClick } = props; + const { onClick, isEpic = false } = props; return ( ); diff --git a/web/core/components/issues/issue-layouts/quick-add/button/kanban.tsx b/web/core/components/issues/issue-layouts/quick-add/button/kanban.tsx index 5338cba9df..918ef33120 100644 --- a/web/core/components/issues/issue-layouts/quick-add/button/kanban.tsx +++ b/web/core/components/issues/issue-layouts/quick-add/button/kanban.tsx @@ -4,7 +4,7 @@ import { PlusIcon } from "lucide-react"; import { TQuickAddIssueButton } from "../root"; export const KanbanQuickAddIssueButton: FC = observer((props) => { - const { onClick } = props; + const { onClick, isEpic = false } = props; return (
= observer((pro onClick={onClick} > - New Issue + {`New ${isEpic ? "Epic" : "Issue"}`}
); }); diff --git a/web/core/components/issues/issue-layouts/quick-add/button/list.tsx b/web/core/components/issues/issue-layouts/quick-add/button/list.tsx index 09b90dbf46..3dcbf5990a 100644 --- a/web/core/components/issues/issue-layouts/quick-add/button/list.tsx +++ b/web/core/components/issues/issue-layouts/quick-add/button/list.tsx @@ -5,7 +5,7 @@ import { Row } from "@plane/ui"; import { TQuickAddIssueButton } from "../root"; export const ListQuickAddIssueButton: FC = observer((props) => { - const { onClick } = props; + const { onClick, isEpic = false } = props; return ( = observer((props onClick={onClick} > - New Issue + {`New ${isEpic ? "Epic" : "Issue"}`} ); }); diff --git a/web/core/components/issues/issue-layouts/quick-add/button/spreadsheet.tsx b/web/core/components/issues/issue-layouts/quick-add/button/spreadsheet.tsx index b5663bb569..170f891909 100644 --- a/web/core/components/issues/issue-layouts/quick-add/button/spreadsheet.tsx +++ b/web/core/components/issues/issue-layouts/quick-add/button/spreadsheet.tsx @@ -4,7 +4,7 @@ import { PlusIcon } from "lucide-react"; import { TQuickAddIssueButton } from "../root"; export const SpreadsheetAddIssueButton: FC = observer((props) => { - const { onClick } = props; + const { onClick, isEpic = false } = props; return (
@@ -14,7 +14,7 @@ export const SpreadsheetAddIssueButton: FC = observer((pro onClick={onClick} > - New Issue + {`New ${isEpic ? "Epic" : "Issue"}`}
); diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx index 5cf0e3d202..72020ccab6 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx @@ -15,10 +15,11 @@ interface Props { displayFilters: IIssueDisplayFilterOptions; handleDisplayFilterUpdate: (data: Partial) => void; onClose: () => void; + isEpic?: boolean; } export const HeaderColumn = (props: Props) => { - const { displayFilters, handleDisplayFilterUpdate, property, onClose } = props; + const { displayFilters, handleDisplayFilterUpdate, property, onClose, isEpic = false } = props; const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage( "spreadsheetViewSorting", @@ -46,7 +47,7 @@ export const HeaderColumn = (props: Props) => {
{} - {propertyDetails.title} + {propertyDetails.title === "Sub-issue" && isEpic ? "Issues" : propertyDetails.title}
{activeSortingProperty === property && ( diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx index 4db6da5070..b050655fa6 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/sub-issue-column.tsx @@ -18,16 +18,19 @@ export const SpreadsheetSubIssueColumn: React.FC = observer((props: Props // router const router = useAppRouter(); // hooks - const { workspaceSlug } = useParams(); + const { workspaceSlug, epicId } = useParams(); // derived values const subIssueCount = issue?.sub_issues_count ?? 0; const redirectToIssueDetail = () => { router.push( - `/${workspaceSlug?.toString()}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}issues/${issue.id}#sub-issues` + `/${workspaceSlug?.toString()}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${epicId ? "epics" : "issues"}/${issue.id}#sub-issues` ); }; + const issueLabel = epicId ? "issue" : "sub-issue"; + const label = `${subIssueCount} ${issueLabel}${subIssueCount !== 1 ? "s" : ""}`; + return ( {}} @@ -38,7 +41,7 @@ export const SpreadsheetSubIssueColumn: React.FC = observer((props: Props } )} > - {subIssueCount} {subIssueCount === 1 ? "sub-issue" : "sub-issues"} + {label} ); }); diff --git a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx index e66fe74df5..f75c4ddb31 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx @@ -12,9 +12,17 @@ interface Props { isEstimateEnabled: boolean; displayFilters: IIssueDisplayFilterOptions; handleDisplayFilterUpdate: (data: Partial) => void; + isEpic?: boolean; } export const SpreadsheetHeaderColumn = observer((props: Props) => { - const { displayProperties, displayFilters, property, isEstimateEnabled, handleDisplayFilterUpdate } = props; + const { + displayProperties, + displayFilters, + property, + isEstimateEnabled, + handleDisplayFilterUpdate, + isEpic = false, + } = props; //hooks const tableHeaderCellRef = useRef(null); @@ -39,6 +47,7 @@ export const SpreadsheetHeaderColumn = observer((props: Props) => { onClose={() => { tableHeaderCellRef?.current?.focus(); }} + isEpic={isEpic} /> diff --git a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx index ca4ea948e7..7b67209896 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx @@ -21,6 +21,7 @@ interface Props { isEstimateEnabled: boolean; spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; selectionHelpers: TSelectionHelper; + isEpic?: boolean; } export const SpreadsheetHeader = observer((props: Props) => { @@ -32,6 +33,7 @@ export const SpreadsheetHeader = observer((props: Props) => { isEstimateEnabled, spreadsheetColumnsList, selectionHelpers, + isEpic = false, } = props; // router const { projectId } = useParams(); @@ -62,7 +64,7 @@ export const SpreadsheetHeader = observer((props: Props) => { />
)} - Issues + {`${isEpic ? "Epics" : "Issues"}`}
@@ -74,6 +76,7 @@ export const SpreadsheetHeader = observer((props: Props) => { displayFilters={displayFilters} handleDisplayFilterUpdate={handleDisplayFilterUpdate} isEstimateEnabled={isEstimateEnabled} + isEpic={isEpic} /> ))} diff --git a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx index 14c9ee3228..7f6e5669a7 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx @@ -112,6 +112,7 @@ export const SpreadsheetTable = observer((props: Props) => { isEstimateEnabled={isEstimateEnabled} spreadsheetColumnsList={spreadsheetColumnsList} selectionHelpers={selectionHelpers} + isEpic={isEpic} /> {issueIds.map((id) => ( diff --git a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index 6d70b923f2..d7ac791af1 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -117,6 +117,7 @@ export const SpreadsheetView: React.FC = observer((props) => { layout={EIssueLayoutTypes.SPREADSHEET} QuickAddButton={SpreadsheetAddIssueButton} quickAddCallback={quickAddCallback} + isEpic={isEpic} /> )}
diff --git a/web/core/components/issues/parent-issues-list-modal.tsx b/web/core/components/issues/parent-issues-list-modal.tsx index 193b2327e2..5d2d46caf6 100644 --- a/web/core/components/issues/parent-issues-list-modal.tsx +++ b/web/core/components/issues/parent-issues-list-modal.tsx @@ -69,7 +69,7 @@ export const ParentIssuesListModal: React.FC = ({ projectService .projectIssuesSearch(workspaceSlug as string, projectId as string, { search: debouncedSearchTerm, - parent: true, + parent: searchEpic ? undefined : true, issue_id: issueId, workspace_search: false, epic: searchEpic ? true : undefined, diff --git a/web/core/components/issues/sub-issues/issues-list.tsx b/web/core/components/issues/sub-issues/issues-list.tsx index 2ac8f7394d..9fe1a9abab 100644 --- a/web/core/components/issues/sub-issues/issues-list.tsx +++ b/web/core/components/issues/sub-issues/issues-list.tsx @@ -60,6 +60,7 @@ export const IssueList: FC = observer((props) => { disabled={disabled} handleIssueCrudState={handleIssueCrudState} subIssueOperations={subIssueOperations} + issueServiceType={issueServiceType} /> ))} diff --git a/web/core/constants/empty-state.ts b/web/core/constants/empty-state.ts index 70df416a46..9545b57c90 100644 --- a/web/core/constants/empty-state.ts +++ b/web/core/constants/empty-state.ts @@ -29,6 +29,9 @@ export enum EmptyStateType { WORKSPACE_DASHBOARD = "workspace-dashboard", WORKSPACE_ANALYTICS = "workspace-analytics", WORKSPACE_PROJECTS = "workspace-projects", + WORKSPACE_TEAMS = "workspace-teams", + WORKSPACE_INITIATIVES = "workspace-initiatives", + WORKSPACE_INITIATIVES_EMPTY_SEARCH = "workspace-initiatives-empty-search", WORKSPACE_ALL_ISSUES = "workspace-all-issues", WORKSPACE_ASSIGNED = "workspace-assigned", WORKSPACE_CREATED = "workspace-created", @@ -96,6 +99,7 @@ export enum EmptyStateType { ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE = "active-cycle-assignee-empty-state", ACTIVE_CYCLE_LABEL_EMPTY_STATE = "active-cycle-label-empty-state", + WORKSPACE_ACTIVE_CYCLES = "workspace-active-cycles", DISABLED_PROJECT_INBOX = "disabled-project-inbox", DISABLED_PROJECT_CYCLE = "disabled-project-cycle", DISABLED_PROJECT_MODULE = "disabled-project-module", @@ -110,6 +114,11 @@ export enum EmptyStateType { WORKSPACE_DRAFT_ISSUES = "workspace-draft-issues", PROJECT_NO_EPICS = "project-no-epics", + // Teams + TEAM_NO_ISSUES = "team-no-issues", + TEAM_EMPTY_FILTER = "team-empty-filter", + TEAM_VIEW = "team-view", + TEAM_PAGE = "team-page", } const emptyStateDetails = { @@ -165,6 +174,35 @@ const emptyStateDetails = { accessType: "workspace", access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, + [EmptyStateType.WORKSPACE_TEAMS]: { + key: EmptyStateType.WORKSPACE_TEAMS, + title: "Teams", + description: "Teams are groups of people who collaborate on projects. Create a team to get started.", + path: "/empty-state/teams/teams", + primaryButton: { + text: "Create new team", + }, + accessType: "workspace", + access: [EUserPermissions.ADMIN], + }, + [EmptyStateType.WORKSPACE_INITIATIVES]: { + key: EmptyStateType.WORKSPACE_INITIATIVES, + title: "Organize work at the highest level with Initiatives", + description: + "When you need to organize work spanning several projects and teams, Initiatives come in handy. Connect projects and epics to initiatives, see automatically rolled up updates, and see the forests before you get to the trees.", + path: "/empty-state/initiatives/initiatives", + primaryButton: { + text: "Create an initiative", + }, + accessType: "workspace", + access: [EUserPermissions.ADMIN], + }, + [EmptyStateType.WORKSPACE_INITIATIVES_EMPTY_SEARCH]: { + key: EmptyStateType.WORKSPACE_INITIATIVES_EMPTY_SEARCH, + title: "No matching initiatives", + description: "No initiatives detected with the matching criteria. \n Create a new initiative instead.", + path: "/empty-state/search/project", + }, // all-issues [EmptyStateType.WORKSPACE_ALL_ISSUES]: { key: EmptyStateType.WORKSPACE_ALL_ISSUES, @@ -695,6 +733,13 @@ const emptyStateDetails = { title: "Add labels to issues to see the \n breakdown of work by labels.", path: "/empty-state/active-cycle/label", }, + [EmptyStateType.WORKSPACE_ACTIVE_CYCLES]: { + key: EmptyStateType.WORKSPACE_ACTIVE_CYCLES, + title: "No active cycles", + description: + "Cycles of your projects that includes any period that encompasses today's date within its range. Find the progress and details of all your active cycle here.", + path: "/empty-state/onboarding/workspace-active-cycles", + }, [EmptyStateType.DISABLED_PROJECT_INBOX]: { key: EmptyStateType.DISABLED_PROJECT_INBOX, title: "Intake is not enabled for the project.", @@ -795,9 +840,63 @@ const emptyStateDetails = { 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", + primaryButton: { + text: "Create an Epic", + }, accessType: "project", access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], }, + // Teams + [EmptyStateType.TEAM_NO_ISSUES]: { + key: EmptyStateType.TEAM_NO_ISSUES, + title: "Create an issue in your team projects and assign it to someone, even yourself", + description: + "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", + path: "/empty-state/onboarding/issues", + primaryButton: { + text: "Create your first issue", + comicBox: { + title: "Issues are building blocks in Plane.", + description: + "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", + }, + }, + accessType: "workspace", + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + }, + [EmptyStateType.TEAM_EMPTY_FILTER]: { + key: EmptyStateType.TEAM_EMPTY_FILTER, + title: "No issues found matching the filters applied", + path: "/empty-state/empty-filters/", + secondaryButton: { + text: "Clear all filters", + }, + accessType: "workspace", + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + }, + [EmptyStateType.TEAM_VIEW]: { + key: EmptyStateType.TEAM_VIEW, + title: "Save filtered views for your team. Create as many as you need", + description: + "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a team can see everyone’s views and choose whichever suits their needs best.", + path: "/empty-state/onboarding/views", + primaryButton: { + text: "Create your first view", + comicBox: { + title: "Views work atop Issue properties.", + description: "You can create a view from here with as many properties as filters as you see fit.", + }, + }, + accessType: "workspace", + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + }, + [EmptyStateType.TEAM_PAGE]: { + key: EmptyStateType.TEAM_PAGE, + title: "Team pages are coming soon!", + description: + "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started. Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", + path: "/empty-state/onboarding/pages", + }, } as const; export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 5ef54075a5..02d7ee01ad 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -78,8 +78,8 @@ export const ISSUE_FILTER_OPTIONS: { title: string; }[] = [ { key: null, title: "All" }, - { key: "active", title: "Active Issues" }, - { key: "backlog", title: "Backlog Issues" }, + { key: "active", title: "Active" }, + { key: "backlog", title: "Backlog" }, // { key: "draft", title: "Draft Issues" }, ]; diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts index f140eb49f0..66daf82a6e 100644 --- a/web/core/services/issue/issue.service.ts +++ b/web/core/services/issue/issue.service.ts @@ -43,7 +43,7 @@ export class IssueService extends APIService { ): Promise { const path = (queries.expand as string)?.includes("issue_relation") && !queries.group_by - ? `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues-detail/` + ? `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}-detail/` : `/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/`; return this.get( path, @@ -76,8 +76,9 @@ export class IssueService extends APIService { } async getIssues(workspaceSlug: string, projectId: string, queries?: any, config = {}): Promise { - if (getIssuesShouldFallbackToServer(queries)) + if (getIssuesShouldFallbackToServer(queries) || this.serviceType !== EIssueServiceType.ISSUES) { return await this.getIssuesFromServer(workspaceSlug, projectId, queries, config); + } const response = await persistence.getIssues(workspaceSlug, projectId, queries, config); return response as TIssuesResponse; @@ -112,7 +113,8 @@ export class IssueService extends APIService { params: queries, }) .then((response) => { - if (response.data) { + // skip issue update when the service type is epic + if (response.data && this.serviceType === EIssueServiceType.ISSUES) { updateIssue({ ...response.data, is_local_update: 1 }); } return response?.data; @@ -127,7 +129,7 @@ export class IssueService extends APIService { params: { issues: issueIds.join(",") }, }) .then((response) => { - if (response?.data && Array.isArray(response?.data)) { + if (response?.data && Array.isArray(response?.data) && this.serviceType === EIssueServiceType.ISSUES) { addIssuesBulk(response.data); } return response?.data; @@ -233,7 +235,9 @@ export class IssueService extends APIService { } async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise { - deleteIssueFromLocal(issuesId); + if (this.serviceType === EIssueServiceType.ISSUES) { + deleteIssueFromLocal(issuesId); + } return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issuesId}/`) .then((response) => response?.data) .catch((error) => { @@ -335,7 +339,9 @@ export class IssueService extends APIService { async bulkOperations(workspaceSlug: string, projectId: string, data: TBulkOperationsPayload): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-operation-issues/`, data) .then((response) => { - persistence.syncIssues(projectId); + if (this.serviceType === EIssueServiceType.ISSUES) { + persistence.syncIssues(projectId); + } return response?.data; }) .catch((error) => { @@ -352,7 +358,9 @@ export class IssueService extends APIService { ): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-delete-issues/`, data) .then((response) => { - persistence.syncIssues(projectId); + if (this.serviceType === EIssueServiceType.ISSUES) { + persistence.syncIssues(projectId); + } return response?.data; }) .catch((error) => { @@ -371,7 +379,9 @@ export class IssueService extends APIService { }> { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-archive-issues/`, data) .then((response) => { - persistence.syncIssues(projectId); + if (this.serviceType === EIssueServiceType.ISSUES) { + persistence.syncIssues(projectId); + } return response?.data; }) .catch((error) => { @@ -411,4 +421,18 @@ export class IssueService extends APIService { throw error?.response?.data; }); } + + async bulkSubscribeIssues( + workspaceSlug: string, + projectId: string, + data: { + issue_ids: string[]; + } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-subscribe-issues/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } diff --git a/web/core/store/issue/helpers/base-issues.store.ts b/web/core/store/issue/helpers/base-issues.store.ts index 9ce6b45cb4..42c1c384c5 100644 --- a/web/core/store/issue/helpers/base-issues.store.ts +++ b/web/core/store/issue/helpers/base-issues.store.ts @@ -672,6 +672,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore { const issueBeforeRemoval = clone(this.rootIssueStore.issues.getIssueById(issueId)); // update parent stats optimistically this.updateParentStats(issueBeforeRemoval, undefined); + // Male API call await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); // Remove from Respective issue Id list diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts index 3fe1898420..08c6e4bb30 100644 --- a/web/core/store/issue/issue-details/issue.store.ts +++ b/web/core/store/issue/issue-details/issue.store.ts @@ -49,6 +49,7 @@ export class IssueStore implements IIssueStore { // services serviceType; issueService; + epicService; issueArchiveService; issueDraftService; @@ -62,6 +63,7 @@ export class IssueStore implements IIssueStore { // services this.serviceType = serviceType; this.issueService = new IssueService(serviceType); + this.epicService = new IssueService(EIssueServiceType.EPICS); this.issueArchiveService = new IssueArchiveService(serviceType); this.issueDraftService = new IssueDraftService(); } @@ -93,7 +95,9 @@ export class IssueStore implements IIssueStore { let issue: TIssue | undefined; // fetch issue from local db - issue = await persistence.getIssue(issueId); + if (this.serviceType === EIssueServiceType.ISSUES) { + issue = await persistence.getIssue(issueId); + } this.fetchingIssueDetails = issueId; 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 df87df67c5..c2c160c390 100644 --- a/web/core/store/issue/issue-details/sub_issues.store.ts +++ b/web/core/store/issue/issue-details/sub_issues.store.ts @@ -4,6 +4,7 @@ import set from "lodash/set"; import uniq from "lodash/uniq"; import update from "lodash/update"; import { action, makeObservable, observable, runInAction } from "mobx"; +import { EIssueServiceType } from "@plane/constants"; // types import { TIssue, @@ -64,6 +65,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { // root store rootIssueDetailStore: IIssueDetail; // services + serviceType; issueService; constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) { @@ -84,6 +86,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { // root store this.rootIssueDetailStore = rootStore; // services + this.serviceType = serviceType; this.issueService = new IssueService(serviceType); } @@ -182,7 +185,10 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { [parentIssueId, "sub_issues_count"], this.subIssues[parentIssueId].length ); - updatePersistentLayer([parentIssueId, ...issueIds]); + + if (this.serviceType === EIssueServiceType.ISSUES) { + updatePersistentLayer([parentIssueId, ...issueIds]); + } return; }; @@ -280,7 +286,9 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { ); }); - updatePersistentLayer([parentIssueId]); + if (this.serviceType === EIssueServiceType.ISSUES) { + updatePersistentLayer([parentIssueId]); + } return; }; @@ -315,7 +323,9 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { ); }); - updatePersistentLayer([parentIssueId]); + if (this.serviceType === EIssueServiceType.ISSUES) { + updatePersistentLayer([parentIssueId]); + } return; };