chore: code refactor and build fix (#6285)

* chore: code refactor and build fix

* chore: code refactor

* chore: code refactor
This commit is contained in:
Anmol Singh Bhatia 2024-12-27 18:18:45 +05:30 committed by GitHub
parent 3c6bbaef3c
commit 211d5e1cd0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 292 additions and 101 deletions

View file

@ -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<void>;
isEpic?: boolean;
};
export const DeleteIssueModal: React.FC<Props> = (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> = (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> = (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"} `}
<span className="break-words font-medium text-custom-text-100">
{projectDetails?.identifier}-{issue?.sequence_id}
</span>
{""}? 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.`}
</>
}
/>

View file

@ -123,6 +123,7 @@ const HeaderFilters = observer((props: Props) => {
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
isEpic={storeType === EIssuesStoreType.EPIC}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -134,6 +135,7 @@ const HeaderFilters = observer((props: Props) => {
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
isEpic={storeType === EIssuesStoreType.EPIC}
/>
</FiltersDropdown>
{canUserCreateIssue ? (

View file

@ -53,11 +53,10 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
},
});
// store hooks
const { toggleCreateIssueModal, toggleDeleteIssueModal } = useIssueDetail();
const {
subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers },
toggleCreateIssueModal,
toggleDeleteIssueModal,
} = useIssueDetail();
} = useIssueDetail(issueServiceType);
// helpers
const subIssueOperations = useSubIssueOperations(issueServiceType);

View file

@ -38,7 +38,7 @@ export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
return (
<CollapsibleButton
isOpen={isOpen}
title="Sub-issues"
title={`${issueServiceType === EIssueServiceType.EPICS ? "Issues" : "Sub-issues"}`}
indicatorElement={
<div className="flex items-center gap-1.5 text-custom-text-300 text-sm">
<CircularProgressIndicator size={18} percentage={percentage} strokeWidth={3} />

View file

@ -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<TActivitySortRoot> = memo((props) => (
<div
className={cn(getButtonStyling("neutral-primary", "sm"), "px-2 text-custom-text-300 cursor-pointer")}
className={cn(
getButtonStyling("neutral-primary", "sm"),
"px-2 text-custom-text-300 cursor-pointer",
props.className
)}
onClick={() => {
props.toggleSort();
}}
>
{props.sortOrder === "asc" ? (
<ArrowUpWideNarrow className="size-4 " />
<ArrowUpWideNarrow className={cn("size-4", props.iconClassName)} />
) : (
<ArrowDownWideNarrow className="size-4 " />
<ArrowDownWideNarrow className={cn("size-4", props.iconClassName)} />
)}
</div>
));

View file

@ -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 {

View file

@ -87,6 +87,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
}}
quickAddCallback={quickAddCallback}
addIssuesToView={addIssuesToView}
isEpic={isEpic}
/>
</div>
)}

View file

@ -25,6 +25,7 @@ type Props = {
ignoreGroupedFilters?: Partial<TIssueGroupByOptions>[];
cycleViewDisabled?: boolean;
moduleViewDisabled?: boolean;
isEpic?: boolean;
};
export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
@ -37,6 +38,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
ignoreGroupedFilters = [],
cycleViewDisabled = false,
moduleViewDisabled = false,
isEpic = false,
} = props;
const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) =>
@ -61,6 +63,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
handleUpdate={handleDisplayPropertiesUpdate}
cycleViewDisabled={cycleViewDisabled}
moduleViewDisabled={moduleViewDisabled}
isEpic={isEpic}
/>
</div>
)}

View file

@ -16,6 +16,7 @@ type Props = {
handleUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
cycleViewDisabled?: boolean;
moduleViewDisabled?: boolean;
isEpic?: boolean;
};
export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
@ -25,6 +26,7 @@ export const FilterDisplayProperties: React.FC<Props> = 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<Props> = 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 (

View file

@ -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<Props> = 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<Props> = observer((props) => {
return (
<>
<FilterHeader
title="Issue Grouping"
title={`${isEpic ? "Epic" : "Issue"} Grouping`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
@ -34,7 +35,7 @@ export const FilterIssueGrouping: React.FC<Props> = 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}
/>
))}

View file

@ -42,6 +42,7 @@ type Props = {
states?: IState[] | undefined;
cycleViewDisabled?: boolean;
moduleViewDisabled?: boolean;
isEpic?: boolean;
};
export const FilterSelection: React.FC<Props> = observer((props) => {
@ -56,6 +57,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
states,
cycleViewDisabled = false,
moduleViewDisabled = false,
isEpic = false,
} = props;
// hooks
const { isMobile } = usePlatformOS();
@ -234,6 +236,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
type: val,
})
}
isEpic={isEpic}
/>
</div>
)}

View file

@ -111,6 +111,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
target_date: renderFormattedPayloadDate(targetDate),
}}
quickAddCallback={quickAddIssue}
isEpic={isEpic}
/>
) : undefined;
@ -120,8 +121,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
<div className="h-full w-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
title={isEpic ? "Epics" : "Issues"}
loaderTitle={isEpic ? "Epics" : "Issues"}
blockIds={issuesIds}
blockUpdateHandler={updateIssueBlockStructure}
blockToRender={(data: TIssue) => <IssueGanttBlock issueId={data.id} isEpic={isEpic} />}

View file

@ -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 <span className="font-semibold">{readableOrderBy}</span>.
</span>
)}
<span>Drop here to move the issue.</span>
<span>{`Drop here to move the ${isEpic ? "epic" : "issue"}.`}</span>
</>
)}
</div>

View file

@ -284,6 +284,7 @@ export const ListGroup = observer((props: Props) => {
dropErrorMessage={group.dropErrorMessage}
orderBy={orderBy}
isDraggingOverColumn={isDraggingOverColumn}
isEpic={isEpic}
/>
{groupIssueIds && (
<IssueBlocksList
@ -312,6 +313,7 @@ export const ListGroup = observer((props: Props) => {
prePopulatedData={prePopulateQuickAddData(group_by, group.id)}
containerClassName="border-b border-t border-custom-border-200 bg-custom-background-100 "
quickAddCallback={quickAddCallback}
isEpic={isEpic}
/>
</div>
)}

View file

@ -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<IIssueProperties> = 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<IIssueProperties> = observer((props) => {
const maxDate = getDate(issue.target_date);
maxDate?.setDate(maxDate.getDate());
const handleEventPropagation = (e: React.MouseEvent) => {
const handleEventPropagation = (e: SyntheticEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
};
@ -275,7 +275,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
{/* basic properties */}
{/* state */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="state">
<div className="h-5" onClick={handleEventPropagation}>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<StateDropdown
buttonContainerClassName="truncate max-w-40"
value={issue.state_id}
@ -291,7 +291,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
{/* priority */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="priority">
<div className="h-5" onClick={handleEventPropagation}>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<PriorityDropdown
value={issue?.priority}
onChange={handlePriority}
@ -306,7 +306,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
{/* start date */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="start_date">
<div className="h-5" onClick={handleEventPropagation}>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<DateDropdown
value={issue.start_date ?? null}
onChange={handleStartDate}
@ -324,7 +324,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
{/* target/due date */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
<div className="h-5" onClick={handleEventPropagation}>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<DateDropdown
value={issue?.target_date ?? null}
onChange={handleTargetDate}
@ -344,7 +344,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
{/* assignee */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="assignee">
<div className="h-5" onClick={handleEventPropagation}>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<MemberDropdown
projectId={issue?.project_id}
value={issue?.assignee_ids}
@ -362,52 +362,54 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
</div>
</WithDisplayPropertiesHOC>
{!isEpic && (
<>
{/* modules */}
{projectDetails?.module_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
<div className="h-5" onClick={handleEventPropagation}>
<ModuleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
value={issue?.module_ids ?? []}
onChange={handleModule}
disabled={isReadOnly}
renderByDefault={isMobile}
multiple
buttonVariant="border-with-text"
showCount
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
)}
<>
{!isEpic && (
<>
{/* modules */}
{projectDetails?.module_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<ModuleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
value={issue?.module_ids ?? []}
onChange={handleModule}
disabled={isReadOnly}
renderByDefault={isMobile}
multiple
buttonVariant="border-with-text"
showCount
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
)}
{/* cycles */}
{projectDetails?.cycle_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
<div className="h-5" onClick={handleEventPropagation}>
<CycleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
value={issue?.cycle_id}
onChange={handleCycle}
disabled={isReadOnly}
buttonVariant="border-with-text"
renderByDefault={isMobile}
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
)}
</>
)}
{/* cycles */}
{projectDetails?.cycle_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<CycleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
value={issue?.cycle_id}
onChange={handleCycle}
disabled={isReadOnly}
buttonVariant="border-with-text"
renderByDefault={isMobile}
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
)}
</>
)}
</>
{/* estimates */}
{projectId && areEstimateEnabledByProjectId(projectId?.toString()) && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
<div className="h-5" onClick={handleEventPropagation}>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<EstimateDropdown
value={issue.estimate_point ?? undefined}
onChange={handleEstimate}
@ -429,12 +431,13 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!subIssueCount}
>
<Tooltip
tooltipHeading="Sub-issues"
tooltipHeading={isEpic ? "Issues" : "Sub-issues"}
tooltipContent={`${subIssueCount}`}
isMobile={isMobile}
renderByDefault={false}
>
<div
onFocus={handleEventPropagation}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
@ -467,6 +470,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
>
<div
className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1"
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
@ -489,6 +493,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
>
<div
className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1"
onFocus={handleEventPropagation}
onClick={handleEventPropagation}
>
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />

View file

@ -5,7 +5,7 @@ import { Row } from "@plane/ui";
import { TQuickAddIssueButton } from "../root";
export const GanttQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((props) => {
const { onClick } = props;
const { onClick, isEpic = false } = props;
return (
<button
@ -15,7 +15,7 @@ export const GanttQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((prop
>
<Row className="flex py-2 gap-2">
<PlusIcon className="h-3.5 w-3.5 stroke-2 my-auto" />
<span className="text-sm font-medium">New Issue</span>
<span className="text-sm font-medium">{`New ${isEpic ? "Epic" : "Issue"}`}</span>
</Row>
</button>
);

View file

@ -4,7 +4,7 @@ import { PlusIcon } from "lucide-react";
import { TQuickAddIssueButton } from "../root";
export const KanbanQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((props) => {
const { onClick } = props;
const { onClick, isEpic = false } = props;
return (
<div
@ -12,7 +12,7 @@ export const KanbanQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((pro
onClick={onClick}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium">New Issue</span>
<span className="text-sm font-medium">{`New ${isEpic ? "Epic" : "Issue"}`}</span>
</div>
);
});

View file

@ -5,7 +5,7 @@ import { Row } from "@plane/ui";
import { TQuickAddIssueButton } from "../root";
export const ListQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((props) => {
const { onClick } = props;
const { onClick, isEpic = false } = props;
return (
<Row
@ -13,7 +13,7 @@ export const ListQuickAddIssueButton: FC<TQuickAddIssueButton> = observer((props
onClick={onClick}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium">New Issue</span>
<span className="text-sm font-medium">{`New ${isEpic ? "Epic" : "Issue"}`}</span>
</Row>
);
});

View file

@ -4,7 +4,7 @@ import { PlusIcon } from "lucide-react";
import { TQuickAddIssueButton } from "../root";
export const SpreadsheetAddIssueButton: FC<TQuickAddIssueButton> = observer((props) => {
const { onClick } = props;
const { onClick, isEpic = false } = props;
return (
<div className="flex items-center">
@ -14,7 +14,7 @@ export const SpreadsheetAddIssueButton: FC<TQuickAddIssueButton> = observer((pro
onClick={onClick}
>
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
<span className="text-sm font-medium">New Issue</span>
<span className="text-sm font-medium">{`New ${isEpic ? "Epic" : "Issue"}`}</span>
</button>
</div>
);

View file

@ -15,10 +15,11 @@ interface Props {
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => 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) => {
<Row className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
<div className="flex items-center gap-1.5">
{<propertyDetails.icon className="h-4 w-4 text-custom-text-400" />}
{propertyDetails.title}
{propertyDetails.title === "Sub-issue" && isEpic ? "Issues" : propertyDetails.title}
</div>
<div className="ml-3 flex">
{activeSortingProperty === property && (

View file

@ -18,16 +18,19 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = 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 (
<Row
onClick={subIssueCount ? redirectToIssueDetail : () => {}}
@ -38,7 +41,7 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = observer((props: Props
}
)}
>
{subIssueCount} {subIssueCount === 1 ? "sub-issue" : "sub-issues"}
{label}
</Row>
);
});

View file

@ -12,9 +12,17 @@ interface Props {
isEstimateEnabled: boolean;
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => 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<HTMLTableCellElement | null>(null);
@ -39,6 +47,7 @@ export const SpreadsheetHeaderColumn = observer((props: Props) => {
onClose={() => {
tableHeaderCellRef?.current?.focus();
}}
isEpic={isEpic}
/>
</th>
</WithDisplayPropertiesHOC>

View file

@ -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) => {
/>
</div>
)}
<span className="flex h-full w-full flex-grow items-center py-2.5">Issues</span>
<span className="flex h-full w-full flex-grow items-center py-2.5">{`${isEpic ? "Epics" : "Issues"}`}</span>
</Row>
</th>
@ -74,6 +76,7 @@ export const SpreadsheetHeader = observer((props: Props) => {
displayFilters={displayFilters}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
isEstimateEnabled={isEstimateEnabled}
isEpic={isEpic}
/>
))}
</tr>

View file

@ -112,6 +112,7 @@ export const SpreadsheetTable = observer((props: Props) => {
isEstimateEnabled={isEstimateEnabled}
spreadsheetColumnsList={spreadsheetColumnsList}
selectionHelpers={selectionHelpers}
isEpic={isEpic}
/>
<tbody>
{issueIds.map((id) => (

View file

@ -117,6 +117,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
layout={EIssueLayoutTypes.SPREADSHEET}
QuickAddButton={SpreadsheetAddIssueButton}
quickAddCallback={quickAddCallback}
isEpic={isEpic}
/>
)}
</div>

View file

@ -69,7 +69,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
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,

View file

@ -60,6 +60,7 @@ export const IssueList: FC<IIssueList> = observer((props) => {
disabled={disabled}
handleIssueCrudState={handleIssueCrudState}
subIssueOperations={subIssueOperations}
issueServiceType={issueServiceType}
/>
</Fragment>
))}

View file

@ -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 everyones 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, Planes 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 projects context. To make short work of any doc, invoke Galileo, Planes AI, with a shortcut or the click of a button.",
path: "/empty-state/onboarding/pages",
},
} as const;
export const EMPTY_STATE_DETAILS: Record<EmptyStateType, EmptyStateDetails> = emptyStateDetails;

View file

@ -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" },
];

View file

@ -43,7 +43,7 @@ export class IssueService extends APIService {
): Promise<TIssuesResponse> {
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<TIssuesResponse> {
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<any> {
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<any> {
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<any> {
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<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-subscribe-issues/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}

View file

@ -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

View file

@ -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;

View file

@ -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;
};