diff --git a/components/command-palette/index.tsx b/components/command-palette/index.tsx index 0decad5a5..cd3870277 100644 --- a/components/command-palette/index.tsx +++ b/components/command-palette/index.tsx @@ -5,18 +5,18 @@ import { useRouter } from "next/router"; import { Combobox, Dialog, Transition } from "@headlessui/react"; // hooks import useUser from "lib/hooks/useUser"; +import useTheme from "lib/hooks/useTheme"; +import useToast from "lib/hooks/useToast"; // icons import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; import { DocumentPlusIcon, FolderPlusIcon, FolderIcon } from "@heroicons/react/24/outline"; // commons -import { classNames } from "constants/common"; +import { classNames, copyTextToClipboard } from "constants/common"; // components import ShortcutsModal from "components/command-palette/shortcuts"; import CreateProjectModal from "components/project/CreateProjectModal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; -// hooks -import useTheme from "lib/hooks/useTheme"; // types import { IIssue } from "types"; type ItemType = { @@ -40,6 +40,8 @@ const CommandPalette: React.FC = () => { const { toggleCollapsed } = useTheme(); + const { setToastAlert } = useToast(); + const filteredIssues: IIssue[] = query === "" ? issues?.results ?? [] @@ -72,7 +74,7 @@ const CommandPalette: React.FC = () => { const handleKeyDown = useCallback( (e: KeyboardEvent) => { - if (e.key === "/") { + if (e.ctrlKey && e.key === "/") { e.preventDefault(); setIsPaletteOpen(true); } else if (e.ctrlKey && e.key === "i") { @@ -90,9 +92,28 @@ const CommandPalette: React.FC = () => { } else if (e.ctrlKey && e.key === "q") { e.preventDefault(); setIsCreateCycleModalOpen(true); + } else if (e.ctrlKey && e.altKey && e.key === "c") { + e.preventDefault(); + + if (!router.query.issueId) return; + + const url = new URL(window.location.href); + copyTextToClipboard(url.href) + .then(() => { + setToastAlert({ + type: "success", + title: "Copied to clipboard", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Some error occurred", + }); + }); } }, - [toggleCollapsed] + [toggleCollapsed, setToastAlert, router] ); useEffect(() => { diff --git a/components/command-palette/shortcuts.tsx b/components/command-palette/shortcuts.tsx index e8db47684..f1942d134 100644 --- a/components/command-palette/shortcuts.tsx +++ b/components/command-palette/shortcuts.tsx @@ -59,7 +59,7 @@ const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { { title: "Navigation", shortcuts: [ - { key: "/", description: "To open navigator" }, + { key: "Ctrl + /", description: "To open navigator" }, { key: "↑", description: "Move up" }, { key: "↓", description: "Move down" }, { key: "←", description: "Move left" }, @@ -75,6 +75,10 @@ const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { { key: "Ctrl + i", description: "To open create issue modal" }, { key: "Ctrl + q", description: "To open create cycle modal" }, { key: "Ctrl + h", description: "To open shortcuts guide" }, + { + key: "Ctrl + alt + c", + description: "To copy issue url when on issue detail page.", + }, ], }, ].map(({ title, shortcuts }) => ( diff --git a/components/project/CreateProjectModal.tsx b/components/project/CreateProjectModal.tsx index 5b976847c..2010170c7 100644 --- a/components/project/CreateProjectModal.tsx +++ b/components/project/CreateProjectModal.tsx @@ -92,7 +92,6 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { const checkIdentifier = (slug: string, value: string) => { projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => { console.log(response); - if (response.exists) setError("identifier", { message: "Identifier already exists" }); }); }; diff --git a/components/project/cycles/CycleView.tsx b/components/project/cycles/CycleView.tsx index 5fc0bde36..7cc28ed54 100644 --- a/components/project/cycles/CycleView.tsx +++ b/components/project/cycles/CycleView.tsx @@ -130,11 +130,11 @@ const SprintView: React.FC = ({ - {issue.issue_details.state_detail.name} + {issue.issue_details.state_detail?.name}
diff --git a/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx b/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx index b98f35376..7bfcf207f 100644 --- a/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx +++ b/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx @@ -1,24 +1,21 @@ -import React, { useContext } from "react"; +import React from "react"; // swr import useSWR from "swr"; // react hook form import { Controller } from "react-hook-form"; -// headless ui -import { Listbox, Transition } from "@headlessui/react"; // service import projectServices from "lib/services/project.service"; // hooks import useUser from "lib/hooks/useUser"; // fetch keys import { PROJECT_MEMBERS } from "constants/fetch-keys"; -// icons -import { CheckIcon } from "@heroicons/react/20/solid"; - // types import type { Control } from "react-hook-form"; import type { IIssue, WorkspaceMember } from "types"; import { UserIcon } from "@heroicons/react/24/outline"; +import { SearchListbox } from "ui"; + type Props = { control: Control; }; @@ -38,86 +35,17 @@ const SelectAssignee: React.FC = ({ control }) => { control={control} name="assignees_list" render={({ field: { value, onChange } }) => ( - { + return { value: person.member.id, display: person.member.first_name }; + })} + multiple={true} value={value} - onChange={(data: any) => { - const valueCopy = [...(value ?? [])]; - if (valueCopy.some((i) => i === data)) onChange(valueCopy.filter((i) => i !== data)); - else onChange([...valueCopy, data]); - }} - > - {({ open }) => ( - <> -
- - - - {value && value.length > 0 - ? value - .map( - (id) => - people - ?.find((i) => i.member.id === id) - ?.member.email.substring(0, 4) + "..." - ) - .join(", ") - : "Assignees"} - - - - - -
- {people?.map((person) => ( - - `${ - active ? "text-white bg-theme" : "text-gray-900" - } cursor-pointer select-none relative p-2 rounded-md` - } - value={person.member.id} - > - {({ selected, active }) => ( - <> - i === person.member.id) - ? "font-semibold" - : "font-normal" - } block truncate`} - > - {person.member.email} - - - {selected ? ( - i === person.member.id) - ? "text-white" - : "text-indigo-600" - }`} - > - - ) : null} - - )} - - ))} -
-
-
-
- - )} -
+ onChange={onChange} + icon={} + /> )} > ); diff --git a/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx b/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx index ab2f9fada..7fead660b 100644 --- a/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx +++ b/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx @@ -1,12 +1,8 @@ import React from "react"; // react hook form import { Controller } from "react-hook-form"; -// headless ui -import { Listbox, Transition } from "@headlessui/react"; // hooks import useUser from "lib/hooks/useUser"; -// icons -import { CheckIcon } from "@heroicons/react/20/solid"; // types import type { IIssue } from "types"; import type { Control } from "react-hook-form"; @@ -16,12 +12,14 @@ type Props = { control: Control; }; +import { SearchListbox } from "ui"; + const SelectParent: React.FC = ({ control }) => { const { issues: projectIssues } = useUser(); const getSelectedIssueKey = (issueId: string | undefined) => { const identifier = projectIssues?.results?.find((i) => i.id.toString() === issueId?.toString()) - ?.project_detail.identifier; + ?.project_detail?.identifier; const sequenceId = projectIssues?.results?.find( (i) => i.id.toString() === issueId?.toString() @@ -36,53 +34,29 @@ const SelectParent: React.FC = ({ control }) => { control={control} name="parent" render={({ field: { value, onChange } }) => ( - - {({ open }) => ( - <> -
- - - {getSelectedIssueKey(value?.toString())} - - - - -
- {projectIssues?.results?.map((issue) => ( - - `relative cursor-pointer select-none p-2 rounded-md ${ - active ? "bg-theme text-white" : "text-gray-900" - }` - } - > - {({ active, selected }) => ( - <> - - - {issue.project_detail.identifier}-{issue.sequence_id} - {" "} - {issue.name} - - - )} - - ))} -
-
-
-
- - )} -
+ { + return { + value: issue.id, + display: issue.name, + element: ( +
+
+ {`${getSelectedIssueKey(issue.id)}`} + {issue.name} +
+
+ ), + }; + })} + value={value} + buttonClassName="max-h-30 overflow-y-scroll" + optionsClassName="max-h-30 overflow-y-scroll" + onChange={onChange} + icon={} + /> )} /> ); diff --git a/components/project/issues/CreateUpdateIssueModal/SelectProject.tsx b/components/project/issues/CreateUpdateIssueModal/SelectProject.tsx index 70a9138ce..2f188bbfb 100644 --- a/components/project/issues/CreateUpdateIssueModal/SelectProject.tsx +++ b/components/project/issues/CreateUpdateIssueModal/SelectProject.tsx @@ -6,8 +6,7 @@ import { Listbox, Transition } from "@headlessui/react"; // hooks import useUser from "lib/hooks/useUser"; // icons -import { CheckIcon } from "@heroicons/react/20/solid"; -import { ClipboardDocumentListIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline"; // ui import { Spinner } from "ui"; // types diff --git a/components/project/issues/CreateUpdateIssueModal/SelectState.tsx b/components/project/issues/CreateUpdateIssueModal/SelectState.tsx index f93da9b84..06522f063 100644 --- a/components/project/issues/CreateUpdateIssueModal/SelectState.tsx +++ b/components/project/issues/CreateUpdateIssueModal/SelectState.tsx @@ -1,16 +1,12 @@ import React from "react"; // react hook form import { Controller } from "react-hook-form"; -// headless ui -import { Listbox, Transition } from "@headlessui/react"; // hooks import useUser from "lib/hooks/useUser"; -// components -import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal"; // icons -import { CheckIcon, PlusIcon } from "@heroicons/react/20/solid"; +import { PlusIcon } from "@heroicons/react/20/solid"; // ui -import { Spinner } from "ui"; +import { CustomListbox } from "ui"; // types import type { Control } from "react-hook-form"; import type { IIssue } from "types"; @@ -18,11 +14,10 @@ import { Squares2X2Icon } from "@heroicons/react/24/outline"; type Props = { control: Control; - data?: IIssue; setIsOpen: React.Dispatch>; }; -const SelectState: React.FC = ({ control, data, setIsOpen }) => { +const SelectState: React.FC = ({ control, setIsOpen }) => { const { states } = useUser(); return ( @@ -31,90 +26,30 @@ const SelectState: React.FC = ({ control, data, setIsOpen }) => { control={control} name="state" render={({ field: { value, onChange } }) => ( - - {({ open }) => ( - <> -
- - - - {states?.find((i) => i.id === value)?.name ?? "State"} - - - - - -
- {states ? ( - states.filter((i) => i.id !== data?.id).length > 0 ? ( - states - .filter((i) => i.id !== data?.id) - .map((state) => ( - - `${ - active ? "text-white bg-theme" : "text-gray-900" - } cursor-pointer select-none relative p-2 rounded-md` - } - value={state.id} - > - {({ selected, active }) => ( - <> - - {state.name} - - - {selected ? ( - - - ) : null} - - )} - - )) - ) : ( -

No states found!

- ) - ) : ( -
- -
- )} -
- -
-
-
- - )} -
+ { + return { value: state.id, display: state.name }; + })} + value={value} + optionsFontsize="sm" + onChange={onChange} + icon={} + footerOption={ + + } + /> )} > diff --git a/components/project/issues/CreateUpdateIssueModal/index.tsx b/components/project/issues/CreateUpdateIssueModal/index.tsx index d27486795..a73094390 100644 --- a/components/project/issues/CreateUpdateIssueModal/index.tsx +++ b/components/project/issues/CreateUpdateIssueModal/index.tsx @@ -1,10 +1,18 @@ import React, { useEffect, useState } from "react"; +// next +import Link from "next/link"; +import { useRouter } from "next/router"; // swr import { mutate } from "swr"; // react hook form import { useForm } from "react-hook-form"; // fetching keys -import { PROJECT_ISSUES_DETAILS, PROJECT_ISSUES_LIST, CYCLE_ISSUES } from "constants/fetch-keys"; +import { + PROJECT_ISSUES_DETAILS, + PROJECT_ISSUES_LIST, + CYCLE_ISSUES, + USER_ISSUE, +} from "constants/fetch-keys"; // headless import { Dialog, Transition } from "@headlessui/react"; // services @@ -15,7 +23,7 @@ import useToast from "lib/hooks/useToast"; // ui import { Button, Input, TextArea } from "ui"; // commons -import { renderDateFormat } from "constants/common"; +import { renderDateFormat, cosineSimilarity } from "constants/common"; // components import SelectState from "./SelectState"; import SelectCycles from "./SelectCycles"; @@ -55,6 +63,10 @@ const CreateUpdateIssuesModal: React.FC = ({ const [isCycleModalOpen, setIsCycleModalOpen] = useState(false); const [isStateModalOpen, setIsStateModalOpen] = useState(false); + const [mostSimilarIssue, setMostSimilarIssue] = useState(); + + const router = useRouter(); + const handleClose = () => { setIsOpen(false); if (data) { @@ -69,7 +81,7 @@ const CreateUpdateIssuesModal: React.FC = ({ }, 500); }; - const { activeWorkspace, activeProject } = useUser(); + const { activeWorkspace, activeProject, user, issues } = useUser(); const { setToastAlert } = useToast(); @@ -165,6 +177,7 @@ const CreateUpdateIssuesModal: React.FC = ({ }, false ); + if (formData.sprints && formData.sprints !== null) { await addIssueToSprint(res.id, formData.sprints, formData); } @@ -175,6 +188,15 @@ const CreateUpdateIssuesModal: React.FC = ({ type: "success", message: `Issue ${data ? "updated" : "created"} successfully`, }); + if (formData.assignees_list.some((assignee) => assignee === user?.id)) { + mutate( + USER_ISSUE, + (prevData) => { + return [res, ...(prevData ?? [])]; + }, + false + ); + } }) .catch((err) => { Object.keys(err).map((key) => { @@ -235,6 +257,10 @@ const CreateUpdateIssuesModal: React.FC = ({ }); }, [data, prePopulateData, reset, projectId, activeProject, isOpen, watch]); + useEffect(() => { + return () => setMostSimilarIssue(undefined); + }, []); + return ( <> {activeProject && ( @@ -293,6 +319,13 @@ const CreateUpdateIssuesModal: React.FC = ({ label="Name" name="name" rows={1} + onChange={(e) => { + const value = e.target.value; + const similarIssue = issues?.results.find( + (i) => cosineSimilarity(i.name, value) > 0.7 + ); + setMostSimilarIssue(similarIssue?.id); + }} className="resize-none" placeholder="Enter name" autoComplete="off" @@ -302,6 +335,42 @@ const CreateUpdateIssuesModal: React.FC = ({ required: "Name is required", }} /> + {mostSimilarIssue && ( +
+

+ Did you mean{" "} + + ? +

+ +
+ )}