diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index ffbe67ca5..cb2330b24 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -81,7 +81,7 @@ export const CommandPalette: React.FC = () => { const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false); - const [searchTerm, setSearchTerm] = React.useState(""); + const [searchTerm, setSearchTerm] = useState(""); const [results, setResults] = useState({ results: { workspace: [], diff --git a/apps/app/components/core/existing-issues-list-modal.tsx b/apps/app/components/core/existing-issues-list-modal.tsx index fe6f1e1b4..e9a004a5e 100644 --- a/apps/app/components/core/existing-issues-list-modal.tsx +++ b/apps/app/components/core/existing-issues-list-modal.tsx @@ -1,23 +1,24 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { mutate } from "swr"; -// react-hook-form -import { Controller, SubmitHandler, useForm } from "react-hook-form"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; +// services +import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; import useIssuesView from "hooks/use-issues-view"; +import useDebounce from "hooks/use-debounce"; // ui -import { PrimaryButton, SecondaryButton } from "components/ui"; +import { Loader, PrimaryButton, SecondaryButton } from "components/ui"; // icons -import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // types -import { IIssue } from "types"; +import { ISearchIssueResponse, TProjectIssuesSearchParams } from "types"; // fetch-keys import { CYCLE_DETAILS, @@ -26,27 +27,30 @@ import { MODULE_ISSUES_WITH_PARAMS, } from "constants/fetch-keys"; -type FormInput = { - issues: string[]; -}; - type Props = { isOpen: boolean; handleClose: () => void; - issues: IIssue[]; - handleOnSubmit: any; + searchParams: Partial; + handleOnSubmit: (data: ISearchIssueResponse[]) => Promise; }; export const ExistingIssuesListModal: React.FC = ({ isOpen, handleClose: onClose, - issues, + searchParams, handleOnSubmit, }) => { - const [query, setQuery] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); + const [issues, setIssues] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isSearching, setIsSearching] = useState(false); + const [selectedIssues, setSelectedIssues] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + + const debouncedSearchTerm: string = useDebounce(searchTerm, 500); const router = useRouter(); - const { cycleId, moduleId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { setToastAlert } = useToast(); @@ -54,23 +58,12 @@ export const ExistingIssuesListModal: React.FC = ({ const handleClose = () => { onClose(); - setQuery(""); - reset(); + setSearchTerm(""); + setSelectedIssues([]); }; - const { - handleSubmit, - reset, - control, - formState: { isSubmitting }, - } = useForm({ - defaultValues: { - issues: [], - }, - }); - - const onSubmit: SubmitHandler = async (data) => { - if (!data.issues || data.issues.length === 0) { + const onSubmit = async () => { + if (selectedIssues.length === 0) { setToastAlert({ type: "error", title: "Error!", @@ -80,11 +73,15 @@ export const ExistingIssuesListModal: React.FC = ({ return; } - await handleOnSubmit(data); + setIsSubmitting(true); + + await handleOnSubmit(selectedIssues).finally(() => setIsSubmitting(false)); + if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)); mutate(CYCLE_DETAILS(cycleId as string)); } + if (moduleId) { mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params)); mutate(MODULE_DETAILS(moduleId as string)); @@ -95,18 +92,45 @@ export const ExistingIssuesListModal: React.FC = ({ setToastAlert({ title: "Success", type: "success", - message: `Issue${data.issues.length > 1 ? "s" : ""} added successfully`, + message: `Issue${selectedIssues.length > 1 ? "s" : ""} added successfully`, }); }; - const filteredIssues: IIssue[] = - query === "" - ? issues ?? [] - : issues.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; + useEffect(() => { + if (!workspaceSlug || !projectId) return; + + setIsLoading(true); + + if (debouncedSearchTerm) { + setIsSearching(true); + + projectService + .projectIssuesSearch(workspaceSlug as string, projectId as string, { + search: debouncedSearchTerm, + ...searchParams, + }) + .then((res) => { + setIssues(res); + }) + .finally(() => { + setIsLoading(false); + setIsSearching(false); + }); + } else { + setIssues([]); + setIsLoading(false); + setIsSearching(false); + } + }, [debouncedSearchTerm, workspaceSlug, projectId, searchParams]); return ( <> - setQuery("")} appear> + setSearchTerm("")} + appear + > = ({ leaveTo="opacity-0 scale-95" > -
- ( - -
-
+ { + if (selectedIssues.some((i) => i.id === val.id)) + setSelectedIssues((prevData) => prevData.filter((i) => i.id !== val.id)); + else setSelectedIssues((prevData) => [...prevData, val]); + }} + > +
+
- - {filteredIssues.length > 0 ? ( -
  • - {query === "" && ( -

    - Select issues to add -

    - )} -
      - {filteredIssues.map((issue) => ( - - `flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${ - active ? "bg-brand-surface-2 text-brand-base" : "" - } ${selected ? "text-brand-base" : ""}` - } - > - {({ selected }) => ( - <> - - - - {issue.project_detail.identifier}-{issue.sequence_id} - - {issue.name} - - )} - - ))} -
    -
  • - ) : ( -
    - -

    - No issues found. Create a new issue with{" "} -
    C
    - . -

    -
    - )} -
    -
    +
    + {selectedIssues.length > 0 ? ( +
    + {selectedIssues.map((issue) => ( +
    + {issue.project__identifier}-{issue.sequence_id} + +
    + ))} +
    + ) : ( +
    + No issues selected +
    )} - /> - {filteredIssues.length > 0 && ( -
    - Cancel - - {isSubmitting ? "Adding..." : "Add selected issues"} - -
    - )} - +
    + + + {debouncedSearchTerm !== "" && ( +
    + Search results for{" "} + + {'"'} + {debouncedSearchTerm} + {'"'} + {" "} + in project: +
    + )} + + {!isLoading && + issues.length === 0 && + searchTerm !== "" && + debouncedSearchTerm !== "" && ( +
    + +

    + No issues found. Create a new issue with{" "} +
    +                              C
    +                            
    + . +

    +
    + )} + + {isLoading || isSearching ? ( + + + + + + + ) : ( +
      0 ? "p-2" : ""}`}> + {issues.map((issue) => { + const selected = selectedIssues.some((i) => i.id === issue.id); + + return ( + + `flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${ + active ? "bg-brand-surface-2 text-brand-base" : "" + } ${selected ? "text-brand-base" : ""}` + } + > + + + + {issue.project__identifier}-{issue.sequence_id} + + {issue.name} + + ); + })} +
    + )} +
    +
    + {selectedIssues.length > 0 && ( +
    + Cancel + + {isSubmitting ? "Adding..." : "Add selected issues"} + +
    + )}
    diff --git a/apps/app/components/issues/parent-issues-list-modal.tsx b/apps/app/components/issues/parent-issues-list-modal.tsx index 7510d5e75..b93c07d3c 100644 --- a/apps/app/components/issues/parent-issues-list-modal.tsx +++ b/apps/app/components/issues/parent-issues-list-modal.tsx @@ -1,23 +1,28 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; + +import { useRouter } from "next/router"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; -// icons -import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; -// ui -import { PrimaryButton, SecondaryButton } from "components/ui"; -// types -import { IIssue } from "types"; +// services +import projectService from "services/project.service"; +// hooks +import useDebounce from "hooks/use-debounce"; +// components import { LayerDiagonalIcon } from "components/icons"; +// ui +import { Loader } from "components/ui"; +// icons +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +// types +import { ISearchIssueResponse } from "types"; type Props = { isOpen: boolean; handleClose: () => void; value?: any; onChange: (...event: any[]) => void; - issues: IIssue[]; - title?: string; - multiple?: boolean; + issueId?: string; customDisplay?: JSX.Element; }; @@ -26,28 +31,60 @@ export const ParentIssuesListModal: React.FC = ({ handleClose: onClose, value, onChange, - issues, - title = "Issues", - multiple = false, + issueId, customDisplay, }) => { - const [query, setQuery] = useState(""); - const [values, setValues] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [issues, setIssues] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isSearching, setIsSearching] = useState(false); + + const debouncedSearchTerm: string = useDebounce(searchTerm, 500); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; const handleClose = () => { onClose(); - setQuery(""); - setValues([]); + setSearchTerm(""); }; - const filteredIssues: IIssue[] = - query === "" - ? issues ?? [] - : issues?.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; + useEffect(() => { + if (!workspaceSlug || !projectId) return; + + setIsLoading(true); + + if (debouncedSearchTerm) { + setIsSearching(true); + + projectService + .projectIssuesSearch(workspaceSlug as string, projectId as string, { + search: debouncedSearchTerm, + parent: true, + issue_id: issueId, + }) + .then((res) => { + setIssues(res); + }) + .finally(() => { + setIsLoading(false); + setIsSearching(false); + }); + } else { + setIssues([]); + setIsLoading(false); + setIsSearching(false); + } + }, [debouncedSearchTerm, workspaceSlug, projectId, issueId]); return ( <> - setQuery("")} appear> + setSearchTerm("")} + appear + > = ({ leaveTo="opacity-0 scale-95" > - {multiple ? ( - <> - ({})} multiple> -
    -
    - {customDisplay &&
    {customDisplay}
    } - - {filteredIssues.length > 0 && ( -
  • - {query === "" && ( -

    {title}

    - )} -
      - {filteredIssues.map((issue) => ( - - `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${ - active ? "bg-brand-surface-2 text-brand-base" : "" - } ${selected ? "text-brand-base" : ""}` - } - > - {({ selected }) => ( - <> - - - - {issue.project_detail?.identifier}-{issue.sequence_id} - {" "} - {issue.id} - - )} - - ))} -
    -
  • - )} -
    + +
    +
    + {customDisplay &&
    {customDisplay}
    } + + {debouncedSearchTerm !== "" && ( +
    + Search results for{" "} + + {'"'} + {debouncedSearchTerm} + {'"'} + {" "} + in project: +
    + )} - {query !== "" && filteredIssues.length === 0 && ( -
    -
    - )} -
    -
    - Cancel - onChange(values)}>Add issues -
    - - ) : ( - -
    -
    - {customDisplay &&
    {customDisplay}
    } - - {filteredIssues.length > 0 ? ( -
  • - {query === "" && ( -

    {title}

    - )} -
      - {filteredIssues.map((issue) => ( - - `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${ - active ? "bg-brand-surface-2 text-brand-base" : "" - } ${selected ? "text-brand-base" : ""}` - } - onClick={handleClose} - > - <> - - - {issue.project_detail?.identifier}-{issue.sequence_id} - {" "} - {issue.name} - - - ))} -
    -
  • - ) : ( + {!isLoading && + issues.length === 0 && + searchTerm !== "" && + debouncedSearchTerm !== "" && (

    @@ -208,9 +152,45 @@ export const ParentIssuesListModal: React.FC = ({

    )} -
    -
    - )} + + {isLoading || isSearching ? ( + + + + + + + ) : ( +
      0 ? "p-2" : ""}`}> + {issues.map((issue) => ( + + `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${ + active ? "bg-brand-surface-2 text-brand-base" : "" + } ${selected ? "text-brand-base" : ""}` + } + onClick={handleClose} + > + <> + + + {issue.project__identifier}-{issue.sequence_id} + {" "} + {issue.name} + + + ))} +
    + )} + +
    diff --git a/apps/app/components/issues/select/parent.tsx b/apps/app/components/issues/select/parent.tsx index c04e89b92..d73cd4e73 100644 --- a/apps/app/components/issues/select/parent.tsx +++ b/apps/app/components/issues/select/parent.tsx @@ -21,7 +21,6 @@ export const IssueParentSelect: React.FC = ({ control, isOpen, setIsOpen, isOpen={isOpen} handleClose={() => setIsOpen(false)} onChange={onChange} - issues={issues} /> )} /> diff --git a/apps/app/components/issues/sidebar-select/blocked.tsx b/apps/app/components/issues/sidebar-select/blocked.tsx index c07f80817..de8985792 100644 --- a/apps/app/components/issues/sidebar-select/blocked.tsx +++ b/apps/app/components/issues/sidebar-select/blocked.tsx @@ -3,299 +3,135 @@ import React, { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; -import useSWR from "swr"; - // react-hook-form -import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form"; -// headless ui -import { Combobox, Dialog, Transition } from "@headlessui/react"; +import { UseFormWatch } from "react-hook-form"; // hooks import useToast from "hooks/use-toast"; -// services -import issuesService from "services/issues.service"; -// ui -import { PrimaryButton, SecondaryButton } from "components/ui"; +import useProjectDetails from "hooks/use-project-details"; +// components +import { ExistingIssuesListModal } from "components/core"; // icons -import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { BlockedIcon, LayerDiagonalIcon } from "components/icons"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import { BlockedIcon } from "components/icons"; // types -import { IIssue, UserAuth } from "types"; -// fetch-keys -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; - -type FormInput = { - blocked_issue_ids: string[]; -}; +import { BlockeIssue, IIssue, ISearchIssueResponse, UserAuth } from "types"; type Props = { + issueId?: string; submitChanges: (formData: Partial) => void; - issuesList: IIssue[]; watch: UseFormWatch; userAuth: UserAuth; }; export const SidebarBlockedSelect: React.FC = ({ + issueId, submitChanges, - issuesList, watch, userAuth, }) => { - const [query, setQuery] = useState(""); const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); const { setToastAlert } = useToast(); + const { projectDetails } = useProjectDetails(); const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: issues } = useSWR( - workspaceSlug && projectId - ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) - : null, - workspaceSlug && projectId - ? () => issuesService.getIssues(workspaceSlug as string, projectId as string) - : null - ); - - const { - handleSubmit, - reset, - watch: watchBlocked, - setValue, - } = useForm({ - defaultValues: { - blocked_issue_ids: [], - }, - }); - const handleClose = () => { setIsBlockedModalOpen(false); - reset(); }; - const onSubmit: SubmitHandler = (data) => { - if (!data.blocked_issue_ids || data.blocked_issue_ids.length === 0) { + const onSubmit = async (data: ISearchIssueResponse[]) => { + if (data.length === 0) { setToastAlert({ title: "Error", type: "error", - message: "Please select atleast one issue", + message: "Please select at least one issue", }); + return; } - if (!Array.isArray(data.blocked_issue_ids)) data.blocked_issue_ids = [data.blocked_issue_ids]; + const selectedIssues: BlockeIssue[] = data.map((i) => ({ + blocked_issue_detail: { + id: i.id, + name: i.name, + sequence_id: i.sequence_id, + }, + })); - const newBlocked = [...watch("blocked_list"), ...data.blocked_issue_ids]; - submitChanges({ blocks_list: newBlocked }); + const newBlocked = [...watch("blocked_issues"), ...selectedIssues]; + + submitChanges({ + blocked_issues: newBlocked, + blocks_list: newBlocked.map((i) => i.blocked_issue_detail?.id ?? ""), + }); handleClose(); }; - const filteredIssues: IIssue[] = - query === "" - ? issuesList - : issuesList.filter( - (issue) => - issue.name.toLowerCase().includes(query.toLowerCase()) || - `${issue.project_detail.identifier}-${issue.sequence_id}` - .toLowerCase() - .includes(query.toLowerCase()) - ); - const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( -
    -
    - -

    Blocked by

    -
    -
    -
    - {watch("blocked_list") && watch("blocked_list").length > 0 - ? watch("blocked_list").map((issue) => ( -
    - i.id === issue)?.id - }`} - > - - - {`${issues?.find((i) => i.id === issue)?.project_detail?.identifier}-${ - issues?.find((i) => i.id === issue)?.sequence_id - }`} - - - -
    - )) - : null} + <> + setIsBlockedModalOpen(false)} + searchParams={{ blocker_blocked_by: true, issue_id: issueId }} + handleOnSubmit={onSubmit} + /> +
    +
    + +

    Blocked by

    - setQuery("")} - appear - > - - -
    - +
    +
    + {watch("blocked_issues") && watch("blocked_issues").length > 0 + ? watch("blocked_issues").map((issue) => ( +
    + + + + {`${projectDetails?.identifier}-${issue.blocked_issue_detail?.sequence_id}`} + + +
    -
    - + + +
    + )) + : null} +
    + +
    - + ); }; diff --git a/apps/app/components/issues/sidebar-select/blocker.tsx b/apps/app/components/issues/sidebar-select/blocker.tsx index aeede09bb..40f1eb10f 100644 --- a/apps/app/components/issues/sidebar-select/blocker.tsx +++ b/apps/app/components/issues/sidebar-select/blocker.tsx @@ -3,296 +3,137 @@ import React, { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; -import useSWR from "swr"; - // react-hook-form -import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form"; -// headless ui -import { Combobox, Dialog, Transition } from "@headlessui/react"; +import { UseFormWatch } from "react-hook-form"; // hooks import useToast from "hooks/use-toast"; -// services -import issuesServices from "services/issues.service"; -// ui -import { PrimaryButton, SecondaryButton } from "components/ui"; +import useProjectDetails from "hooks/use-project-details"; +// components +import { ExistingIssuesListModal } from "components/core"; // icons -import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { BlockerIcon, LayerDiagonalIcon } from "components/icons"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import { BlockerIcon } from "components/icons"; // types -import { IIssue, UserAuth } from "types"; -// fetch-keys -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; - -type FormInput = { - blocker_issue_ids: string[]; -}; +import { BlockeIssue, IIssue, ISearchIssueResponse, UserAuth } from "types"; type Props = { + issueId?: string; submitChanges: (formData: Partial) => void; - issuesList: IIssue[]; watch: UseFormWatch; userAuth: UserAuth; }; export const SidebarBlockerSelect: React.FC = ({ + issueId, submitChanges, - issuesList, watch, userAuth, }) => { - const [query, setQuery] = useState(""); const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); const { setToastAlert } = useToast(); + const { projectDetails } = useProjectDetails(); const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { data: issues } = useSWR( - workspaceSlug && projectId - ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) - : null, - workspaceSlug && projectId - ? () => issuesServices.getIssues(workspaceSlug as string, projectId as string) - : null - ); - - const { - handleSubmit, - reset, - watch: watchBlocker, - setValue, - } = useForm({ - defaultValues: { - blocker_issue_ids: [], - }, - }); - const handleClose = () => { setIsBlockerModalOpen(false); - reset(); }; - const onSubmit: SubmitHandler = (data) => { - if (!data.blocker_issue_ids || data.blocker_issue_ids.length === 0) { + const onSubmit = async (data: ISearchIssueResponse[]) => { + if (data.length === 0) { setToastAlert({ - title: "Error", type: "error", - message: "Please select atleast one issue", + title: "Error!", + message: "Please select at least one issue.", }); + return; } - if (!Array.isArray(data.blocker_issue_ids)) data.blocker_issue_ids = [data.blocker_issue_ids]; + const selectedIssues: BlockeIssue[] = data.map((i) => ({ + blocker_issue_detail: { + id: i.id, + name: i.name, + sequence_id: i.sequence_id, + }, + })); - const newBlockers = [...watch("blockers_list"), ...data.blocker_issue_ids]; - submitChanges({ blockers_list: newBlockers }); + const newBlockers = [...watch("blocker_issues"), ...selectedIssues]; + + submitChanges({ + blocker_issues: newBlockers, + blockers_list: newBlockers.map((i) => i.blocker_issue_detail?.id ?? ""), + }); handleClose(); }; - const filteredIssues: IIssue[] = - query === "" - ? issuesList - : issuesList.filter( - (issue) => - issue.name.toLowerCase().includes(query.toLowerCase()) || - `${issue.project_detail.identifier}-${issue.sequence_id}` - .toLowerCase() - .includes(query.toLowerCase()) - ); - const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( -
    -
    - -

    Blocking

    -
    -
    -
    - {watch("blockers_list") && watch("blockers_list").length > 0 - ? watch("blockers_list").map((issue) => ( -
    - i.id === issue)?.id - }`} - > - - - {`${issues?.find((i) => i.id === issue)?.project_detail?.identifier}-${ - issues?.find((i) => i.id === issue)?.sequence_id - }`} - - - -
    - )) - : null} + <> + setIsBlockerModalOpen(false)} + searchParams={{ blocker_blocked_by: true, issue_id: issueId }} + handleOnSubmit={onSubmit} + /> +
    +
    + +

    Blocking

    - setQuery("")} - appear - > - - -
    - - -
    - - - { - const selectedIssues = watchBlocker("blocker_issue_ids"); - if (selectedIssues.includes(val)) - setValue( - "blocker_issue_ids", - selectedIssues.filter((i) => i !== val) - ); - else setValue("blocker_issue_ids", [...selectedIssues, val]); - }} +
    +
    + {watch("blocker_issues") && watch("blocker_issues").length > 0 + ? watch("blocker_issues").map((issue) => ( +
    -
    -
    - - - {filteredIssues.length > 0 ? ( -
  • - {query === "" && ( -

    - Select blocker issues -

    - )} -
      - {filteredIssues.map((issue) => { - if ( - !watch("blockers_list").includes(issue.id) && - !watch("blocked_list").includes(issue.id) - ) - return ( - - `flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-brand-secondary ${ - active ? "bg-brand-surface-2 text-brand-base" : "" - } ` - } - > -
      - - - - { - issues?.find((i) => i.id === issue.id)?.project_detail - ?.identifier - } - -{issue.sequence_id} - - {issue.name} -
      -
      - ); - })} -
    -
  • - ) : ( -
    - -

    - No issues found. Create a new issue with{" "} -
    C
    . -

    -
    - )} -
    - + + + {`${projectDetails?.identifier}-${issue.blocker_issue_detail?.sequence_id}`} + + +
    -
    -
    - + submitChanges({ + blocker_issues: updatedBlockers, + blockers_list: updatedBlockers.map( + (i) => i.blocker_issue_detail?.id ?? "" + ), + }); + }} + > + + +
    + )) + : null} +
    + +
    - + ); }; diff --git a/apps/app/components/issues/sidebar-select/parent.tsx b/apps/app/components/issues/sidebar-select/parent.tsx index 92a51269f..9d183d262 100644 --- a/apps/app/components/issues/sidebar-select/parent.tsx +++ b/apps/app/components/issues/sidebar-select/parent.tsx @@ -20,7 +20,6 @@ import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; type Props = { control: Control; submitChanges: (formData: Partial) => void; - issuesList: IIssue[]; customDisplay: JSX.Element; watch: UseFormWatch; userAuth: UserAuth; @@ -29,7 +28,6 @@ type Props = { export const SidebarParentSelect: React.FC = ({ control, submitChanges, - issuesList, customDisplay, watch, userAuth, @@ -37,7 +35,7 @@ export const SidebarParentSelect: React.FC = ({ const [isParentModalOpen, setIsParentModalOpen] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, issueId } = router.query; const { data: issues } = useSWR( workspaceSlug && projectId @@ -68,8 +66,7 @@ export const SidebarParentSelect: React.FC = ({ submitChanges({ parent: val }); onChange(val); }} - issues={issuesList} - title="Select Parent" + issueId={issueId as string} value={value} customDisplay={customDisplay} /> diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index cdc815d3c..6f231871b 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -370,14 +370,6 @@ export const IssueDetailsSidebar: React.FC = ({ - i.id !== issueDetail?.id && - i.id !== issueDetail?.parent && - i.parent !== issueDetail?.id - ) ?? [] - } customDisplay={ issueDetail?.parent_detail ? ( ) : ( -
    +
    No parent selected
    ) @@ -400,16 +393,16 @@ export const IssueDetailsSidebar: React.FC = ({ )} {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( i.id !== issueDetail?.id) ?? []} watch={watchIssue} userAuth={memberRole} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && ( i.id !== issueDetail?.id) ?? []} watch={watchIssue} userAuth={memberRole} /> diff --git a/apps/app/components/issues/sub-issues-list.tsx b/apps/app/components/issues/sub-issues-list.tsx index 76424767e..ac550348e 100644 --- a/apps/app/components/issues/sub-issues-list.tsx +++ b/apps/app/components/issues/sub-issues-list.tsx @@ -21,7 +21,7 @@ import { ChevronRightIcon, PlusIcon, XMarkIcon } from "@heroicons/react/24/outli // helpers import { orderArrayBy } from "helpers/array.helper"; // types -import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types"; +import { ICurrentUserResponse, IIssue, ISearchIssueResponse, ISubIssueResponse } from "types"; // fetch-keys import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys"; @@ -58,14 +58,16 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => { : null ); - const addAsSubIssue = async (data: { issues: string[] }) => { + const addAsSubIssue = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId) return; + const payload = { + sub_issue_ids: data.map((i) => i.id), + }; + await issuesService - .addSubIssues(workspaceSlug as string, projectId as string, parentIssue?.id ?? "", { - sub_issue_ids: data.issues, - }) - .then((res) => { + .addSubIssues(workspaceSlug as string, projectId as string, parentIssue?.id ?? "", payload) + .then(() => { mutate( SUB_ISSUES(parentIssue?.id ?? ""), (prevData) => { @@ -74,10 +76,12 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => { const stateDistribution = { ...prevData.state_distribution }; - data.issues.forEach((issueId: string) => { + payload.sub_issue_ids.forEach((issueId: string) => { const issue = issues?.find((i) => i.id === issueId); + if (issue) { newSubIssues.push(issue); + const issueGroup = issue.state_detail.group; stateDistribution[issueGroup] = stateDistribution[issueGroup] + 1; } @@ -96,7 +100,7 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => { PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), (prevData) => (prevData ?? []).map((p) => { - if (data.issues.includes(p.id)) + if (payload.sub_issue_ids.includes(p.id)) return { ...p, parent: parentIssue.id, @@ -188,14 +192,7 @@ export const SubIssuesList: FC = ({ parentIssue, user }) => { setSubIssuesListModal(false)} - issues={ - issues?.filter( - (i) => - (i.parent === "" || i.parent === null) && - i.id !== parentIssue?.id && - i.id !== parentIssue?.parent - ) ?? [] - } + searchParams={{ sub_issue: true, issue_id: parentIssue?.id }} handleOnSubmit={addAsSubIssue} /> {subIssuesResponse && diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index af9f073a8..78af8e9e1 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; // icons import { ArrowLeftIcon } from "@heroicons/react/24/outline"; import { CyclesIcon } from "components/icons"; @@ -16,7 +16,6 @@ import { CycleDetailsSidebar } from "components/cycles"; // services import issuesService from "services/issues.service"; import cycleServices from "services/cycles.service"; -import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; import useUserAuth from "hooks/use-user-auth"; @@ -28,14 +27,10 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // helpers import { truncateText } from "helpers/string.helper"; import { getDateRangeStatus } from "helpers/date-time.helper"; +// types +import { ISearchIssueResponse } from "types"; // fetch-keys -import { - CYCLE_ISSUES, - CYCLES_LIST, - PROJECT_DETAILS, - CYCLE_DETAILS, - PROJECT_ISSUES_LIST, -} from "constants/fetch-keys"; +import { CYCLES_LIST, CYCLE_DETAILS } from "constants/fetch-keys"; const SingleCycle: React.FC = () => { const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); @@ -49,13 +44,6 @@ const SingleCycle: React.FC = () => { const { setToastAlert } = useToast(); - const { data: activeProject } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.getProject(workspaceSlug as string, projectId as string) - : null - ); - const { data: cycles } = useSWR( workspaceSlug && projectId ? CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId @@ -75,15 +63,6 @@ const SingleCycle: React.FC = () => { : null ); - const { data: issues } = useSWR( - workspaceSlug && projectId - ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) - : null, - workspaceSlug && projectId - ? () => issuesService.getIssues(workspaceSlug as string, projectId as string) - : null - ); - const cycleStatus = cycleDetails?.start_date && cycleDetails?.end_date ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) @@ -93,14 +72,21 @@ const SingleCycle: React.FC = () => { setCycleIssuesListModal(true); }; - const handleAddIssuesToCycle = async (data: { issues: string[] }) => { + const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId) return; + const payload = { + issues: data.map((i) => i.id), + }; + await issuesService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, data, user) - .then(() => { - mutate(CYCLE_ISSUES(cycleId as string)); - }) + .addIssueToCycle( + workspaceSlug as string, + projectId as string, + cycleId as string, + payload, + user + ) .catch(() => { setToastAlert({ type: "error", @@ -115,15 +101,15 @@ const SingleCycle: React.FC = () => { setCycleIssuesListModal(false)} - issues={issues?.filter((i) => !i.cycle_id) ?? []} + searchParams={{ cycle: true }} handleOnSubmit={handleAddIssuesToCycle} /> } @@ -142,7 +128,7 @@ const SingleCycle: React.FC = () => { {truncateText(cycle.name, 40)} diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 516a2471b..93bfecfe0 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -31,8 +31,6 @@ const defaultValues = { state: "", assignees_list: [], priority: "low", - blockers_list: [], - blocked_list: [], target_date: new Date().toString(), issue_cycle: null, issue_module: null, @@ -65,6 +63,7 @@ const IssueDetailsPage: NextPage = () => { ISSUE_DETAILS(issueId as string), (prevData) => { if (!prevData) return prevData; + return { ...prevData, ...formData, @@ -73,10 +72,13 @@ const IssueDetailsPage: NextPage = () => { false ); - const payload = { ...formData }; + const payload: Partial = { + ...formData, + }; + await issuesService .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) - .then((res) => { + .then(() => { mutateIssueDetails(); mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); }) @@ -93,12 +95,6 @@ const IssueDetailsPage: NextPage = () => { mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); reset({ ...issueDetails, - blockers_list: - issueDetails.blockers_list ?? - issueDetails.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id), - blocked_list: - issueDetails.blocks_list ?? - issueDetails.blocked_issues?.map((issue) => issue.blocked_issue_detail?.id), assignees_list: issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), labels_list: issueDetails.labels_list ?? issueDetails.labels, diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index d63af5865..51b6b7a5b 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -2,13 +2,12 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; // icons import { ArrowLeftIcon, RectangleGroupIcon } from "@heroicons/react/24/outline"; // services import modulesService from "services/modules.service"; -import issuesService from "services/issues.service"; // hooks import useToast from "hooks/use-toast"; import useUserAuth from "hooks/use-user-auth"; @@ -21,20 +20,14 @@ import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "component import { ModuleDetailsSidebar } from "components/modules"; import { AnalyticsProjectModal } from "components/analytics"; // ui -import { CustomMenu, EmptySpace, EmptySpaceItem, SecondaryButton, Spinner } from "components/ui"; +import { CustomMenu, SecondaryButton } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // helpers import { truncateText } from "helpers/string.helper"; // types -import { IModule } from "types"; - +import { ISearchIssueResponse } from "types"; // fetch-keys -import { - MODULE_DETAILS, - MODULE_ISSUES, - MODULE_LIST, - PROJECT_ISSUES_LIST, -} from "constants/fetch-keys"; +import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys"; const SingleModule: React.FC = () => { const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); @@ -48,15 +41,6 @@ const SingleModule: React.FC = () => { const { setToastAlert } = useToast(); - const { data: issues } = useSWR( - workspaceSlug && projectId - ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) - : null, - workspaceSlug && projectId - ? () => issuesService.getIssues(workspaceSlug as string, projectId as string) - : null - ); - const { data: modules } = useSWR( workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null, workspaceSlug && projectId @@ -76,7 +60,7 @@ const SingleModule: React.FC = () => { : null ); - const { data: moduleDetails } = useSWR( + const { data: moduleDetails } = useSWR( moduleId ? MODULE_DETAILS(moduleId as string) : null, workspaceSlug && projectId ? () => @@ -88,18 +72,21 @@ const SingleModule: React.FC = () => { : null ); - const handleAddIssuesToModule = async (data: { issues: string[] }) => { + const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId) return; + const payload = { + issues: data.map((i) => i.id), + }; + await modulesService .addIssuesToModule( workspaceSlug as string, projectId as string, moduleId as string, - data, + payload, user ) - .then(() => mutate(MODULE_ISSUES(moduleId as string))) .catch(() => setToastAlert({ type: "error", @@ -118,7 +105,7 @@ const SingleModule: React.FC = () => { setModuleIssuesListModal(false)} - issues={issues?.filter((i) => !i.module_id) ?? []} + searchParams={{ module: true }} handleOnSubmit={handleAddIssuesToModule} /> { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/search-issues/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new ProjectServices(); diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index 0ac0207be..a8881924b 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -69,12 +69,8 @@ export interface IIssue { assignees_list: string[]; attachment_count: number; attachments: any[]; - blocked_by_issue_details: any[]; - blocked_issue_details: any[]; blocked_issues: BlockeIssue[]; - blocked_list: string[]; blocker_issues: BlockeIssue[]; - blockers: any[]; blockers_list: string[]; blocks_list: string[]; bridge_id?: string | null; @@ -141,26 +137,14 @@ export interface ISubIssueResponse { } export interface BlockeIssue { - id: string; blocked_issue_detail?: BlockeIssueDetail; - created_at: Date; - updated_at: Date; - created_by: string; - updated_by: string; - project: string; - workspace: string; - block: string; - blocked_by: string; blocker_issue_detail?: BlockeIssueDetail; } export interface BlockeIssueDetail { id: string; name: string; - description: string; - priority: null; - start_date: null; - target_date: null; + sequence_id: number; } export interface IIssueComment { diff --git a/apps/app/types/projects.d.ts b/apps/app/types/projects.d.ts index 51cc4ba9a..c9972bc62 100644 --- a/apps/app/types/projects.d.ts +++ b/apps/app/types/projects.d.ts @@ -124,3 +124,25 @@ export interface GithubRepositoriesResponse { repositories: IGithubRepository[]; total_count: number; } + +export type TProjectIssuesSearchParams = { + search: string; + parent?: boolean; + blocker_blocked_by?: boolean; + cycle?: boolean; + module?: boolean; + sub_issue?: boolean; + issue_id?: string; +}; + +export interface ISearchIssueResponse { + id: string; + name: string; + project_id: string; + project__identifier: string; + sequence_id: number; + state__color: string; + state__group: string; + state__name: string; + workspace__slug: string; +}