diff --git a/apiserver/plane/app/views/search/issue.py b/apiserver/plane/app/views/search/issue.py index 54002ea495..090466d41f 100644 --- a/apiserver/plane/app/views/search/issue.py +++ b/apiserver/plane/app/views/search/issue.py @@ -73,7 +73,7 @@ class IssueSearchEndpoint(BaseAPIView): if issue_relation == "true" and issue_id: issues = search_issues(query, issues_and_epics) - issue = Issue.issue_objects.filter(pk=issue_id).first() + issue = Issue.objects.filter(pk=issue_id).first() related_issue_ids = ( IssueRelation.objects.filter(Q(related_issue=issue) | Q(issue=issue)) diff --git a/web/ee/components/epics/details/main/overview-section/modals-root.tsx b/web/ee/components/epics/details/main/overview-section/modals-root.tsx index 5b176ae67e..31bf1d739a 100644 --- a/web/ee/components/epics/details/main/overview-section/modals-root.tsx +++ b/web/ee/components/epics/details/main/overview-section/modals-root.tsx @@ -174,7 +174,7 @@ export const EpicOverviewWidgetModals: FC = observer((props) => { projectId={projectId} isOpen={isRelationModalOpen?.issueId === epicId && isRelationModalOpen?.relationType === relationKey} handleClose={handleRelationOnClose} - searchParams={{ issue_relation: true }} + searchParams={{ issue_relation: true, issue_id: epicId }} handleOnSubmit={handleExistingIssueModalOnSubmit} workspaceLevelToggle /> diff --git a/web/ee/components/initiatives/details/main/collapsible-section-root.tsx b/web/ee/components/initiatives/details/main/collapsible-section-root.tsx index 1bbae5b46b..b5e816310d 100644 --- a/web/ee/components/initiatives/details/main/collapsible-section-root.tsx +++ b/web/ee/components/initiatives/details/main/collapsible-section-root.tsx @@ -1,21 +1,14 @@ "use client"; -import React, { FC, useState } from "react"; +import React, { FC } from "react"; import { observer } from "mobx-react"; import { Plus } from "lucide-react"; -// ui -import { setToast, TOAST_TYPE } from "@plane/ui"; -// components -import { ProjectMultiSelectModal } from "@/components/project"; -// hooks -import { useProject } from "@/hooks/store"; // plane web import { CollapsibleDetailSection } from "@/plane-web/components/common/layout/main/sections/collapsible-root"; import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; // local components import { InitiativeAttachmentRoot } from "./collapsible-section/attachment"; import { InitiativeEpicsCollapsibleContent } from "./collapsible-section/epics/content"; -import { WorkspaceEpicsListModal } from "./collapsible-section/epics/workspace-epic-modal"; import { InitiativeLinksCollapsibleContent } from "./collapsible-section/links/link-components/content"; import { InitiativeProjectsCollapsibleContent } from "./collapsible-section/projects/content"; import { InitiativeAttachmentActionButton } from "./info-section/attachment-button"; @@ -25,28 +18,24 @@ type Props = { workspaceSlug: string; initiativeId: string; disabled: boolean; + toggleEpicModal: (value?: boolean) => void; + toggleProjectModal: (value?: boolean) => void; }; export const InitiativeCollapsibleSection: FC = observer((props) => { - const { workspaceSlug, initiativeId, disabled } = props; - // states - const [isProjectsOpen, setIsProjectsOpen] = useState(false); - const [isEpicModalOpen, setIsEpicModalOpen] = useState(false); + const { workspaceSlug, initiativeId, disabled, toggleEpicModal, toggleProjectModal } = props; // store hooks const { initiative: { getInitiativeEpicsById, getInitiativeById, - updateInitiative, initiativeLinks: { getInitiativeLinks }, initiativeAttachments: { getAttachmentsByInitiativeId }, openCollapsibleSection, toggleOpenCollapsibleSection, - addEpicsToInitiative, }, } = useInitiatives(); - const { workspaceProjectIds } = useProject(); // derived values const initiative = getInitiativeById(initiativeId); @@ -72,56 +61,6 @@ export const InitiativeCollapsibleSection: FC = observer((props) => { const epicCount = initiativeEpics?.length ?? 0; - // handlers - const handleProjectsUpdate = async (initiativeProjectIds: string[]) => { - if (!initiativeId) return; - - if (initiativeProjectIds.length === 0) { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Please select at least one project.", - }); - return; - } - - await updateInitiative(workspaceSlug?.toString(), initiativeId, { project_ids: initiativeProjectIds }) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: `Initiative projects updated successfully.`, - }); - }) - .catch((error) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: error?.error ?? `Failed to update initiative projects. Please try again!`, - }); - }); - }; - - const handleAddEpicToInitiative = async (epicIds: string[]) => { - try { - addEpicsToInitiative(workspaceSlug?.toString(), initiativeId, epicIds).then(() => { - setToast({ - title: "Success!", - type: TOAST_TYPE.SUCCESS, - message: `Epic${epicIds.length > 1 ? "s" : ""} added to Initiative successfully.`, - }); - }); - } catch { - setToast({ - title: "Error!", - type: TOAST_TYPE.ERROR, - message: "Epic addition to Initiative failed. Please try again later.", - }); - } - }; - - const toggleEpicModal = (value: boolean) => setIsEpicModalOpen(value); - return ( <> {shouldRenderProjects && ( @@ -134,7 +73,7 @@ export const InitiativeCollapsibleSection: FC = observer((props) => { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - setIsProjectsOpen(!isProjectsOpen); + toggleProjectModal(true); }} disabled={disabled} > @@ -165,7 +104,7 @@ export const InitiativeCollapsibleSection: FC = observer((props) => { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - setIsEpicModalOpen(!isEpicModalOpen); + toggleEpicModal(); }} disabled={disabled} > @@ -226,25 +165,6 @@ export const InitiativeCollapsibleSection: FC = observer((props) => { onToggle={() => toggleOpenCollapsibleSection("attachments")} /> )} - - setIsProjectsOpen(false)} - onSubmit={handleProjectsUpdate} - selectedProjectIds={projectsIds ?? []} - projectIds={workspaceProjectIds || []} - /> - setIsEpicModalOpen(false)} - handleOnSubmit={async (data) => { - handleAddEpicToInitiative(data.map((epic) => epic.id)); - }} - /> ); }); diff --git a/web/ee/components/initiatives/details/main/root.tsx b/web/ee/components/initiatives/details/main/root.tsx index a6fe8d6a5b..982ec03635 100644 --- a/web/ee/components/initiatives/details/main/root.tsx +++ b/web/ee/components/initiatives/details/main/root.tsx @@ -16,10 +16,12 @@ type Props = { workspaceSlug: string; initiativeId: string; disabled?: boolean; + toggleEpicModal: (value?: boolean) => void; + toggleProjectModal: (value?: boolean) => void; }; export const InitiativeMainContentRoot: FC = observer((props) => { - const { workspaceSlug, initiativeId, disabled = false } = props; + const { workspaceSlug, initiativeId, disabled = false, toggleEpicModal, toggleProjectModal } = props; // store hooks const { initiativesSidebarCollapsed } = useAppTheme(); @@ -27,7 +29,13 @@ export const InitiativeMainContentRoot: FC = observer((props) => { - + ); diff --git a/web/ee/components/initiatives/details/root.tsx b/web/ee/components/initiatives/details/root.tsx index d977820d97..0833e2569c 100644 --- a/web/ee/components/initiatives/details/root.tsx +++ b/web/ee/components/initiatives/details/root.tsx @@ -1,11 +1,15 @@ +import { useState } from "react"; import { observer } from "mobx-react"; // hooks -import { useUserPermissions } from "@/hooks/store"; +import { setToast, TOAST_TYPE } from "@plane/ui"; +import { ProjectMultiSelectModal } from "@/components/project"; +import { useProject, useUserPermissions } from "@/hooks/store"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants"; import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; // local components import { LayoutRoot } from "../../common"; import { InitiativeEmptyState } from "../details/empty-state"; +import { WorkspaceEpicsListModal } from "./main/collapsible-section/epics/workspace-epic-modal"; import { InitiativeMainContentRoot } from "./main/root"; import { InitiativeSidebarRoot } from "./sidebar/root"; @@ -16,27 +20,113 @@ type Props = { export const InitiativeDetailRoot = observer((props: Props) => { const { workspaceSlug, initiativeId } = props; + + // states + const [isProjectsOpen, setIsProjectsOpen] = useState(false); + const [isEpicModalOpen, setIsEpicModalOpen] = useState(false); // store hooks const { - initiative: { getInitiativeById }, + initiative: { getInitiativeById, updateInitiative, addEpicsToInitiative }, } = useInitiatives(); + const { workspaceProjectIds } = useProject(); const { allowPermissions } = useUserPermissions(); // derived values const initiative = getInitiativeById(initiativeId); - + const projectsIds = initiative?.project_ids ?? []; const isEditable = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE ); + // handlers + const handleProjectsUpdate = async (initiativeProjectIds: string[]) => { + if (!initiativeId) return; + + if (initiativeProjectIds.length === 0) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Please select at least one project.", + }); + return; + } + + await updateInitiative(workspaceSlug?.toString(), initiativeId, { project_ids: initiativeProjectIds }) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: `Initiative projects updated successfully.`, + }); + }) + .catch((error) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: error?.error ?? `Failed to update initiative projects. Please try again!`, + }); + }); + }; + + const handleAddEpicToInitiative = async (epicIds: string[]) => { + try { + addEpicsToInitiative(workspaceSlug?.toString(), initiativeId, epicIds).then(() => { + setToast({ + title: "Success!", + type: TOAST_TYPE.SUCCESS, + message: `Epic${epicIds.length > 1 ? "s" : ""} added to Initiative successfully.`, + }); + }); + } catch { + setToast({ + title: "Error!", + type: TOAST_TYPE.ERROR, + message: "Epic addition to Initiative failed. Please try again later.", + }); + } + }; + + const toggleEpicModal = (value?: boolean) => setIsEpicModalOpen(value || !isEpicModalOpen); + const toggleProjectsModal = (value?: boolean) => setIsProjectsOpen(value || !isProjectsOpen); + return ( } > - - + + + setIsProjectsOpen(false)} + onSubmit={handleProjectsUpdate} + selectedProjectIds={projectsIds ?? []} + projectIds={workspaceProjectIds || []} + /> + setIsEpicModalOpen(false)} + handleOnSubmit={async (data) => { + handleAddEpicToInitiative(data.map((epic) => epic.id)); + }} + /> ); }); diff --git a/web/ee/components/initiatives/details/sidebar/properties-tab-root.tsx b/web/ee/components/initiatives/details/sidebar/properties-tab-root.tsx index 92749ebe55..c3ee1abc0b 100644 --- a/web/ee/components/initiatives/details/sidebar/properties-tab-root.tsx +++ b/web/ee/components/initiatives/details/sidebar/properties-tab-root.tsx @@ -3,9 +3,9 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { Briefcase, Calendar, UserCircle2 } from "lucide-react"; -import { setToast, TOAST_TYPE } from "@plane/ui"; +import { EpicIcon } from "@plane/ui"; // components -import { DateRangeDropdown, MemberDropdown, ProjectDropdown } from "@/components/dropdowns"; +import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; @@ -14,22 +14,26 @@ import { useMember } from "@/hooks/store"; // plane web components import { SidebarContentWrapper } from "@/plane-web/components/common/layout/sidebar/content-wrapper"; import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; - type Props = { workspaceSlug: string; initiativeId: string; disabled: boolean; + toggleEpicModal: (value?: boolean) => void; + toggleProjectModal: (value?: boolean) => void; }; export const InitiativeSidebarPropertiesRoot: FC = observer((props) => { - const { workspaceSlug, initiativeId, disabled } = props; + const { workspaceSlug, initiativeId, disabled, toggleEpicModal, toggleProjectModal } = props; const { - initiative: { getInitiativeById, updateInitiative }, + initiative: { getInitiativeById, updateInitiative, getInitiativeEpicsById }, } = useInitiatives(); const { getUserDetails } = useMember(); + // derived values + const initiativeEpicIds = getInitiativeEpicsById(initiativeId) ?? []; const initiative = initiativeId ? getInitiativeById(initiativeId) : undefined; + const initiativeProjectIds = initiative?.project_ids || []; const createdByDetails = initiative ? getUserDetails(initiative?.created_by) : undefined; if (!initiative) return <>; @@ -46,37 +50,6 @@ export const InitiativeSidebarPropertiesRoot: FC = observer((props) => { lead: id, }); - const handleProjects = (ids: string | string[]) => { - const projectIds = ids ? (Array.isArray(ids) ? ids : [ids]) : []; - - if (projectIds.length === 0) { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Please select at least one project.", - }); - return; - } - - updateInitiative(workspaceSlug.toString(), initiative.id, { - project_ids: projectIds, - }) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Initiative projects updated successfully.", - }); - }) - .catch((error) => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: error?.error ?? "Failed to update initiative projects. Please try again!", - }); - }); - }; - return (
@@ -86,13 +59,12 @@ export const InitiativeSidebarPropertiesRoot: FC = observer((props) => { Projects
- + {/* Lead Drop down*/}
@@ -111,6 +83,19 @@ export const InitiativeSidebarPropertiesRoot: FC = observer((props) => { showUserDetails />
+ {/* Epics dropdown */} +
+
+ + Epic +
+ +
{/* Dates Drop down*/}
@@ -119,7 +104,7 @@ export const InitiativeSidebarPropertiesRoot: FC = observer((props) => {
void; + toggleProjectModal: (value?: boolean) => void; }; export const InitiativeSidebarRoot: FC = observer((props) => { - const { workspaceSlug, initiativeId, disabled = false } = props; + const { workspaceSlug, initiativeId, disabled = false, toggleEpicModal, toggleProjectModal } = props; // store hooks const { initiativesSidebarCollapsed } = useAppTheme(); @@ -34,6 +36,8 @@ export const InitiativeSidebarRoot: FC = observer((props) => { workspaceSlug={workspaceSlug} initiativeId={initiativeId} disabled={disabled} + toggleEpicModal={toggleEpicModal} + toggleProjectModal={toggleProjectModal} /> ), },