diff --git a/apps/app/components/command-palette/addAsSubIssue.tsx b/apps/app/components/command-palette/addAsSubIssue.tsx new file mode 100644 index 000000000..41c3e3d28 --- /dev/null +++ b/apps/app/components/command-palette/addAsSubIssue.tsx @@ -0,0 +1,196 @@ +import React, { useState } from "react"; +// swr +import { mutate } from "swr"; +// react hook form +import { useForm } from "react-hook-form"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// hooks +import useUser from "lib/hooks/useUser"; +// icons +import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; +import { FolderIcon } from "@heroicons/react/24/outline"; +// commons +import { classNames } from "constants/common"; +// types +import { IIssue, IssueResponse } from "types"; +import { Button } from "ui"; +import { PROJECT_ISSUES_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import issuesServices from "lib/services/issues.services"; + +type Props = { + isOpen: boolean; + setIsOpen: React.Dispatch>; + parentId: string; +}; + +type FormInput = { + issue_ids: string[]; + cycleId: string; +}; + +const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parentId }) => { + const [query, setQuery] = useState(""); + + const { activeWorkspace, activeProject, issues } = useUser(); + + const filteredIssues: IIssue[] = + query === "" + ? issues?.results ?? [] + : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? + []; + + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + control, + reset, + setError, + } = useForm(); + + const handleCommandPaletteClose = () => { + setIsOpen(false); + setQuery(""); + reset(); + }; + + const addAsSubIssue = (issueId: string) => { + if (activeWorkspace && activeProject) { + issuesServices + .patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: parentId }) + .then((res) => { + mutate( + PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), + (prevData) => ({ + ...(prevData as IssueResponse), + results: (prevData?.results ?? []).map((p) => + p.id === issueId ? { ...p, ...res } : p + ), + }), + false + ); + }) + .catch((e) => { + console.log(e); + }); + } + }; + + return ( + <> + setQuery("")} appear> + + +
+ + +
+ + + { + // const { url, onClick } = item; + // if (url) router.push(url); + // else if (onClick) onClick(); + // handleCommandPaletteClose(); + // }} + > +
+
+ + + {filteredIssues.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Issues +

    + )} +
      + {filteredIssues.map((issue) => { + if (issue.parent === "" || issue.parent === null) + return ( + + classNames( + "flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + onClick={() => { + addAsSubIssue(issue.id); + setIsOpen(false); + }} + > + + {issue.name} + + ); + })} +
    +
  • + + )} +
    + + {query !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    +
    +
    +
    +
    +
    + + ); +}; + +export default AddAsSubIssue; diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index 16053564f..3c41d96e1 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; // swr import { mutate } from "swr"; // react hook form -import { SubmitHandler, useForm } from "react-hook-form"; +import { Controller, SubmitHandler, useForm } from "react-hook-form"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // hooks @@ -17,6 +17,7 @@ import { FolderIcon, RectangleStackIcon, ClipboardDocumentListIcon, + ArrowPathIcon, } from "@heroicons/react/24/outline"; // commons import { classNames, copyTextToClipboard } from "constants/common"; @@ -27,7 +28,7 @@ import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssue import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; // types import { IIssue, IProject, IssueResponse } from "types"; -import { Button } from "ui"; +import { Button, SearchListbox } from "ui"; import issuesServices from "lib/services/issues.services"; // fetch keys import { PROJECTS_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; @@ -40,6 +41,7 @@ type ItemType = { type FormInput = { issue_ids: string[]; + cycleId: string; }; const CommandPalette: React.FC = () => { @@ -69,6 +71,7 @@ const CommandPalette: React.FC = () => { register, formState: { errors, isSubmitting }, handleSubmit, + control, reset, setError, } = useForm(); @@ -143,10 +146,24 @@ const CommandPalette: React.FC = () => { ); const handleDelete: SubmitHandler = (data) => { - if (activeWorkspace && activeProject && data.issue_ids) { + if (!data.issue_ids || data.issue_ids.length === 0) { + setToastAlert({ + title: "Error", + type: "error", + message: "Please select atleast one issue", + }); + return; + } + + if (activeWorkspace && activeProject) { issuesServices .bulkDeleteIssues(activeWorkspace.slug, activeProject.id, data) .then((res) => { + setToastAlert({ + title: "Success", + type: "success", + message: res.message, + }); mutate( PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), (prevData) => { @@ -170,10 +187,30 @@ const CommandPalette: React.FC = () => { }; const handleAddToCycle: SubmitHandler = (data) => { - if (activeWorkspace && activeProject && data.issue_ids) { + if (!data.issue_ids || data.issue_ids.length === 0) { + setToastAlert({ + title: "Error", + type: "error", + message: "Please select atleast one issue", + }); + return; + } + + if (!data.cycleId) { + setToastAlert({ + title: "Error", + type: "error", + message: "Please select a cycle", + }); + return; + } + + if (activeWorkspace && activeProject) { issuesServices - .bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, "", data) - .then((res) => {}) + .bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, data.cycleId, data) + .then((res) => { + console.log(res); + }) .catch((e) => { console.log(e); }); @@ -230,7 +267,7 @@ const CommandPalette: React.FC = () => { leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - +
    { @@ -369,6 +406,23 @@ const CommandPalette: React.FC = () => {
    + ( + { + return { value: cycle.id, display: cycle.name }; + })} + multiple={false} + value={value} + onChange={onChange} + icon={} + /> + )} + /> diff --git a/apps/app/components/command-palette/shortcuts.tsx b/apps/app/components/command-palette/shortcuts.tsx index 2c77b95d4..91a8baab3 100644 --- a/apps/app/components/command-palette/shortcuts.tsx +++ b/apps/app/components/command-palette/shortcuts.tsx @@ -37,7 +37,7 @@ const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
    +
    = ({ isOpen, setIsOpen }) => { { title: "Navigation", shortcuts: [ - { key: "Ctrl + /", description: "To open navigator" }, - { key: "↑", description: "Move up" }, - { key: "↓", description: "Move down" }, - { key: "←", description: "Move left" }, - { key: "→", description: "Move right" }, - { key: "Enter", description: "Select" }, - { key: "Esc", description: "Close" }, + { keys: "ctrl,/", description: "To open navigator" }, + { keys: "↑", description: "Move up" }, + { keys: "↓", description: "Move down" }, + { keys: "←", description: "Move left" }, + { keys: "→", description: "Move right" }, + { keys: "Enter", description: "Select" }, + { keys: "Esc", description: "Close" }, ], }, { title: "Common", shortcuts: [ - { key: "Ctrl + p", description: "To create project" }, - { key: "Ctrl + i", description: "To create issue" }, - { key: "Ctrl + q", description: "To create cycle" }, - { key: "Ctrl + h", description: "To open shortcuts guide" }, + { keys: "ctrl,p", description: "To create project" }, + { keys: "ctrl,i", description: "To create issue" }, + { keys: "ctrl,q", description: "To create cycle" }, + { keys: "ctrl,h", description: "To open shortcuts guide" }, { - key: "Ctrl + alt + c", + keys: "ctrl,alt,c", description: "To copy issue url when on issue detail page.", }, ], }, ].map(({ title, shortcuts }) => ( -
    +

    {title}

    - {shortcuts.map(({ key, description }) => ( -
    + {shortcuts.map(({ keys, description }, index) => ( +

    {description}

    -
    - {key} +
    + {keys.split(",").map((key, index) => ( + + + {key} + + {/* {index !== keys.split(",").length - 1 ? ( + + + ) : null} */} + + ))}
    ))} diff --git a/apps/app/components/lexical/editor.tsx b/apps/app/components/lexical/editor.tsx index 3685de22e..e7cd68033 100644 --- a/apps/app/components/lexical/editor.tsx +++ b/apps/app/components/lexical/editor.tsx @@ -1,4 +1,3 @@ -import { FC } from "react"; import { EditorState, LexicalEditor, $getRoot, $getSelection } from "lexical"; import { LexicalComposer } from "@lexical/react/LexicalComposer"; import { ContentEditable } from "@lexical/react/LexicalContentEditable"; @@ -25,12 +24,15 @@ export interface RichTextEditorProps { onChange: (state: string) => void; id: string; value: string; + placeholder?: string; } -const RichTextEditor: FC = (props) => { - // props - const { onChange, value, id } = props; - +const RichTextEditor: React.FC = ({ + onChange, + id, + value, + placeholder = "Enter some text...", +}) => { function handleChange(state: EditorState, editor: LexicalEditor) { state.read(() => { onChange(JSON.stringify(state.toJSON())); @@ -54,8 +56,8 @@ const RichTextEditor: FC = (props) => { } ErrorBoundary={LexicalErrorBoundary} placeholder={ -
    - Enter some text... +
    + {placeholder}
    } /> diff --git a/apps/app/components/lexical/toolbar/block-type-select.tsx b/apps/app/components/lexical/toolbar/block-type-select.tsx index 235046b79..236665f6f 100644 --- a/apps/app/components/lexical/toolbar/block-type-select.tsx +++ b/apps/app/components/lexical/toolbar/block-type-select.tsx @@ -21,16 +21,8 @@ import { $isListNode, ListNode, } from "@lexical/list"; -import { - $isParentElementRTL, - $isAtNodeEnd, - $wrapNodes, -} from "@lexical/selection"; -import { - $createHeadingNode, - $createQuoteNode, - $isHeadingNode, -} from "@lexical/rich-text"; +import { $isParentElementRTL, $isAtNodeEnd, $wrapNodes } from "@lexical/selection"; +import { $createHeadingNode, $createQuoteNode, $isHeadingNode } from "@lexical/rich-text"; import { $createCodeNode, $isCodeNode, @@ -50,15 +42,7 @@ const BLOCK_DATA = [ { type: "ul", name: "Bulleted List" }, ]; -const supportedBlockTypes = new Set([ - "paragraph", - "quote", - "code", - "h1", - "h2", - "ul", - "ol", -]); +const supportedBlockTypes = new Set(["paragraph", "quote", "code", "h1", "h2", "ul", "ol"]); const blockTypeToBlockName: any = { code: "Code Block", @@ -84,8 +68,7 @@ export const BlockTypeSelect: FC = (props) => { // refs const dropDownRef = useRef(null); // states - const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = - useState(false); + const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(false); useEffect(() => { const toolbar = toolbarRef.current; @@ -205,6 +188,7 @@ export const BlockTypeSelect: FC = (props) => { return (
    - + - {isLink && - createPortal(, document.body)} + {isLink && createPortal(, document.body)}