diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index 3df2770c5..5ce741ae8 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -380,7 +380,6 @@ export const CommandPalette: React.FC = () => { user={user} /> )} - setIsIssueModalOpen(false)} diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index 872d6def2..02efa8f88 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -1,6 +1,5 @@ -import React, { ChangeEvent, FC, useState, useEffect, useRef } from "react"; +import React, { FC, useState, useEffect, useRef } from "react"; -import Link from "next/link"; import dynamic from "next/dynamic"; import { useRouter } from "next/router"; @@ -12,12 +11,12 @@ import aiService from "services/ai.service"; import useToast from "hooks/use-toast"; // components import { GptAssistantModal } from "components/core"; +import { ParentIssuesListModal } from "components/issues"; import { IssueAssigneeSelect, IssueDateSelect, IssueEstimateSelect, IssueLabelSelect, - IssueParentSelect, IssuePrioritySelect, IssueProjectSelect, IssueStateSelect, @@ -35,10 +34,8 @@ import { } from "components/ui"; // icons import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline"; -// helpers -import { cosineSimilarity } from "helpers/string.helper"; // types -import type { ICurrentUserResponse, IIssue } from "types"; +import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types"; // rich-text-editor const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { ssr: false, @@ -72,6 +69,7 @@ const defaultValues: Partial = { description_html: "

", estimate_point: null, state: "", + parent: null, priority: null, assignees: [], assignees_list: [], @@ -82,7 +80,6 @@ const defaultValues: Partial = { export interface IssueFormProps { handleFormSubmit: (values: Partial) => Promise; initialData?: Partial; - issues: IIssue[]; projectId: string; setActiveProject: React.Dispatch>; createMore: boolean; @@ -108,7 +105,6 @@ export interface IssueFormProps { export const IssueForm: FC = ({ handleFormSubmit, initialData, - issues = [], projectId, setActiveProject, createMore, @@ -118,11 +114,10 @@ export const IssueForm: FC = ({ user, fieldsToShow, }) => { - // states - const [mostSimilarIssue, setMostSimilarIssue] = useState(); const [stateModal, setStateModal] = useState(false); const [labelModal, setLabelModal] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); + const [selectedParentIssue, setSelectedParentIssue] = useState(null); const [gptAssistantModal, setGptAssistantModal] = useState(false); const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); @@ -151,12 +146,6 @@ export const IssueForm: FC = ({ const issueName = watch("name"); - const handleTitleChange = (e: ChangeEvent) => { - const value = e.target.value; - const similarIssue = issues?.find((i: IIssue) => cosineSimilarity(i.name, value) > 0.7); - setMostSimilarIssue(similarIssue); - }; - const handleCreateUpdateIssue = async (formData: Partial) => { await handleFormSubmit(formData); @@ -283,26 +272,28 @@ export const IssueForm: FC = ({ {watch("parent") && watch("parent") !== "" && - (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( + (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && + selectedParentIssue && (
i.id === watch("parent"))?.state_detail - .color, + backgroundColor: selectedParentIssue.state__color, }} /> - {/* {projects?.find((p) => p.id === projectId)?.identifier}- */} - {issues.find((i) => i.id === watch("parent"))?.sequence_id} + {selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id} - {issues.find((i) => i.id === watch("parent"))?.name.substring(0, 50)} + {selectedParentIssue.name.substring(0, 50)} setValue("parent", null)} + onClick={() => { + setValue("parent", null); + setSelectedParentIssue(null); + }} />
@@ -314,7 +305,6 @@ export const IssueForm: FC = ({ = ({ }, }} /> - {mostSimilarIssue && ( - - )} )} {(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
-
+
{issueName && issueName !== "" && (
)} {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( - ( + setParentIssueListModalOpen(false)} + onChange={(issue) => { + onChange(issue.id); + setSelectedParentIssue(issue); + }} + projectId={projectId} + /> + )} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( diff --git a/apps/app/components/issues/main-content.tsx b/apps/app/components/issues/main-content.tsx index f81933dbe..179df717a 100644 --- a/apps/app/components/issues/main-content.tsx +++ b/apps/app/components/issues/main-content.tsx @@ -58,7 +58,7 @@ export const IssueMainContent: React.FC = ({ <>
{issueDetails?.parent && issueDetails.parent !== "" ? ( -
+
= ({ - - {siblingIssues && siblingIssues.length > 0 ? ( - siblingIssues.map((issue: IIssue) => ( - - - - {issueDetails.project_detail.identifier}-{issue.sequence_id} - - - - )) + + {siblingIssues && siblingIssues.sub_issues.length > 0 ? ( + <> +

Sibling issues

+ {siblingIssues.sub_issues.map((issue) => { + if (issue.id !== issueDetails.id) + return ( + + {issueDetails.project_detail.identifier}-{issue.sequence_id} + + ); + })} + ) : ( - - No other sibling issues - +

+ No sibling issues +

)} + submitChanges({ parent: null })} + > + Remove parent issue +
) : null} diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index f1ec9db29..c8b8a398d 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +import { mutate } from "swr"; // headless ui import { Dialog, Transition } from "@headlessui/react"; @@ -94,15 +94,6 @@ export const CreateUpdateIssueModal: React.FC = ({ assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""], }; - const { data: issues } = useSWR( - workspaceSlug && activeProject - ? PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? "") - : null, - workspaceSlug && activeProject - ? () => issuesService.getIssues(workspaceSlug as string, activeProject ?? "") - : null - ); - useEffect(() => { if (projects && projects.length > 0) setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null); @@ -317,6 +308,8 @@ export const CreateUpdateIssueModal: React.FC = ({ else await updateIssue(payload); }; + if (!projects || projects.length === 0) return null; + return ( handleClose()}> @@ -345,7 +338,6 @@ export const CreateUpdateIssueModal: React.FC = ({ > void; value?: any; - onChange: (...event: any[]) => void; + onChange: (issue: ISearchIssueResponse) => void; + projectId: string; issueId?: string; customDisplay?: JSX.Element; }; @@ -31,6 +32,7 @@ export const ParentIssuesListModal: React.FC = ({ handleClose: onClose, value, onChange, + projectId, issueId, customDisplay, }) => { @@ -42,7 +44,7 @@ export const ParentIssuesListModal: React.FC = ({ const debouncedSearchTerm: string = useDebounce(searchTerm, 500); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; const handleClose = () => { onClose(); @@ -109,7 +111,13 @@ export const ParentIssuesListModal: React.FC = ({ leaveTo="opacity-0 scale-95" > - + { + onChange(val); + handleClose(); + }} + >
= ({ {issues.map((issue) => ( `flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${ active ? "bg-custom-background-80 text-custom-text-100" : "" } ${selected ? "text-custom-text-100" : ""}` } - onClick={handleClose} > <> ; - isOpen: boolean; - setIsOpen: React.Dispatch>; - issues: IIssue[]; -}; - -export const IssueParentSelect: React.FC = ({ control, isOpen, setIsOpen, issues }) => ( - ( - setIsOpen(false)} - onChange={onChange} - /> - )} - /> -); diff --git a/apps/app/components/issues/sidebar-select/parent.tsx b/apps/app/components/issues/sidebar-select/parent.tsx index 07d4e8317..6ca22045b 100644 --- a/apps/app/components/issues/sidebar-select/parent.tsx +++ b/apps/app/components/issues/sidebar-select/parent.tsx @@ -2,51 +2,31 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import useSWR from "swr"; - -import { Control, Controller, UseFormWatch } from "react-hook-form"; -// fetch keys +// icons import { UserIcon } from "@heroicons/react/24/outline"; -// services -import issuesServices from "services/issues.service"; // components import { ParentIssuesListModal } from "components/issues"; -// icons // types -import { IIssue, UserAuth } from "types"; -// fetch-keys -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import { IIssue, ISearchIssueResponse, UserAuth } from "types"; type Props = { - control: Control; - submitChanges: (formData: Partial) => void; - customDisplay: JSX.Element; - watch: UseFormWatch; + onChange: (value: string) => void; + issueDetails: IIssue | undefined; userAuth: UserAuth; disabled?: boolean; }; export const SidebarParentSelect: React.FC = ({ - control, - submitChanges, - customDisplay, - watch, + onChange, + issueDetails, userAuth, disabled = false, }) => { const [isParentModalOpen, setIsParentModalOpen] = useState(false); + const [selectedParentIssue, setSelectedParentIssue] = useState(null); const router = useRouter(); - const { workspaceSlug, projectId, issueId } = 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 { projectId, issueId } = router.query; const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled; @@ -57,22 +37,15 @@ export const SidebarParentSelect: React.FC = ({

Parent

- ( - setIsParentModalOpen(false)} - onChange={(val) => { - submitChanges({ parent: val }); - onChange(val); - }} - issueId={issueId as string} - value={value} - customDisplay={customDisplay} - /> - )} + setIsParentModalOpen(false)} + onChange={(issue) => { + onChange(issue.id); + setSelectedParentIssue(issue); + }} + issueId={issueId as string} + projectId={projectId as string} /> - ) : ( -
- No parent selected -
- ) - } - watch={watchIssue} - userAuth={memberRole} - disabled={uneditable} + name="parent" + render={({ field: { onChange } }) => ( + { + submitChanges({ parent: val }); + onChange(val); + }} + issueDetails={issueDetail} + userAuth={memberRole} + disabled={uneditable} + /> + )} /> )} {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( diff --git a/apps/app/components/issues/sub-issues-list.tsx b/apps/app/components/issues/sub-issues-list.tsx index cca143256..5c8c7df89 100644 --- a/apps/app/components/issues/sub-issues-list.tsx +++ b/apps/app/components/issues/sub-issues-list.tsx @@ -272,7 +272,7 @@ export const SubIssuesList: FC = ({ parentIssue, user, disabled = false } key={issue.id} href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`} > - +
= ({ parentIssue, user, disabled = false } Add sub-issue } + buttonClassName="whitespace-nowrap" position="left" noBorder noChevron diff --git a/apps/app/components/project/delete-project-modal.tsx b/apps/app/components/project/delete-project-modal.tsx index 863711f0b..2465b7cc0 100644 --- a/apps/app/components/project/delete-project-modal.tsx +++ b/apps/app/components/project/delete-project-modal.tsx @@ -175,7 +175,11 @@ export const DeleteProjectModal: React.FC = ({
Cancel - + {isDeleteLoading ? "Deleting..." : "Delete Project"}
diff --git a/apps/app/components/ui/dropdowns/custom-menu.tsx b/apps/app/components/ui/dropdowns/custom-menu.tsx index 91ad7f006..51e994dac 100644 --- a/apps/app/components/ui/dropdowns/custom-menu.tsx +++ b/apps/app/components/ui/dropdowns/custom-menu.tsx @@ -106,7 +106,7 @@ const CustomMenu = ({ ); type MenuItemProps = { - children: JSX.Element | string; + children: React.ReactNode; renderAs?: "button" | "a"; href?: string; onClick?: (args?: any) => void; diff --git a/apps/app/helpers/string.helper.ts b/apps/app/helpers/string.helper.ts index ae78d6f97..82fb363cf 100644 --- a/apps/app/helpers/string.helper.ts +++ b/apps/app/helpers/string.helper.ts @@ -49,45 +49,6 @@ export const copyTextToClipboard = async (text: string) => { await navigator.clipboard.writeText(text); }; -const wordsVector = (str: string) => { - const words = str.split(" "); - const vector: any = {}; - for (let i = 0; i < words.length; i++) { - const word = words[i]; - if (vector[word]) { - vector[word] += 1; - } else { - vector[word] = 1; - } - } - return vector; -}; - -export const cosineSimilarity = (a: string, b: string) => { - const vectorA = wordsVector(a.trim()); - const vectorB = wordsVector(b.trim()); - - const vectorAKeys = Object.keys(vectorA); - const vectorBKeys = Object.keys(vectorB); - - const union = vectorAKeys.concat(vectorBKeys); - - let dotProduct = 0; - let magnitudeA = 0; - let magnitudeB = 0; - - for (let i = 0; i < union.length; i++) { - const key = union[i]; - const valueA = vectorA[key] || 0; - const valueB = vectorB[key] || 0; - dotProduct += valueA * valueB; - magnitudeA += valueA * valueA; - magnitudeB += valueB * valueB; - } - - return dotProduct / Math.sqrt(magnitudeA * magnitudeB); -}; - export const generateRandomColor = (string: string): string => { if (!string) return "rgb(var(--color-primary-100))"; diff --git a/apps/app/services/issues.service.ts b/apps/app/services/issues.service.ts index 627d0093a..b848b3469 100644 --- a/apps/app/services/issues.service.ts +++ b/apps/app/services/issues.service.ts @@ -9,6 +9,7 @@ import type { IIssueComment, IIssueLabels, IIssueViewOptions, + ISubIssueResponse, } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -420,7 +421,11 @@ class ProjectIssuesServices extends APIService { }); } - async subIssues(workspaceSlug: string, projectId: string, issueId: string) { + async subIssues( + workspaceSlug: string, + projectId: string, + issueId: string + ): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/` )