diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index 32f71a3db..8aafbf6da 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -24,7 +24,7 @@ import { import ShortcutsModal from "components/command-palette/shortcuts"; import CreateProjectModal from "components/project/create-project-modal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; -import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; +import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; // ui import { Button } from "ui"; // types @@ -33,6 +33,7 @@ import { IIssue, IssueResponse } from "types"; import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; // constants import { classNames, copyTextToClipboard } from "constants/common"; +import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal"; type FormInput = { issue_ids: string[]; @@ -47,6 +48,7 @@ const CommandPalette: React.FC = () => { const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false); const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false); + const [isCreateModuleModalOpen, setisCreateModuleModalOpen] = useState(false); const { activeWorkspace, activeProject, issues } = useUser(); @@ -109,6 +111,9 @@ const CommandPalette: React.FC = () => { } else if ((e.ctrlKey || e.metaKey) && e.key === "q") { e.preventDefault(); setIsCreateCycleModalOpen(true); + } else if ((e.ctrlKey || e.metaKey) && e.key === "m") { + e.preventDefault(); + setisCreateModuleModalOpen(true); } else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") { e.preventDefault(); @@ -184,11 +189,18 @@ const CommandPalette: React.FC = () => { {activeProject && ( - + <> + + + )} = ({ isOpen, setIsOpen }) => { { keys: "ctrl,p", description: "To create project" }, { keys: "ctrl,i", description: "To create issue" }, { keys: "ctrl,q", description: "To create cycle" }, + { keys: "ctrl,m", description: "To create module" }, { keys: "ctrl,h", description: "To open shortcuts guide" }, { keys: "ctrl,alt,c", diff --git a/apps/app/components/project/cycles/board-view/index.tsx b/apps/app/components/project/cycles/board-view/index.tsx index 07d68f20d..97ecd8290 100644 --- a/apps/app/components/project/cycles/board-view/index.tsx +++ b/apps/app/components/project/cycles/board-view/index.tsx @@ -14,8 +14,8 @@ type Props = { selectedGroup: NestedKeyOf | null; members: IProjectMember[] | undefined; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; - openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; + openIssuesListModal: () => void; + removeIssueFromCycle: (bridgeId: string) => void; }; const CyclesBoardView: React.FC = ({ diff --git a/apps/app/components/project/cycles/board-view/single-board.tsx b/apps/app/components/project/cycles/board-view/single-board.tsx index c437824e4..16b9cd0b4 100644 --- a/apps/app/components/project/cycles/board-view/single-board.tsx +++ b/apps/app/components/project/cycles/board-view/single-board.tsx @@ -50,8 +50,8 @@ type Props = { createdBy: string | null; bgColor?: string; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; - openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; + openIssuesListModal: () => void; + removeIssueFromCycle: (bridgeId: string) => void; }; const SingleCycleBoard: React.FC = ({ @@ -106,12 +106,6 @@ const SingleCycleBoard: React.FC = ({ backgroundColor: `${bgColor}20`, }} > -

= ({ leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - -
+ +
{(active) => ( diff --git a/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx b/apps/app/components/project/cycles/confirm-cycle-deletion.tsx similarity index 100% rename from apps/app/components/project/cycles/ConfirmCycleDeletion.tsx rename to apps/app/components/project/cycles/confirm-cycle-deletion.tsx diff --git a/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx b/apps/app/components/project/cycles/create-update-cycle-modal.tsx similarity index 100% rename from apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx rename to apps/app/components/project/cycles/create-update-cycle-modal.tsx diff --git a/apps/app/components/project/cycles/CycleIssuesListModal.tsx b/apps/app/components/project/cycles/cycle-issues-list-modal.tsx similarity index 77% rename from apps/app/components/project/cycles/CycleIssuesListModal.tsx rename to apps/app/components/project/cycles/cycle-issues-list-modal.tsx index d9f3226c0..8132ca19f 100644 --- a/apps/app/components/project/cycles/CycleIssuesListModal.tsx +++ b/apps/app/components/project/cycles/cycle-issues-list-modal.tsx @@ -145,37 +145,37 @@ const CycleIssuesListModal: React.FC = ({ )}
    {filteredIssues.map((issue) => { - // if (issue.cycle !== cycleId) - return ( - - 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} - - )} - - ); + if (!issue.issue_cycle) + return ( + + 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} + + )} + + ); })}
diff --git a/apps/app/components/project/cycles/list-view/index.tsx b/apps/app/components/project/cycles/list-view/index.tsx index 2ec0c6731..aa658f2a6 100644 --- a/apps/app/components/project/cycles/list-view/index.tsx +++ b/apps/app/components/project/cycles/list-view/index.tsx @@ -11,7 +11,7 @@ import cycleServices from "lib/services/cycles.service"; // hooks import useUser from "lib/hooks/useUser"; // ui -import { Spinner } from "ui"; +import { CustomMenu, Spinner } from "ui"; // icons import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; import { CalendarDaysIcon } from "@heroicons/react/24/outline"; @@ -35,7 +35,7 @@ type Props = { selectedGroup: NestedKeyOf | null; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; + removeIssueFromCycle: (bridgeId: string) => void; }; const CyclesListView: React.FC = ({ @@ -241,51 +241,19 @@ const CyclesListView: React.FC = ({
)} - - + openCreateIssueModal(issue, "edit")} > - - - - - - - -
- -
-
- -
- -
-
-
-
+ Edit + + removeIssueFromCycle(issue.bridge ?? "")} + > + Remove from cycle + + Delete permanently + ); diff --git a/apps/app/components/project/cycles/stats-view/index.tsx b/apps/app/components/project/cycles/stats-view/index.tsx index 4eb14608b..a72d57b13 100644 --- a/apps/app/components/project/cycles/stats-view/index.tsx +++ b/apps/app/components/project/cycles/stats-view/index.tsx @@ -1,20 +1,53 @@ -// next -import Link from "next/link"; -// hooks -import useUser from "lib/hooks/useUser"; +// react +import { useState } from "react"; +// components +import SingleStat from "components/project/cycles/stats-view/single-stat"; +import ConfirmCycleDeletion from "components/project/cycles/confirm-cycle-deletion"; // types -import { ICycle } from "types"; -import SingleStat from "./single-stat"; +import { ICycle, SelectSprintType } from "types"; type Props = { cycles: ICycle[]; + setCreateUpdateCycleModal: React.Dispatch>; + setSelectedCycle: React.Dispatch>; }; -const CycleStatsView: React.FC = ({ cycles }) => { +const CycleStatsView: React.FC = ({ + cycles, + setCreateUpdateCycleModal, + setSelectedCycle, +}) => { + const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); + const [cycleDeleteModal, setCycleDeleteModal] = useState(false); + + const handleDeleteCycle = (cycle: ICycle) => { + setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); + setCycleDeleteModal(true); + }; + + const handleEditCycle = (cycle: ICycle) => { + setSelectedCycle({ ...cycle, actionType: "edit" }); + setCreateUpdateCycleModal(true); + }; + return ( <> + {cycles.map((cycle) => ( - + handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + /> ))} ); diff --git a/apps/app/components/project/cycles/stats-view/single-stat.tsx b/apps/app/components/project/cycles/stats-view/single-stat.tsx index ae5301fe9..f2382e279 100644 --- a/apps/app/components/project/cycles/stats-view/single-stat.tsx +++ b/apps/app/components/project/cycles/stats-view/single-stat.tsx @@ -1,37 +1,46 @@ +// react +import React, { useState } from "react"; // next import Link from "next/link"; +import Image from "next/image"; // swr import useSWR from "swr"; // services import cyclesService from "lib/services/cycles.service"; // hooks import useUser from "lib/hooks/useUser"; +// ui +import { Button, CustomMenu } from "ui"; // types import { CycleIssueResponse, ICycle } from "types"; // fetch-keys import { CYCLE_ISSUES } from "constants/fetch-keys"; import { groupBy, renderShortNumericDateFormat } from "constants/common"; -import { - CheckIcon, - ExclamationCircleIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/outline"; +import { ArrowPathIcon, CheckIcon, UserIcon } from "@heroicons/react/24/outline"; +import { CalendarDaysIcon } from "@heroicons/react/20/solid"; +import { useRouter } from "next/router"; -type Props = { cycle: ICycle }; - -const stateGroupIcons: { - [key: string]: JSX.Element; -} = { - backlog: , - unstarted: , - started: , - cancelled: , - completed: , +type Props = { + cycle: ICycle; + handleEditCycle: () => void; + handleDeleteCycle: () => void; }; -const SingleStat: React.FC = ({ cycle }) => { +const stateGroupColours: { + [key: string]: string; +} = { + backlog: "#3f76ff", + unstarted: "#ff9e9e", + started: "#d687ff", + cancelled: "#ff5353", + completed: "#096e8d", +}; + +const SingleStat: React.FC = ({ cycle, handleEditCycle, handleDeleteCycle }) => { const { activeWorkspace, activeProject } = useUser(); + const router = useRouter(); + const { data: cycleIssues } = useSWR( activeWorkspace && activeProject && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null, activeWorkspace && activeProject && cycle.id @@ -48,7 +57,6 @@ const SingleStat: React.FC = ({ cycle }) => { ...groupBy(cycleIssues ?? [], "issue_details.state_detail.group"), }; - // status calculator const startDate = new Date(cycle.start_date ?? ""); const endDate = new Date(cycle.end_date ?? ""); const today = new Date(); @@ -56,43 +64,110 @@ const SingleStat: React.FC = ({ cycle }) => { return ( <>
-
-
+
+
- {cycle.name} + +

{cycle.name}

+
+ + {today.getDate() < startDate.getDate() + ? "Not started" + : today.getDate() > endDate.getDate() + ? "Over" + : "Active"} + + + Edit cycle + + Delete cycle permanently + + +
+
-
- {renderShortNumericDateFormat(startDate)} - {" - "} - {renderShortNumericDateFormat(endDate)} + +
+
+ + Cycle dates +
+
+ {renderShortNumericDateFormat(startDate)} - {renderShortNumericDateFormat(endDate)} +
+
+ + Created by +
+
+ {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( + {cycle.owned_by.first_name} + ) : ( + + {cycle.owned_by.first_name.charAt(0)} + + )} + {cycle.owned_by.first_name} +
+
+ + Active members +
+
+
+
+ +
-
- {today.getDate() < startDate.getDate() - ? "Not started" - : today.getDate() > endDate.getDate() - ? "Over" - : "Active"} -
-
-
-
- {Object.keys(groupedIssues).map((group) => { - return ( -
-
-
- {stateGroupIcons[group]} +
+

PROGRESS

+
+ {Object.keys(groupedIssues).map((group) => { + return ( +
+
+ +
{group}
+
+
+ + {groupedIssues[group].length}{" "} + + -{" "} + {cycleIssues && cycleIssues.length > 0 + ? `${(groupedIssues[group].length / cycleIssues.length) * 100}%` + : "0%"} + +
-
-
{group}
- {groupedIssues[group].length} -
-
- ); - })} + ); + })} +
+
diff --git a/apps/app/components/project/issues/BoardView/single-board.tsx b/apps/app/components/project/issues/BoardView/single-board.tsx index 7e843a8ca..18c16304b 100644 --- a/apps/app/components/project/issues/BoardView/single-board.tsx +++ b/apps/app/components/project/issues/BoardView/single-board.tsx @@ -130,12 +130,6 @@ const SingleBoard: React.FC = ({ backgroundColor: `${bgColor}20`, }} > -

; diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx index c7bcd1ad5..e872d4753 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx @@ -33,7 +33,7 @@ import SelectPriority from "./SelectPriority"; import SelectAssignee from "./SelectAssignee"; import SelectParent from "./SelectParentIssue"; import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal"; -import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; +import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; // types import type { IIssue, IssueResponse, CycleIssueResponse } from "types"; import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline"; diff --git a/apps/app/components/project/issues/IssuesListModal.tsx b/apps/app/components/project/issues/IssuesListModal.tsx deleted file mode 100644 index b635a16b6..000000000 --- a/apps/app/components/project/issues/IssuesListModal.tsx +++ /dev/null @@ -1,180 +0,0 @@ -// react -import React, { useState } from "react"; -// headless ui -import { Combobox, Dialog, Transition } from "@headlessui/react"; -// ui -import { Button } from "ui"; -// icons -import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; -// types -import { IIssue } from "types"; -import { classNames } from "constants/common"; -import useUser from "lib/hooks/useUser"; - -type Props = { - isOpen: boolean; - handleClose: () => void; - value?: any; - onChange: (...event: any[]) => void; - issues: IIssue[]; - title?: string; - multiple?: boolean; - customDisplay?: JSX.Element; -}; - -const IssuesListModal: React.FC = ({ - isOpen, - handleClose: onClose, - value, - onChange, - issues, - title = "Issues", - multiple = false, - customDisplay, -}) => { - const [query, setQuery] = useState(""); - const [values, setValues] = useState([]); - - const { activeProject } = useUser(); - - const handleClose = () => { - onClose(); - setQuery(""); - setValues([]); - }; - - const filteredIssues: IIssue[] = - query === "" - ? issues ?? [] - : issues?.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; - - return ( - <> - setQuery("")} appear> - - -
- - -
- - - { - if (multiple) setValues(val); - else onChange(val); - }} - // multiple={multiple} - > -
-
-
{customDisplay}
- - {filteredIssues.length > 0 && ( -
  • - {query === "" && ( -

    - {title} -

    - )} -
      - {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={() => { - if (!multiple) handleClose(); - }} - > - {({ selected }) => ( - <> - {multiple ? ( - - ) : null} - - - {activeProject?.identifier}-{issue.sequence_id} - {" "} - {issue.name} - - )} - - ))} -
    -
  • - )} -
    - - {query !== "" && filteredIssues.length === 0 && ( -
    -
    - )} -
    - {multiple ? ( -
    - - -
    - ) : null} -
    -
    -
    -
    -
    - - ); -}; - -export default IssuesListModal; diff --git a/apps/app/components/command-palette/addAsSubIssue.tsx b/apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx similarity index 100% rename from apps/app/components/command-palette/addAsSubIssue.tsx rename to apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx index 0e839ca94..b1b6bc15e 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx @@ -6,33 +6,23 @@ import { Listbox, Transition } from "@headlessui/react"; // react hook form import { useForm, Controller, UseFormWatch } from "react-hook-form"; // services -import stateServices from "lib/services/state.service"; import issuesServices from "lib/services/issues.service"; -import workspaceService from "lib/services/workspace.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; -// components -import IssuesListModal from "components/project/issues/IssuesListModal"; // fetching keys -import { STATE_LIST, WORKSPACE_MEMBERS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; // commons -import { classNames, copyTextToClipboard } from "constants/common"; -import { PRIORITIES } from "constants/"; +import { copyTextToClipboard } from "constants/common"; // ui import { Input, Button, Spinner } from "ui"; import { Popover } from "@headlessui/react"; // icons import { - UserIcon, TagIcon, - UserGroupIcon, ChevronDownIcon, - Squares2X2Icon, - ChartBarIcon, ClipboardDocumentIcon, LinkIcon, - ArrowPathIcon, CalendarDaysIcon, TrashIcon, PlusIcon, @@ -40,7 +30,7 @@ import { } from "@heroicons/react/24/outline"; // types import type { Control } from "react-hook-form"; -import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types"; +import type { IIssue, IIssueLabels, NestedKeyOf } from "types"; import { TwitterPicker } from "react-color"; import { positionEditorElement } from "components/lexical/helpers/editor"; import SelectState from "./select-state"; @@ -48,6 +38,8 @@ import SelectPriority from "./select-priority"; import SelectParent from "./select-parent"; import SelectCycle from "./select-cycle"; import SelectAssignee from "./select-assignee"; +import SelectBlocker from "./select-blocker"; +import SelectBlocked from "./select-blocked"; type Props = { control: Control; @@ -69,11 +61,9 @@ const IssueDetailSidebar: React.FC = ({ watch: watchIssue, setDeleteIssueModal, }) => { - const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); - const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); const [createLabelForm, setCreateLabelForm] = useState(false); - const { activeWorkspace, activeProject, cycles, issues } = useUser(); + const { activeWorkspace, activeProject, issues } = useUser(); const { setToastAlert } = useToast(); @@ -106,43 +96,6 @@ const IssueDetailSidebar: React.FC = ({ }); }; - const sidebarSections: Array< - Array<{ - label: string; - name: NestedKeyOf; - canSelectMultipleOptions: boolean; - icon: (props: any) => JSX.Element; - options?: Array<{ label: string; value: any; color?: string }>; - modal: boolean; - issuesList?: Array; - isOpen?: boolean; - setIsOpen?: (arg: boolean) => void; - }> - > = [ - [ - // { - // label: "Blocker", - // name: "blockers_list", - // canSelectMultipleOptions: true, - // icon: UserIcon, - // issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [], - // modal: true, - // isOpen: isBlockerModalOpen, - // setIsOpen: setIsBlockerModalOpen, - // }, - // { - // label: "Blocked", - // name: "blocked_list", - // canSelectMultipleOptions: true, - // icon: UserIcon, - // issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [], - // modal: true, - // isOpen: isBlockedModalOpen, - // setIsOpen: setIsBlockedModalOpen, - // }, - ], - ]; - const handleCycleChange = (cycleId: string) => { if (activeWorkspace && activeProject && issueDetail) issuesServices.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, { @@ -150,6 +103,8 @@ const IssueDetailSidebar: React.FC = ({ }); }; + console.log(issueDetail); + return ( <>
    @@ -215,7 +170,7 @@ const IssueDetailSidebar: React.FC = ({
    - +
    = ({
    ) } - watchIssue={watchIssue} + watch={watchIssue} + /> + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} + /> + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} />
    @@ -274,72 +239,6 @@ const IssueDetailSidebar: React.FC = ({
    - {/* {sidebarSections.map((section, index) => ( -
    - {section.map((item) => ( -
    -
    - -

    {item.label}

    -
    -
    - ( - <> - item.setIsOpen && item.setIsOpen(false)} - onChange={(val) => { - console.log(val); - submitChanges({ [item.name]: val }); - onChange(val); - }} - issues={item?.issuesList ?? []} - title={`Select ${item.label}`} - multiple={item.canSelectMultipleOptions} - value={value} - customDisplay={ - issueDetail?.parent_detail ? ( - - ) : ( -
    - No parent selected -
    - ) - } - /> - - - )} - /> -
    -
    - ))} -
    - ))} */}
    @@ -376,7 +275,7 @@ const IssueDetailSidebar: React.FC = ({ as="div" value={value} multiple - onChange={(val) => submitChanges({ labels_list: val })} + onChange={(val: any) => submitChanges({ labels_list: val })} className="flex-shrink-0" > {({ open }) => ( @@ -395,7 +294,7 @@ const IssueDetailSidebar: React.FC = ({ leaveTo="opacity-0" > -
    +
    {issueLabels ? ( issueLabels.length > 0 ? ( issueLabels.map((label: IIssueLabels) => ( @@ -403,10 +302,8 @@ const IssueDetailSidebar: React.FC = ({ key={label.id} className={({ active, selected }) => `${ - active || selected - ? "text-white bg-theme" - : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + active || selected ? "bg-indigo-50" : "" + } flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 truncate` } value={label.id} > diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx index 0fff8006b..c5451c548 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx @@ -126,7 +126,7 @@ const SelectAssignee: React.FC = ({ control, submitChanges }) => { leaveTo="opacity-0" > -
    +
    {people ? ( people.length > 0 ? ( people.map((option) => ( @@ -134,8 +134,8 @@ const SelectAssignee: React.FC = ({ control, submitChanges }) => { key={option.member.id} className={({ active, selected }) => `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + active || selected ? "bg-indigo-50" : "" + } flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 rounded-md truncate` } value={option.member.id} > diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx new file mode 100644 index 000000000..9c3b10093 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx @@ -0,0 +1,238 @@ +// react +import React, { useState } from "react"; +// react-hook-form +import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form"; +// hooks +import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; +// icons +import { + FolderIcon, + MagnifyingGlassIcon, + UserGroupIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +// types +import { IIssue } from "types"; +// constants +import { classNames } from "constants/common"; + +type FormInput = { + issue_ids: string[]; +}; + +type Props = { + submitChanges: (formData: Partial) => void; + issuesList: IIssue[]; + watch: UseFormWatch; +}; + +const SelectBlocked: React.FC = ({ submitChanges, issuesList, watch }) => { + const [query, setQuery] = useState(""); + const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); + + const { activeProject, issues } = useUser(); + const { setToastAlert } = useToast(); + + const { register, handleSubmit, reset, watch: watchIssues } = useForm(); + + const handleClose = () => { + setIsBlockedModalOpen(false); + reset(); + }; + + const onSubmit: SubmitHandler = (data) => { + if (!data.issue_ids || data.issue_ids.length === 0) { + setToastAlert({ + title: "Error", + type: "error", + message: "Please select atleast one issue", + }); + return; + } + const newBlocked = [...watch("blocked_list"), ...data.issue_ids]; + submitChanges({ blocked_list: newBlocked }); + handleClose(); + }; + + return ( +
    +
    + +

    Blocked issues

    +
    +
    +
    + {watch("blocked_list") && watch("blocked_list").length > 0 + ? watch("blocked_list").map((issue) => ( + { + const updatedBlockers = watch("blocked_list").filter((i) => i !== issue); + submitChanges({ + blocked_list: updatedBlockers, + }); + }} + > + {`${activeProject?.identifier}-${ + issues?.results.find((i) => i.id === issue)?.sequence_id + }`} + + + )) + : null} +
    + setQuery("")} + appear + > + + +
    + + +
    + + +
    + +
    +
    + + + {issuesList.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Select blocked issues +

    + )} +
      + {issuesList.map((issue) => { + if (!watch("blocked_list").includes(issue.id)) { + return ( + + classNames( + "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 }) => ( + <> +
      + + + + {activeProject?.identifier}-{issue.sequence_id} + + {issue.name} +
      + + )} +
      + ); + } + })} +
    +
  • + + )} +
    + + {query !== "" && issuesList.length === 0 && ( +
    +
    + )} +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + ); +}; + +export default SelectBlocked; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx new file mode 100644 index 000000000..ee2352620 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx @@ -0,0 +1,238 @@ +// react +import React, { useState } from "react"; +// react-hook-form +import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form"; +// hooks +import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; +// icons +import { + FolderIcon, + MagnifyingGlassIcon, + UserGroupIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +// types +import { IIssue } from "types"; +// constants +import { classNames } from "constants/common"; + +type FormInput = { + issue_ids: string[]; +}; + +type Props = { + submitChanges: (formData: Partial) => void; + issuesList: IIssue[]; + watch: UseFormWatch; +}; + +const SelectBlocker: React.FC = ({ submitChanges, issuesList, watch }) => { + const [query, setQuery] = useState(""); + const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); + + const { activeProject, issues } = useUser(); + const { setToastAlert } = useToast(); + + const { register, handleSubmit, reset } = useForm(); + + const handleClose = () => { + setIsBlockerModalOpen(false); + reset(); + }; + + const onSubmit: SubmitHandler = (data) => { + if (!data.issue_ids || data.issue_ids.length === 0) { + setToastAlert({ + title: "Error", + type: "error", + message: "Please select atleast one issue", + }); + return; + } + const newBlockers = [...watch("blockers_list"), ...data.issue_ids]; + submitChanges({ blockers_list: newBlockers }); + handleClose(); + }; + + return ( +
    +
    + +

    Blocker issues

    +
    +
    +
    + {watch("blockers_list") && watch("blockers_list").length > 0 + ? watch("blockers_list").map((issue) => ( + { + const updatedBlockers = watch("blockers_list").filter((i) => i !== issue); + submitChanges({ + blockers_list: updatedBlockers, + }); + }} + > + {`${activeProject?.identifier}-${ + issues?.results.find((i) => i.id === issue)?.sequence_id + }`} + + + )) + : null} +
    + setQuery("")} + appear + > + + +
    + + +
    + + +
    + +
    +
    + + + {issuesList.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Select blocker issues +

    + )} +
      + {issuesList.map((issue) => { + if (!watch("blockers_list").includes(issue.id)) { + return ( + + classNames( + "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 }) => ( + <> +
      + + + + {activeProject?.identifier}-{issue.sequence_id} + + {issue.name} +
      + + )} +
      + ); + } + })} +
    +
  • + + )} +
    + + {query !== "" && issuesList.length === 0 && ( +
    +
    + )} +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + ); +}; + +export default SelectBlocker; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx index 0e36c54af..80dd4a899 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx @@ -10,6 +10,7 @@ import { classNames } from "constants/common"; import { Spinner } from "ui"; import React from "react"; import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +import CustomSelect from "ui/custom-select"; type Props = { control: Control; @@ -30,64 +31,38 @@ const SelectCycle: React.FC = ({ control, handleCycleChange }) => { control={control} name="cycle" render={({ field: { value } }) => ( - { - handleCycleChange(value); - }} - className="flex-shrink-0" - > - {({ open }) => ( -
    - - - {value ? cycles?.find((c) => c.id === value)?.name : "None"} - - - - - + - -
    - {cycles ? ( - cycles.length > 0 ? ( - cycles.map((option) => ( - - `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={option.id} - > - {option.name} - - )) - ) : ( -
    No cycles found
    - ) - ) : ( - - )} -
    -
    -
    -
    - )} -
    + {value ? cycles?.find((c) => c.id === value)?.name : "None"} + + } + value={value} + onChange={(value: any) => { + handleCycleChange(value); + }} + > + {cycles ? ( + cycles.length > 0 ? ( + cycles.map((option) => ( + + {option.name} + + )) + ) : ( +
    No cycles found
    + ) + ) : ( + + )} + + )} />
    diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx index 03cf0be4e..3ec5922fe 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx @@ -5,7 +5,7 @@ import { Control, Controller, UseFormWatch } from "react-hook-form"; // hooks import useUser from "lib/hooks/useUser"; // components -import IssuesListModal from "components/project/issues/IssuesListModal"; +import IssuesListModal from "components/project/issues/issues-list-modal"; // icons import { UserIcon } from "@heroicons/react/24/outline"; // types @@ -16,7 +16,7 @@ type Props = { submitChanges: (formData: Partial) => void; issuesList: IIssue[]; customDisplay: JSX.Element; - watchIssue: UseFormWatch; + watch: UseFormWatch; }; const SelectParent: React.FC = ({ @@ -24,7 +24,7 @@ const SelectParent: React.FC = ({ submitChanges, issuesList, customDisplay, - watchIssue, + watch, }) => { const [isParentModalOpen, setIsParentModalOpen] = useState(false); @@ -60,11 +60,11 @@ const SelectParent: React.FC = ({ className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full" onClick={() => setIsParentModalOpen(true)} > - {watchIssue("parent") && watchIssue("parent") !== "" + {watch("parent") && watch("parent") !== "" ? `${activeProject?.identifier}-${ - issues?.results.find((i) => i.id === watchIssue("parent"))?.sequence_id + issues?.results.find((i) => i.id === watch("parent"))?.sequence_id }` - : "Select Parent"} + : "Select issue"}
    diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx index e76beeb25..7c098f957 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx @@ -1,7 +1,7 @@ // react import React from "react"; // react-hook-form -import { Control, Controller } from "react-hook-form"; +import { Control, Controller, UseFormWatch } from "react-hook-form"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // icons @@ -11,13 +11,15 @@ import { IIssue } from "types"; // constants import { classNames } from "constants/common"; import { PRIORITIES } from "constants/"; +import CustomSelect from "ui/custom-select"; type Props = { control: Control; submitChanges: (formData: Partial) => void; + watch: UseFormWatch; }; -const SelectPriority: React.FC = ({ control, submitChanges }) => { +const SelectPriority: React.FC = ({ control, submitChanges, watch }) => { return (
    @@ -29,51 +31,23 @@ const SelectPriority: React.FC = ({ control, submitChanges }) => { control={control} name="state" render={({ field: { value } }) => ( - + {watch("priority") && watch("priority") !== "" ? watch("priority") : "None"} + + } value={value} onChange={(value: any) => { submitChanges({ priority: value }); }} - className="flex-shrink-0" > - {({ open }) => ( -
    - - - {value} - - - - - - -
    - {PRIORITIES.map((option) => ( - - `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate capitalize` - } - value={option} - > - {option} - - ))} -
    -
    -
    -
    - )} -
    + {PRIORITIES.map((option) => ( + + {option} + + ))} + )} />
    diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx index cc129c536..e55bd9b14 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx @@ -7,9 +7,10 @@ import { Listbox, Transition } from "@headlessui/react"; // types import { IIssue } from "types"; import { classNames } from "constants/common"; -import { Spinner } from "ui"; +import { CustomMenu, Spinner } from "ui"; import React from "react"; import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +import CustomSelect from "ui/custom-select"; type Props = { control: Control; @@ -30,82 +31,56 @@ const SelectState: React.FC = ({ control, submitChanges }) => { control={control} name="state" render={({ field: { value } }) => ( - + {value ? ( + <> + option.id === value)?.color, + }} + > + {states?.find((option) => option.id === value)?.name} + + ) : ( + "None" + )} + + } value={value} onChange={(value: any) => { submitChanges({ state: value }); }} - className="flex-shrink-0" > - {({ open }) => ( -
    - - - {value ? ( - <> + {states ? ( + states.length > 0 ? ( + states.map((option) => ( + + <> + {option.color && ( option.id === value)?.color, - }} + style={{ backgroundColor: option.color }} > - {states?.find((option) => option.id === value)?.name} - - ) : ( - "None" - )} - - - - - - -
    - {states ? ( - states.length > 0 ? ( - states.map((option) => ( - - `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={option.id} - > - {option.color && ( - - )} - {option.name} - - )) - ) : ( -
    No states found
    - ) - ) : ( - )} -
    -
    -
    -
    + {option.name} + + + )) + ) : ( +
    No states found
    + ) + ) : ( + )} -
    + )} />
    diff --git a/apps/app/components/project/issues/issues-list-modal.tsx b/apps/app/components/project/issues/issues-list-modal.tsx new file mode 100644 index 000000000..b158aab3b --- /dev/null +++ b/apps/app/components/project/issues/issues-list-modal.tsx @@ -0,0 +1,249 @@ +// react +import React, { useState } from "react"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; +// icons +import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue } from "types"; +import { classNames } from "constants/common"; +import useUser from "lib/hooks/useUser"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + value?: any; + onChange: (...event: any[]) => void; + issues: IIssue[]; + title?: string; + multiple?: boolean; + customDisplay?: JSX.Element; +}; + +const IssuesListModal: React.FC = ({ + isOpen, + handleClose: onClose, + value, + onChange, + issues, + title = "Issues", + multiple = false, + customDisplay, +}) => { + const [query, setQuery] = useState(""); + const [values, setValues] = useState([]); + + const { activeProject } = useUser(); + + const handleClose = () => { + onClose(); + setQuery(""); + setValues([]); + }; + + const filteredIssues: IIssue[] = + query === "" + ? issues ?? [] + : issues?.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; + + return ( + <> + setQuery("")} appear> + + +
    + + +
    + + + {multiple ? ( + <> + { + // setValues(val); + console.log(val); + }} + multiple + > +
    +
    +
    {customDisplay}
    + + {filteredIssues.length > 0 && ( +
  • + {query === "" && ( +

    + {title} +

    + )} +
      + {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" : "" + ) + } + > + {({ selected }) => ( + <> + + + + {activeProject?.identifier}-{issue.sequence_id} + {" "} + {issue.id} + + )} + + ))} +
    +
  • + )} +
    + + {query !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    +
    + + +
    + + ) : ( + +
    +
    +
    {customDisplay}
    + + {filteredIssues.length > 0 && ( +
  • + {query === "" && ( +

    + {title} +

    + )} +
      + {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={() => handleClose()} + > + {({ selected }) => ( + <> + + + {activeProject?.identifier}-{issue.sequence_id} + {" "} + {issue.name} + + )} + + ))} +
    +
  • + )} +
    + + {query !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    + )} +
    +
    +
    +
    +
    + + ); +}; + +export default IssuesListModal; diff --git a/apps/app/components/project/modules/create-update-module-modal.tsx b/apps/app/components/project/modules/create-update-module-modal.tsx new file mode 100644 index 000000000..d7059d345 --- /dev/null +++ b/apps/app/components/project/modules/create-update-module-modal.tsx @@ -0,0 +1,245 @@ +import React, { useEffect } from "react"; +// swr +import { mutate } from "swr"; +// react hook form +import { useForm } from "react-hook-form"; +// headless +import { Dialog, Transition } from "@headlessui/react"; +// ui +import { Button, Input, TextArea, Select } from "ui"; +// services +import modulesService from "lib/services/modules.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// types +import type { IModule } from "types"; +// common +import { renderDateFormat } from "constants/common"; +// fetch keys +import { MODULE_LIST } from "constants/fetch-keys"; + +type Props = { + isOpen: boolean; + setIsOpen: React.Dispatch>; + projectId: string; + data?: IModule; +}; + +const defaultValues: Partial = { + name: "", + description: "", +}; + +const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, projectId }) => { + const handleClose = () => { + setIsOpen(false); + const timeout = setTimeout(() => { + reset(defaultValues); + clearTimeout(timeout); + }, 500); + }; + + const { activeWorkspace } = useUser(); + + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + reset, + setError, + } = useForm({ + defaultValues, + }); + + const onSubmit = async (formData: IModule) => { + if (!activeWorkspace) return; + const payload = { + ...formData, + start_date: formData.start_date ? renderDateFormat(formData.start_date) : null, + target_date: formData.target_date ? renderDateFormat(formData.target_date) : null, + }; + if (!data) { + await modulesService + .createModule(activeWorkspace.slug, projectId, payload) + .then((res) => { + mutate( + MODULE_LIST(projectId), + (prevData) => [res, ...(prevData ?? [])], + false + ); + handleClose(); + }) + .catch((err) => { + Object.keys(err).map((key) => { + setError(key as keyof typeof defaultValues, { + message: err[key].join(", "), + }); + }); + }); + } else { + await modulesService + .updateModule(activeWorkspace.slug, projectId, data.id, payload) + .then((res) => { + mutate( + MODULE_LIST(projectId), + (prevData) => { + const newData = prevData?.map((item) => { + if (item.id === res.id) { + return res; + } + return item; + }); + return newData; + }, + false + ); + handleClose(); + }) + .catch((err) => { + Object.keys(err).map((key) => { + setError(key as keyof typeof defaultValues, { + message: err[key].join(", "), + }); + }); + }); + } + }; + + useEffect(() => { + if (data) { + setIsOpen(true); + reset(data); + } else { + reset(defaultValues); + } + }, [data, setIsOpen, reset]); + + return ( + + + +
    + + +
    +
    + + +
    +
    + + {data ? "Update" : "Create"} Module + +
    +
    + +
    +
    +