From 9727994c0815b03ca2c476be95f70f14c9e98af8 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 29 Nov 2022 19:49:39 +0530 Subject: [PATCH 1/6] style: issue details page, feat: add to cycles --- components/command-palette/index.tsx | 248 ++++++---- components/lexical/config.ts | 31 ++ components/lexical/editor.tsx | 73 +++ components/lexical/helpers/editor.ts | 33 ++ components/lexical/helpers/node.ts | 17 + components/lexical/plugins/code-highlight.tsx | 11 + components/lexical/plugins/read-only.tsx | 21 + components/lexical/theme.ts | 67 +++ .../lexical/toolbar/block-type-select.tsx | 333 ++++++++++++++ .../lexical/toolbar/floating-link-editor.tsx | 156 +++++++ components/lexical/toolbar/index.tsx | 430 ++++++++++++++++++ components/lexical/viewer.tsx | 58 +++ components/project/cycles/CycleView.tsx | 2 +- .../project/issues/BoardView/SingleBoard.tsx | 45 +- components/project/issues/BoardView/index.tsx | 255 ++++++----- .../SelectParentIssues.tsx | 1 + components/project/issues/ListView/index.tsx | 10 +- .../issue-detail/IssueDetailSidebar.tsx | 103 +++-- .../issues/issue-detail/activity/index.tsx | 2 +- components/project/memberInvitations.tsx | 2 +- components/workspace/SingleInvitation.tsx | 77 ++++ contexts/user.context.tsx | 14 +- layouts/Navbar/Sidebar.tsx | 99 ++-- lib/services/issues.services.ts | 2 - package.json | 4 + pages/editor.tsx | 53 +-- pages/invitations.tsx | 43 +- pages/me/my-issues.tsx | 4 +- pages/projects/[projectId]/cycles.tsx | 37 +- .../projects/[projectId]/issues/[issueId].tsx | 15 +- pages/projects/[projectId]/issues/index.tsx | 298 ++++++------ pages/projects/[projectId]/settings.tsx | 67 ++- pages/signin.tsx | 2 +- .../[invitationId].tsx | 128 +++--- public/favicon/android-chrome-192x192.png | Bin 4573 -> 2663 bytes public/favicon/android-chrome-512x512.png | Bin 14123 -> 11416 bytes public/favicon/apple-touch-icon.png | Bin 4046 -> 2324 bytes public/favicon/favicon-16x16.png | Bin 267 -> 287 bytes public/favicon/favicon-32x32.png | Bin 584 -> 366 bytes public/favicon/favicon.ico | Bin 15406 -> 15406 bytes public/favicon/site.webmanifest | 1 + ui/Button/index.tsx | 6 +- ui/SearchListbox/index.tsx | 6 +- ui/SearchListbox/types.d.ts | 2 +- ui/Tooltip/index.tsx | 2 +- yarn.lock | 191 ++++++++ 46 files changed, 2349 insertions(+), 600 deletions(-) create mode 100644 components/lexical/config.ts create mode 100644 components/lexical/editor.tsx create mode 100644 components/lexical/helpers/editor.ts create mode 100644 components/lexical/helpers/node.ts create mode 100644 components/lexical/plugins/code-highlight.tsx create mode 100644 components/lexical/plugins/read-only.tsx create mode 100644 components/lexical/theme.ts create mode 100644 components/lexical/toolbar/block-type-select.tsx create mode 100644 components/lexical/toolbar/floating-link-editor.tsx create mode 100644 components/lexical/toolbar/index.tsx create mode 100644 components/lexical/viewer.tsx create mode 100644 components/workspace/SingleInvitation.tsx create mode 100644 public/favicon/site.webmanifest diff --git a/components/command-palette/index.tsx b/components/command-palette/index.tsx index cd3870277..b626eac7a 100644 --- a/components/command-palette/index.tsx +++ b/components/command-palette/index.tsx @@ -9,7 +9,11 @@ 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"; +import { + FolderIcon, + RectangleStackIcon, + ClipboardDocumentListIcon, +} from "@heroicons/react/24/outline"; // commons import { classNames, copyTextToClipboard } from "constants/common"; // components @@ -19,12 +23,19 @@ import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssue import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; // types import { IIssue } from "types"; +import { Button } from "ui"; +import { SubmitHandler, useForm } from "react-hook-form"; + type ItemType = { name: string; url?: string; onClick?: () => void; }; +type FormInput = { + issue: string[]; +}; + const CommandPalette: React.FC = () => { const router = useRouter(); @@ -51,7 +62,7 @@ const CommandPalette: React.FC = () => { const quickActions = [ { name: "Add new issue...", - icon: DocumentPlusIcon, + icon: RectangleStackIcon, shortcut: "I", onClick: () => { setIsIssueModalOpen(true); @@ -59,7 +70,7 @@ const CommandPalette: React.FC = () => { }, { name: "Add new project...", - icon: FolderPlusIcon, + icon: ClipboardDocumentListIcon, shortcut: "P", onClick: () => { setIsProjectModalOpen(true); @@ -116,6 +127,23 @@ const CommandPalette: React.FC = () => { [toggleCollapsed, setToastAlert, router] ); + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + reset, + setError, + control, + } = useForm(); + + const handleDelete: SubmitHandler = (data) => { + console.log("Deleting... " + JSON.stringify(data)); + }; + + const handleAddToCycle: SubmitHandler = (data) => { + console.log("Adding to cycle..."); + }; + useEffect(() => { document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); @@ -137,7 +165,6 @@ const CommandPalette: React.FC = () => { setIsOpen={setIsIssueModalOpen} projectId={activeProject?.id} /> - { leaveTo="opacity-0 scale-95" > - { - const { url, onClick } = item; - if (url) router.push(url); - else if (onClick) onClick(); - handleCommandPaletteClose(); - }} - > -
-
- - + { + // const { url, onClick } = item; + // if (url) router.push(url); + // else if (onClick) onClick(); + // handleCommandPaletteClose(); + // }} > - {filteredIssues.length > 0 && ( - <> +
+
+ + + {filteredIssues.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Issues +

    + )} +
      + {filteredIssues.map((issue) => ( + + classNames( + "flex cursor-pointer select-none items-center rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + > + {({ active }) => ( + <> + {/* + ))} +
    +
  • + + )} + {query === "" && (
  • - {query === "" && ( -

    - Issues -

    - )} +

    Quick actions

      - {filteredIssues.map((issue) => ( + {quickActions.map((action) => ( classNames( - "flex cursor-pointer select-none items-center rounded-md px-3 py-2", + "flex cursor-default select-none items-center rounded-md px-3 py-2", active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" ) } > {({ active }) => ( <> - ))}
  • - - )} - {query === "" && ( -
  • -

    Quick actions

    -
      - {quickActions.map((action) => ( - - classNames( - "flex cursor-default select-none items-center rounded-md px-3 py-2", - active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" - ) - } - > - {({ active }) => ( - <> - - ))} -
    -
  • - )} -
    + )} +
    - {query !== "" && filteredIssues.length === 0 && ( -
    -
    diff --git a/components/lexical/config.ts b/components/lexical/config.ts new file mode 100644 index 000000000..41f9f1a67 --- /dev/null +++ b/components/lexical/config.ts @@ -0,0 +1,31 @@ +import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { TableCellNode, TableNode, TableRowNode } from "@lexical/table"; +import { ListItemNode, ListNode } from "@lexical/list"; +import { CodeHighlightNode, CodeNode } from "@lexical/code"; +import { AutoLinkNode, LinkNode } from "@lexical/link"; +// theme +import { defaultTheme } from "./theme"; + +export const initialConfig = { + namespace: "LexicalEditor", + // The editor theme + theme: defaultTheme, + // Handling of errors during update + onError(error: any) { + console.error(error); + }, + // Any custom nodes go here + nodes: [ + HeadingNode, + ListNode, + ListItemNode, + QuoteNode, + CodeNode, + CodeHighlightNode, + TableNode, + TableCellNode, + TableRowNode, + AutoLinkNode, + LinkNode, + ], +}; diff --git a/components/lexical/editor.tsx b/components/lexical/editor.tsx new file mode 100644 index 000000000..e4e9ef359 --- /dev/null +++ b/components/lexical/editor.tsx @@ -0,0 +1,73 @@ +import { FC } from "react"; +import { EditorState, LexicalEditor, $getRoot, $getSelection } from "lexical"; +import { LexicalComposer } from "@lexical/react/LexicalComposer"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; +import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; +import { ListPlugin } from "@lexical/react/LexicalListPlugin"; +import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; +import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; +import { TRANSFORMERS, CHECK_LIST } from "@lexical/markdown"; +import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"; +import { $generateHtmlFromNodes } from "@lexical/html"; +import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin"; + +// custom plugins +import { CodeHighlightPlugin } from "./plugins/code-highlight"; +import { LexicalToolbar } from "./toolbar"; +// config +import { initialConfig } from "./config"; +// helpers +import { getValidatedValue } from "./helpers/editor"; + +export interface RichTextEditorProps { + onChange: (state: string) => void; + id: string; + value: string; +} + +const RichTextEditor: FC = (props) => { + // props + const { onChange, value, id } = props; + + function handleChange(state: EditorState, editor: LexicalEditor) { + state.read(() => { + onChange(JSON.stringify(state.toJSON())); + }); + } + + return ( + +
    + +
    + + } + placeholder={ +
    + Enter some text... +
    + } + /> + + + + + + + +
    +
    +
    + ); +}; + +export default RichTextEditor; diff --git a/components/lexical/helpers/editor.ts b/components/lexical/helpers/editor.ts new file mode 100644 index 000000000..2f929a99b --- /dev/null +++ b/components/lexical/helpers/editor.ts @@ -0,0 +1,33 @@ +export const positionEditorElement = (editor: any, rect: any) => { + if (window) { + if (rect === null) { + editor.style.opacity = "0"; + editor.style.top = "-1000px"; + editor.style.left = "-1000px"; + } else { + editor.style.opacity = "1"; + editor.style.top = `${ + rect.top + rect.height + window.pageYOffset + 10 + }px`; + editor.style.left = `${ + rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2 + }px`; + } + } +}; + +export const getValidatedValue = (value: string) => { + const defaultValue = + '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'; + + if (value) { + try { + const json = JSON.parse(value); + return JSON.stringify(json); + } catch (error) { + return defaultValue; + } + } + + return defaultValue; +}; diff --git a/components/lexical/helpers/node.ts b/components/lexical/helpers/node.ts new file mode 100644 index 000000000..bedf95dd9 --- /dev/null +++ b/components/lexical/helpers/node.ts @@ -0,0 +1,17 @@ +import { $isAtNodeEnd } from "@lexical/selection"; + +export const getSelectedNode = (selection: any) => { + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = selection.anchor.getNode(); + const focusNode = selection.focus.getNode(); + if (anchorNode === focusNode) { + return anchorNode; + } + const isBackward = selection.isBackward(); + if (isBackward) { + return $isAtNodeEnd(focus) ? anchorNode : focusNode; + } else { + return $isAtNodeEnd(anchor) ? focusNode : anchorNode; + } +}; diff --git a/components/lexical/plugins/code-highlight.tsx b/components/lexical/plugins/code-highlight.tsx new file mode 100644 index 000000000..b75fb9af4 --- /dev/null +++ b/components/lexical/plugins/code-highlight.tsx @@ -0,0 +1,11 @@ +import { useEffect } from "react"; +import { registerCodeHighlighting } from "@lexical/code"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; + +export const CodeHighlightPlugin = () => { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + return registerCodeHighlighting(editor); + }, [editor]); + return null; +}; diff --git a/components/lexical/plugins/read-only.tsx b/components/lexical/plugins/read-only.tsx new file mode 100644 index 000000000..5c9b85db7 --- /dev/null +++ b/components/lexical/plugins/read-only.tsx @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react"; + +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { getValidatedValue } from "../helpers/editor"; + +const ReadOnlyPlugin = ({ value }: { value: string }) => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (editor && value) { + const initialEditorState = editor?.parseEditorState( + getValidatedValue(value) || "" + ); + editor.setEditorState(initialEditorState); + } + }, [editor, value]); + + return <>; +}; + +export default ReadOnlyPlugin; diff --git a/components/lexical/theme.ts b/components/lexical/theme.ts new file mode 100644 index 000000000..4ffe94ddd --- /dev/null +++ b/components/lexical/theme.ts @@ -0,0 +1,67 @@ +export const defaultTheme = { + ltr: "ltr", + rtl: "rtl", + placeholder: "editor-placeholder", + paragraph: "mb-1", + quote: "editor-quote", + heading: { + h1: "text-3xl font-bold", + h2: "text-2xl font-bold", + h3: "text-xl font-bold", + h4: "text-lg font-bold", + h5: "text-base font-bold", + }, + list: { + nested: { + listitem: "list-item", + }, + ol: "list-decimal pl-4", + ul: "list-disc pl-4", + listitem: "list-item", + }, + image: "editor-image", + link: "editor-link", + text: { + bold: "font-bold", + italic: "italic", + overflowed: "editor-text-overflowed", + hashtag: "editor-text-hashtag", + underline: "underline", + strikethrough: "line-through", + underlineStrikethrough: "editor-text-underlineStrikethrough", + code: "editor-text-code", + }, + code: "editor-code", + codeHighlight: { + atrule: "editor-tokenAttr", + attr: "editor-tokenAttr", + boolean: "editor-tokenProperty", + builtin: "editor-tokenSelector", + cdata: "editor-tokenComment", + char: "editor-tokenSelector", + class: "editor-tokenFunction", + "class-name": "editor-tokenFunction", + comment: "editor-tokenComment", + constant: "editor-tokenProperty", + deleted: "editor-tokenProperty", + doctype: "editor-tokenComment", + entity: "editor-tokenOperator", + function: "editor-tokenFunction", + important: "editor-tokenVariable", + inserted: "editor-tokenSelector", + keyword: "editor-tokenAttr", + namespace: "editor-tokenVariable", + number: "editor-tokenProperty", + operator: "editor-tokenOperator", + prolog: "editor-tokenComment", + property: "editor-tokenProperty", + punctuation: "editor-tokenPunctuation", + regex: "editor-tokenVariable", + selector: "editor-tokenSelector", + string: "editor-tokenSelector", + symbol: "editor-tokenProperty", + tag: "editor-tokenProperty", + url: "editor-tokenOperator", + variable: "editor-tokenVariable", + }, +}; diff --git a/components/lexical/toolbar/block-type-select.tsx b/components/lexical/toolbar/block-type-select.tsx new file mode 100644 index 000000000..235046b79 --- /dev/null +++ b/components/lexical/toolbar/block-type-select.tsx @@ -0,0 +1,333 @@ +import { FC, Fragment, useState, useEffect, useRef } from "react"; +import { createPortal } from "react-dom"; +import { + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, + REDO_COMMAND, + UNDO_COMMAND, + SELECTION_CHANGE_COMMAND, + FORMAT_TEXT_COMMAND, + FORMAT_ELEMENT_COMMAND, + $getSelection, + $isRangeSelection, + $createParagraphNode, + $getNodeByKey, +} from "lexical"; +import { + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, + INSERT_CHECK_LIST_COMMAND, + REMOVE_LIST_COMMAND, + $isListNode, + ListNode, +} from "@lexical/list"; +import { + $isParentElementRTL, + $isAtNodeEnd, + $wrapNodes, +} from "@lexical/selection"; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode, +} from "@lexical/rich-text"; +import { + $createCodeNode, + $isCodeNode, + getDefaultCodeLanguage, + getCodeLanguages, +} from "@lexical/code"; + +const BLOCK_DATA = [ + { type: "paragraph", name: "Normal" }, + { type: "h1", name: "Large Heading" }, + { type: "h2", name: "Small Heading" }, + { type: "h3", name: "Heading" }, + { type: "h4", name: "Heading" }, + { type: "h5", name: "Heading" }, + { type: "Quote", name: "quote" }, + { type: "ol", name: "Numbered List" }, + { type: "ul", name: "Bulleted List" }, +]; + +const supportedBlockTypes = new Set([ + "paragraph", + "quote", + "code", + "h1", + "h2", + "ul", + "ol", +]); + +const blockTypeToBlockName: any = { + code: "Code Block", + h1: "Large Heading", + h2: "Small Heading", + h3: "Heading", + h4: "Heading", + h5: "Heading", + ol: "Numbered List", + paragraph: "Normal", + quote: "Quote", + ul: "Bulleted List", +}; + +export interface BlockTypeSelectProps { + editor: any; + toolbarRef: any; + blockType: string; +} + +export const BlockTypeSelect: FC = (props) => { + const { editor, toolbarRef, blockType } = props; + // refs + const dropDownRef = useRef(null); + // states + const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = + useState(false); + + useEffect(() => { + const toolbar = toolbarRef.current; + const dropDown = dropDownRef.current; + + if (toolbar !== null && dropDown !== null) { + const { top, left } = toolbar.getBoundingClientRect(); + dropDown.style.top = `${top + 40}px`; + dropDown.style.left = `${left}px`; + } + }, [dropDownRef, toolbarRef]); + + useEffect(() => { + const dropDown = dropDownRef.current; + const toolbar = toolbarRef.current; + + if (dropDown !== null && toolbar !== null) { + const handle = (event: any) => { + const target = event.target; + + if (!dropDown.contains(target) && !toolbar.contains(target)) { + setShowBlockOptionsDropDown(false); + } + }; + document.addEventListener("click", handle); + + return () => { + document.removeEventListener("click", handle); + }; + } + }, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]); + + const formatParagraph = () => { + if (blockType !== "paragraph") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createParagraphNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatLargeHeading = () => { + console.log("blockType ", blockType); + if (blockType !== "h1") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h1")); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatSmallHeading = () => { + if (blockType !== "h2") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createHeadingNode("h2")); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatBulletList = () => { + if (blockType !== "ul") { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND); + } + setShowBlockOptionsDropDown(false); + }; + + const formatNumberedList = () => { + if (blockType !== "ol") { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND); + } + setShowBlockOptionsDropDown(false); + }; + + const formatQuote = () => { + if (blockType !== "quote") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createQuoteNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + const formatCode = () => { + if (blockType !== "code") { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createCodeNode()); + } + }); + } + setShowBlockOptionsDropDown(false); + }; + + return ( +
    + + {showBlockOptionsDropDown && ( +
      +
    • + + Normal + {blockType === "paragraph" && } +
    • +
    • + + Large Heading + {blockType === "h1" && } +
    • +
    • + + Small Heading + {blockType === "h2" && } +
    • +
    • + + Bullet List + {blockType === "ul" && } +
    • +
    • + + Numbered List + {blockType === "ol" && } +
    • + +
    • + + Quote + {blockType === "quote" && } +
    • + {/* */} +
    + )} +
    + ); +}; + +// export const BlockTypeSelect: FC = () => { +// const [selected, setSelected] = useState(BLOCK_DATA[0]); + +// return ( +//
    +// +//
    +// +// {selected.name} +// +// +// +// +// +// +// +// +// {BLOCK_DATA.map((blockType, index) => ( +// +// `relative cursor-default select-none py-2 px-2 ${ +// active ? 'bg-amber-100 text-amber-900' : 'text-gray-900' +// }` +// } +// value={blockType} +// > +// {({ selected }) => ( +// <> +// +// {blockType.name} +// +// +// )} +// +// ))} +// +// +//
    +//
    +//
    +// ); +// }; diff --git a/components/lexical/toolbar/floating-link-editor.tsx b/components/lexical/toolbar/floating-link-editor.tsx new file mode 100644 index 000000000..6eda2cda5 --- /dev/null +++ b/components/lexical/toolbar/floating-link-editor.tsx @@ -0,0 +1,156 @@ +import { useRef, useState, useCallback, useEffect } from 'react'; +import { SELECTION_CHANGE_COMMAND, $getSelection, $isRangeSelection } from 'lexical'; +import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { mergeRegister } from '@lexical/utils'; + +// helper functions +import { positionEditorElement } from '../helpers/editor'; +import { getSelectedNode } from '../helpers/node'; + +const LowPriority = 1; + +export interface FloatingLinkEditorProps { + editor: any; +} + +export const FloatingLinkEditor = ({ editor }: FloatingLinkEditorProps) => { + // refs + const editorRef = useRef(null); + const inputRef = useRef(null); + const mouseDownRef = useRef(false); + // states + const [linkUrl, setLinkUrl] = useState(''); + const [isEditMode, setEditMode] = useState(false); + const [lastSelection, setLastSelection] = useState(null); + + const updateLinkEditor = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent)) { + setLinkUrl(parent.getURL()); + } else if ($isLinkNode(node)) { + setLinkUrl(node.getURL()); + } else { + setLinkUrl(''); + } + } + const editorElem = editorRef.current; + const nativeSelection = window?.getSelection(); + const activeElement = document.activeElement; + + if (editorElem === null) { + return; + } + + const rootElement = editor.getRootElement(); + if ( + selection !== null && + !nativeSelection?.isCollapsed && + rootElement !== null && + rootElement.contains(nativeSelection?.anchorNode) + ) { + const domRange = nativeSelection?.getRangeAt(0); + let rect; + if (nativeSelection?.anchorNode === rootElement) { + let inner = rootElement; + while (inner.firstElementChild != null) { + inner = inner.firstElementChild; + } + rect = inner.getBoundingClientRect(); + } else { + rect = domRange?.getBoundingClientRect(); + } + + if (!mouseDownRef.current) { + positionEditorElement(editorElem, rect); + } + setLastSelection(selection); + } else if (!activeElement || activeElement.className !== 'link-input') { + positionEditorElement(editorElem, null); + setLastSelection(null); + setEditMode(false); + setLinkUrl(''); + } + + return true; + }, [editor]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }: any) => { + editorState.read(() => { + updateLinkEditor(); + }); + }), + + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + () => { + updateLinkEditor(); + return true; + }, + LowPriority + ) + ); + }, [editor, updateLinkEditor]); + + useEffect(() => { + editor.getEditorState().read(() => { + updateLinkEditor(); + }); + }, [editor, updateLinkEditor]); + + useEffect(() => { + if (isEditMode && inputRef?.current) { + inputRef.current.focus(); + } + }, [isEditMode]); + + return ( +
    + {isEditMode ? ( + { + setLinkUrl(event.target.value); + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + if (lastSelection !== null) { + if (linkUrl !== '') { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl); + } + setEditMode(false); + } + } else if (event.key === 'Escape') { + event.preventDefault(); + setEditMode(false); + } + }} + /> + ) : ( + <> +
    + + {linkUrl} + +
    event.preventDefault()} + onClick={() => { + setEditMode(true); + }} + /> +
    + + )} +
    + ); +}; diff --git a/components/lexical/toolbar/index.tsx b/components/lexical/toolbar/index.tsx new file mode 100644 index 000000000..c0d5e098a --- /dev/null +++ b/components/lexical/toolbar/index.tsx @@ -0,0 +1,430 @@ +import { useEffect, useState, useRef, useCallback } from "react"; +import { createPortal } from "react-dom"; +import { + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, + REDO_COMMAND, + UNDO_COMMAND, + SELECTION_CHANGE_COMMAND, + FORMAT_TEXT_COMMAND, + FORMAT_ELEMENT_COMMAND, + $getSelection, + $isRangeSelection, + $createParagraphNode, + $getNodeByKey, + RangeSelection, + NodeSelection, + GridSelection, +} from "lexical"; +import { + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, + REMOVE_LIST_COMMAND, + $isListNode, + ListNode, +} from "@lexical/list"; +import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { + $isParentElementRTL, + $wrapNodes, + $isAtNodeEnd, +} from "@lexical/selection"; +import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode, +} from "@lexical/rich-text"; +// custom elements +import { FloatingLinkEditor } from "./floating-link-editor"; +import { BlockTypeSelect } from "./block-type-select"; + +const LowPriority = 1; + +function getSelectedNode(selection: any) { + const anchor = selection.anchor; + const focus = selection.focus; + const anchorNode = selection.anchor.getNode(); + const focusNode = selection.focus.getNode(); + if (anchorNode === focusNode) { + return anchorNode; + } + const isBackward = selection.isBackward(); + if (isBackward) { + return $isAtNodeEnd(focus) ? anchorNode : focusNode; + } else { + return $isAtNodeEnd(anchor) ? focusNode : anchorNode; + } +} + +export const LexicalToolbar = () => { + // editor + const [editor] = useLexicalComposerContext(); + // ref + const toolbarRef = useRef(null); + // states + const [canUndo, setCanUndo] = useState(false); + const [canRedo, setCanRedo] = useState(false); + const [blockType, setBlockType] = useState("paragraph"); + const [selectedElementKey, setSelectedElementKey] = useState( + null + ); + const [isRTL, setIsRTL] = useState(false); + const [isLink, setIsLink] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + const [isUnderline, setIsUnderline] = useState(false); + const [isStrikethrough, setIsStrikethrough] = useState(false); + const [isCode, setIsCode] = useState(false); + + const updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + const element = + anchorNode.getKey() === "root" + ? anchorNode + : anchorNode.getTopLevelElementOrThrow(); + const elementKey = element.getKey(); + const elementDOM = editor.getElementByKey(elementKey); + if (elementDOM !== null) { + setSelectedElementKey(elementKey); + if ($isListNode(element)) { + const parentList = $getNearestNodeOfType(anchorNode, ListNode); + const type = parentList ? parentList.getTag() : element.getTag(); + setBlockType(type); + } else { + const type = $isHeadingNode(element) + ? element.getTag() + : element.getType(); + setBlockType(type); + } + } + // Update text format + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + setIsUnderline(selection.hasFormat("underline")); + setIsStrikethrough(selection.hasFormat("strikethrough")); + setIsRTL($isParentElementRTL(selection)); + + // Update links + const node = getSelectedNode(selection); + const parent = node.getParent(); + if ($isLinkNode(parent) || $isLinkNode(node)) { + setIsLink(true); + } else { + setIsLink(false); + } + } + }, [editor]); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, newEditor) => { + updateToolbar(); + return false; + }, + LowPriority + ), + editor.registerCommand( + CAN_UNDO_COMMAND, + (payload) => { + setCanUndo(payload); + return false; + }, + LowPriority + ), + editor.registerCommand( + CAN_REDO_COMMAND, + (payload) => { + setCanRedo(payload); + return false; + }, + LowPriority + ) + ); + }, [editor, updateToolbar]); + + const insertLink = useCallback( + (e: any) => { + e.preventDefault(); + if (!isLink) { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); + } else { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, + [editor, isLink] + ); + + return ( +
    + + + + + + + + + + {isLink && + createPortal(, document.body)} + + + + {" "} +
    + ); +}; diff --git a/components/lexical/viewer.tsx b/components/lexical/viewer.tsx new file mode 100644 index 000000000..bad4f41c7 --- /dev/null +++ b/components/lexical/viewer.tsx @@ -0,0 +1,58 @@ +import { FC } from "react"; +import { LexicalComposer } from "@lexical/react/LexicalComposer"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; +import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; +import { ListPlugin } from "@lexical/react/LexicalListPlugin"; +import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; +import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; +import { TRANSFORMERS } from "@lexical/markdown"; +// custom plugins +import { CodeHighlightPlugin } from "./plugins/code-highlight"; +import ReadOnlyPlugin from "./plugins/read-only"; +// config +import { initialConfig } from "./config"; +// helpers +import { getValidatedValue } from "./helpers/editor"; + +export interface RichTextViewerProps { + id: string; + value: string; +} + +const RichTextViewer: FC = (props) => { + // props + const { value, id } = props; + + return ( + +
    + + } + placeholder={ +
    + Enter some text... +
    + } + /> + + + + + + +
    +
    + ); +}; + +export default RichTextViewer; diff --git a/components/project/cycles/CycleView.tsx b/components/project/cycles/CycleView.tsx index 7cc28ed54..dd1ee9041 100644 --- a/components/project/cycles/CycleView.tsx +++ b/components/project/cycles/CycleView.tsx @@ -175,7 +175,7 @@ const SprintView: React.FC = ({
    )) ) : ( -

    This sprint has no issues.

    +

    This cycle has no issues.

    ) ) : (
    diff --git a/components/project/issues/BoardView/SingleBoard.tsx b/components/project/issues/BoardView/SingleBoard.tsx index 63065de1d..4f2c3b1d6 100644 --- a/components/project/issues/BoardView/SingleBoard.tsx +++ b/components/project/issues/BoardView/SingleBoard.tsx @@ -18,6 +18,7 @@ import { PlusIcon, } from "@heroicons/react/24/outline"; import Image from "next/image"; +import { divide } from "lodash"; type Props = { selectedGroup: NestedKeyOf | null; @@ -190,7 +191,7 @@ const SingleBoard: React.FC = ({ {(provided, snapshot) => (
    = ({ key={key} className={`${ key === "name" - ? "text-sm font-medium mb-2" + ? "text-sm mb-2" : key === "description" ? "text-xs text-black" : key === "priority" @@ -236,7 +237,7 @@ const SingleBoard: React.FC = ({ ? "text-xs bg-indigo-50 px-2 py-1 mt-2 flex items-center gap-x-1 rounded w-min whitespace-nowrap" : "text-sm text-gray-500" } gap-1 - `} + `} > {key === "target_date" ? ( <> @@ -300,27 +301,27 @@ const SingleBoard: React.FC = ({
    {/*
    + +
    + -
    - - -
    -
    */} + +
    +
    */} )} diff --git a/components/project/issues/BoardView/index.tsx b/components/project/issues/BoardView/index.tsx index 2dfd5575c..557ec406f 100644 --- a/components/project/issues/BoardView/index.tsx +++ b/components/project/issues/BoardView/index.tsx @@ -21,6 +21,8 @@ import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssue import { Spinner } from "ui"; // types import type { IState, IIssue, Properties, NestedKeyOf, ProjectMember } from "types"; +import ConfirmIssueDeletion from "../ConfirmIssueDeletion"; +import { TrashIcon } from "@heroicons/react/24/outline"; type Props = { properties: Properties; @@ -35,6 +37,8 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues const [isOpen, setIsOpen] = useState(false); const [isIssueOpen, setIsIssueOpen] = useState(false); + const [isIssueDeletionOpen, setIsIssueDeletionOpen] = useState(false); + const [issueDeletionData, setIssueDeletionData] = useState(); const [preloadedData, setPreloadedData] = useState< (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined @@ -58,72 +62,96 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues if (!result.destination) return; const { source, destination, type } = result; - if (type === "state") { - const newStates = Array.from(states ?? []); - const [reorderedState] = newStates.splice(source.index, 1); - newStates.splice(destination.index, 0, reorderedState); - const prevSequenceNumber = newStates[destination.index - 1]?.sequence; - const nextSequenceNumber = newStates[destination.index + 1]?.sequence; + if (destination.droppableId === "trashBox") { + const removedItem = groupedByIssues[source.droppableId][source.index]; - const sequenceNumber = - prevSequenceNumber && nextSequenceNumber - ? (prevSequenceNumber + nextSequenceNumber) / 2 - : nextSequenceNumber - ? nextSequenceNumber - 15000 / 2 - : prevSequenceNumber - ? prevSequenceNumber + 15000 / 2 - : 15000; + setIssueDeletionData(removedItem); + setIsIssueDeletionOpen(true); - newStates[destination.index].sequence = sequenceNumber; - - mutateState(newStates, false); - if (!activeWorkspace) return; - stateServices - .patchState(activeWorkspace.slug, projectId as string, newStates[destination.index].id, { - sequence: sequenceNumber, - }) - .then((response) => { - console.log(response); - }) - .catch((err) => { - console.error(err); - }); + console.log(removedItem); } else { - if (source.droppableId !== destination.droppableId) { - const sourceGroup = source.droppableId; // source group id - const destinationGroup = destination.droppableId; // destination group id - if (!sourceGroup || !destinationGroup) return; + if (type === "state") { + const newStates = Array.from(states ?? []); + const [reorderedState] = newStates.splice(source.index, 1); + newStates.splice(destination.index, 0, reorderedState); + const prevSequenceNumber = newStates[destination.index - 1]?.sequence; + const nextSequenceNumber = newStates[destination.index + 1]?.sequence; - // removed/dragged item - const removedItem = groupedByIssues[source.droppableId][source.index]; + const sequenceNumber = + prevSequenceNumber && nextSequenceNumber + ? (prevSequenceNumber + nextSequenceNumber) / 2 + : nextSequenceNumber + ? nextSequenceNumber - 15000 / 2 + : prevSequenceNumber + ? prevSequenceNumber + 15000 / 2 + : 15000; - if (selectedGroup === "priority") { - // update the removed item for mutation - removedItem.priority = destinationGroup; + newStates[destination.index].sequence = sequenceNumber; - // patch request - issuesServices.patchIssue(activeWorkspace!.slug, projectId as string, removedItem.id, { - priority: destinationGroup, + mutateState(newStates, false); + if (!activeWorkspace) return; + stateServices + .patchState( + activeWorkspace.slug, + projectId as string, + newStates[destination.index].id, + { + sequence: sequenceNumber, + } + ) + .then((response) => { + console.log(response); + }) + .catch((err) => { + console.error(err); }); - } else if (selectedGroup === "state_detail.name") { - const destinationState = states?.find((s) => s.name === destinationGroup); - const destinationStateId = destinationState?.id; + } else { + if (source.droppableId !== destination.droppableId) { + const sourceGroup = source.droppableId; // source group id + const destinationGroup = destination.droppableId; // destination group id + if (!sourceGroup || !destinationGroup) return; - // update the removed item for mutation - if (!destinationStateId || !destinationState) return; - removedItem.state = destinationStateId; - removedItem.state_detail = destinationState; + // removed/dragged item + const removedItem = groupedByIssues[source.droppableId][source.index]; - // patch request - issuesServices.patchIssue(activeWorkspace!.slug, projectId as string, removedItem.id, { - state: destinationStateId, - }); + if (selectedGroup === "priority") { + // update the removed item for mutation + removedItem.priority = destinationGroup; + + // patch request + issuesServices.patchIssue( + activeWorkspace!.slug, + projectId as string, + removedItem.id, + { + priority: destinationGroup, + } + ); + } else if (selectedGroup === "state_detail.name") { + const destinationState = states?.find((s) => s.name === destinationGroup); + const destinationStateId = destinationState?.id; + + // update the removed item for mutation + if (!destinationStateId || !destinationState) return; + removedItem.state = destinationStateId; + removedItem.state_detail = destinationState; + + // patch request + issuesServices.patchIssue( + activeWorkspace!.slug, + projectId as string, + removedItem.id, + { + state: destinationStateId, + } + ); + } + + // remove item from the source group + groupedByIssues[source.droppableId].splice(source.index, 1); + // add item to the destination group + groupedByIssues[destination.droppableId].splice(destination.index, 0, removedItem); } - - // remove item from the source group - groupedByIssues[source.droppableId].splice(source.index, 1); - // add item to the destination group - groupedByIssues[destination.droppableId].splice(destination.index, 0, removedItem); } } }, @@ -155,6 +183,11 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues setIsOpen={setIsOpen} data={preloadedData as Partial} /> */} + setIsIssueDeletionOpen(false)} + data={issueDeletionData} + /> = ({ properties, selectedGroup, groupedByIssues projectId={projectId as string} /> {groupedByIssues ? ( - groupedByIssues ? ( -
    - -
    - - {(provided) => ( -
    -
    - {Object.keys(groupedByIssues).map((singleGroup, index) => ( - m.member.id === singleGroup)?.member - .first_name - : undefined - } - groupedByIssues={groupedByIssues} - index={index} - setIsIssueOpen={setIsIssueOpen} - properties={properties} - setPreloadedData={setPreloadedData} - stateId={ - selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup)?.id - : undefined - } - bgColor={ - selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup)?.color - : undefined - } - /> - ))} -
    - {provided.placeholder} +
    + + {/* + {(provided, snapshot) => ( + + )} + */} +
    + + {(provided) => ( +
    +
    + {Object.keys(groupedByIssues).map((singleGroup, index) => ( + m.member.id === singleGroup)?.member.first_name + : undefined + } + groupedByIssues={groupedByIssues} + index={index} + setIsIssueOpen={setIsIssueOpen} + properties={properties} + setPreloadedData={setPreloadedData} + stateId={ + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup)?.id + : undefined + } + bgColor={ + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup)?.color + : undefined + } + /> + ))}
    - )} - -
    - -
    - ) : null + {provided.placeholder} +
    + )} + +
    + +
    ) : ( -
    +
    )} diff --git a/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx b/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx index 7fead660b..83db0b895 100644 --- a/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx +++ b/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx @@ -52,6 +52,7 @@ const SelectParent: React.FC = ({ control }) => { }; })} value={value} + width="xs" buttonClassName="max-h-30 overflow-y-scroll" optionsClassName="max-h-30 overflow-y-scroll" onChange={onChange} diff --git a/components/project/issues/ListView/index.tsx b/components/project/issues/ListView/index.tsx index 9c12ac1c3..ff6fc68d9 100644 --- a/components/project/issues/ListView/index.tsx +++ b/components/project/issues/ListView/index.tsx @@ -77,11 +77,11 @@ const ListView: React.FC = ({ const handleHover = (issueId: string) => { document.addEventListener("keydown", (e) => { - if (e.code === "Space") { - e.preventDefault(); - setPreviewModalIssueId(issueId); - setIssuePreviewModal(true); - } + // if (e.code === "Space") { + // e.preventDefault(); + // setPreviewModalIssueId(issueId); + // setIssuePreviewModal(true); + // } }); }; diff --git a/components/project/issues/issue-detail/IssueDetailSidebar.tsx b/components/project/issues/issue-detail/IssueDetailSidebar.tsx index 985146cdc..efa2aebc2 100644 --- a/components/project/issues/issue-detail/IssueDetailSidebar.tsx +++ b/components/project/issues/issue-detail/IssueDetailSidebar.tsx @@ -21,7 +21,7 @@ import { // commons import { classNames, copyTextToClipboard } from "constants/common"; // ui -import { Input, Button } from "ui"; +import { Input, Button, Spinner } from "ui"; // icons import { UserIcon, @@ -32,6 +32,7 @@ import { ChartBarIcon, ClipboardDocumentIcon, LinkIcon, + ArrowPathIcon, } from "@heroicons/react/24/outline"; // types import type { Control } from "react-hook-form"; @@ -50,7 +51,7 @@ const defaultValues: Partial = { }; const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDetail }) => { - const { activeWorkspace, activeProject } = useUser(); + const { activeWorkspace, activeProject, cycles } = useUser(); const { data: states } = useSWR( activeWorkspace && activeProject ? STATE_LIST(activeProject.id) : null, @@ -121,6 +122,16 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta value: state.id, })), }, + { + label: "Cycle", + name: "cycle", + canSelectMultipleOptions: false, + icon: ArrowPathIcon, + options: cycles?.map((cycle) => ({ + label: cycle.name, + value: cycle.id, + })), + }, { label: "Assignees", name: "assignees_list", @@ -153,6 +164,13 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta }, ]; + const handleCycleChange = (cycleId: string) => { + if (activeWorkspace && activeProject && issueDetail) + issuesServices.addIssueToSprint(activeWorkspace.slug, activeProject.id, cycleId, { + issue: issueDetail.id, + }); + }; + return (
    @@ -193,7 +211,10 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta as="div" value={value} multiple={item.canSelectMultipleOptions} - onChange={(value: any) => submitChanges({ [item.name]: value })} + onChange={(value: any) => { + if (item.name === "cycle") handleCycleChange(value); + else submitChanges({ [item.name]: value }); + }} className="flex-shrink-0" > {({ open }) => ( @@ -229,21 +250,31 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta >
    - {item.options?.map((option) => ( - - `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } ${ - item.label === "Priority" && "capitalize" - } cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={option.value} - > - {option.label} - - ))} + {item.options ? ( + item.options.length > 0 ? ( + item.options.map((option) => ( + + `${ + active || selected + ? "text-white bg-theme" + : "text-gray-900" + } ${ + item.label === "Priority" && "capitalize" + } cursor-pointer select-none relative p-2 rounded-md truncate` + } + value={option.value} + > + {option.label} + + )) + ) : ( +
    No {item.label}s found
    + ) + ) : ( + + )}
    @@ -321,19 +352,29 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta >
    - {issueLabels?.map((label: any) => ( - - `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={label.id} - > - {label.name} - - ))} + {issueLabels ? ( + issueLabels.length > 0 ? ( + issueLabels.map((label: any) => ( + + `${ + active || selected + ? "text-white bg-theme" + : "text-gray-900" + } cursor-pointer select-none relative p-2 rounded-md truncate` + } + value={label.id} + > + {label.name} + + )) + ) : ( +
    No labels found
    + ) + ) : ( + + )}
    diff --git a/components/project/issues/issue-detail/activity/index.tsx b/components/project/issues/issue-detail/activity/index.tsx index c48ba6ea8..45965ca24 100644 --- a/components/project/issues/issue-detail/activity/index.tsx +++ b/components/project/issues/issue-detail/activity/index.tsx @@ -45,7 +45,7 @@ const IssueActivitySection: React.FC = ({ issueActivities, states }) => {
    ) : ( -
    +
    {activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
    diff --git a/components/workspace/SingleInvitation.tsx b/components/workspace/SingleInvitation.tsx new file mode 100644 index 000000000..27614e984 --- /dev/null +++ b/components/workspace/SingleInvitation.tsx @@ -0,0 +1,77 @@ +// next +import Image from "next/image"; +// react +import { useState } from "react"; +// types +import { IWorkspaceInvitation } from "types"; + +type Props = { + invitation: IWorkspaceInvitation; + invitationsRespond: string[]; + handleInvitation: any; +}; + +const SingleInvitation: React.FC = ({ + invitation, + invitationsRespond, + handleInvitation, +}) => { + const [isChecked, setIsChecked] = useState(invitationsRespond.includes(invitation.id)); + + return ( + <> +
  • + +
  • + + ); +}; + +export default SingleInvitation; diff --git a/contexts/user.context.tsx b/contexts/user.context.tsx index 55037af14..6b52b0939 100644 --- a/contexts/user.context.tsx +++ b/contexts/user.context.tsx @@ -16,7 +16,6 @@ import { CURRENT_USER, PROJECTS_LIST, USER_WORKSPACES, - USER_WORKSPACE_INVITATIONS, PROJECT_ISSUES_LIST, STATE_LIST, CYCLE_LIST, @@ -24,7 +23,8 @@ import { // types import type { KeyedMutator } from "swr"; -import type { IUser, IWorkspace, IProject, IIssue, IssueResponse, ICycle, IState } from "types"; +import type { IUser, IWorkspace, IProject, IssueResponse, ICycle, IState } from "types"; + interface IUserContextProps { user?: IUser; isUserLoading: boolean; @@ -38,8 +38,8 @@ interface IUserContextProps { activeProject?: IProject; issues?: IssueResponse; mutateIssues: KeyedMutator; - sprints?: ICycle[]; - mutateSprints: KeyedMutator; + cycles?: ICycle[]; + mutateCycles: KeyedMutator; states?: IState[]; mutateStates: KeyedMutator; } @@ -92,7 +92,7 @@ export const UserProvider = ({ children }: { children: ReactElement }) => { : null ); - const { data: sprints, mutate: mutateSprints } = useSWR( + const { data: cycles, mutate: mutateCycles } = useSWR( activeWorkspace && activeProject ? CYCLE_LIST(activeProject.id) : null, activeWorkspace && activeProject ? () => sprintsServices.getCycles(activeWorkspace.slug, activeProject.id) @@ -141,8 +141,8 @@ export const UserProvider = ({ children }: { children: ReactElement }) => { activeProject, issues, mutateIssues, - sprints, - mutateSprints, + cycles, + mutateCycles, states, mutateStates, setActiveProject, diff --git a/layouts/Navbar/Sidebar.tsx b/layouts/Navbar/Sidebar.tsx index 03863f4cf..48c6f7842 100644 --- a/layouts/Navbar/Sidebar.tsx +++ b/layouts/Navbar/Sidebar.tsx @@ -28,11 +28,13 @@ import { XMarkIcon, ArrowLongLeftIcon, QuestionMarkCircleIcon, + EllipsisHorizontalIcon, + ClipboardDocumentIcon, } from "@heroicons/react/24/outline"; // constants -import { classNames } from "constants/common"; +import { classNames, copyTextToClipboard } from "constants/common"; // ui -import { Spinner, Tooltip } from "ui"; +import { CustomListbox, Spinner, Tooltip } from "ui"; // types import type { IUser } from "types"; @@ -423,23 +425,66 @@ const Sidebar: React.FC = () => { {({ open }) => ( <> - - - {project?.name.charAt(0)} - - {!sidebarCollapse && ( - - {project?.name} - +
    + + + {project?.name.charAt(0)} + {!sidebarCollapse && ( + + {project?.name} + + + + + )} + + {!sidebarCollapse && ( + + + + + + + +
    + + {(active) => ( + + )} + +
    +
    +
    +
    )} - +
    { )}
    - + +
    diff --git a/pages/me/my-issues.tsx b/pages/me/my-issues.tsx index 91f79ae81..eaf206a26 100644 --- a/pages/me/my-issues.tsx +++ b/pages/me/my-issues.tsx @@ -145,7 +145,9 @@ const MyIssues: NextPage = () => { {myIssue.name} - {myIssue.description} + + {myIssue.description} + {myIssue.project_detail?.name}
    diff --git a/pages/projects/[projectId]/cycles.tsx b/pages/projects/[projectId]/cycles.tsx index be21dfc41..eec2bbc91 100644 --- a/pages/projects/[projectId]/cycles.tsx +++ b/pages/projects/[projectId]/cycles.tsx @@ -14,21 +14,18 @@ import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys"; // layouts import AdminLayout from "layouts/AdminLayout"; // components -import SprintView from "components/project/cycles/CycleView"; +import CycleView from "components/project/cycles/CycleView"; import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion"; import ConfirmSprintDeletion from "components/project/cycles/ConfirmCycleDeletion"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateSprintsModal from "components/project/cycles/CreateUpdateCyclesModal"; // ui -import { Spinner } from "ui"; +import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySpaceItem } from "ui"; // icons import { PlusIcon } from "@heroicons/react/20/solid"; +import { ArrowPathIcon } from "@heroicons/react/24/outline"; // types import { IIssue, ICycle, SelectSprintType, SelectIssue } from "types"; -import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace"; -import { ArrowPathIcon } from "@heroicons/react/24/outline"; -import HeaderButton from "ui/HeaderButton"; -import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; const ProjectSprints: NextPage = () => { const [isOpen, setIsOpen] = useState(false); @@ -44,7 +41,7 @@ const ProjectSprints: NextPage = () => { const { projectId } = router.query; - const { data: sprints } = useSWR( + const { data: cycles } = useSWR( projectId && activeWorkspace ? CYCLE_LIST(projectId as string) : null, activeWorkspace && projectId ? () => sprintService.getCycles(activeWorkspace.slug, projectId as string) @@ -52,14 +49,14 @@ const ProjectSprints: NextPage = () => { ); const openIssueModal = ( - sprintId: string, + cycleId: string, issue?: IIssue, actionType: "create" | "edit" | "delete" = "create" ) => { - const sprint = sprints?.find((sprint) => sprint.id === sprintId); - if (sprint) { + const cycle = cycles?.find((cycle) => cycle.id === cycleId); + if (cycle) { setSelectedSprint({ - ...sprint, + ...cycle, actionType: "create-issue", }); if (issue) setSelectedIssues({ ...issue, actionType }); @@ -67,16 +64,16 @@ const ProjectSprints: NextPage = () => { } }; - const addIssueToSprint = (sprintId: string, issueId: string) => { + const addIssueToSprint = (cycleId: string, issueId: string) => { if (!activeWorkspace || !projectId) return; issuesServices - .addIssueToSprint(activeWorkspace.slug, projectId as string, sprintId, { + .addIssueToSprint(activeWorkspace.slug, projectId as string, cycleId, { issue: issueId, }) .then((response) => { console.log(response); - mutate(CYCLE_ISSUES(sprintId)); + mutate(CYCLE_ISSUES(cycleId)); }) .catch((error) => { console.log(error); @@ -134,8 +131,8 @@ const ProjectSprints: NextPage = () => { setIsOpen={setIsOpen} projectId={projectId as string} /> - {sprints ? ( - sprints.length > 0 ? ( + {cycles ? ( + cycles.length > 0 ? (
    @@ -146,15 +143,15 @@ const ProjectSprints: NextPage = () => { setIsOpen(true)} />
    - {sprints.map((sprint) => ( - ( + ))}
    diff --git a/pages/projects/[projectId]/issues/[issueId].tsx b/pages/projects/[projectId]/issues/[issueId].tsx index 0c3471983..4d98b33c5 100644 --- a/pages/projects/[projectId]/issues/[issueId].tsx +++ b/pages/projects/[projectId]/issues/[issueId].tsx @@ -162,12 +162,14 @@ const IssueDetail: NextPage = () => { />
    -

    {`${activeProject?.name}/${activeProject?.identifier}-${issueDetail?.sequence_id}`}

    +

    {`${activeProject?.name ?? "Project"}/${ + activeProject?.identifier ?? "..." + }-${issueDetail?.sequence_id ?? "..."}`}

    { if (!prevIssue) return; router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`); @@ -177,6 +179,7 @@ const IssueDetail: NextPage = () => { Icon={ChevronRightIcon} disabled={!nextIssue} label="Next" + className={`${!nextIssue ? "cursor-not-allowed opacity-70" : ""}`} onClick={() => { if (!nextIssue) return; router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`); @@ -188,7 +191,7 @@ const IssueDetail: NextPage = () => { {issueDetail && activeProject ? (
    -
    +