diff --git a/web/core/components/issues/delete-issue-modal.tsx b/web/core/components/issues/delete-issue-modal.tsx index f191412347d..2129aab1184 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 f96c582c9a2..6cb089465d3 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 428cf02f6c2..2d197ebcbfb 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 ad88c112ede..e4ff1ea731d 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 ed4d371b526..b1fc2e8f304 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 07e124bf0bf..582d20c3690 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 674819bbf7a..5eaae6d57df 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 cd33525c8e7..12338658efa 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 1e607838229..9651343492c 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 f3c0fdf1360..0de60f625be 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 c45db7f4907..f0975740817 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 c0c97a22ac0..1b56eee4da4 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 822b2e0df11..db19336e43d 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 1e3413662bb..22faef8438f 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 e27c4d1119d..c23d3052299 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 && ( - -
- -
-
- )} - - {/* cycles */} - {projectDetails?.cycle_view && ( - -
- -
-
- )} - - )} + <> + {!isEpic && ( + <> + {/* modules */} + {projectDetails?.module_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 eb9dd35a85d..e9297abc9ce 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 5338cba9df8..918ef33120e 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 09b90dbf46f..3dcbf5990a8 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 b5663bb5699..170f891909c 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 5cf0e3d202a..72020ccab60 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 4db6da50706..b050655fa6f 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 e66fe74df53..f75c4ddb31d 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 ca4ea948e7a..7b67209896b 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 14c9ee3228c..7f6e5669a74 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 6d70b923f27..d7ac791af14 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 193b2327e2e..5d2d46caf68 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 2ac8f7394d3..9fe1a9ababc 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 70df416a466..9545b57c904 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 5ef54075a5b..02d7ee01ad3 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 f140eb49f08..66daf82a6ec 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 9ce6b45cb4d..42c1c384c5e 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 3fe18984206..08c6e4bb308 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 df87df67c5f..c2c160c3902 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; };