From 01fc205cf0136ad33abf0962cc326e8b41214741 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:07:36 +0530 Subject: [PATCH] [WEB-3217] feat: added epics to initiatives modal and list layout (#2287) * feat: added epics to initiatives modal and list layout * fix: initiative indicator * chore: add epics while creating initiative * feat: Update epics in same API * fix: epics dropdown * fix: epics api call * activity logs of initiative epics * Return epic ids in list initiative list api * Update initative activity * fix: patch api response only updating on second call * fix: progress indicator moved + empty initiatives * feat: Create initiatves without project and epic --------- Co-authored-by: sangeethailango --- .../ee/bgtasks/initiative_activity_task.py | 122 +++++---- .../plane/ee/serializers/app/initiative.py | 34 +++ .../plane/ee/views/app/initiative/base.py | 65 +++-- web/ee/components/dropdowns/epics.tsx | 252 ++++++++++++++++++ web/ee/components/dropdowns/index.ts | 1 + .../components/block-properties.tsx | 35 +-- .../create-update-initiatives-modal.tsx | 1 + .../initiatives/components/form.tsx | 21 +- .../components/initiative-block.tsx | 12 +- .../project-list-item/quick-actions.tsx | 15 +- .../main/info-section/indicator-item.tsx | 6 +- web/ee/types/initiative/initiative.d.ts | 1 + 12 files changed, 466 insertions(+), 99 deletions(-) create mode 100644 web/ee/components/dropdowns/epics.tsx create mode 100644 web/ee/components/dropdowns/index.ts diff --git a/apiserver/plane/ee/bgtasks/initiative_activity_task.py b/apiserver/plane/ee/bgtasks/initiative_activity_task.py index 9f390c64fb..f0dfb4939f 100644 --- a/apiserver/plane/ee/bgtasks/initiative_activity_task.py +++ b/apiserver/plane/ee/bgtasks/initiative_activity_task.py @@ -9,18 +9,15 @@ from celery import shared_task from django.utils import timezone # Module imports -from plane.db.models import ( - CommentReaction, - Label, - Project, - Workspace, -) +from plane.db.models import CommentReaction, Label, Project, Workspace from plane.ee.models import ( Initiative, InitiativeActivity, InitiativeReaction, InitiativeComment, ) +from plane.db.models import Issue + from plane.settings.redis import redis_instance from plane.utils.exception_logger import log_exception @@ -130,9 +127,7 @@ def track_end_date( initiative_activities, epoch, ): - if current_instance.get("end_date") != requested_data.get( - "end_date" - ): + if current_instance.get("end_date") != requested_data.get("end_date"): initiative_activities.append( InitiativeActivity( initiative_id=initiative_id, @@ -200,12 +195,8 @@ def track_labels( initiative_activities, epoch, ): - requested_labels = set( - [str(lab) for lab in requested_data.get("label_ids", [])] - ) - current_labels = set( - [str(lab) for lab in current_instance.get("label_ids", [])] - ) + requested_labels = set([str(lab) for lab in requested_data.get("label_ids", [])]) + current_labels = set([str(lab) for lab in current_instance.get("label_ids", [])]) added_labels = requested_labels - current_labels dropped_labels = current_labels - requested_labels @@ -259,7 +250,6 @@ def track_projects( initiative_activities, epoch, ): - requested_projects = ( set([str(asg) for asg in requested_data.get("project_ids", [])]) if requested_data is not None @@ -309,6 +299,65 @@ def track_projects( ) +def track_epics( + requested_data, + current_instance, + initiative_id, + workspace_id, + actor_id, + initiative_activities, + epoch, +): + requested_epics = ( + set([str(asg) for asg in requested_data.get("epic_ids", [])]) + if requested_data is not None + else set() + ) + current_epics = ( + set([str(asg) for asg in current_instance.get("epic_ids", [])]) + if current_instance is not None + else set() + ) + + added_epics = requested_epics - current_epics + dropped_epics = current_epics - requested_epics + + for added_epic in added_epics: + epic = Issue.objects.get(pk=added_epic) + + initiative_activities.append( + InitiativeActivity( + initiative_id=initiative_id, + actor_id=actor_id, + workspace_id=workspace_id, + verb="updated", + old_value="", + new_value=epic.name, + field="epics", + comment="added epic ", + new_identifier=epic.id, + epoch=epoch, + ) + ) + + for dropped_epic in dropped_epics: + epic = Issue.objects.get(pk=dropped_epic) + initiative_activities.append( + InitiativeActivity( + initiative_id=initiative_id, + actor_id=actor_id, + workspace_id=workspace_id, + verb="updated", + old_value=epic.name, + new_value="", + field="epics", + comment="removed epic ", + old_identifier=epic.id, + epoch=epoch, + ) + ) + + def track_lead( requested_data, current_instance, @@ -378,11 +427,11 @@ def update_initiative_activity( "lead": track_lead, "label_ids": track_labels, "project_ids": track_projects, + "epic_ids": track_epics, } - requested_data = ( - json.loads(requested_data) if requested_data is not None else None - ) + requested_data = json.loads(requested_data) if requested_data is not None else None + current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -432,9 +481,7 @@ def create_comment_activity( initiative_activities, epoch, ): - requested_data = ( - json.loads(requested_data) if requested_data is not None else None - ) + requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -464,16 +511,12 @@ def update_comment_activity( initiative_activities, epoch, ): - requested_data = ( - json.loads(requested_data) if requested_data is not None else None - ) + requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( json.loads(current_instance) if current_instance is not None else None ) - if current_instance.get("comment_html") != requested_data.get( - "comment_html" - ): + if current_instance.get("comment_html") != requested_data.get("comment_html"): initiative_activities.append( InitiativeActivity( initiative_id=initiative_id, @@ -523,9 +566,7 @@ def create_link_activity( initiative_activities, epoch, ): - requested_data = ( - json.loads(requested_data) if requested_data is not None else None - ) + requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -554,9 +595,7 @@ def update_link_activity( initiative_activities, epoch, ): - requested_data = ( - json.loads(requested_data) if requested_data is not None else None - ) + requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -616,9 +655,7 @@ def create_attachment_activity( initiative_activities, epoch, ): - requested_data = ( - json.loads(requested_data) if requested_data is not None else None - ) + requested_data = json.loads(requested_data) if requested_data is not None else None current_instance = ( json.loads(current_instance) if current_instance is not None else None ) @@ -669,9 +706,7 @@ def create_initiative_reaction_activity( initiative_activities, epoch, ): - requested_data = ( - json.loads(requested_data) if requested_data is not None else None - ) + requested_data = json.loads(requested_data) if requested_data is not None else None if requested_data and requested_data.get("reaction") is not None: initiative_reaction = ( InitiativeReaction.objects.filter( @@ -739,9 +774,7 @@ def create_comment_reaction_activity( initiative_activities, epoch, ): - requested_data = ( - json.loads(requested_data) if requested_data is not None else None - ) + requested_data = json.loads(requested_data) if requested_data is not None else None if requested_data and requested_data.get("reaction") is not None: comment_reaction_id, comment_id = ( CommentReaction.objects.filter( @@ -792,8 +825,7 @@ def delete_comment_reaction_activity( if current_instance and current_instance.get("reaction") is not None: initiative_id = ( InitiativeComment.objects.filter( - pk=current_instance.get("comment_id"), - initiative_id=initiative_id, + pk=current_instance.get("comment_id"), initiative_id=initiative_id ) .values_list("initiative_id", flat=True) .first() diff --git a/apiserver/plane/ee/serializers/app/initiative.py b/apiserver/plane/ee/serializers/app/initiative.py index 27ce6cdac4..4f32e8e2fb 100644 --- a/apiserver/plane/ee/serializers/app/initiative.py +++ b/apiserver/plane/ee/serializers/app/initiative.py @@ -37,6 +37,7 @@ class InitiativeCommentReactionSerializer(BaseSerializer): class InitiativeSerializer(BaseSerializer): project_ids = serializers.ListField(child=serializers.UUIDField(), required=False) + epic_ids = serializers.ListField(child=serializers.UUIDField(), required=False) label_ids = serializers.ListField(child=serializers.UUIDField(), required=False) reactions = InitiativeCommentReactionSerializer( read_only=True, many=True, source="initiative_reactions" @@ -49,6 +50,7 @@ class InitiativeSerializer(BaseSerializer): def create(self, validated_data): projects = validated_data.pop("project_ids", None) + epics = validated_data.pop("epic_ids", None) labels = validated_data.pop("label_ids", None) workspace_id = self.context["workspace_id"] lead = self.context["lead"] @@ -91,11 +93,27 @@ class InitiativeSerializer(BaseSerializer): batch_size=10, ) + if epics is not None and len(epics): + InitiativeEpic.objects.bulk_create( + [ + InitiativeEpic( + epic_id=epic_id, + initiative=initiative, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for epic_id in epics + ], + batch_size=10, + ) + return initiative def update(self, instance, validated_data): projects = validated_data.pop("project_ids", None) labels = validated_data.pop("label_ids", None) + epics = validated_data.pop("epic_ids", None) # Related models workspace_id = instance.workspace_id @@ -134,6 +152,22 @@ class InitiativeSerializer(BaseSerializer): batch_size=10, ) + if epics is not None: + InitiativeEpic.objects.filter(initiative=instance).delete() + InitiativeEpic.objects.bulk_create( + [ + InitiativeEpic( + epic_id=epic, + initiative=instance, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for epic in epics + ], + batch_size=10, + ) + # Time updation occurs even when other related models are updated instance.updated_at = timezone.now() return super().update(instance, validated_data) diff --git a/apiserver/plane/ee/views/app/initiative/base.py b/apiserver/plane/ee/views/app/initiative/base.py index a4c6e44b49..0d6f08e8ac 100644 --- a/apiserver/plane/ee/views/app/initiative/base.py +++ b/apiserver/plane/ee/views/app/initiative/base.py @@ -53,7 +53,19 @@ class InitiativeEndpoint(BaseAPIView): .values("project_ids") ), [], - ) + ), + epic_ids=Coalesce( + Subquery( + InitiativeEpic.objects.filter( + initiative_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + .values("initiative_id") + .annotate(epic_ids=ArrayAgg("epic_id", distinct=True)) + .values("epic_ids") + ), + [], + ), ) .order_by(self.kwargs.get("order_by", "-created_at")) .distinct() @@ -77,7 +89,19 @@ class InitiativeEndpoint(BaseAPIView): .values("project_ids") ), [], - ) + ), + epic_ids=Coalesce( + Subquery( + InitiativeEpic.objects.filter( + initiative_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + .values("initiative_id") + .annotate(epic_ids=ArrayAgg("epic_id", distinct=True)) + .values("epic_ids") + ), + [], + ), ) .annotate( link_count=InitiativeLink.objects.filter( @@ -107,6 +131,7 @@ class InitiativeEndpoint(BaseAPIView): .first() ) serializer = InitiativeSerializer(initiative) + return Response(serializer.data, status=status.HTTP_200_OK) # Get all initiatives in workspace @@ -118,13 +143,6 @@ class InitiativeEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) - project_ids = request.data.get("project_ids", []) - - if not project_ids: - return Response( - {"error": "Project id's are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) serializer = InitiativeSerializer( data=request.data, @@ -149,6 +167,7 @@ class InitiativeEndpoint(BaseAPIView): ) initiative = self.get_queryset().get(pk=serializer.data.get("id")) + serializer = InitiativeSerializer(initiative) return Response(serializer.data, status=status.HTTP_200_OK) @@ -168,18 +187,32 @@ class InitiativeEndpoint(BaseAPIView): .values("project_ids") ), [], - ) + ), + epic_ids=Coalesce( + Subquery( + InitiativeEpic.objects.filter( + initiative_id=OuterRef("pk"), workspace__slug=slug + ) + .values("initiative_id") + .annotate(epic_ids=ArrayAgg("epic_id", distinct=True)) + .values("epic_ids") + ), + [], + ), ) .first() ) + current_instance = json.dumps( InitiativeSerializer(initiative).data, cls=DjangoJSONEncoder ) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + serializer = InitiativeSerializer(initiative, data=request.data, partial=True) if serializer.is_valid(): serializer.save() + initiative_activity.delay( type="initiative.activity.updated", slug=slug, @@ -191,6 +224,9 @@ class InitiativeEndpoint(BaseAPIView): notification=True, origin=request.META.get("HTTP_ORIGIN"), ) + initiative = self.get_queryset().get(pk=pk) + serializer = InitiativeSerializer(initiative) + return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -479,21 +515,16 @@ class InitiativeEpicAnalytics(BaseAPIView): ) result = [] for epic_id in initiative_epic: - # get all the issues of the particular epic issue_ids = get_all_related_issues(epic_id) completed_issues = ( - issues.filter( - id__in=issue_ids, workspace__slug=slug - ) + issues.filter(id__in=issue_ids, workspace__slug=slug) .filter(state__group="completed") .count() ) - total_issues = issues.filter( - id__in=issue_ids, workspace__slug=slug - ).count() + total_issues = issues.filter(id__in=issue_ids, workspace__slug=slug).count() result.append( { diff --git a/web/ee/components/dropdowns/epics.tsx b/web/ee/components/dropdowns/epics.tsx new file mode 100644 index 0000000000..810e8e5826 --- /dev/null +++ b/web/ee/components/dropdowns/epics.tsx @@ -0,0 +1,252 @@ +import { ReactNode, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { usePopper } from "react-popper"; +import useSWR from "swr"; +import { Check, ChevronDown, Search } from "lucide-react"; +import { Combobox } from "@headlessui/react"; +// ui +import { useTranslation } from "@plane/i18n"; +import { TWorkspaceEpicsSearchParams } from "@plane/types"; +import { ComboDropDown, EpicIcon } from "@plane/ui"; +// helpers +import { DropdownButton } from "@/components/dropdowns/buttons"; +import { BUTTON_VARIANTS_WITH_TEXT } from "@/components/dropdowns/constants"; +import { TDropdownProps } from "@/components/dropdowns/types"; +import { cn } from "@/helpers/common.helper"; +// hooks +import { useDropdown } from "@/hooks/use-dropdown"; +// plane web types +import { WorkspaceService } from "@/plane-web/services"; + +type Props = TDropdownProps & { + button?: ReactNode; + dropdownArrow?: boolean; + dropdownArrowClassName?: string; + onClose?: () => void; + renderByDefault?: boolean; + searchParams: Partial; +} & ( + | { + multiple: false; + onChange: (val: string) => void; + value: string | null; + } + | { + multiple: true; + onChange: (val: string[]) => void; + value: string[]; + } + ); +const workspaceService = new WorkspaceService(); +export const EpicsDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + dropdownArrowClassName = "", + hideIcon = false, + multiple, + onChange, + onClose, + placeholder = "Epic", + placement, + showTooltip = false, + tabIndex, + value, + renderByDefault = true, + searchParams, + } = props; + // router + const { workspaceSlug } = useParams(); + // states + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); + const inputRef = useRef(null); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // store hooks + const { t } = useTranslation(); + const { handleClose, handleKeyDown, handleOnClick, searchInputKeyDown } = useDropdown({ + dropdownRef, + inputRef, + isOpen, + onClose, + query, + setIsOpen, + setQuery, + }); + const { data: epics, isLoading } = useSWR( + workspaceSlug ? `WORKSPACE_EPICS_${workspaceSlug}` : null, + workspaceSlug ? () => workspaceService.fetchWorkspaceEpics(workspaceSlug.toString(), {}) : null, + { + revalidateOnFocus: false, + } + ); + + const getEpicById = (id: string) => epics?.find((epic) => epic.id === id); + const filteredEpics = epics + ? query + ? epics?.filter((epic) => epic.name.toLowerCase().includes(query.toLowerCase())) + : epics + : []; + + const dropdownOnChange = (val: string & string[]) => { + onChange(val); + if (!multiple) handleClose(); + }; + const getDisplayName = (value: string | string[] | null, placeholder: string = "") => { + if (Array.isArray(value)) { + const firstEpic = getEpicById(value[0]); + return value.length ? (value.length === 1 ? firstEpic?.name : `${value.length} epics`) : placeholder; + } else { + return value ? (getEpicById(value)?.name ?? placeholder) : placeholder; + } + }; + + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); + + return ( + + {isOpen && + (isLoading ? ( +
+

{t("loading")}

+
+ ) : ( + +
+
+ + setQuery(e.target.value)} + placeholder={t("search")} + displayValue={(assigned: any) => assigned?.name} + onKeyDown={searchInputKeyDown} + /> +
+
+ {filteredEpics ? ( + filteredEpics.length > 0 ? ( + filteredEpics.map((option) => { + if (!option) return; + return ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + + {option.name} + {selected && } + + )} + + ); + }) + ) : ( +

{t("no_matching_results")}

+ ) + ) : ( +

{t("loading")}

+ )} +
+
+
+ ))} +
+ ); +}); diff --git a/web/ee/components/dropdowns/index.ts b/web/ee/components/dropdowns/index.ts new file mode 100644 index 0000000000..937641681d --- /dev/null +++ b/web/ee/components/dropdowns/index.ts @@ -0,0 +1 @@ +export * from "./epics"; diff --git a/web/ee/components/initiatives/components/block-properties.tsx b/web/ee/components/initiatives/components/block-properties.tsx index 5d453cb4fe..c0c392541a 100644 --- a/web/ee/components/initiatives/components/block-properties.tsx +++ b/web/ee/components/initiatives/components/block-properties.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Users } from "lucide-react"; // plane -import { Avatar, setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; +import { Avatar, Tooltip } from "@plane/ui"; import { cn } from "@plane/utils"; // components import { DateRangeDropdown, MemberDropdown, ProjectDropdown } from "@/components/dropdowns"; @@ -14,6 +14,7 @@ import { useMember } from "@/hooks/store"; // Plane Web import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; import { TInitiative } from "@/plane-web/types/initiative"; +import { EpicsDropdown } from "../../dropdowns"; type Props = { initiative: TInitiative; @@ -46,23 +47,11 @@ export const BlockProperties = observer((props: Props) => { }); }; - const handleProjects = (ids: string | string[]) => { - if (ids.length === 0) { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Please select at least one project.", - }); - return; - } - + const handleChange = (updatedData: Partial) => { if (updateInitiative) { - updateInitiative(workspaceSlug.toString(), initiative.id, { - project_ids: ids ? (Array.isArray(ids) ? ids : [ids]) : null, - }); + updateInitiative(workspaceSlug.toString(), initiative.id, updatedData); } }; - return (
{
handleChange({ project_ids: ids ? (Array.isArray(ids) ? ids : [ids]) : null })} value={initiative.project_ids || []} multiple showTooltip disabled={disabled} />
+ +
+ handleChange({ epic_ids: ids ? (Array.isArray(ids) ? ids : [ids]) : null })} + value={initiative.epic_ids || []} + multiple + showTooltip + disabled={disabled} + searchParams={{ + initiative_id: initiative.id, + }} + /> +
); }); diff --git a/web/ee/components/initiatives/components/create-update-initiatives-modal.tsx b/web/ee/components/initiatives/components/create-update-initiatives-modal.tsx index 0eeb22007c..42644a146a 100644 --- a/web/ee/components/initiatives/components/create-update-initiatives-modal.tsx +++ b/web/ee/components/initiatives/components/create-update-initiatives-modal.tsx @@ -22,6 +22,7 @@ const defaultValues: Partial = { end_date: null, lead: null, project_ids: [], + epic_ids: [], }; export const CreateUpdateInitiativeModal = observer((props: Props) => { diff --git a/web/ee/components/initiatives/components/form.tsx b/web/ee/components/initiatives/components/form.tsx index ad6017b013..de0a9f2e1f 100644 --- a/web/ee/components/initiatives/components/form.tsx +++ b/web/ee/components/initiatives/components/form.tsx @@ -1,8 +1,8 @@ import React, { FC, useState } from "react"; import { useParams } from "next/navigation"; import { EFileAssetType } from "@plane/types/src/enums"; -import { cn } from "@plane/utils"; import { Button, Input, setToast, TOAST_TYPE } from "@plane/ui"; +import { cn } from "@plane/utils"; // components import { DateRangeDropdown, MemberDropdown, ProjectDropdown } from "@/components/dropdowns"; import { RichTextEditor } from "@/components/editor"; @@ -17,6 +17,7 @@ import { useEditorMentionSearch } from "@/plane-web/hooks/use-editor-mention-sea import { TInitiative } from "@/plane-web/types/initiative"; // services import { FileService } from "@/services/file.service"; +import { EpicsDropdown } from "../../dropdowns"; const fileService = new FileService(); @@ -50,9 +51,6 @@ export const CreateUpdateInitiativeForm: FC = (props) => { if (!data.name || data.name.trim() === "") { newErrors.name = "Name is required"; } - if (!data.project_ids || data.project_ids.length === 0) { - newErrors.project_ids = "Project is required"; - } setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -147,6 +145,21 @@ export const CreateUpdateInitiativeForm: FC = (props) => { tabIndex={2} /> +
+ { + handleFormDataChange("epic_ids", val); + }} + value={formData.epic_ids || []} + multiple + showTooltip + tabIndex={2} + searchParams={{ + initiative_id: initiativeDetail?.id, + }} + /> +
{ EUserPermissionsLevel.WORKSPACE ); - const progress = getProgress(initiativeStats?.completed_issues, initiativeStats?.total_issues); + const progress = getProgress(initiativeStats?.completed_issues ?? 0, initiativeStats?.total_issues); return ( { } - quickActionElement={ -
+ appendTitleElement={ + <> {initiativeStats && initiativeStats.total_issues > 0 && (
{`${progress}%`}
)} + + } + quickActionElement={ +
= observer((props: Props) => { const { workspaceSlug, initiativeId, project } = props; // store hooks const { - initiative: { getInitiativeById, updateInitiative }, + initiative: { updateInitiative, getInitiativeById }, } = useInitiatives(); // derived states const projectLink = `${workspaceSlug}/projects/${project.id}/issues`; - const initiative = initiativeId ? getInitiativeById(initiativeId) : undefined; - - const shouldRenderRemove = (initiative?.project_ids || [])?.length > 1; + const initiative = getInitiativeById(initiativeId); // handler const handleCopyText = () => @@ -44,14 +42,15 @@ export const QuickActions: React.FC = observer((props: Props) => { action: handleCopyText, title: "Copy link", icon: LinkIcon, - shouldRender: true, }, { key: "remove", - action: () => updateInitiative(workspaceSlug, initiativeId, { project_ids: [project.id] }), + action: () => + updateInitiative(workspaceSlug, initiativeId, { + project_ids: initiative?.project_ids ? initiative?.project_ids.filter((id) => id !== project.id) : [], + }), title: "Remove", icon: Trash2, - shouldRender: shouldRenderRemove, }, ]; @@ -67,7 +66,7 @@ export const QuickActions: React.FC = observer((props: Props) => { customButtonClassName="grid place-items-center" placement="bottom-start" > - {MENU_ITEMS.filter((item) => item.shouldRender).map((item) => ( + {MENU_ITEMS.map((item) => ( { diff --git a/web/ee/components/initiatives/details/main/info-section/indicator-item.tsx b/web/ee/components/initiatives/details/main/info-section/indicator-item.tsx index 1ecdddfe66..592e7b6afd 100644 --- a/web/ee/components/initiatives/details/main/info-section/indicator-item.tsx +++ b/web/ee/components/initiatives/details/main/info-section/indicator-item.tsx @@ -35,11 +35,7 @@ export const InitiativeInfoIndicatorItem: FC = observer((props) => { ? Object.values(omit(initiativeAnalytics, "overdue_issues")).reduce((acc, val) => acc + val, 0) : 0; - const completedIssue = initiativeAnalytics - ? initiativeAnalytics.completed_issues + initiativeAnalytics.cancelled_issues - : 0; - - const completePercentage = getProgress(completedIssue, totalIssues); + const completePercentage = getProgress(initiativeAnalytics?.completed_issues ?? 0, totalIssues); if (!hasProject || totalIssues === 0) return <>; return ( diff --git a/web/ee/types/initiative/initiative.d.ts b/web/ee/types/initiative/initiative.d.ts index e2761676b7..0c33d352fc 100644 --- a/web/ee/types/initiative/initiative.d.ts +++ b/web/ee/types/initiative/initiative.d.ts @@ -26,6 +26,7 @@ export type TInitiative = { workspace: string; lead: string | null; project_ids: string[] | null; + epic_ids: string[] | null; }; export type TInitiativeProject = {