[WEB-3838]feat:sub work items sorting (#6967)
* refactor: sub-work items components, hooks and types * feat: added orderby and display properties toggle for sub work items * fix: build errors * chore: removed issue type from filters * chore: added null check * fix: added null check --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
55340f9f48
commit
14dc6a56bc
26 changed files with 843 additions and 995 deletions
|
|
@ -23,6 +23,7 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
|||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from collections import defaultdict
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
|
||||
class SubIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
|
|
@ -102,6 +103,15 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
# Ordering
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
group_by = request.GET.get("group_by", False)
|
||||
|
||||
if order_by_param:
|
||||
sub_issues, order_by_param = order_issue_queryset(
|
||||
sub_issues, order_by_param
|
||||
)
|
||||
|
||||
# create's a dict with state group name with their respective issue id's
|
||||
result = defaultdict(list)
|
||||
for sub_issue in sub_issues:
|
||||
|
|
@ -138,6 +148,26 @@ class SubIssuesEndpoint(BaseAPIView):
|
|||
sub_issues = user_timezone_converter(
|
||||
sub_issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
# Grouping
|
||||
if group_by:
|
||||
result_dict = defaultdict(list)
|
||||
|
||||
for issue in sub_issues:
|
||||
if group_by == "assignees__ids":
|
||||
if issue["assignee_ids"]:
|
||||
assignee_ids = issue["assignee_ids"]
|
||||
for assignee_id in assignee_ids:
|
||||
result_dict[str(assignee_id)].append(issue)
|
||||
elif issue["assignee_ids"] == []:
|
||||
result_dict["None"].append(issue)
|
||||
|
||||
elif group_by:
|
||||
result_dict[str(issue[group_by])].append(issue)
|
||||
|
||||
return Response(
|
||||
{"sub_issues": result_dict, "state_distribution": result},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
return Response(
|
||||
{"sub_issues": sub_issues, "state_distribution": result},
|
||||
status=status.HTTP_200_OK,
|
||||
|
|
|
|||
|
|
@ -165,6 +165,15 @@ export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
|
|||
"issue_type",
|
||||
];
|
||||
|
||||
export const SUB_ISSUES_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = [
|
||||
"key",
|
||||
"assignee",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"priority",
|
||||
"state",
|
||||
];
|
||||
|
||||
export const ISSUE_DISPLAY_PROPERTIES: {
|
||||
key: keyof IIssueDisplayProperties;
|
||||
titleTranslationKey: string;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
TIssueFilterPriorityObject,
|
||||
ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
EIssuesStoreType,
|
||||
SUB_ISSUES_DISPLAY_PROPERTIES_KEYS,
|
||||
} from "./common";
|
||||
|
||||
import { TIssueLayout } from "./layout";
|
||||
|
|
@ -96,23 +97,11 @@ export type TIssueFiltersToDisplayByPageType = {
|
|||
export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
||||
profile_issues: {
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: ["state_detail.group", "priority", "project", "labels", null],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -121,23 +110,11 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
},
|
||||
},
|
||||
kanban: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
],
|
||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: ["state_detail.group", "priority", "project", "labels"],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -173,13 +150,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
"created_by",
|
||||
null,
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -190,34 +161,11 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
},
|
||||
draft_issues: {
|
||||
list: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date", "issue_type"],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state_detail.group",
|
||||
"cycle",
|
||||
"module",
|
||||
"priority",
|
||||
"project",
|
||||
"labels",
|
||||
null,
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -226,33 +174,11 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
},
|
||||
},
|
||||
kanban: {
|
||||
filters: [
|
||||
"priority",
|
||||
"state_group",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"issue_type",
|
||||
],
|
||||
filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date", "issue_type"],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state_detail.group",
|
||||
"cycle",
|
||||
"module",
|
||||
"priority",
|
||||
"project",
|
||||
"labels",
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -323,24 +249,8 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state",
|
||||
"priority",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
null,
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
"target_date",
|
||||
],
|
||||
group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -364,33 +274,9 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
group_by: [
|
||||
"state",
|
||||
"priority",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
],
|
||||
sub_group_by: [
|
||||
"state",
|
||||
"priority",
|
||||
"cycle",
|
||||
"module",
|
||||
"labels",
|
||||
"assignees",
|
||||
"created_by",
|
||||
null,
|
||||
],
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
"target_date",
|
||||
],
|
||||
group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by"],
|
||||
sub_group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -436,13 +322,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
],
|
||||
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
|
||||
display_filters: {
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -466,13 +346,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
],
|
||||
display_properties: ["key", "issue_type"],
|
||||
display_filters: {
|
||||
order_by: [
|
||||
"sort_order",
|
||||
"-created_at",
|
||||
"-updated_at",
|
||||
"start_date",
|
||||
"-priority",
|
||||
],
|
||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
|
||||
type: [null, "active", "backlog"],
|
||||
},
|
||||
extra_options: {
|
||||
|
|
@ -481,6 +355,19 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
sub_work_items: {
|
||||
list: {
|
||||
display_properties: SUB_ISSUES_DISPLAY_PROPERTIES_KEYS,
|
||||
filters: [],
|
||||
display_filters: {
|
||||
order_by: ["-created_at", "-updated_at", "start_date", "-priority"],
|
||||
},
|
||||
extra_options: {
|
||||
access: true,
|
||||
values: ["sub_issue"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<
|
||||
|
|
|
|||
21
packages/types/src/issues/issue_sub_issues.d.ts
vendored
21
packages/types/src/issues/issue_sub_issues.d.ts
vendored
|
|
@ -10,9 +10,11 @@ export type TSubIssuesStateDistribution = {
|
|||
|
||||
export type TIssueSubIssues = {
|
||||
state_distribution: TSubIssuesStateDistribution;
|
||||
sub_issues: TIssue[];
|
||||
sub_issues: TSubIssueResponse;
|
||||
};
|
||||
|
||||
export type TSubIssueResponse = TIssue[] | { [key: string]: TIssue[] };
|
||||
|
||||
export type TIssueSubIssuesStateDistributionMap = {
|
||||
[issue_id: string]: TSubIssuesStateDistribution;
|
||||
};
|
||||
|
|
@ -20,3 +22,20 @@ export type TIssueSubIssuesStateDistributionMap = {
|
|||
export type TIssueSubIssuesIdMap = {
|
||||
[issue_id: string]: string[];
|
||||
};
|
||||
|
||||
export type TSubIssueOperations = {
|
||||
copyLink: (path: string) => void;
|
||||
fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise<void>;
|
||||
addSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => Promise<void>;
|
||||
updateSubIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue?: Partial<TIssue>,
|
||||
fromModal?: boolean
|
||||
) => Promise<void>;
|
||||
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
|
|
|
|||
14
packages/ui/src/icons/display-properties.tsx
Normal file
14
packages/ui/src/icons/display-properties.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const DisplayPropertiesIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
|
||||
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" {...rest} className={className}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.93934 0.93934C3.22064 0.658035 3.60218 0.5 4 0.5C4.39782 0.5 4.77936 0.658035 5.06066 0.93934C5.22036 1.09904 5.34033 1.29105 5.41421 1.5H11.5C11.7761 1.5 12 1.72386 12 2C12 2.27614 11.7761 2.5 11.5 2.5H5.41421C5.34033 2.70895 5.22036 2.90096 5.06066 3.06066C4.77936 3.34197 4.39782 3.5 4 3.5C3.60218 3.5 3.22064 3.34197 2.93934 3.06066C2.77964 2.90096 2.65967 2.70895 2.58579 2.5H0.5C0.223858 2.5 0 2.27614 0 2C0 1.72386 0.223858 1.5 0.5 1.5H2.58579C2.65967 1.29105 2.77964 1.09904 2.93934 0.93934ZM4 1.5C3.86739 1.5 3.74021 1.55268 3.64645 1.64645C3.55268 1.74021 3.5 1.86739 3.5 2C3.5 2.13261 3.55268 2.25979 3.64645 2.35355C3.74021 2.44732 3.86739 2.5 4 2.5C4.13261 2.5 4.25979 2.44732 4.35355 2.35355C4.44732 2.25979 4.5 2.13261 4.5 2C4.5 1.86739 4.44732 1.74021 4.35355 1.64645C4.25979 1.55268 4.13261 1.5 4 1.5ZM6.93934 4.93934C7.22064 4.65804 7.60218 4.5 8 4.5C8.39783 4.5 8.77936 4.65804 9.06066 4.93934C9.22036 5.09904 9.34033 5.29105 9.41422 5.5H11.5C11.7761 5.5 12 5.72386 12 6C12 6.27614 11.7761 6.5 11.5 6.5H9.41422C9.34033 6.70895 9.22036 6.90096 9.06066 7.06066C8.77936 7.34196 8.39783 7.5 8 7.5C7.60218 7.5 7.22064 7.34196 6.93934 7.06066C6.77964 6.90096 6.65967 6.70895 6.58579 6.5H0.5C0.223858 6.5 0 6.27614 0 6C0 5.72386 0.223858 5.5 0.5 5.5H6.58579C6.65967 5.29105 6.77964 5.09904 6.93934 4.93934ZM8 5.5C7.86739 5.5 7.74021 5.55268 7.64645 5.64645C7.55268 5.74021 7.5 5.86739 7.5 6C7.5 6.13261 7.55268 6.25979 7.64645 6.35355C7.74021 6.44732 7.86739 6.5 8 6.5C8.13261 6.5 8.25978 6.44732 8.35355 6.35355C8.44732 6.25979 8.5 6.13261 8.5 6C8.5 5.86739 8.44732 5.74022 8.35355 5.64645C8.25978 5.55268 8.13261 5.5 8 5.5ZM2.93934 8.93934C3.22064 8.65804 3.60217 8.5 4 8.5C4.39783 8.5 4.77936 8.65804 5.06066 8.93934C5.22036 9.09904 5.34033 9.29105 5.41421 9.5H11.5C11.7761 9.5 12 9.72386 12 10C12 10.2761 11.7761 10.5 11.5 10.5H5.41421C5.34033 10.709 5.22036 10.901 5.06066 11.0607C4.77936 11.342 4.39783 11.5 4 11.5C3.60217 11.5 3.22064 11.342 2.93934 11.0607C2.77964 10.901 2.65967 10.709 2.58579 10.5H0.5C0.223858 10.5 0 10.2761 0 10C0 9.72386 0.223858 9.5 0.5 9.5H2.58579C2.65967 9.29105 2.77964 9.09904 2.93934 8.93934ZM4 9.5C3.86739 9.5 3.74022 9.55268 3.64645 9.64645C3.55268 9.74022 3.5 9.86739 3.5 10C3.5 10.1326 3.55268 10.2598 3.64645 10.3536C3.74022 10.4473 3.86739 10.5 4 10.5C4.13261 10.5 4.25979 10.4473 4.35355 10.3536C4.44732 10.2598 4.5 10.1326 4.5 10C4.5 9.86739 4.44732 9.74022 4.35355 9.64645C4.25979 9.55268 4.13261 9.5 4 9.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -51,3 +51,4 @@ export * from "./multiple-sticky";
|
|||
export * from "./sticky-note-icon";
|
||||
export * from "./bar-icon";
|
||||
export * from "./tree-map-icon";
|
||||
export * from "./display-properties";
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
"use client";
|
||||
import { useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueServiceType, ISSUE_DELETED, ISSUE_UPDATED } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
// helper
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail } from "@/hooks/store";
|
||||
|
||||
export type TRelationIssueOperations = {
|
||||
copyText: (text: string) => void;
|
||||
copyLink: (path: string) => void;
|
||||
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
|
|
@ -29,9 +28,8 @@ export const useRelationOperations = (
|
|||
|
||||
const issueOperations: TRelationIssueOperations = useMemo(
|
||||
() => ({
|
||||
copyText: (text: string) => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}${text}`).then(() => {
|
||||
copyLink: (path) => {
|
||||
copyUrlToClipboard(path).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("common.link_copied"),
|
||||
|
|
@ -39,7 +37,7 @@ export const useRelationOperations = (
|
|||
});
|
||||
});
|
||||
},
|
||||
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||
update: async (workspaceSlug, projectId, issueId, data) => {
|
||||
try {
|
||||
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
captureIssueEvent({
|
||||
|
|
@ -56,7 +54,7 @@ export const useRelationOperations = (
|
|||
type: TOAST_TYPE.SUCCESS,
|
||||
message: t("entity.update.success", { entity: entityName }),
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { state: "FAILED", element: "Issue detail page" },
|
||||
|
|
@ -73,7 +71,7 @@ export const useRelationOperations = (
|
|||
});
|
||||
}
|
||||
},
|
||||
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
remove: async (workspaceSlug, projectId, issueId) => {
|
||||
try {
|
||||
return removeIssue(workspaceSlug, projectId, issueId).then(() => {
|
||||
captureIssueEvent({
|
||||
|
|
@ -82,7 +80,7 @@ export const useRelationOperations = (
|
|||
path: pathname,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_DELETED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
|
|
@ -91,7 +89,7 @@ export const useRelationOperations = (
|
|||
}
|
||||
},
|
||||
}),
|
||||
[pathname, removeIssue, updateIssue]
|
||||
[captureIssueEvent, entityName, pathname, removeIssue, t, updateIssue]
|
||||
);
|
||||
|
||||
return issueOperations;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import { TIssue, TIssueServiceType } from "@plane/types";
|
|||
// components
|
||||
import { DeleteIssueModal } from "@/components/issues/delete-issue-modal";
|
||||
import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
|
||||
import { IssueList } from "@/components/issues/sub-issues/issues-list";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// helper
|
||||
// local imports
|
||||
import { useSubIssueOperations } from "./helper";
|
||||
import { SubIssuesListRoot } from "./issues-list/root";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -53,8 +53,9 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
|
|||
},
|
||||
});
|
||||
// store hooks
|
||||
const { toggleCreateIssueModal, toggleDeleteIssueModal } = useIssueDetail();
|
||||
const {
|
||||
toggleCreateIssueModal,
|
||||
toggleDeleteIssueModal,
|
||||
subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
|
|
@ -63,20 +64,19 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
|
|||
const subIssueHelpers = subIssueHelpersByIssueId(`${parentIssueId}_root`);
|
||||
|
||||
// handler
|
||||
const handleIssueCrudState = (
|
||||
key: "create" | "existing" | "update" | "delete",
|
||||
_parentIssueId: string | null,
|
||||
issue: TIssue | null = null
|
||||
) => {
|
||||
setIssueCrudState({
|
||||
...issueCrudState,
|
||||
[key]: {
|
||||
toggle: !issueCrudState[key].toggle,
|
||||
parentIssueId: _parentIssueId,
|
||||
issue: issue,
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleIssueCrudState = useCallback(
|
||||
(key: "create" | "existing" | "update" | "delete", _parentIssueId: string | null, issue: TIssue | null = null) => {
|
||||
setIssueCrudState({
|
||||
...issueCrudState,
|
||||
[key]: {
|
||||
toggle: !issueCrudState[key].toggle,
|
||||
parentIssueId: _parentIssueId,
|
||||
issue,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueCrudState]
|
||||
);
|
||||
|
||||
const handleFetchSubIssues = useCallback(async () => {
|
||||
if (!subIssueHelpers.issue_visibility.includes(parentIssueId)) {
|
||||
|
|
@ -116,7 +116,7 @@ export const SubIssuesCollapsibleContent: FC<Props> = observer((props) => {
|
|||
return (
|
||||
<>
|
||||
{subIssueHelpers.issue_visibility.includes(parentIssueId) && (
|
||||
<IssueList
|
||||
<SubIssuesListRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import { FC } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { observer } from "mobx-react";
|
||||
import { IIssueDisplayFilterOptions, ILayoutDisplayFiltersOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
import { DisplayPropertiesIcon } from "@plane/ui";
|
||||
import { FilterDisplayProperties, FilterOrderBy, FiltersDropdown } from "@/components/issues";
|
||||
|
||||
type TSubIssueDisplayFiltersProps = {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayFilters: IIssueDisplayFilterOptions;
|
||||
handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => void;
|
||||
handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
|
||||
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
|
||||
export const SubIssueDisplayFilters: FC<TSubIssueDisplayFiltersProps> = observer((props) => {
|
||||
const {
|
||||
isEpic = false,
|
||||
displayProperties,
|
||||
layoutDisplayFiltersOptions,
|
||||
handleDisplayPropertiesUpdate,
|
||||
handleDisplayFiltersUpdate,
|
||||
displayFilters,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{layoutDisplayFiltersOptions?.display_filters && layoutDisplayFiltersOptions?.display_properties.length > 0 && (
|
||||
<FiltersDropdown
|
||||
placement="bottom-end"
|
||||
menuButton={<DisplayPropertiesIcon className="h-3.5 w-3.5 text-custom-text-100" />}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
className="vertical-scrollbar scrollbar-sm relative h-full w-full divide-y divide-custom-border-200 overflow-hidden overflow-y-auto px-2.5 max-h-[25rem] text-left"
|
||||
>
|
||||
{/* display properties */}
|
||||
<div className="py-2">
|
||||
<FilterDisplayProperties
|
||||
displayProperties={displayProperties}
|
||||
displayPropertiesToRender={layoutDisplayFiltersOptions.display_properties}
|
||||
handleUpdate={handleDisplayPropertiesUpdate}
|
||||
isEpic={isEpic}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* order by */}
|
||||
{!isEmpty(layoutDisplayFiltersOptions?.display_filters?.order_by) && (
|
||||
<div className="py-2">
|
||||
<FilterOrderBy
|
||||
selectedOrderBy={displayFilters?.order_by}
|
||||
handleUpdate={(val) =>
|
||||
handleDisplayFiltersUpdate({
|
||||
order_by: val,
|
||||
})
|
||||
}
|
||||
orderByOptions={layoutDisplayFiltersOptions?.display_filters.order_by ?? []}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,29 +1,23 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
import { TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helper
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail, useProjectState } from "@/hooks/store";
|
||||
// plane-web
|
||||
// plane web helpers
|
||||
import { updateEpicAnalytics } from "@/plane-web/helpers/epic-analytics";
|
||||
// type
|
||||
import { TSubIssueOperations } from "../../sub-issues";
|
||||
|
||||
export type TRelationIssueOperations = {
|
||||
copyText: (text: string) => void;
|
||||
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSubIssueOperations => {
|
||||
// router
|
||||
const { epicId: epicIdParam } = useParams();
|
||||
const pathname = usePathname();
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const {
|
||||
|
|
@ -46,9 +40,8 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
|||
|
||||
const subIssueOperations: TSubIssueOperations = useMemo(
|
||||
() => ({
|
||||
copyText: (text: string) => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${text}`).then(() => {
|
||||
copyLink: (path) => {
|
||||
copyUrlToClipboard(`/${path}`).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("common.link_copied"),
|
||||
|
|
@ -61,7 +54,7 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
|||
});
|
||||
});
|
||||
},
|
||||
fetchSubIssues: async (workspaceSlug: string, projectId: string, parentIssueId: string) => {
|
||||
fetchSubIssues: async (workspaceSlug, projectId, parentIssueId) => {
|
||||
try {
|
||||
await fetchSubIssues(workspaceSlug, projectId, parentIssueId);
|
||||
} catch {
|
||||
|
|
@ -77,7 +70,7 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
|||
});
|
||||
}
|
||||
},
|
||||
addSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => {
|
||||
addSubIssue: async (workspaceSlug, projectId, parentIssueId, issueIds) => {
|
||||
try {
|
||||
await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds);
|
||||
setToast({
|
||||
|
|
@ -94,7 +87,6 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
|||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("toast.error"),
|
||||
// message: `Error adding ${issueServiceType === EIssueServiceType.ISSUES ? "sub-issues" : "issues"}`,
|
||||
message: t("entity.add.failed", {
|
||||
entity:
|
||||
issueServiceType === EIssueServiceType.ISSUES
|
||||
|
|
@ -105,13 +97,13 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
|||
}
|
||||
},
|
||||
updateSubIssue: async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue: Partial<TIssue> = {},
|
||||
fromModal: boolean = false
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
issueId,
|
||||
issueData,
|
||||
oldIssue = {},
|
||||
fromModal = false
|
||||
) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
|
|
@ -172,7 +164,7 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
|||
});
|
||||
}
|
||||
},
|
||||
removeSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
removeSubIssue: async (workspaceSlug, projectId, parentIssueId, issueId) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
|
|
@ -218,7 +210,7 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
|||
});
|
||||
}
|
||||
},
|
||||
deleteSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
deleteSubIssue: async (workspaceSlug, projectId, parentIssueId, issueId) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
return deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId).then(() => {
|
||||
|
|
@ -244,20 +236,20 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
|
|||
},
|
||||
}),
|
||||
[
|
||||
fetchSubIssues,
|
||||
captureIssueEvent,
|
||||
createSubIssues,
|
||||
epicId,
|
||||
updateSubIssue,
|
||||
removeSubIssue,
|
||||
deleteSubIssue,
|
||||
setSubIssueHelpers,
|
||||
epicId,
|
||||
fetchSubIssues,
|
||||
getIssueById,
|
||||
getStateById,
|
||||
updateAnalytics,
|
||||
captureIssueEvent,
|
||||
pathname,
|
||||
t,
|
||||
issueServiceType,
|
||||
pathname,
|
||||
removeSubIssue,
|
||||
setSubIssueHelpers,
|
||||
t,
|
||||
updateAnalytics,
|
||||
updateSubIssue,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -2,3 +2,4 @@ export * from "./content";
|
|||
export * from "./title";
|
||||
export * from "./root";
|
||||
export * from "./quick-action-button";
|
||||
export * from "./display-filters";
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
// ui
|
||||
import { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
||||
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { useSubIssueOperations } from "@/components/issues/issue-detail-widgets/sub-issues/helper";
|
||||
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/properties/with-display-properties-HOC";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { generateWorkItemLink } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
|
|
@ -18,15 +19,10 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
|||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
import { useSubIssueOperations } from "../issue-detail-widgets/sub-issues/helper";
|
||||
import { IssueList } from "./issues-list";
|
||||
import { IssueProperty } from "./properties";
|
||||
// ui
|
||||
// types
|
||||
import { TSubIssueOperations } from "./root";
|
||||
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||
import { SubIssuesListItemProperties } from "./properties";
|
||||
import { SubIssuesListRoot } from "./root";
|
||||
|
||||
export interface ISubIssues {
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
|
|
@ -41,9 +37,9 @@ export interface ISubIssues {
|
|||
subIssueOperations: TSubIssueOperations;
|
||||
issueId: string;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
}
|
||||
};
|
||||
|
||||
export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||
export const SubIssuesListItem: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
|
|
@ -59,6 +55,9 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||
const { t } = useTranslation();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
subIssues: {
|
||||
filters: { getSubIssueFilters },
|
||||
},
|
||||
} = useIssueDetail(issueServiceType);
|
||||
const {
|
||||
subIssues: { subIssueHelpersByIssueId, setSubIssueHelpers },
|
||||
|
|
@ -80,6 +79,10 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||
const subIssueHelpers = subIssueHelpersByIssueId(parentIssueId);
|
||||
const subIssueCount = issue?.sub_issues_count ?? 0;
|
||||
|
||||
// derived values
|
||||
const subIssueFilters = getSubIssueFilters(parentIssueId);
|
||||
const displayProperties = subIssueFilters.displayProperties ?? {};
|
||||
|
||||
//
|
||||
const handleIssuePeekOverview = (issue: TIssue) => handleRedirection(workspaceSlug, issue, isMobile);
|
||||
|
||||
|
|
@ -150,17 +153,19 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||
backgroundColor: currentIssueStateDetail?.color ?? "#737373",
|
||||
}}
|
||||
/>
|
||||
<div className="flex-shrink-0">
|
||||
{projectDetail && (
|
||||
<IssueIdentifier
|
||||
projectId={projectDetail.id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={projectDetail.identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties || {}} displayPropertyKey="key">
|
||||
<div className="flex-shrink-0">
|
||||
{projectDetail && (
|
||||
<IssueIdentifier
|
||||
projectId={projectDetail.id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={projectDetail.identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
|
||||
<span className="w-full truncate text-sm text-custom-text-100">{issue.name}</span>
|
||||
</Tooltip>
|
||||
|
|
@ -173,13 +178,14 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<IssueProperty
|
||||
<SubIssuesListItemProperties
|
||||
workspaceSlug={workspaceSlug}
|
||||
parentIssueId={parentIssueId}
|
||||
issueId={issueId}
|
||||
disabled={disabled}
|
||||
subIssueOperations={subIssueOperations}
|
||||
issueServiceType={issueServiceType}
|
||||
updateSubIssue={subIssueOperations.updateSubIssue}
|
||||
displayProperties={displayProperties}
|
||||
issue={issue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -205,7 +211,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
subIssueOperations.copyText(workItemLink);
|
||||
subIssueOperations.copyLink(workItemLink);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -258,7 +264,7 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
|||
issue.project_id &&
|
||||
subIssueCount > 0 &&
|
||||
!isCurrentIssueRoot && (
|
||||
<IssueList
|
||||
<SubIssuesListRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
parentIssueId={issue.id}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
// plane imports
|
||||
import { SyntheticEvent } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarClock } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||
// components
|
||||
import { PriorityDropdown, MemberDropdown, StateDropdown, DateDropdown } from "@/components/dropdowns";
|
||||
// hooks
|
||||
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/properties/with-display-properties-HOC";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
parentIssueId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
updateSubIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue?: Partial<TIssue>
|
||||
) => Promise<void>;
|
||||
displayProperties?: IIssueDisplayProperties;
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const SubIssuesListItemProperties: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, parentIssueId, issueId, disabled, updateSubIssue, displayProperties, issue } = props;
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEventPropagation = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
if (!displayProperties) return <></>;
|
||||
|
||||
const maxDate = getDate(issue.target_date);
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="start_date">
|
||||
<div className="h-5 flex-shrink-0" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
|
||||
<DateDropdown
|
||||
value={issue.start_date ?? null}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
updateSubIssue(
|
||||
workspaceSlug,
|
||||
issue.project_id,
|
||||
parentIssueId,
|
||||
issueId,
|
||||
{
|
||||
start_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
},
|
||||
{ ...issue }
|
||||
)
|
||||
}
|
||||
maxDate={maxDate}
|
||||
placeholder={t("common.order_by.start_date")}
|
||||
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
|
||||
optionsClassName="z-30"
|
||||
disabled={!disabled}
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
|
||||
<div className="h-5 flex-shrink-0" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
|
||||
<DateDropdown
|
||||
value={issue.target_date ?? null}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
updateSubIssue(
|
||||
workspaceSlug,
|
||||
issue.project_id,
|
||||
parentIssueId,
|
||||
issueId,
|
||||
{
|
||||
target_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
},
|
||||
{ ...issue }
|
||||
)
|
||||
}
|
||||
maxDate={maxDate}
|
||||
placeholder={t("common.order_by.due_date")}
|
||||
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
optionsClassName="z-30"
|
||||
disabled={!disabled}
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="state">
|
||||
<div className="h-5 flex-shrink-0">
|
||||
<StateDropdown
|
||||
value={issue.state_id}
|
||||
projectId={issue.project_id ?? undefined}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
updateSubIssue(
|
||||
workspaceSlug,
|
||||
issue.project_id,
|
||||
parentIssueId,
|
||||
issueId,
|
||||
{
|
||||
state_id: val,
|
||||
},
|
||||
{ ...issue }
|
||||
)
|
||||
}
|
||||
disabled={!disabled}
|
||||
buttonVariant="border-with-text"
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="priority">
|
||||
<div className="h-5 flex-shrink-0">
|
||||
<PriorityDropdown
|
||||
value={issue.priority}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, {
|
||||
priority: val,
|
||||
})
|
||||
}
|
||||
disabled={!disabled}
|
||||
buttonVariant="border-without-text"
|
||||
buttonClassName="border"
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="assignee">
|
||||
<div className="h-5 flex-shrink-0">
|
||||
<MemberDropdown
|
||||
value={issue.assignee_ids}
|
||||
projectId={issue.project_id ?? undefined}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, {
|
||||
assignee_ids: val,
|
||||
})
|
||||
}
|
||||
disabled={!disabled}
|
||||
multiple
|
||||
buttonVariant={(issue?.assignee_ids || []).length > 0 ? "transparent-without-text" : "border-without-text"}
|
||||
buttonClassName={(issue?.assignee_ids || []).length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { TIssue, TIssueServiceType, TSubIssueOperations } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// local imports
|
||||
import { SubIssuesListItem } from "./list-item";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
rootIssueId: string;
|
||||
spacingLeft: number;
|
||||
disabled: boolean;
|
||||
handleIssueCrudState: (
|
||||
key: "create" | "existing" | "update" | "delete",
|
||||
issueId: string,
|
||||
issue?: TIssue | null
|
||||
) => void;
|
||||
subIssueOperations: TSubIssueOperations;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
};
|
||||
|
||||
export const SubIssuesListRoot: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
rootIssueId,
|
||||
spacingLeft = 10,
|
||||
disabled,
|
||||
handleIssueCrudState,
|
||||
subIssueOperations,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
} = props;
|
||||
// store hooks
|
||||
const {
|
||||
subIssues: { subIssuesByIssueId },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
// derived values
|
||||
const subIssueIds = subIssuesByIssueId(parentIssueId);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{subIssueIds?.map((issueId) => (
|
||||
<SubIssuesListItem
|
||||
key={issueId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
rootIssueId={rootIssueId}
|
||||
issueId={issueId}
|
||||
spacingLeft={spacingLeft}
|
||||
disabled={disabled}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
subIssueOperations={subIssueOperations}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -21,7 +21,7 @@ export const SubIssuesCollapsible: FC<Props> = observer((props) => {
|
|||
const { workspaceSlug, projectId, issueId, disabled = false, issueServiceType } = props;
|
||||
// store hooks
|
||||
const { openWidgets, toggleOpenWidget } = useIssueDetail(issueServiceType);
|
||||
// derived state
|
||||
// derived values
|
||||
const isCollapsibleOpen = openWidgets.includes("sub-issues");
|
||||
|
||||
return (
|
||||
|
|
@ -33,7 +33,8 @@ export const SubIssuesCollapsible: FC<Props> = observer((props) => {
|
|||
isOpen={isCollapsibleOpen}
|
||||
parentIssueId={issueId}
|
||||
disabled={disabled}
|
||||
issueServiceType={issueServiceType}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
}
|
||||
buttonClassName="w-full"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
import { FC, useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { EIssueFilterType, EIssueServiceType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueServiceType } from "@plane/types";
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { SubIssueDisplayFilters } from "./display-filters";
|
||||
import { SubIssuesActionButton } from "./quick-action-button";
|
||||
|
||||
type TSubWorkItemTitleActionsProps = {
|
||||
disabled: boolean;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
parentId: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export const SubWorkItemTitleActions: FC<TSubWorkItemTitleActionsProps> = observer((props) => {
|
||||
const { disabled, issueServiceType = EIssueServiceType.ISSUES, parentId, workspaceSlug, projectId } = props;
|
||||
|
||||
// store hooks
|
||||
const {
|
||||
subIssues: {
|
||||
filters: { getSubIssueFilters, updateSubIssueFilters },
|
||||
},
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
// derived values
|
||||
const subIssueFilters = getSubIssueFilters(parentId);
|
||||
|
||||
const layoutDisplayFiltersOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE["sub_work_items"].list;
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateSubIssueFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, parentId);
|
||||
},
|
||||
[workspaceSlug, projectId, parentId, updateSubIssueFilters]
|
||||
);
|
||||
|
||||
const handleDisplayPropertiesUpdate = useCallback(
|
||||
(updatedDisplayProperties: Partial<IIssueDisplayProperties>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
updateSubIssueFilters(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
EIssueFilterType.DISPLAY_PROPERTIES,
|
||||
updatedDisplayProperties,
|
||||
parentId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, projectId, parentId, updateSubIssueFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<SubIssueDisplayFilters
|
||||
isEpic={issueServiceType === EIssueServiceType.EPICS}
|
||||
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
|
||||
displayProperties={subIssueFilters?.displayProperties ?? {}}
|
||||
displayFilters={subIssueFilters?.displayFilters ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
/>
|
||||
{!disabled && (
|
||||
<SubIssuesActionButton issueId={parentId} disabled={disabled} issueServiceType={issueServiceType} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,34 +1,46 @@
|
|||
"use client";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssueServiceType } from "@plane/types";
|
||||
import { CircularProgressIndicator, CollapsibleButton } from "@plane/ui";
|
||||
// components
|
||||
import { SubIssuesActionButton } from "@/components/issues/issue-detail-widgets";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
import { SubWorkItemTitleActions } from "./title-actions";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
parentIssueId: string;
|
||||
disabled: boolean;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
|
||||
const { isOpen, parentIssueId, disabled, issueServiceType = EIssueServiceType.ISSUES } = props;
|
||||
const {
|
||||
isOpen,
|
||||
parentIssueId,
|
||||
disabled,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
} = props;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const {
|
||||
subIssues: { subIssuesByIssueId, stateDistributionByIssueId },
|
||||
subIssues: {
|
||||
subIssuesByIssueId,
|
||||
stateDistributionByIssueId,
|
||||
},
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
// derived data
|
||||
// derived values
|
||||
const subIssuesDistribution = stateDistributionByIssueId(parentIssueId);
|
||||
const subIssues = subIssuesByIssueId(parentIssueId);
|
||||
|
||||
// if there are no sub-issues, return null
|
||||
if (!subIssues) return null;
|
||||
|
||||
|
|
@ -50,9 +62,13 @@ export const SubIssuesCollapsibleTitle: FC<Props> = observer((props) => {
|
|||
</div>
|
||||
}
|
||||
actionItemElement={
|
||||
!disabled && (
|
||||
<SubIssuesActionButton issueId={parentIssueId} disabled={disabled} issueServiceType={issueServiceType} />
|
||||
)
|
||||
<SubWorkItemTitleActions
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentId={parentIssueId}
|
||||
disabled={disabled}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
|||
const handleCopyIssueLink = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
issueOperations.copyText(workItemLink);
|
||||
issueOperations.copyLink(workItemLink);
|
||||
};
|
||||
|
||||
const handleRemoveRelation = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export * from "./root";
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { TIssue, TIssueServiceType } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// components
|
||||
import { IssueListItem } from "./issue-list-item";
|
||||
// types
|
||||
import { TSubIssueOperations } from "./root";
|
||||
|
||||
export interface IIssueList {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
rootIssueId: string;
|
||||
spacingLeft: number;
|
||||
disabled: boolean;
|
||||
handleIssueCrudState: (
|
||||
key: "create" | "existing" | "update" | "delete",
|
||||
issueId: string,
|
||||
issue?: TIssue | null
|
||||
) => void;
|
||||
subIssueOperations: TSubIssueOperations;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
}
|
||||
|
||||
export const IssueList: FC<IIssueList> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
rootIssueId,
|
||||
spacingLeft = 10,
|
||||
disabled,
|
||||
handleIssueCrudState,
|
||||
subIssueOperations,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
} = props;
|
||||
// hooks
|
||||
const {
|
||||
subIssues: { subIssuesByIssueId },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
const subIssueIds = subIssuesByIssueId(parentIssueId);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{subIssueIds &&
|
||||
subIssueIds.length > 0 &&
|
||||
subIssueIds.map((issueId) => (
|
||||
<Fragment key={issueId}>
|
||||
<IssueListItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
rootIssueId={rootIssueId}
|
||||
issueId={issueId}
|
||||
spacingLeft={spacingLeft}
|
||||
disabled={disabled}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
subIssueOperations={subIssueOperations}
|
||||
issueServiceType={issueServiceType}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
export interface IProgressBar {
|
||||
total: number;
|
||||
done: number;
|
||||
}
|
||||
|
||||
export const ProgressBar = ({ total = 0, done = 0 }: IProgressBar) => {
|
||||
const calPercentage = (doneValue: number, totalValue: number): string => {
|
||||
if (doneValue === 0 || totalValue === 0) return (0).toFixed(0);
|
||||
return ((100 * doneValue) / totalValue).toFixed(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="w-full">
|
||||
<div className="w-full overflow-hidden rounded-full bg-custom-background-80 shadow">
|
||||
<div
|
||||
className="h-[6px] rounded-full bg-green-500 transition-all"
|
||||
style={{ width: `${calPercentage(done, total)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-xs font-medium">{calPercentage(done, total)}% Done</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import React from "react";
|
||||
import { TIssueServiceType } from "@plane/types";
|
||||
// hooks
|
||||
import { PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns";
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// components
|
||||
// types
|
||||
import { TSubIssueOperations } from "./root";
|
||||
|
||||
export interface IIssueProperty {
|
||||
workspaceSlug: string;
|
||||
parentIssueId: string;
|
||||
issueId: string;
|
||||
disabled: boolean;
|
||||
subIssueOperations: TSubIssueOperations;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
}
|
||||
|
||||
export const IssueProperty: React.FC<IIssueProperty> = (props) => {
|
||||
const { workspaceSlug, parentIssueId, issueId, disabled, subIssueOperations, issueServiceType } = props;
|
||||
// hooks
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail(issueServiceType);
|
||||
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return <></>;
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="h-5 flex-shrink-0">
|
||||
<StateDropdown
|
||||
value={issue.state_id}
|
||||
projectId={issue.project_id ?? undefined}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
subIssueOperations.updateSubIssue(
|
||||
workspaceSlug,
|
||||
issue.project_id,
|
||||
parentIssueId,
|
||||
issueId,
|
||||
{
|
||||
state_id: val,
|
||||
},
|
||||
{ ...issue }
|
||||
)
|
||||
}
|
||||
disabled={!disabled}
|
||||
buttonVariant="border-with-text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-5 flex-shrink-0">
|
||||
<PriorityDropdown
|
||||
value={issue.priority}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
subIssueOperations.updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, {
|
||||
priority: val,
|
||||
})
|
||||
}
|
||||
disabled={!disabled}
|
||||
buttonVariant="border-without-text"
|
||||
buttonClassName="border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-5 flex-shrink-0">
|
||||
<MemberDropdown
|
||||
value={issue.assignee_ids}
|
||||
projectId={issue.project_id ?? undefined}
|
||||
onChange={(val) =>
|
||||
issue.project_id &&
|
||||
subIssueOperations.updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, {
|
||||
assignee_ids: val,
|
||||
})
|
||||
}
|
||||
disabled={!disabled}
|
||||
multiple
|
||||
buttonVariant={(issue?.assignee_ids || []).length > 0 ? "transparent-without-text" : "border-without-text"}
|
||||
buttonClassName={(issue?.assignee_ids || []).length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,546 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// icons
|
||||
import { Plus, ChevronRight, Loader, Pencil } from "lucide-react";
|
||||
// types
|
||||
import { IUser, TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { CircularProgressIndicator, CustomMenu, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core";
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssueDetail } from "@/hooks/store";
|
||||
// local components
|
||||
import useURLHash from "@/hooks/use-url-hash";
|
||||
import { IssueList } from "./issues-list";
|
||||
|
||||
export interface ISubIssuesRoot {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
parentIssueId: string;
|
||||
currentUser: IUser;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export type TSubIssueOperations = {
|
||||
copyText: (text: string) => void;
|
||||
fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise<void>;
|
||||
addSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => Promise<void>;
|
||||
updateSubIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue?: Partial<TIssue>,
|
||||
fromModal?: boolean
|
||||
) => Promise<void>;
|
||||
removeSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
deleteSubIssue: (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, parentIssueId, disabled = false } = props;
|
||||
// router
|
||||
const pathname = usePathname();
|
||||
const hashValue = useURLHash();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
subIssues: { subIssuesByIssueId, stateDistributionByIssueId, subIssueHelpersByIssueId, setSubIssueHelpers },
|
||||
fetchSubIssues,
|
||||
createSubIssues,
|
||||
updateSubIssue,
|
||||
removeSubIssue,
|
||||
deleteSubIssue,
|
||||
isCreateIssueModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
isSubIssuesModalOpen,
|
||||
toggleSubIssuesModal,
|
||||
toggleDeleteIssueModal,
|
||||
} = useIssueDetail();
|
||||
const { setTrackElement, captureIssueEvent } = useEventTracker();
|
||||
// state
|
||||
|
||||
type TIssueCrudState = { toggle: boolean; parentIssueId: string | undefined; issue: TIssue | undefined };
|
||||
const [issueCrudState, setIssueCrudState] = useState<{
|
||||
create: TIssueCrudState;
|
||||
existing: TIssueCrudState;
|
||||
update: TIssueCrudState;
|
||||
delete: TIssueCrudState;
|
||||
}>({
|
||||
create: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
existing: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
update: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
delete: {
|
||||
toggle: false,
|
||||
parentIssueId: undefined,
|
||||
issue: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const scrollToSubIssuesView = useCallback(() => {
|
||||
if (hashValue === "sub-issues") {
|
||||
setTimeout(() => {
|
||||
const subIssueDiv = document.getElementById(`sub-issues`);
|
||||
if (subIssueDiv)
|
||||
subIssueDiv.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}, [hashValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hashValue) {
|
||||
scrollToSubIssuesView();
|
||||
}
|
||||
}, [hashValue, scrollToSubIssuesView]);
|
||||
|
||||
const handleIssueCrudState = (
|
||||
key: "create" | "existing" | "update" | "delete",
|
||||
_parentIssueId: string | null,
|
||||
issue: TIssue | null = null
|
||||
) => {
|
||||
setIssueCrudState({
|
||||
...issueCrudState,
|
||||
[key]: {
|
||||
toggle: !issueCrudState[key].toggle,
|
||||
parentIssueId: _parentIssueId,
|
||||
issue: issue,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const subIssueOperations: TSubIssueOperations = useMemo(
|
||||
() => ({
|
||||
copyText: (text: string) => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}${text}`).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link Copied!",
|
||||
message: "Work item link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
},
|
||||
fetchSubIssues: async (workspaceSlug: string, projectId: string, parentIssueId: string) => {
|
||||
try {
|
||||
await fetchSubIssues(workspaceSlug, projectId, parentIssueId);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error fetching sub-work items",
|
||||
});
|
||||
}
|
||||
},
|
||||
addSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueIds: string[]) => {
|
||||
try {
|
||||
await createSubIssues(workspaceSlug, projectId, parentIssueId, issueIds);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Sub-work items added successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error adding sub-work item",
|
||||
});
|
||||
}
|
||||
},
|
||||
updateSubIssue: async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string,
|
||||
issueData: Partial<TIssue>,
|
||||
oldIssue: Partial<TIssue> = {},
|
||||
fromModal: boolean = false
|
||||
) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await updateSubIssue(workspaceSlug, projectId, parentIssueId, issueId, issueData, oldIssue, fromModal);
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue updated",
|
||||
payload: { ...oldIssue, ...issueData, state: "SUCCESS", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: Object.keys(issueData).join(","),
|
||||
change_details: Object.values(issueData).join(","),
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Sub-work item updated successfully",
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue updated",
|
||||
payload: { ...oldIssue, ...issueData, state: "FAILED", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: Object.keys(issueData).join(","),
|
||||
change_details: Object.values(issueData).join(","),
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error updating sub-work item",
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Sub-work item removed successfully",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue removed",
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "parent_id",
|
||||
change_details: parentIssueId,
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue removed",
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
updates: {
|
||||
changed_property: "parent_id",
|
||||
change_details: parentIssueId,
|
||||
},
|
||||
path: pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error removing sub-work item",
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteSubIssue: async (workspaceSlug: string, projectId: string, parentIssueId: string, issueId: string) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await deleteSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue deleted",
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
|
||||
path: pathname,
|
||||
});
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
captureIssueEvent({
|
||||
eventName: "Sub-issue removed",
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },
|
||||
path: pathname,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error deleting work item",
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[fetchSubIssues, createSubIssues, updateSubIssue, removeSubIssue, deleteSubIssue, setSubIssueHelpers]
|
||||
);
|
||||
|
||||
const issue = getIssueById(parentIssueId);
|
||||
const subIssuesDistribution = stateDistributionByIssueId(parentIssueId);
|
||||
const subIssues = subIssuesByIssueId(parentIssueId);
|
||||
const subIssueHelpers = subIssueHelpersByIssueId(`${parentIssueId}_root`);
|
||||
|
||||
const handleFetchSubIssues = useCallback(async () => {
|
||||
if (!subIssueHelpers.issue_visibility.includes(parentIssueId)) {
|
||||
setSubIssueHelpers(`${parentIssueId}_root`, "preview_loader", parentIssueId);
|
||||
await subIssueOperations.fetchSubIssues(workspaceSlug, projectId, parentIssueId);
|
||||
setSubIssueHelpers(`${parentIssueId}_root`, "preview_loader", parentIssueId);
|
||||
}
|
||||
setSubIssueHelpers(`${parentIssueId}_root`, "issue_visibility", parentIssueId);
|
||||
}, [
|
||||
parentIssueId,
|
||||
projectId,
|
||||
setSubIssueHelpers,
|
||||
subIssueHelpers.issue_visibility,
|
||||
subIssueOperations,
|
||||
workspaceSlug,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
handleFetchSubIssues();
|
||||
|
||||
return () => {
|
||||
handleFetchSubIssues();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [parentIssueId]);
|
||||
|
||||
if (!issue) return <></>;
|
||||
return (
|
||||
<div id="sub-issues" className="h-full w-full space-y-2">
|
||||
{!subIssues ? (
|
||||
<div className="py-3 text-center text-sm font-medium text-custom-text-300">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{subIssues && subIssues?.length > 0 ? (
|
||||
<>
|
||||
<div className="relative flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded py-1 px-2 transition-all hover:bg-custom-background-80 font-medium"
|
||||
onClick={handleFetchSubIssues}
|
||||
>
|
||||
<div className="flex flex-shrink-0 items-center justify-center">
|
||||
{subIssueHelpers.preview_loader.includes(parentIssueId) ? (
|
||||
<Loader strokeWidth={2} className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<ChevronRight
|
||||
className={cn("h-3 w-3 transition-all", {
|
||||
"rotate-90": subIssueHelpers.issue_visibility.includes(parentIssueId),
|
||||
})}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>Sub-work items</div>
|
||||
</button>
|
||||
<div className="flex items-center gap-2 text-custom-text-300">
|
||||
<CircularProgressIndicator
|
||||
size={16}
|
||||
percentage={
|
||||
subIssuesDistribution?.completed?.length && subIssues.length
|
||||
? (subIssuesDistribution?.completed?.length / subIssues.length) * 100
|
||||
: 0
|
||||
}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<span>
|
||||
{subIssuesDistribution?.completed?.length ?? 0}/{subIssues.length} Done
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!disabled && (
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add sub-work item
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
placement="bottom-end"
|
||||
noBorder
|
||||
noChevron
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("create", parentIssueId, null);
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Create new</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(issue.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayersIcon className="h-3 w-3" />
|
||||
<span>Add existing</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{subIssueHelpers.issue_visibility.includes(parentIssueId) && (
|
||||
<IssueList
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={parentIssueId}
|
||||
rootIssueId={parentIssueId}
|
||||
spacingLeft={10}
|
||||
disabled={!disabled}
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
subIssueOperations={subIssueOperations}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
!disabled && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs italic text-custom-text-300">No sub-work items yet</div>
|
||||
<CustomMenu
|
||||
label={
|
||||
<>
|
||||
<Plus className="h-3 w-3" />
|
||||
Add sub-work item
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
placement="bottom-end"
|
||||
noBorder
|
||||
noChevron
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("create", parentIssueId, null);
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
<span>Create new</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(issue.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<LayersIcon className="h-3 w-3" />
|
||||
<span>Add existing</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* issue create, add from existing , update and delete modals */}
|
||||
{issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && (
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudState?.create?.toggle}
|
||||
data={{
|
||||
parent_id: issueCrudState?.create?.parentIssueId,
|
||||
}}
|
||||
onClose={() => {
|
||||
handleIssueCrudState("create", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
}}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
if (_issue.parent_id) {
|
||||
await subIssueOperations.addSubIssue(workspaceSlug, projectId, _issue.parent_id, [_issue.id]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen && (
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={issueCrudState?.existing?.toggle}
|
||||
handleClose={() => {
|
||||
handleIssueCrudState("existing", null, null);
|
||||
toggleSubIssuesModal(null);
|
||||
}}
|
||||
searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }}
|
||||
handleOnSubmit={(_issue) =>
|
||||
subIssueOperations.addSubIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
_issue.map((issue) => issue.id)
|
||||
)
|
||||
}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
)}
|
||||
|
||||
{issueCrudState?.update?.toggle && issueCrudState?.update?.issue && (
|
||||
<>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={issueCrudState?.update?.toggle}
|
||||
onClose={() => {
|
||||
handleIssueCrudState("update", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
}}
|
||||
data={issueCrudState?.update?.issue ?? undefined}
|
||||
onSubmit={async (_issue: TIssue) => {
|
||||
await subIssueOperations.updateSubIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
parentIssueId,
|
||||
_issue.id,
|
||||
_issue,
|
||||
issueCrudState?.update?.issue,
|
||||
true
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{issueCrudState?.delete?.toggle &&
|
||||
issueCrudState?.delete?.issue &&
|
||||
issueCrudState.delete.parentIssueId &&
|
||||
issueCrudState.delete.issue.id && (
|
||||
<DeleteIssueModal
|
||||
isOpen={issueCrudState?.delete?.toggle}
|
||||
handleClose={() => {
|
||||
handleIssueCrudState("delete", null, null);
|
||||
toggleDeleteIssueModal(null);
|
||||
}}
|
||||
data={issueCrudState?.delete?.issue as TIssue}
|
||||
onSubmit={async () =>
|
||||
await subIssueOperations.deleteSubIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueCrudState?.delete?.parentIssueId as string,
|
||||
issueCrudState?.delete?.issue?.id as string
|
||||
)
|
||||
}
|
||||
isSubIssue
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -267,9 +267,15 @@ export class IssueService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async subIssues(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssueSubIssues> {
|
||||
async subIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
queries?: Partial<Record<TIssueParams, string | boolean>>
|
||||
): Promise<TIssueSubIssues> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "issues" : "sub-issues"}/`
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/${this.serviceType === EIssueServiceType.EPICS ? "issues" : "sub-issues"}/`,
|
||||
{ params: queries }
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,16 @@ import {
|
|||
TIssueSubIssuesIdMap,
|
||||
TSubIssuesStateDistribution,
|
||||
TIssueServiceType,
|
||||
TLoader,
|
||||
TGroupedIssues,
|
||||
TGroupedIssueCount,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
import { updatePersistentLayer } from "@/local-db/utils/utils";
|
||||
import { IssueService } from "@/services/issue";
|
||||
// store
|
||||
import { IIssueDetail } from "./root.store";
|
||||
import { IWorkItemSubIssueFiltersStore, WorkItemSubIssueFiltersStore } from "./sub_issues_filter.store";
|
||||
|
||||
export interface IIssueSubIssuesStoreActions {
|
||||
fetchSubIssues: (workspaceSlug: string, projectId: string, parentIssueId: string) => Promise<TIssueSubIssues>;
|
||||
|
|
@ -47,11 +51,16 @@ export interface IIssueSubIssuesStore extends IIssueSubIssuesStoreActions {
|
|||
// observables
|
||||
subIssuesStateDistribution: TIssueSubIssuesStateDistributionMap;
|
||||
subIssues: TIssueSubIssuesIdMap;
|
||||
groupedSubIssuesMap: Record<string, TGroupedIssues>;
|
||||
groupedSubIssuesCount: TGroupedIssueCount;
|
||||
subIssueHelpers: Record<string, TSubIssueHelpers>; // parent_issue_id -> TSubIssueHelpers
|
||||
loader: TLoader;
|
||||
filters: IWorkItemSubIssueFiltersStore;
|
||||
// helper methods
|
||||
stateDistributionByIssueId: (issueId: string) => TSubIssuesStateDistribution | undefined;
|
||||
subIssuesByIssueId: (issueId: string) => string[] | undefined;
|
||||
subIssueHelpersByIssueId: (issueId: string) => TSubIssueHelpers;
|
||||
groupedSubIssuesByIssueId: (issueId: string) => TGroupedIssues | undefined;
|
||||
// actions
|
||||
fetchOtherProjectProperties: (workspaceSlug: string, projectIds: string[]) => Promise<void>;
|
||||
setSubIssueHelpers: (parentIssueId: string, key: TSubIssueHelpersKeys, value: string) => void;
|
||||
|
|
@ -61,7 +70,12 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
|||
// observables
|
||||
subIssuesStateDistribution: TIssueSubIssuesStateDistributionMap = {};
|
||||
subIssues: TIssueSubIssuesIdMap = {};
|
||||
groupedSubIssuesMap: Record<string, TGroupedIssues> = {};
|
||||
groupedSubIssuesCount: TGroupedIssueCount = {};
|
||||
subIssueHelpers: Record<string, TSubIssueHelpers> = {};
|
||||
loader: TLoader = undefined;
|
||||
|
||||
filters: IWorkItemSubIssueFiltersStore;
|
||||
// root store
|
||||
rootIssueDetailStore: IIssueDetail;
|
||||
// services
|
||||
|
|
@ -74,6 +88,8 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
|||
subIssuesStateDistribution: observable,
|
||||
subIssues: observable,
|
||||
subIssueHelpers: observable,
|
||||
groupedSubIssuesMap: observable,
|
||||
loader: observable.ref,
|
||||
// actions
|
||||
setSubIssueHelpers: action,
|
||||
fetchSubIssues: action,
|
||||
|
|
@ -82,7 +98,9 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
|||
removeSubIssue: action,
|
||||
deleteSubIssue: action,
|
||||
fetchOtherProjectProperties: action,
|
||||
groupedSubIssuesByIssueId: action,
|
||||
});
|
||||
this.filters = new WorkItemSubIssueFiltersStore(this);
|
||||
// root store
|
||||
this.rootIssueDetailStore = rootStore;
|
||||
// services
|
||||
|
|
@ -101,6 +119,8 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
|||
return this.subIssues[issueId] ?? undefined;
|
||||
};
|
||||
|
||||
groupedSubIssuesByIssueId = (issueId: string) => this.groupedSubIssuesMap[issueId] ?? undefined;
|
||||
|
||||
subIssueHelpersByIssueId = (issueId: string) => ({
|
||||
preview_loader: this.subIssueHelpers?.[issueId]?.preview_loader || [],
|
||||
issue_visibility: this.subIssueHelpers?.[issueId]?.issue_visibility || [],
|
||||
|
|
@ -118,20 +138,29 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
|||
};
|
||||
|
||||
fetchSubIssues = async (workspaceSlug: string, projectId: string, parentIssueId: string) => {
|
||||
const response = await this.issueService.subIssues(workspaceSlug, projectId, parentIssueId);
|
||||
// get filter params
|
||||
const filterParams = this.filters.computedFilterParams(parentIssueId);
|
||||
const response = await this.issueService.subIssues(workspaceSlug, projectId, parentIssueId, filterParams);
|
||||
|
||||
const subIssuesStateDistribution = response?.state_distribution ?? {};
|
||||
const subIssues = (response.sub_issues ?? []) as TIssue[];
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue(subIssues);
|
||||
// fetch other issues states and members when sub-issues are from different project
|
||||
if (subIssues && subIssues.length > 0) {
|
||||
|
||||
// process sub issues response
|
||||
const { issueList, groupedIssues } = this.filters.processSubIssueResponse(response.sub_issues);
|
||||
|
||||
// set grouped issues count
|
||||
set(this.groupedSubIssuesMap, [parentIssueId], groupedIssues);
|
||||
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue(issueList);
|
||||
|
||||
if (issueList && issueList.length > 0) {
|
||||
const otherProjectIds = uniq(
|
||||
subIssues.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId)
|
||||
issueList.map((issue) => issue.project_id).filter((id) => !!id && id !== projectId)
|
||||
) as string[];
|
||||
this.fetchOtherProjectProperties(workspaceSlug, otherProjectIds);
|
||||
}
|
||||
if (subIssues) {
|
||||
if (issueList) {
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.updateIssue(parentIssueId, {
|
||||
sub_issues_count: subIssues.length,
|
||||
sub_issues_count: issueList.length,
|
||||
});
|
||||
}
|
||||
runInAction(() => {
|
||||
|
|
@ -139,7 +168,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
|||
set(
|
||||
this.subIssues,
|
||||
parentIssueId,
|
||||
subIssues.map((issue) => issue.id)
|
||||
issueList.map((issue) => issue.id)
|
||||
);
|
||||
});
|
||||
return response;
|
||||
|
|
@ -282,7 +311,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
|||
set(
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.issuesMap,
|
||||
[parentIssueId, "sub_issues_count"],
|
||||
this.subIssues[parentIssueId].length
|
||||
this.subIssues[parentIssueId]?.length
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -319,7 +348,7 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore {
|
|||
set(
|
||||
this.rootIssueDetailStore.rootIssueStore.issues.issuesMap,
|
||||
[parentIssueId, "sub_issues_count"],
|
||||
this.subIssues[parentIssueId].length
|
||||
this.subIssues[parentIssueId]?.length
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
202
web/core/store/issue/issue-details/sub_issues_filter.store.ts
Normal file
202
web/core/store/issue/issue-details/sub_issues_filter.store.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import set from "lodash/set";
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import { ALL_ISSUES, EIssueFilterType, EIssueGroupByToServerOptions } from "@plane/constants";
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilters,
|
||||
TGroupedIssueCount,
|
||||
TGroupedIssues,
|
||||
TIssue,
|
||||
TIssueParams,
|
||||
TIssues,
|
||||
TSubGroupedIssues,
|
||||
TSubIssueResponse,
|
||||
} from "@plane/types";
|
||||
import { IIssueSubIssuesStore } from "./sub_issues.store";
|
||||
|
||||
export interface IWorkItemSubIssueFiltersStore {
|
||||
subIssueFiltersMap: Record<string, Partial<IIssueFilters>>;
|
||||
// helpers methods
|
||||
updateSubIssueFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueDisplayFilterOptions | IIssueDisplayProperties,
|
||||
parentId: string
|
||||
) => Promise<void>;
|
||||
getSubIssueFilters: (parentId: string) => Partial<IIssueFilters>;
|
||||
computedFilterParams: (parentId: string) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
processSubIssueResponse: (issueResponse: TSubIssueResponse) => {
|
||||
issueList: TIssue[];
|
||||
groupedIssues: TIssues;
|
||||
groupedIssueCount: TGroupedIssueCount;
|
||||
};
|
||||
}
|
||||
|
||||
export class WorkItemSubIssueFiltersStore implements IWorkItemSubIssueFiltersStore {
|
||||
// observables
|
||||
subIssueFiltersMap: Record<string, Partial<IIssueFilters>> = {};
|
||||
|
||||
subIssueStore: IIssueSubIssuesStore;
|
||||
|
||||
constructor(subIssueStore: IIssueSubIssuesStore) {
|
||||
makeObservable(this, {
|
||||
subIssueFiltersMap: observable,
|
||||
updateSubIssueFilters: action,
|
||||
getSubIssueFilters: action,
|
||||
});
|
||||
// sub issue store
|
||||
this.subIssueStore = subIssueStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This method is used to initialize the sub issue filters
|
||||
* @param parentId
|
||||
*/
|
||||
initSubIssueFilters = (parentId: string) => {
|
||||
set(this.subIssueFiltersMap, [parentId], {
|
||||
displayFilters: {},
|
||||
displayProperties: {
|
||||
key: true,
|
||||
issue_type: true,
|
||||
assignee: true,
|
||||
start_date: true,
|
||||
due_date: true,
|
||||
labels: true,
|
||||
priority: true,
|
||||
state: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to process the sub issue response to provide the data to update the store
|
||||
* @param issueResponse
|
||||
* @returns issueList, list of issues data
|
||||
* @returns groupedIssues, grouped issue ids
|
||||
* @returns groupedIssueCount, object containing issue counts of individual groups
|
||||
*/
|
||||
processSubIssueResponse = (
|
||||
issueResponse: TSubIssueResponse
|
||||
): {
|
||||
issueList: TIssue[];
|
||||
groupedIssues: TIssues;
|
||||
groupedIssueCount: TGroupedIssueCount;
|
||||
} => {
|
||||
const issueResult = issueResponse;
|
||||
|
||||
if (!issueResult) {
|
||||
return {
|
||||
issueList: [],
|
||||
groupedIssues: {},
|
||||
groupedIssueCount: {},
|
||||
};
|
||||
}
|
||||
|
||||
//if is an array then it's an ungrouped response. return values with groupId as ALL_ISSUES
|
||||
if (Array.isArray(issueResult)) {
|
||||
return {
|
||||
issueList: issueResult,
|
||||
groupedIssues: {
|
||||
[ALL_ISSUES]: issueResult.map((issue) => issue.id),
|
||||
},
|
||||
groupedIssueCount: {
|
||||
[ALL_ISSUES]: issueResult.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const issueList: TIssue[] = [];
|
||||
const groupedIssues: TGroupedIssues | TSubGroupedIssues = {};
|
||||
const groupedIssueCount: TGroupedIssueCount = {};
|
||||
|
||||
// update total issue count to ALL_ISSUES
|
||||
set(groupedIssueCount, [ALL_ISSUES], issueResult.length);
|
||||
|
||||
// loop through all the groupIds from issue Result
|
||||
for (const groupId in issueResult) {
|
||||
const groupIssueResult = issueResult[groupId];
|
||||
|
||||
// if groupIssueResult is undefined then continue the loop
|
||||
if (!groupIssueResult) continue;
|
||||
|
||||
// set grouped Issue count of the current groupId
|
||||
set(groupedIssueCount, [groupId], groupIssueResult.length);
|
||||
|
||||
// add the result to issueList
|
||||
issueList.push(...groupIssueResult);
|
||||
// set the issue Ids to the groupId path
|
||||
set(
|
||||
groupedIssues,
|
||||
[groupId],
|
||||
groupIssueResult.map((issue) => issue.id)
|
||||
);
|
||||
}
|
||||
|
||||
return { issueList, groupedIssues, groupedIssueCount };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to get the sub issue filters
|
||||
* @param parentId
|
||||
* @returns IIssueFilters
|
||||
*/
|
||||
getSubIssueFilters = (parentId: string) => {
|
||||
if (!this.subIssueFiltersMap[parentId]) {
|
||||
this.initSubIssueFilters(parentId);
|
||||
}
|
||||
return this.subIssueFiltersMap[parentId];
|
||||
};
|
||||
|
||||
computedFilterParams = (parentId: string) => {
|
||||
const displayFilters = this.getSubIssueFilters(parentId).displayFilters;
|
||||
|
||||
const computedFilters: Partial<Record<TIssueParams, undefined | string[] | boolean | string>> = {
|
||||
order_by: displayFilters?.order_by || undefined,
|
||||
group_by: displayFilters?.group_by ? EIssueGroupByToServerOptions[displayFilters.group_by] : undefined,
|
||||
};
|
||||
|
||||
const issueFiltersParams: Partial<Record<TIssueParams, boolean | string>> = {};
|
||||
Object.keys(computedFilters).forEach((key) => {
|
||||
const _key = key as TIssueParams;
|
||||
const _value: string | boolean | string[] | undefined = computedFilters[_key];
|
||||
const nonEmptyArrayValue = Array.isArray(_value) && _value.length === 0 ? undefined : _value;
|
||||
if (nonEmptyArrayValue != undefined)
|
||||
issueFiltersParams[_key] = Array.isArray(nonEmptyArrayValue)
|
||||
? nonEmptyArrayValue.join(",")
|
||||
: nonEmptyArrayValue;
|
||||
});
|
||||
|
||||
return issueFiltersParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to update the sub issue filters
|
||||
* @param projectId
|
||||
* @param filterType
|
||||
* @param filters
|
||||
*/
|
||||
updateSubIssueFilters = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueDisplayFilterOptions | IIssueDisplayProperties,
|
||||
parentId: string
|
||||
) => {
|
||||
const _filters = this.getSubIssueFilters(parentId);
|
||||
switch (filterType) {
|
||||
case EIssueFilterType.DISPLAY_FILTERS: {
|
||||
set(this.subIssueFiltersMap, [parentId, "displayFilters"], { ..._filters.displayFilters, ...filters });
|
||||
this.subIssueStore.fetchSubIssues(workspaceSlug, projectId, parentId);
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.DISPLAY_PROPERTIES:
|
||||
set(this.subIssueFiltersMap, [parentId, "displayProperties"], {
|
||||
..._filters.displayProperties,
|
||||
...filters,
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue