diff --git a/README.md b/README.md index 67e66d8f4..bc3166313 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Coming soon. The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects. -To chat with other community members you can join the [Plane Discord](https://discord.com/invite/8SR2N9PAcJ). +To chat with other community members you can join the [Plane Discord](https://discord.com/invite/q9HKAdau). Our Code of Conduct applies to all Plane community channels. diff --git a/apps/app/components/command-palette/addAsSubIssue.tsx b/apps/app/components/command-palette/addAsSubIssue.tsx index 41c3e3d28..e8c585e45 100644 --- a/apps/app/components/command-palette/addAsSubIssue.tsx +++ b/apps/app/components/command-palette/addAsSubIssue.tsx @@ -1,3 +1,4 @@ +// react import React, { useState } from "react"; // swr import { mutate } from "swr"; @@ -5,23 +6,23 @@ import { mutate } from "swr"; import { useForm } from "react-hook-form"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; +// services +import issuesServices from "lib/services/issues.services"; // hooks import useUser from "lib/hooks/useUser"; // icons -import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; -import { FolderIcon } from "@heroicons/react/24/outline"; +import { RectangleStackIcon, MagnifyingGlassIcon } 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"; +// constants +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; - parentId: string; + parent: IIssue | undefined; }; type FormInput = { @@ -29,7 +30,7 @@ type FormInput = { cycleId: string; }; -const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parentId }) => { +const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parent }) => { const [query, setQuery] = useState(""); const { activeWorkspace, activeProject, issues } = useUser(); @@ -41,24 +42,19 @@ const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parentId }) => { []; 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 }) + .patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: parent?.id }) .then((res) => { mutate( PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), @@ -78,118 +74,113 @@ const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parentId }) => { }; return ( - <> - setQuery("")} appear> - + setQuery("")} appear> + + +
+ + +
-
- + + +
+
-
- - - { - // const { url, onClick } = item; - // if (url) router.push(url); - // else if (onClick) onClick(); - // handleCommandPaletteClose(); - // }} + -
- 0 && ( + <> +
  • + {query === "" && ( +

    + Issues +

    + )} +
      + {filteredIssues.map((issue) => { + if ( + (issue.parent === "" || issue.parent === null) && // issue does not have any other parent + issue.id !== parent?.id && // issue is not itself + issue.id !== parent?.parent // issue is not it's parent + ) + 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 && ( +
    +
    - - - {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 && ( -
    -
    - )} - - - -
    -
    -
    - + )} + + + + +
    +
    ); }; diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index 3c41d96e1..1b5694616 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -4,40 +4,35 @@ import { useRouter } from "next/router"; // swr import { mutate } from "swr"; // react hook form -import { Controller, SubmitHandler, useForm } from "react-hook-form"; +import { SubmitHandler, useForm } from "react-hook-form"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; +// services +import issuesServices from "lib/services/issues.services"; // 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 { FolderIcon, RectangleStackIcon, ClipboardDocumentListIcon, - ArrowPathIcon, + MagnifyingGlassIcon, } from "@heroicons/react/24/outline"; -// commons -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"; +// ui +import { Button } from "ui"; // types -import { IIssue, IProject, IssueResponse } from "types"; -import { Button, SearchListbox } from "ui"; -import issuesServices from "lib/services/issues.services"; +import { IIssue, IssueResponse } from "types"; // fetch keys -import { PROJECTS_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; - -type ItemType = { - name: string; - url?: string; - onClick?: () => void; -}; +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +// constants +import { classNames, copyTextToClipboard } from "constants/common"; type FormInput = { issue_ids: string[]; @@ -45,8 +40,6 @@ type FormInput = { }; const CommandPalette: React.FC = () => { - const router = useRouter(); - const [query, setQuery] = useState(""); const [isPaletteOpen, setIsPaletteOpen] = useState(false); @@ -55,7 +48,9 @@ const CommandPalette: React.FC = () => { const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false); const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false); - const { activeWorkspace, activeProject, issues, cycles } = useUser(); + const { activeWorkspace, activeProject, issues } = useUser(); + + const router = useRouter(); const { toggleCollapsed } = useTheme(); @@ -67,14 +62,7 @@ const CommandPalette: React.FC = () => { : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; - const { - register, - formState: { errors, isSubmitting }, - handleSubmit, - control, - reset, - setError, - } = useForm(); + const { register, handleSubmit, reset } = useForm(); const quickActions = [ { @@ -103,25 +91,25 @@ const CommandPalette: React.FC = () => { const handleKeyDown = useCallback( (e: KeyboardEvent) => { - if (e.ctrlKey && e.key === "/") { + if ((e.ctrlKey || e.metaKey) && e.key === "/") { e.preventDefault(); setIsPaletteOpen(true); - } else if (e.ctrlKey && e.key === "i") { + } else if ((e.ctrlKey || e.metaKey) && e.key === "i") { e.preventDefault(); setIsIssueModalOpen(true); - } else if (e.ctrlKey && e.key === "p") { + } else if ((e.ctrlKey || e.metaKey) && e.key === "p") { e.preventDefault(); setIsProjectModalOpen(true); - } else if (e.ctrlKey && e.key === "b") { + } else if ((e.ctrlKey || e.metaKey) && e.key === "b") { e.preventDefault(); toggleCollapsed(); - } else if (e.ctrlKey && e.key === "h") { + } else if ((e.ctrlKey || e.metaKey) && e.key === "h") { e.preventDefault(); setIsShortcutsModalOpen(true); - } else if (e.ctrlKey && e.key === "q") { + } else if ((e.ctrlKey || e.metaKey) && e.key === "q") { e.preventDefault(); setIsCreateCycleModalOpen(true); - } else if (e.ctrlKey && e.altKey && e.key === "c") { + } else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") { e.preventDefault(); if (!router.query.issueId) return; @@ -186,37 +174,6 @@ const CommandPalette: React.FC = () => { } }; - const handleAddToCycle: SubmitHandler = (data) => { - 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.cycleId, data) - .then((res) => { - console.log(res); - }) - .catch((e) => { - console.log(e); - }); - } - }; - useEffect(() => { document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); @@ -269,14 +226,7 @@ const CommandPalette: React.FC = () => { >
    - { - // const { url, onClick } = item; - // if (url) router.push(url); - // else if (onClick) onClick(); - // handleCommandPaletteClose(); - // }} - > +
    { {filteredIssues.map((issue) => ( classNames( - "flex cursor-pointer select-none items-center rounded-md px-3 py-2", + "flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2", active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" ) } > {({ active }) => ( <> - {/*
    + + + ); +}; + +export default ConfirmProjectMemberRemove; diff --git a/apps/app/components/project/SendProjectInvitationModal.tsx b/apps/app/components/project/SendProjectInvitationModal.tsx index f5658a304..a01d36658 100644 --- a/apps/app/components/project/SendProjectInvitationModal.tsx +++ b/apps/app/components/project/SendProjectInvitationModal.tsx @@ -12,6 +12,7 @@ import useToast from "lib/hooks/useToast"; import projectService from "lib/services/project.service"; import workspaceService from "lib/services/workspace.service"; // constants +import { ROLE } from "constants/"; import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // ui import { Button, Select, TextArea } from "ui"; @@ -30,13 +31,9 @@ type Props = { const defaultValues: Partial = { email: "", message: "", -}; - -const ROLE = { - 5: "Guest", - 10: "Viewer", - 15: "Member", - 20: "Admin", + role: 5, + member_id: "", + user_id: "", }; const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, members }) => { diff --git a/apps/app/components/project/cycles/CycleIssuesListModal.tsx b/apps/app/components/project/cycles/CycleIssuesListModal.tsx new file mode 100644 index 000000000..5f29a5bfd --- /dev/null +++ b/apps/app/components/project/cycles/CycleIssuesListModal.tsx @@ -0,0 +1,208 @@ +// react +import React, { useState } from "react"; +// react-hook-form +import { Controller, SubmitHandler, useForm } from "react-hook-form"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; +// services +import issuesServices from "lib/services/issues.services"; +// hooks +import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; +// icons +import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue, IssueResponse } from "types"; +// constants +import { classNames } from "constants/common"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + issues: IssueResponse | undefined; + cycleId: string; +}; + +type FormInput = { + issue_ids: string[]; +}; + +const CycleIssuesListModal: React.FC = ({ + isOpen, + handleClose: onClose, + issues, + cycleId, +}) => { + const [query, setQuery] = useState(""); + + const { activeWorkspace, activeProject } = useUser(); + + const { setToastAlert } = useToast(); + + const handleClose = () => { + onClose(); + setQuery(""); + reset(); + }; + + const { handleSubmit, reset, control } = useForm({ + defaultValues: { + issue_ids: [], + }, + }); + + const handleAddToCycle: SubmitHandler = (data) => { + 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 + .bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, cycleId, data) + .then((res) => { + console.log(res); + }) + .catch((e) => { + console.log(e); + }); + } + }; + + const filteredIssues: IIssue[] = + query === "" + ? issues?.results ?? [] + : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? + []; + + return ( + <> + setQuery("")} appear> + + +
    + + +
    + + + + ( + +
    +
    + + + {filteredIssues.length > 0 && ( +
  • + {query === "" && ( +

    + Select issues to add to cycle +

    + )} +
      + {filteredIssues.map((issue) => ( + + classNames( + "flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + > + {({ selected }) => ( + <> + + + + {activeProject?.identifier}-{issue.sequence_id} + + {issue.name} + + )} + + ))} +
    +
  • + )} +
    + + {query !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    + )} + /> +
    + + +
    + +
    +
    +
    +
    +
    + + ); +}; + +export default CycleIssuesListModal; diff --git a/apps/app/components/project/cycles/CycleView.tsx b/apps/app/components/project/cycles/CycleView.tsx index dd1ee9041..f46079165 100644 --- a/apps/app/components/project/cycles/CycleView.tsx +++ b/apps/app/components/project/cycles/CycleView.tsx @@ -1,258 +1,314 @@ -import React from "react"; +// react +import React, { useState } from "react"; // next -import { useRouter } from "next/router"; +import Link from "next/link"; // swr -import useSWR from "swr"; +import useSWR, { mutate } from "swr"; // headless ui -import { Disclosure, Transition, Menu, Listbox } from "@headlessui/react"; -// fetch keys -import { PROJECT_ISSUES_LIST, CYCLE_ISSUES } from "constants/fetch-keys"; +import { Disclosure, Transition, Menu } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; import cycleServices from "lib/services/cycles.services"; -// commons -import { classNames, renderShortNumericDateFormat } from "constants/common"; +// hooks +import useUser from "lib/hooks/useUser"; +// components +import CycleIssuesListModal from "./CycleIssuesListModal"; // ui import { Spinner } from "ui"; // icons import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; // types -import type { ICycle, SprintViewProps as Props, SprintIssueResponse, IssueResponse } from "types"; +import type { CycleViewProps as Props, CycleIssueResponse, IssueResponse } from "types"; +// fetch keys +import { CYCLE_ISSUES } from "constants/fetch-keys"; +// constants +import { renderShortNumericDateFormat } from "constants/common"; +import issuesServices from "lib/services/issues.services"; +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { Draggable } from "react-beautiful-dnd"; -const SprintView: React.FC = ({ - sprint, +const CycleView: React.FC = ({ + cycle, selectSprint, workspaceSlug, projectId, openIssueModal, - addIssueToSprint, }) => { - const router = useRouter(); + const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - const { data: sprintIssues } = useSWR(CYCLE_ISSUES(sprint.id), () => - cycleServices.getCycleIssues(workspaceSlug, projectId, sprint.id) + const { activeWorkspace, activeProject, issues } = useUser(); + + const { data: cycleIssues } = useSWR(CYCLE_ISSUES(cycle.id), () => + cycleServices.getCycleIssues(workspaceSlug, projectId, cycle.id) ); - const { data: projectIssues } = useSWR( - projectId && workspaceSlug ? PROJECT_ISSUES_LIST(workspaceSlug, projectId) : null, - workspaceSlug ? () => issuesServices.getIssues(workspaceSlug, projectId) : null - ); + const removeIssueFromCycle = (cycleId: string, bridgeId: string) => { + if (activeWorkspace && activeProject && cycleIssues) { + mutate( + CYCLE_ISSUES(cycleId), + (prevData) => prevData?.filter((p) => p.id !== bridgeId), + false + ); + + issuesServices + .removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId, bridgeId) + .then((res) => { + console.log(res); + }) + .catch((e) => { + console.log(e); + }); + } + }; return ( -
    - + <> + setCycleIssuesListModal(false)} + issues={issues} + cycleId={cycle.id} + /> + {({ open }) => ( -
    -
    -
    - -
    +
    +
    + +
    + + + +

    {cycle.name}

    +

    - - -

    {sprint.name}

    -

    - {sprint.status === "started" - ? sprint.start_date - ? `${renderShortNumericDateFormat(sprint.start_date)} - ` + {cycle.status === "started" + ? cycle.start_date + ? `${renderShortNumericDateFormat(cycle.start_date)} - ` : "" - : sprint.status} - {sprint.end_date ? renderShortNumericDateFormat(sprint.end_date) : ""} -

    -
    -
    - -
    - - - - - - -
    - -
    -
    - -
    - -
    -
    -
    -
    + : cycle.status} + + + {cycle.end_date ? renderShortNumericDateFormat(cycle.end_date) : ""} + +

    -
    + + + + + + + + + + + + + + + +
    + + + + {(provided) => ( +
    + {cycleIssues ? ( + cycleIssues.length > 0 ? ( + cycleIssues.map((issue, index) => ( + + {(provided, snapshot) => ( +
    + +
    + + {issue.issue_details.state_detail?.name} + + + + + + + + + + +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + )} +
    + )) + ) : ( +

    This cycle has no issue.

    + ) + ) : ( +
    + +
    + )} + {provided.placeholder} +
    + )} +
    +
    +
    + + + + Add issue + - -
    - {sprintIssues ? ( - sprintIssues.length > 0 ? ( - sprintIssues.map((issue) => ( -
    - -
    - - {issue.issue_details.state_detail?.name} - -
    - - - - - - -
    - -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    - )) - ) : ( -

    This cycle has no issues.

    - ) - ) : ( -
    - -
    - )} + +
    + + {(active) => ( + + )} + + + {(active) => ( + + )} +
    - +
    -
    - - -
    - -
    - -
    -

    Add Existing Issue

    -
    -
    -
    - - - -
    - {projectIssues?.results.map((issue) => ( - { - addIssueToSprint(sprint.id, issue.id); - }} - > - {({ active }) => ( -

    - {issue.name} -

    - )} -
    - ))} -
    -
    -
    -
    -
    -
    -
    +
    )} -
    + ); }; -export default SprintView; +export default CycleView; diff --git a/apps/app/components/project/issues/BoardView/SingleBoard.tsx b/apps/app/components/project/issues/BoardView/SingleBoard.tsx index 928261f64..5f81be602 100644 --- a/apps/app/components/project/issues/BoardView/SingleBoard.tsx +++ b/apps/app/components/project/issues/BoardView/SingleBoard.tsx @@ -5,7 +5,11 @@ import Link from "next/link"; import { Draggable } from "react-beautiful-dnd"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; // common -import { addSpaceIfCamelCase, renderShortNumericDateFormat } from "constants/common"; +import { + addSpaceIfCamelCase, + findHowManyDaysLeft, + renderShortNumericDateFormat, +} from "constants/common"; // types import { IIssue, Properties, NestedKeyOf } from "types"; // icons @@ -23,7 +27,9 @@ import { divide } from "lodash"; type Props = { selectedGroup: NestedKeyOf | null; groupTitle: string; - groupedByIssues: any; + groupedByIssues: { + [key: string]: IIssue[]; + }; index: number; setIsIssueOpen: React.Dispatch>; properties: Properties; @@ -69,7 +75,7 @@ const SingleBoard: React.FC = ({ {(provided, snapshot) => (
    = ({ className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none" onClick={() => setPreloadedData({ - // ...state, actionType: "edit", }) } > - {/* */}
    @@ -188,7 +181,7 @@ const SingleBoard: React.FC = ({ {...provided.droppableProps} ref={provided.innerRef} > - {groupedByIssues[groupTitle].map((childIssue: any, index: number) => ( + {groupedByIssues[groupTitle].map((childIssue, index: number) => ( {(provided, snapshot) => ( @@ -203,6 +196,9 @@ const SingleBoard: React.FC = ({ className="px-2 py-3 space-y-1.5 select-none" {...provided.dragHandleProps} > + + {childIssue.name} + {Object.keys(properties).map( (key) => properties[key as keyof Properties] && @@ -227,34 +223,66 @@ const SingleBoard: React.FC = ({ : key === "target_date" ? "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 + } gap-1 relative `} > - {key === "target_date" ? ( - <> - {" "} + {key === "start_date" && childIssue.start_date !== null && ( + + + {renderShortNumericDateFormat(childIssue.start_date)} - {childIssue.target_date ? renderShortNumericDateFormat(childIssue.target_date) - : "N/A"} - - ) : ( - "" + : "None"} + )} - {key === "name" && ( - - {childIssue.name} + {key === "target_date" && ( + <> + + + {childIssue.target_date + ? renderShortNumericDateFormat(childIssue.target_date) + : "N/A"} + {childIssue.target_date && ( + + {childIssue.target_date < new Date().toISOString() + ? `Target date has passed by ${findHowManyDaysLeft( + childIssue.target_date + )} days` + : findHowManyDaysLeft(childIssue.target_date) <= 3 + ? `Target date is in ${findHowManyDaysLeft( + childIssue.target_date + )} days` + : "Target date"} + + )} + + + )} + {key === "key" && ( + + {childIssue.project_detail?.identifier}- + {childIssue.sequence_id} )} {key === "state" && ( <>{addSpaceIfCamelCase(childIssue["state_detail"].name)} )} {key === "priority" && <>{childIssue.priority}} - {key === "description" && <>{childIssue.description}} + {/* {key === "description" && <>{childIssue.description}} */} {key === "assignee" ? (
    {childIssue?.assignee_details?.length > 0 ? ( childIssue?.assignee_details?.map( - (assignee: any, index: number) => ( + (assignee, index: number) => (
    = ({ ) ) ) : ( - None + No assignee. )}
    ) : null} @@ -290,29 +318,6 @@ const SingleBoard: React.FC = ({ ) )}
    - - {/*
    - -
    - - -
    -
    */} )} diff --git a/apps/app/components/project/issues/BoardView/index.tsx b/apps/app/components/project/issues/BoardView/index.tsx index 557ec406f..010b9cbdc 100644 --- a/apps/app/components/project/issues/BoardView/index.tsx +++ b/apps/app/components/project/issues/BoardView/index.tsx @@ -67,8 +67,6 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues setIssueDeletionData(removedItem); setIsIssueDeletionOpen(true); - - console.log(removedItem); } else { if (type === "state") { const newStates = Array.from(states ?? []); @@ -168,21 +166,6 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues return ( <> - {/* } - projectId={projectId as string} - /> */} - {/* } - /> */} setIsIssueDeletionOpen(false)} @@ -199,21 +182,6 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues {groupedByIssues ? (
    - {/* - {(provided, snapshot) => ( - - )} - */}
    {(provided) => ( diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx index bee25039f..376ab65e6 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx @@ -44,7 +44,7 @@ const SelectAssignee: React.FC = ({ control }) => { multiple={true} value={value} onChange={onChange} - icon={} + icon={} /> )} /> diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectCycles.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectCycles.tsx index 7987bff8d..6553c205a 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectCycles.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectCycles.tsx @@ -33,7 +33,7 @@ const SelectSprint: React.FC = ({ control, setIsOpen }) => { <>
    - + {cycles?.find((i) => i.id.toString() === value?.toString())?.name ?? "Cycle"} diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx index ede265c98..a7ef75203 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx @@ -83,7 +83,7 @@ const SelectLabels: React.FC = ({ control }) => { <>
    - + {value && value.length > 0 ? value.map((id) => issueLabels?.find((i) => i.id === id)?.name).join(", ") diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssue.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssue.tsx new file mode 100644 index 000000000..e8f1648e8 --- /dev/null +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssue.tsx @@ -0,0 +1,37 @@ +import React, { useEffect, useState } from "react"; +// react hook form +import { Controller, Control } from "react-hook-form"; +// hooks +import useUser from "lib/hooks/useUser"; +// types +import type { IIssue, IssueResponse } from "types"; +// icons +import { UserIcon } from "@heroicons/react/24/outline"; +// components +import IssuesListModal from "components/project/issues/IssuesListModal"; + +type Props = { + control: Control; + isOpen: boolean; + setIsOpen: React.Dispatch>; + issues: IssueResponse | undefined; +}; + +const SelectParent: React.FC = ({ control, isOpen, setIsOpen, issues }) => { + return ( + ( + setIsOpen(false)} + onChange={onChange} + issues={issues} + /> + )} + /> + ); +}; + +export default SelectParent; diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx deleted file mode 100644 index 83db0b895..000000000 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssues.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from "react"; -// react hook form -import { Controller } from "react-hook-form"; -// hooks -import useUser from "lib/hooks/useUser"; -// types -import type { IIssue } from "types"; -import type { Control } from "react-hook-form"; -import { UserIcon } from "@heroicons/react/24/outline"; - -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; - - const sequenceId = projectIssues?.results?.find( - (i) => i.id.toString() === issueId?.toString() - )?.sequence_id; - - if (issueId) return `${identifier}-${sequenceId}`; - else return "Parent issue"; - }; - - return ( - ( - { - return { - value: issue.id, - display: issue.name, - element: ( -
    -
    - {`${getSelectedIssueKey(issue.id)}`} - {issue.name} -
    -
    - ), - }; - })} - value={value} - width="xs" - buttonClassName="max-h-30 overflow-y-scroll" - optionsClassName="max-h-30 overflow-y-scroll" - onChange={onChange} - icon={} - /> - )} - /> - ); -}; - -export default SelectParent; diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectPriority.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectPriority.tsx index 2452908cd..10cee4c76 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectPriority.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectPriority.tsx @@ -5,6 +5,8 @@ import { Controller } from "react-hook-form"; import { Listbox, Transition } from "@headlessui/react"; // icons import { CheckIcon } from "@heroicons/react/20/solid"; +// constants +import { PRIORITIES } from "constants/"; // types import type { IIssue } from "types"; @@ -15,8 +17,6 @@ type Props = { control: Control; }; -const PRIORITIES = ["high", "medium", "low"]; - const SelectPriority: React.FC = ({ control }) => { return ( = ({ control }) => { <>
    - - {value ?? "Priority"} + + + {value && value !== "" ? value : "Priority"} + = ({ control, setIsOpen }) => { const { states } = useUser(); return ( - <> - ( - { - return { value: state.id, display: state.name }; - })} - value={value} - optionsFontsize="sm" - onChange={onChange} - icon={} - footerOption={ - - } - /> - )} - > - + ( + { + return { value: state.id, display: state.name }; + })} + value={value} + optionsFontsize="sm" + onChange={onChange} + icon={} + footerOption={ + + } + /> + )} + /> ); }; diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx index c993ea22d..1c0fc9d46 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useState } from "react"; // next -import Link from "next/link"; import { useRouter } from "next/router"; +import dynamic from "next/dynamic"; // swr import { mutate } from "swr"; // react hook form -import { useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; // fetching keys import { PROJECT_ISSUES_DETAILS, @@ -14,7 +14,7 @@ import { USER_ISSUE, } from "constants/fetch-keys"; // headless -import { Dialog, Transition } from "@headlessui/react"; +import { Dialog, Menu, Transition } from "@headlessui/react"; // services import issuesServices from "lib/services/issues.services"; // hooks @@ -31,12 +31,13 @@ import SelectLabels from "./SelectLabels"; import SelectProject from "./SelectProject"; import SelectPriority from "./SelectPriority"; import SelectAssignee from "./SelectAssignee"; -import SelectParent from "./SelectParentIssues"; +import SelectParent from "./SelectParentIssue"; import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal"; import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; // types -import type { IIssue, IssueResponse, SprintIssueResponse } from "types"; +import type { IIssue, IssueResponse, CycleIssueResponse } from "types"; +import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline"; type Props = { isOpen: boolean; @@ -48,8 +49,13 @@ type Props = { }; const defaultValues: Partial = { + project: "", name: "", - description: "", + // description: "", + state: "", + sprints: null, + priority: null, + labels_list: [], }; const CreateUpdateIssuesModal: React.FC = ({ @@ -62,9 +68,20 @@ const CreateUpdateIssuesModal: React.FC = ({ }) => { const [isCycleModalOpen, setIsCycleModalOpen] = useState(false); const [isStateModalOpen, setIsStateModalOpen] = useState(false); + const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const [mostSimilarIssue, setMostSimilarIssue] = useState(); + // const [issueDescriptionValue, setIssueDescriptionValue] = useState(""); + // const handleDescriptionChange: any = (value: any) => { + // console.log(value); + // setIssueDescriptionValue(value); + // }; + + const RichTextEditor = dynamic(() => import("components/lexical/editor"), { + ssr: false, + }); + const router = useRouter(); const handleClose = () => { @@ -74,13 +91,6 @@ const CreateUpdateIssuesModal: React.FC = ({ } }; - const resetForm = () => { - const timeout = setTimeout(() => { - reset(defaultValues); - clearTimeout(timeout); - }, 500); - }; - const { activeWorkspace, activeProject, user, issues } = useUser(); const { setToastAlert } = useToast(); @@ -97,6 +107,13 @@ const CreateUpdateIssuesModal: React.FC = ({ defaultValues, }); + const resetForm = () => { + const timeout = setTimeout(() => { + reset(defaultValues); + clearTimeout(timeout); + }, 500); + }; + const addIssueToSprint = async (issueId: string, sprintId: string, issueDetail: IIssue) => { if (!activeWorkspace || !activeProject) return; await issuesServices @@ -104,8 +121,7 @@ const CreateUpdateIssuesModal: React.FC = ({ issue: issueId, }) .then((res) => { - console.log("add to sprint", res); - mutate( + mutate( CYCLE_ISSUES(sprintId), (prevData) => { const targetResponse = prevData?.find((t) => t.cycle === sprintId); @@ -118,7 +134,7 @@ const CreateUpdateIssuesModal: React.FC = ({ { cycle: sprintId, issue_details: issueDetail, - } as SprintIssueResponse, + } as CycleIssueResponse, ]; } }, @@ -166,17 +182,7 @@ const CreateUpdateIssuesModal: React.FC = ({ .createIssues(activeWorkspace.slug, activeProject.id, payload) .then(async (res) => { console.log(res); - mutate( - PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), - (prevData) => { - return { - ...(prevData as IssueResponse), - results: [res, ...(prevData?.results ?? [])], - count: (prevData?.count ?? 0) + 1, - }; - }, - false - ); + mutate(PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id)); if (formData.sprints && formData.sprints !== null) { await addIssueToSprint(res.id, formData.sprints, formData); @@ -189,13 +195,7 @@ const CreateUpdateIssuesModal: React.FC = ({ message: `Issue ${data ? "updated" : "created"} successfully`, }); if (formData.assignees_list.some((assignee) => assignee === user?.id)) { - mutate( - USER_ISSUE, - (prevData) => { - return [res, ...(prevData ?? [])]; - }, - false - ); + mutate(USER_ISSUE); } }) .catch((err) => { @@ -261,6 +261,8 @@ const CreateUpdateIssuesModal: React.FC = ({ return () => setMostSimilarIssue(undefined); }, []); + // console.log(watch("parent")); + return ( <> {activeProject && ( @@ -381,6 +383,13 @@ const CreateUpdateIssuesModal: React.FC = ({ error={errors.description} register={register} /> + {/* ( + + )} + /> */}
    = ({ - - + + + + + + + + + +
    + + + +
    +
    +
    +
    diff --git a/apps/app/components/project/issues/IssuesListModal.tsx b/apps/app/components/project/issues/IssuesListModal.tsx new file mode 100644 index 000000000..701da9beb --- /dev/null +++ b/apps/app/components/project/issues/IssuesListModal.tsx @@ -0,0 +1,140 @@ +// react +import React, { useState } from "react"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// icons +import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue, IssueResponse } from "types"; +import { classNames } from "constants/common"; +import useUser from "lib/hooks/useUser"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + onChange: (...event: any[]) => void; + issues: IssueResponse | undefined; +}; + +const IssuesListModal: React.FC = ({ isOpen, handleClose: onClose, onChange, issues }) => { + const [query, setQuery] = useState(""); + + const { activeProject } = useUser(); + + const handleClose = () => { + onClose(); + setQuery(""); + }; + + const filteredIssues: IIssue[] = + query === "" + ? issues?.results ?? [] + : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? + []; + + return ( + <> + setQuery("")} appear> + + +
    + + +
    + + + +
    +
    + + + {filteredIssues.length > 0 && ( +
  • + {query === "" && ( +

    + Issues +

    + )} +
      + {filteredIssues.map((issue) => ( + + 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={() => { + // setIssueIdFromList(issue.id); + handleClose(); + }} + > + + + {activeProject?.identifier}-{issue.sequence_id} + {" "} + {issue.name} + + ))} +
    +
  • + )} +
    + + {query !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    +
    +
    +
    +
    +
    + + ); +}; + +export default IssuesListModal; diff --git a/apps/app/components/project/issues/ListView/index.tsx b/apps/app/components/project/issues/ListView/index.tsx index 08dfe7737..5bbe2ea84 100644 --- a/apps/app/components/project/issues/ListView/index.tsx +++ b/apps/app/components/project/issues/ListView/index.tsx @@ -14,6 +14,7 @@ import { IIssue, IssueResponse, IState, NestedKeyOf, Properties, WorkspaceMember // hooks import useUser from "lib/hooks/useUser"; // fetch keys +import { PRIORITIES } from "constants/"; import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // services import issuesServices from "lib/services/issues.services"; @@ -36,8 +37,6 @@ type Props = { handleDeleteIssue: React.Dispatch>; }; -const PRIORITIES = ["high", "medium", "low"]; - const ListView: React.FC = ({ properties, groupedByIssues, @@ -175,10 +174,6 @@ const ListView: React.FC = ({ {activeProject?.identifier}-{issue.sequence_id} - ) : (key as keyof Properties) === "description" ? ( - - {issue.description} - ) : (key as keyof Properties) === "priority" ? ( = ({ )} - ) : (key as keyof Properties) === "children" ? ( - - No children. - ) : (key as keyof Properties) === "target_date" ? ( {issue.target_date @@ -449,4 +440,4 @@ const ListView: React.FC = ({ ); }; -export default ListView; +export default ListView; \ No newline at end of file diff --git a/apps/app/components/project/issues/issue-detail/activity/index.tsx b/apps/app/components/project/issues/issue-detail/activity/index.tsx index 34dfd4cd9..2df8c5092 100644 --- a/apps/app/components/project/issues/issue-detail/activity/index.tsx +++ b/apps/app/components/project/issues/issue-detail/activity/index.tsx @@ -1,18 +1,24 @@ // next import Image from "next/image"; +// ui +import { Spinner } from "ui"; +// icons import { CalendarDaysIcon, ChartBarIcon, ChatBubbleBottomCenterTextIcon, Squares2X2Icon, + UserIcon, } from "@heroicons/react/24/outline"; +// types +import { IssueResponse, IState } from "types"; +// constants import { addSpaceIfCamelCase, timeAgo } from "constants/common"; -import { IIssue, IState } from "types"; -import { Spinner } from "ui"; type Props = { issueActivities: any[] | undefined; states: IState[] | undefined; + issues: IssueResponse | undefined; }; const activityIcons: { @@ -23,9 +29,10 @@ const activityIcons: { name: , description: , target_date: , + parent: , }; -const IssueActivitySection: React.FC = ({ issueActivities, states }) => { +const IssueActivitySection: React.FC = ({ issueActivities, states, issues }) => { return ( <> {issueActivities ? ( @@ -92,6 +99,10 @@ const IssueActivitySection: React.FC = ({ issueActivities, states }) => { states?.find((s) => s.id === activity.old_value)?.name ?? "" ) : "None" + : activity.field === "parent" + ? activity.old_value + ? issues?.results.find((i) => i.id === activity.old_value)?.name + : "None" : activity.old_value ?? "None"}
    @@ -102,6 +113,10 @@ const IssueActivitySection: React.FC = ({ issueActivities, states }) => { states?.find((s) => s.id === activity.new_value)?.name ?? "" ) : "None" + : activity.field === "parent" + ? activity.new_value + ? issues?.results.find((i) => i.id === activity.new_value)?.name + : "None" : activity.new_value ?? "None"}
    diff --git a/apps/app/components/project/issues/my-issues/ChangeStateDropdown.tsx b/apps/app/components/project/issues/my-issues/ChangeStateDropdown.tsx index 7dd33cef6..1b84fd937 100644 --- a/apps/app/components/project/issues/my-issues/ChangeStateDropdown.tsx +++ b/apps/app/components/project/issues/my-issues/ChangeStateDropdown.tsx @@ -73,7 +73,7 @@ const ChangeStateDropdown: React.FC = ({ issue, updateIssues }) => { leaveFrom="opacity-100" leaveTo="opacity-0" > - + {states?.map((state) => ( ; + isSubmitting: boolean; +}; + +const ControlSettings: React.FC = ({ control, isSubmitting }) => { + const { activeWorkspace } = useUser(); + + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + return ( + <> +
    +
    +

    Control

    +

    Set the control for the project.

    +
    +
    +
    + ( + + {({ open }) => ( + <> + +
    Project Lead
    +
    +
    + + + {people?.find((person) => person.member.id === value)?.member + .first_name ?? "Select Lead"} + + + + + + + + {people?.map((person) => ( + + `${ + active ? "text-white bg-theme" : "text-gray-900" + } cursor-default select-none relative py-2 pl-3 pr-9` + } + value={person.member.id} + > + {({ selected, active }) => ( + <> + + {person.member.first_name} + + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
    + + )} +
    + )} + /> +
    +
    + ( + + {({ open }) => ( + <> + +
    Default Assignee
    +
    +
    + + + {people?.find((p) => p.member.id === value)?.member.first_name ?? + "Select Default Assignee"} + + + + + + + + {people?.map((person) => ( + + `${ + active ? "text-white bg-theme" : "text-gray-900" + } cursor-default select-none relative py-2 pl-3 pr-9` + } + value={person.member.id} + > + {({ selected, active }) => ( + <> + + {person.member.first_name} + + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
    + + )} +
    + )} + /> +
    +
    +
    + +
    +
    + + ); +}; + +export default ControlSettings; diff --git a/apps/app/components/project/settings/GeneralSettings.tsx b/apps/app/components/project/settings/GeneralSettings.tsx new file mode 100644 index 000000000..2e7f74a93 --- /dev/null +++ b/apps/app/components/project/settings/GeneralSettings.tsx @@ -0,0 +1,119 @@ +// react +import { useCallback } from "react"; +// react-hook-form +import { UseFormRegister, UseFormSetError } from "react-hook-form"; +// services +import projectServices from "lib/services/project.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// ui +import { Input, Select, TextArea } from "ui"; +// types +import { IProject } from "types"; +// constants +import { debounce } from "constants/common"; + +type Props = { + register: UseFormRegister; + errors: any; + setError: UseFormSetError; +}; + +const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; + +const GeneralSettings: React.FC = ({ register, errors, setError }) => { + const { activeWorkspace } = useUser(); + + const checkIdentifier = (slug: string, value: string) => { + projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => { + console.log(response); + if (response.exists) setError("identifier", { message: "Identifier already exists" }); + }); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []); + + return ( + <> +
    +
    +

    General

    +

    + This information will be displayed to every member of the project. +

    +
    +
    +
    + +
    +
    + { + if (!activeWorkspace || !e.target.value) return; + checkIdentifierAvailability(activeWorkspace.slug, e.target.value); + }} + validations={{ + required: "Identifier is required", + minLength: { + value: 1, + message: "Identifier must at least be of 1 character", + }, + maxLength: { + value: 9, + message: "Identifier must at most be of 9 characters", + }, + }} + /> +
    +
    +
    +