From f6ca842d3042914dcc674cc4a382c87a22c3f792 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 22 Dec 2022 21:49:46 +0530 Subject: [PATCH] feat: modules, style: kanban board, shortcut modals --- apps/app/components/command-palette/index.tsx | 102 +---- .../components/command-palette/shortcuts.tsx | 2 + .../common/board-view/single-issue.tsx | 28 +- .../common/bulk-delete-issues-modal.tsx | 230 ++++++++++ .../existing-issues-list-modal.tsx} | 65 ++- .../cycles/board-view/single-board.tsx | 8 +- .../project/cycles/list-view/index.tsx | 8 +- .../create-update-issue-modal/index.tsx | 6 +- .../select-project.tsx | 6 +- .../issue-detail-sidebar/select-assignee.tsx | 6 +- .../issue-detail-sidebar/select-blocked.tsx | 2 +- .../project/modules/board-view/index.tsx | 96 +++++ .../modules/board-view/single-board.tsx | 208 +++++++++ .../modules/confirm-module-deleteion.tsx | 147 +++++++ .../index.tsx} | 34 +- .../select-lead.tsx | 61 +++ .../select-members.tsx | 62 +++ .../select-status.tsx | 39 ++ .../project/modules/list-view/index.tsx | 328 ++++++++++++++ .../modules/module-detail-sidebar/index.tsx | 246 +++++++++++ .../module-detail-sidebar/select-members.tsx | 188 ++++++++ .../module-detail-sidebar/select-status.tsx | 63 +++ .../project/modules/single-module-card.tsx | 102 +++++ apps/app/constants/api-routes.ts | 4 +- apps/app/constants/global.tsx | 6 +- apps/app/constants/index.ts | 9 + apps/app/contexts/user.context.tsx | 15 +- apps/app/lib/services/modules.service.ts | 18 +- apps/app/pages/me/my-issues.tsx | 6 +- .../projects/[projectId]/cycles/[cycleId].tsx | 52 ++- .../projects/[projectId]/issues/index.tsx | 1 + .../[projectId]/modules/[moduleId].tsx | 407 ++++++++++++++++++ .../projects/[projectId]/modules/index.tsx | 15 +- .../projects/[projectId]/settings/control.tsx | 280 ++++++------ .../projects/[projectId]/settings/index.tsx | 170 ++++---- .../[invitationId].tsx | 2 +- apps/app/pages/workspace/index.tsx | 23 +- apps/app/types/cycles.d.ts | 2 +- apps/app/types/modules.d.ts | 24 +- apps/app/ui/custom-listbox/index.tsx | 220 +++++----- .../ui/{EmptySpace => empty-space}/index.tsx | 4 +- apps/app/ui/index.ts | 2 +- apps/app/ui/search-listbox/index.tsx | 2 +- 43 files changed, 2741 insertions(+), 558 deletions(-) create mode 100644 apps/app/components/common/bulk-delete-issues-modal.tsx rename apps/app/components/{project/cycles/cycle-issues-list-modal.tsx => common/existing-issues-list-modal.tsx} (86%) create mode 100644 apps/app/components/project/modules/board-view/index.tsx create mode 100644 apps/app/components/project/modules/board-view/single-board.tsx create mode 100644 apps/app/components/project/modules/confirm-module-deleteion.tsx rename apps/app/components/project/modules/{create-update-module-modal.tsx => create-update-module-modal/index.tsx} (87%) create mode 100644 apps/app/components/project/modules/create-update-module-modal/select-lead.tsx create mode 100644 apps/app/components/project/modules/create-update-module-modal/select-members.tsx create mode 100644 apps/app/components/project/modules/create-update-module-modal/select-status.tsx create mode 100644 apps/app/components/project/modules/list-view/index.tsx create mode 100644 apps/app/components/project/modules/module-detail-sidebar/index.tsx create mode 100644 apps/app/components/project/modules/module-detail-sidebar/select-members.tsx create mode 100644 apps/app/components/project/modules/module-detail-sidebar/select-status.tsx create mode 100644 apps/app/components/project/modules/single-module-card.tsx create mode 100644 apps/app/pages/projects/[projectId]/modules/[moduleId].tsx rename apps/app/ui/{EmptySpace => empty-space}/index.tsx (93%) diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index ea520cd3d7..6541bb38af 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -1,18 +1,22 @@ +// react import React, { useState, useCallback, useEffect } from "react"; // next import { useRouter } from "next/router"; -// swr -import { mutate } from "swr"; -// 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.service"; // hooks import useUser from "lib/hooks/useUser"; import useTheme from "lib/hooks/useTheme"; import useToast from "lib/hooks/useToast"; +// components +import ShortcutsModal from "components/command-palette/shortcuts"; +import CreateProjectModal from "components/project/create-project-modal"; +import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal"; +import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; +import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal"; +import BulkDeleteIssuesModal from "components/common/bulk-delete-issues-modal"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; // icons import { FolderIcon, @@ -20,25 +24,10 @@ import { ClipboardDocumentListIcon, MagnifyingGlassIcon, } from "@heroicons/react/24/outline"; -// components -import ShortcutsModal from "components/command-palette/shortcuts"; -import CreateProjectModal from "components/project/create-project-modal"; -import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal"; -import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; -// ui -import { Button } from "ui"; // types -import { IIssue, IssueResponse } from "types"; -// fetch keys -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; -// constants +import { IIssue } from "types"; +// common import { classNames, copyTextToClipboard } from "constants/common"; -import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal"; - -type FormInput = { - issue_ids: string[]; - cycleId: string; -}; const CommandPalette: React.FC = () => { const [query, setQuery] = useState(""); @@ -49,8 +38,9 @@ const CommandPalette: React.FC = () => { const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false); const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false); const [isCreateModuleModalOpen, setisCreateModuleModalOpen] = useState(false); + const [isBulkDeleteIssuesModalOpen, setIsBulkDeleteIssuesModalOpen] = useState(false); - const { activeWorkspace, activeProject, issues } = useUser(); + const { activeProject, issues } = useUser(); const router = useRouter(); @@ -64,8 +54,6 @@ const CommandPalette: React.FC = () => { : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; - const { register, handleSubmit, reset } = useForm(); - const quickActions = [ { name: "Add new issue...", @@ -88,7 +76,6 @@ const CommandPalette: React.FC = () => { const handleCommandPaletteClose = () => { setIsPaletteOpen(false); setQuery(""); - reset(); }; const handleKeyDown = useCallback( @@ -114,6 +101,9 @@ const CommandPalette: React.FC = () => { } else if ((e.ctrlKey || e.metaKey) && e.key === "m") { e.preventDefault(); setisCreateModuleModalOpen(true); + } else if ((e.ctrlKey || e.metaKey) && e.key === "d") { + e.preventDefault(); + setIsBulkDeleteIssuesModalOpen(true); } else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") { e.preventDefault(); @@ -138,47 +128,6 @@ const CommandPalette: React.FC = () => { [toggleCollapsed, setToastAlert, router] ); - const handleDelete: 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 - .bulkDeleteIssues(activeWorkspace.slug, activeProject.id, data) - .then((res) => { - setToastAlert({ - title: "Success", - type: "success", - message: res.message, - }); - mutate( - PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), - (prevData) => { - return { - ...(prevData as IssueResponse), - count: (prevData?.results ?? []).filter( - (p) => !data.issue_ids.some((id) => p.id === id) - ).length, - results: (prevData?.results ?? []).filter( - (p) => !data.issue_ids.some((id) => p.id === id) - ), - }; - }, - false - ); - }) - .catch((e) => { - console.log(e); - }); - } - }; - useEffect(() => { document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); @@ -207,6 +156,10 @@ const CommandPalette: React.FC = () => { setIsOpen={setIsIssueModalOpen} projectId={activeProject?.id} /> + { {({ active }) => ( <>
- {
-
-
+ {/*
State
{issue.state_detail.name}
-
+
*/} )} @@ -222,10 +224,10 @@ const SingleIssue: React.FC = ({
{issue.start_date ? renderShortNumericDateFormat(issue.start_date) : "N/A"} -
+ {/*
Started at
{renderShortNumericDateFormat(issue.start_date ?? "")}
-
+
*/}
)} {properties.due_date && ( @@ -240,7 +242,7 @@ const SingleIssue: React.FC = ({ > {issue.target_date ? renderShortNumericDateFormat(issue.target_date) : "N/A"} -
+ {/*
Target date
{renderShortNumericDateFormat(issue.target_date ?? "")}
@@ -251,7 +253,7 @@ const SingleIssue: React.FC = ({ ? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days` : "Due date")}
-
+
*/}
)} {properties.assignee && ( @@ -373,14 +375,14 @@ const SingleIssue: React.FC = ({
-
+ {/*
Assigned to
{issue.assignee_details?.length > 0 ? issue.assignee_details.map((assignee) => assignee.first_name).join(", ") : "No one"}
-
+
*/} )} diff --git a/apps/app/components/common/bulk-delete-issues-modal.tsx b/apps/app/components/common/bulk-delete-issues-modal.tsx new file mode 100644 index 0000000000..a8123288e9 --- /dev/null +++ b/apps/app/components/common/bulk-delete-issues-modal.tsx @@ -0,0 +1,230 @@ +// react +import React, { useState } from "react"; +// swr +import { mutate } from "swr"; +// react hook form +import { SubmitHandler, useForm } from "react-hook-form"; +// services +import issuesServices from "lib/services/issues.service"; +// 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 } from "@heroicons/react/24/outline"; +// types +import { IIssue, IssueResponse } from "types"; +// fetch keys +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +// common +import { classNames } from "constants/common"; + +type FormInput = { + issue_ids: string[]; + cycleId: string; +}; + +type Props = { + isOpen: boolean; + setIsOpen: React.Dispatch>; +}; + +const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => { + const [query, setQuery] = useState(""); + + const { activeWorkspace, activeProject, issues } = useUser(); + + const { setToastAlert } = useToast(); + + const filteredIssues: IIssue[] = + query === "" + ? issues?.results ?? [] + : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? + []; + + const { register, handleSubmit, reset } = useForm(); + + const handleClose = () => { + setIsOpen(false); + setQuery(""); + reset(); + }; + + const handleDelete: 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 + .bulkDeleteIssues(activeWorkspace.slug, activeProject.id, data) + .then((res) => { + setToastAlert({ + title: "Success", + type: "success", + message: res.message, + }); + mutate( + PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), + (prevData) => { + return { + ...(prevData as IssueResponse), + count: (prevData?.results ?? []).filter( + (p) => !data.issue_ids.some((id) => p.id === id) + ).length, + results: (prevData?.results ?? []).filter( + (p) => !data.issue_ids.some((id) => p.id === id) + ), + }; + }, + false + ); + }) + .catch((e) => { + console.log(e); + }); + } + }; + + return ( + <> + setQuery("")} appear> + + +
+ + +
+ + +
+ +
+
+ + + {filteredIssues.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Select issues +

    + )} +
      + {filteredIssues.map((issue) => ( + + 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 !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + ); +}; + +export default BulkDeleteIssuesModal; diff --git a/apps/app/components/project/cycles/cycle-issues-list-modal.tsx b/apps/app/components/common/existing-issues-list-modal.tsx similarity index 86% rename from apps/app/components/project/cycles/cycle-issues-list-modal.tsx rename to apps/app/components/common/existing-issues-list-modal.tsx index 54e526d8bb..457bea1804 100644 --- a/apps/app/components/project/cycles/cycle-issues-list-modal.tsx +++ b/apps/app/components/common/existing-issues-list-modal.tsx @@ -2,44 +2,42 @@ import React, { useState } from "react"; // react-hook-form import { Controller, SubmitHandler, useForm } 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"; -// services -import issuesServices from "lib/services/issues.service"; -// 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 +// common import { classNames } from "constants/common"; -import { mutate } from "swr"; -import { CYCLE_ISSUES } from "constants/fetch-keys"; - -type Props = { - isOpen: boolean; - handleClose: () => void; - issues: IssueResponse | undefined; - cycleId: string; -}; type FormInput = { issues: string[]; }; -const CycleIssuesListModal: React.FC = ({ +type Props = { + isOpen: boolean; + handleClose: () => void; + type: string; + issues: IIssue[]; + handleOnSubmit: (data: FormInput) => void; +}; + +const ExistingIssuesListModal: React.FC = ({ isOpen, handleClose: onClose, issues, - cycleId, + handleOnSubmit, + type, }) => { const [query, setQuery] = useState(""); - const { activeWorkspace, activeProject } = useUser(); + const { activeProject } = useUser(); const { setToastAlert } = useToast(); @@ -60,7 +58,7 @@ const CycleIssuesListModal: React.FC = ({ }, }); - const handleAddToCycle: SubmitHandler = (data) => { + const onSubmit: SubmitHandler = (data) => { if (!data.issues || data.issues.length === 0) { setToastAlert({ title: "Error", @@ -70,25 +68,14 @@ const CycleIssuesListModal: React.FC = ({ return; } - if (activeWorkspace && activeProject) { - issuesServices - .addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, data) - .then((res) => { - console.log(res); - mutate(CYCLE_ISSUES(cycleId)); - handleClose(); - }) - .catch((e) => { - console.log(e); - }); - } + handleOnSubmit(data); + handleClose(); }; const filteredIssues: IIssue[] = query === "" - ? issues?.results ?? [] - : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? - []; + ? issues ?? [] + : issues.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; return ( <> @@ -143,12 +130,12 @@ const CycleIssuesListModal: React.FC = ({
  • {query === "" && (

    - Select issues to add to cycle + Select issues to add to {type}

    )}
      {filteredIssues.map((issue) => { - if (!issue.issue_cycle) + if ((type === "cycle" && !issue.issue_cycle) || type === "module") return ( = ({ @@ -222,4 +209,4 @@ const CycleIssuesListModal: React.FC = ({ ); }; -export default CycleIssuesListModal; +export default ExistingIssuesListModal; 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 d40b4ac978..e806329710 100644 --- a/apps/app/components/project/cycles/board-view/single-board.tsx +++ b/apps/app/components/project/cycles/board-view/single-board.tsx @@ -8,8 +8,6 @@ import workspaceService from "lib/services/workspace.service"; import useUser from "lib/hooks/useUser"; // components import SingleIssue from "components/common/board-view/single-issue"; -// headless ui -import { Menu, Transition } from "@headlessui/react"; // ui import { CustomMenu } from "ui"; // icons @@ -19,7 +17,7 @@ import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types"; // fetch-keys import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; // common -import { addSpaceIfCamelCase, classNames } from "constants/common"; +import { addSpaceIfCamelCase } from "constants/common"; type Props = { properties: Properties; @@ -46,7 +44,7 @@ type Props = { stateId: string | null; }; -const SingleCycleBoard: React.FC = ({ +const SingleModuleBoard: React.FC = ({ properties, groupedByIssues, selectedGroup, @@ -207,4 +205,4 @@ const SingleCycleBoard: React.FC = ({ ); }; -export default SingleCycleBoard; +export default SingleModuleBoard; diff --git a/apps/app/components/project/cycles/list-view/index.tsx b/apps/app/components/project/cycles/list-view/index.tsx index 8c34e3109d..24f4c56cd2 100644 --- a/apps/app/components/project/cycles/list-view/index.tsx +++ b/apps/app/components/project/cycles/list-view/index.tsx @@ -35,6 +35,7 @@ type Props = { openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; openIssuesListModal: () => void; removeIssueFromCycle: (bridgeId: string) => void; + handleDeleteIssue: React.Dispatch>; setPreloadedData: React.Dispatch< React.SetStateAction< | (Partial & { @@ -52,6 +53,7 @@ const CyclesListView: React.FC = ({ openIssuesListModal, properties, removeIssueFromCycle, + handleDeleteIssue, setPreloadedData, }) => { const { activeWorkspace, activeProject, states } = useUser(); @@ -264,7 +266,11 @@ const CyclesListView: React.FC = ({ > Remove from cycle - Delete permanently + handleDeleteIssue(issue.id)} + > + Delete permanently + diff --git a/apps/app/components/project/issues/create-update-issue-modal/index.tsx b/apps/app/components/project/issues/create-update-issue-modal/index.tsx index 275a098bcc..07985f9d1d 100644 --- a/apps/app/components/project/issues/create-update-issue-modal/index.tsx +++ b/apps/app/components/project/issues/create-update-issue-modal/index.tsx @@ -115,7 +115,7 @@ const CreateUpdateIssuesModal: React.FC = ({ }, 500); }; - const addIssueToCycle = async (issueId: string, cycleId: string, issueDetail: IIssue) => { + const addIssueToCycle = async (issueId: string, cycleId: string) => { if (!activeWorkspace || !activeProject) return; await issuesServices .addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, { @@ -169,7 +169,7 @@ const CreateUpdateIssuesModal: React.FC = ({ mutate(PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id)); if (formData.sprints && formData.sprints !== null) { - await addIssueToCycle(res.id, formData.sprints, formData); + await addIssueToCycle(res.id, formData.sprints); } handleClose(); resetForm(); @@ -209,7 +209,7 @@ const CreateUpdateIssuesModal: React.FC = ({ false ); if (formData.sprints && formData.sprints !== null) { - await addIssueToCycle(res.id, formData.sprints, formData); + await addIssueToCycle(res.id, formData.sprints); } handleClose(); resetForm(); diff --git a/apps/app/components/project/issues/create-update-issue-modal/select-project.tsx b/apps/app/components/project/issues/create-update-issue-modal/select-project.tsx index 762c655d32..d334e92e28 100644 --- a/apps/app/components/project/issues/create-update-issue-modal/select-project.tsx +++ b/apps/app/components/project/issues/create-update-issue-modal/select-project.tsx @@ -51,7 +51,7 @@ const SelectProject: React.FC = ({ control }) => { leaveTo="opacity-0" > -
      +
      {projects ? ( projects.length > 0 ? ( projects.map((project) => ( @@ -59,8 +59,8 @@ const SelectProject: React.FC = ({ control }) => { key={project.id} className={({ active }) => `${ - active ? "text-white bg-theme" : "text-gray-900" - } cursor-pointer select-none p-2 rounded-md` + active ? "bg-indigo-50" : "" + } text-gray-900 cursor-pointer select-none p-2` } value={project.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 f4b97789a5..495cdf8de5 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 @@ -15,7 +15,7 @@ import { Listbox, Transition } from "@headlessui/react"; // ui import { Spinner } from "ui"; // icons -import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +import { UserGroupIcon } from "@heroicons/react/24/outline"; import User from "public/user.png"; // types import { IIssue } from "types"; @@ -39,7 +39,7 @@ const SelectAssignee: React.FC = ({ control, submitChanges }) => { return (
      - +

      Assignees

      @@ -128,7 +128,7 @@ const SelectAssignee: React.FC = ({ control, submitChanges }) => { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - +
      {people ? ( people.length > 0 ? ( 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 index c4944d20fc..994b2b6284 100644 --- 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 @@ -79,7 +79,7 @@ const SelectBlocked: React.FC = ({ issueDetail, issuesList, watch }) => { }); }); - // handleClose(); + handleClose(); }; const removeBlocked = (issueId: string) => { diff --git a/apps/app/components/project/modules/board-view/index.tsx b/apps/app/components/project/modules/board-view/index.tsx new file mode 100644 index 0000000000..cdbc563733 --- /dev/null +++ b/apps/app/components/project/modules/board-view/index.tsx @@ -0,0 +1,96 @@ +// components +import SingleBoard from "components/project/modules/board-view/single-board"; +// ui +import { Spinner } from "ui"; +// types +import { IIssue, IProjectMember, NestedKeyOf, Properties } from "types"; +import useUser from "lib/hooks/useUser"; + +type Props = { + groupedByIssues: { + [key: string]: IIssue[]; + }; + properties: Properties; + selectedGroup: NestedKeyOf | null; + members: IProjectMember[] | undefined; + openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; + openIssuesListModal: () => void; + removeIssueFromModule: (issueId: string) => void; + partialUpdateIssue: (formData: Partial, issueId: string) => void; + handleDeleteIssue: React.Dispatch>; + setPreloadedData: React.Dispatch< + React.SetStateAction< + | (Partial & { + actionType: "createIssue" | "edit" | "delete"; + }) + | undefined + > + >; +}; + +const ModulesBoardView: React.FC = ({ + groupedByIssues, + properties, + selectedGroup, + members, + openCreateIssueModal, + openIssuesListModal, + removeIssueFromModule, + partialUpdateIssue, + handleDeleteIssue, + setPreloadedData, +}) => { + const { states } = useUser(); + + return ( + <> + {groupedByIssues ? ( +
      +
      +
      +
      + {Object.keys(groupedByIssues).map((singleGroup) => ( + m.member.id === singleGroup)?.member.first_name ?? + "loading..." + : null + } + groupedByIssues={groupedByIssues} + bgColor={ + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup)?.color + : undefined + } + properties={properties} + removeIssueFromModule={removeIssueFromModule} + openIssuesListModal={openIssuesListModal} + openCreateIssueModal={openCreateIssueModal} + partialUpdateIssue={partialUpdateIssue} + handleDeleteIssue={handleDeleteIssue} + setPreloadedData={setPreloadedData} + stateId={ + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup)?.id ?? null + : null + } + /> + ))} +
      +
      +
      +
      + ) : ( +
      + +
      + )} + + ); +}; + +export default ModulesBoardView; diff --git a/apps/app/components/project/modules/board-view/single-board.tsx b/apps/app/components/project/modules/board-view/single-board.tsx new file mode 100644 index 0000000000..99e9687f82 --- /dev/null +++ b/apps/app/components/project/modules/board-view/single-board.tsx @@ -0,0 +1,208 @@ +// react +import React, { useState } from "react"; +// swr +import useSWR from "swr"; +// services +import workspaceService from "lib/services/workspace.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// components +import SingleIssue from "components/common/board-view/single-issue"; +// ui +import { CustomMenu } from "ui"; +// icons +import { PlusIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types"; +// fetch-keys +import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; +// common +import { addSpaceIfCamelCase } from "constants/common"; + +type Props = { + properties: Properties; + groupedByIssues: { + [key: string]: IIssue[]; + }; + selectedGroup: NestedKeyOf | null; + groupTitle: string; + createdBy: string | null; + bgColor?: string; + openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; + openIssuesListModal: () => void; + removeIssueFromModule: (bridgeId: string) => void; + partialUpdateIssue: (formData: Partial, issueId: string) => void; + handleDeleteIssue: React.Dispatch>; + setPreloadedData: React.Dispatch< + React.SetStateAction< + | (Partial & { + actionType: "createIssue" | "edit" | "delete"; + }) + | undefined + > + >; + stateId: string | null; +}; + +const SingleCycleBoard: React.FC = ({ + properties, + groupedByIssues, + selectedGroup, + groupTitle, + createdBy, + bgColor, + openCreateIssueModal, + openIssuesListModal, + removeIssueFromModule, + partialUpdateIssue, + handleDeleteIssue, + setPreloadedData, + stateId, +}) => { + // Collapse/Expand + const [show, setState] = useState(true); + + const { activeWorkspace } = useUser(); + + if (selectedGroup === "priority") + groupTitle === "high" + ? (bgColor = "#dc2626") + : groupTitle === "medium" + ? (bgColor = "#f97316") + : groupTitle === "low" + ? (bgColor = "#22c55e") + : (bgColor = "#ff0000"); + + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + + return ( +
      +
      +
      +
      +
      +

      + {groupTitle === null || groupTitle === "null" + ? "None" + : createdBy + ? createdBy + : addSpaceIfCamelCase(groupTitle)} +

      + + {groupedByIssues[groupTitle].length} + +
      + + + { + openCreateIssueModal(); + if (selectedGroup !== null) { + setPreloadedData({ + state: stateId !== null ? stateId : undefined, + [selectedGroup]: groupTitle, + actionType: "createIssue", + }); + } + }} + > + Create new + + openIssuesListModal()}> + Add an existing issue + + +
      +
      +
      + {groupedByIssues[groupTitle].map((childIssue, index: number) => { + const assignees = [ + ...(childIssue?.assignees_list ?? []), + ...(childIssue?.assignees ?? []), + ]?.map((assignee) => { + const tempPerson = people?.find((p) => p.member.id === assignee)?.member; + + return { + avatar: tempPerson?.avatar, + first_name: tempPerson?.first_name, + email: tempPerson?.email, + }; + }); + + return ( + + ); + })} + + + + Add issue + + } + className="mt-1" + optionsPosition="left" + withoutBorder + > + { + openCreateIssueModal(); + if (selectedGroup !== null) { + setPreloadedData({ + state: stateId !== null ? stateId : undefined, + [selectedGroup]: groupTitle, + actionType: "createIssue", + }); + } + }} + > + Create new + + openIssuesListModal()}> + Add an existing issue + + +
      +
      +
      + ); +}; + +export default SingleCycleBoard; diff --git a/apps/app/components/project/modules/confirm-module-deleteion.tsx b/apps/app/components/project/modules/confirm-module-deleteion.tsx new file mode 100644 index 0000000000..bb485b2515 --- /dev/null +++ b/apps/app/components/project/modules/confirm-module-deleteion.tsx @@ -0,0 +1,147 @@ +// react +import React, { useEffect, useRef, useState } from "react"; +// next +import { useRouter } from "next/router"; +// swr +import { mutate } from "swr"; +// services +import modulesService from "lib/services/modules.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; +// icons +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +// types +import type { IModule } from "types"; +// fetch-keys +import { MODULE_LIST } from "constants/fetch-keys"; + +type Props = { + isOpen: boolean; + setIsOpen: React.Dispatch>; + data?: IModule; +}; + +const ConfirmModuleDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const { activeWorkspace } = useUser(); + + const router = useRouter(); + + const cancelButtonRef = useRef(null); + + const handleClose = () => { + setIsOpen(false); + setIsDeleteLoading(false); + }; + + const handleDeletion = async () => { + setIsDeleteLoading(true); + + if (!activeWorkspace || !data) return; + await modulesService + .deleteModule(activeWorkspace.slug, data.project, data.id) + .then(() => { + mutate(MODULE_LIST(data.project)); + router.push(`/projects/${data.project}/modules`); + handleClose(); + }) + .catch((error) => { + console.log(error); + setIsDeleteLoading(false); + }); + }; + + useEffect(() => { + data && setIsOpen(true); + }, [data, setIsOpen]); + + return ( + + + +
      + + +
      +
      + + +
      +
      +
      +
      +
      + + Delete Module + +
      +

      + Are you sure you want to delete module - {`"`} + {data?.name} + {`?"`} All of the data related to the module will be permanently removed. + This action cannot be undone. +

      +
      +
      +
      +
      +
      + + +
      +
      +
      +
      +
      +
      +
      + ); +}; + +export default ConfirmModuleDeletion; diff --git a/apps/app/components/project/modules/create-update-module-modal.tsx b/apps/app/components/project/modules/create-update-module-modal/index.tsx similarity index 87% rename from apps/app/components/project/modules/create-update-module-modal.tsx rename to apps/app/components/project/modules/create-update-module-modal/index.tsx index d7059d3452..91b56fd3ae 100644 --- a/apps/app/components/project/modules/create-update-module-modal.tsx +++ b/apps/app/components/project/modules/create-update-module-modal/index.tsx @@ -17,6 +17,9 @@ import type { IModule } from "types"; import { renderDateFormat } from "constants/common"; // fetch keys import { MODULE_LIST } from "constants/fetch-keys"; +import SelectLead from "./select-lead"; +import SelectMembers from "./select-members"; +import SelectStatus from "./select-status"; type Props = { isOpen: boolean; @@ -28,6 +31,9 @@ type Props = { const defaultValues: Partial = { name: "", description: "", + status: null, + lead: null, + members_list: [], }; const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, projectId }) => { @@ -45,6 +51,7 @@ const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, pro register, formState: { errors, isSubmitting }, handleSubmit, + control, reset, setError, } = useForm({ @@ -140,7 +147,7 @@ const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, pro leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
      @@ -172,26 +179,6 @@ const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, pro register={register} />
      -
      - = ({ isOpen, setIsOpen, data, pro />
      +
      + + + +
      diff --git a/apps/app/components/project/modules/create-update-module-modal/select-lead.tsx b/apps/app/components/project/modules/create-update-module-modal/select-lead.tsx new file mode 100644 index 0000000000..2564eabbc2 --- /dev/null +++ b/apps/app/components/project/modules/create-update-module-modal/select-lead.tsx @@ -0,0 +1,61 @@ +// react +import React from "react"; +// swr +import useSWR from "swr"; +// react hook form +import { Controller } from "react-hook-form"; +import type { Control } from "react-hook-form"; +// service +import projectServices from "lib/services/project.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// ui +import { SearchListbox } from "ui"; +// icons +import { UserIcon } from "@heroicons/react/24/outline"; +// types +import type { IModule } from "types"; +// fetch-keys +import { PROJECT_MEMBERS } from "constants/fetch-keys"; + +type Props = { + control: Control; +}; + +const SelectLead: React.FC = ({ control }) => { + const { activeWorkspace, activeProject } = useUser(); + + const { data: people } = useSWR( + activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null, + activeWorkspace && activeProject + ? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id) + : null + ); + + return ( + ( + { + return { + value: person.member.id, + display: + person.member.first_name && person.member.first_name !== "" + ? person.member.first_name + : person.member.email, + }; + })} + value={value} + onChange={onChange} + icon={} + /> + )} + /> + ); +}; + +export default SelectLead; diff --git a/apps/app/components/project/modules/create-update-module-modal/select-members.tsx b/apps/app/components/project/modules/create-update-module-modal/select-members.tsx new file mode 100644 index 0000000000..bac370c629 --- /dev/null +++ b/apps/app/components/project/modules/create-update-module-modal/select-members.tsx @@ -0,0 +1,62 @@ +// react +import React from "react"; +// swr +import useSWR from "swr"; +// react hook form +import { Controller } from "react-hook-form"; +import type { Control } from "react-hook-form"; +// service +import projectServices from "lib/services/project.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// ui +import { SearchListbox } from "ui"; +// icons +import { UserIcon } from "@heroicons/react/24/outline"; +// types +import type { IModule } from "types"; +// fetch-keys +import { PROJECT_MEMBERS } from "constants/fetch-keys"; + +type Props = { + control: Control; +}; + +const SelectMembers: React.FC = ({ control }) => { + const { activeWorkspace, activeProject } = useUser(); + + const { data: people } = useSWR( + activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null, + activeWorkspace && activeProject + ? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id) + : null + ); + + return ( + ( + { + return { + value: person.member.id, + display: + person.member.first_name && person.member.first_name !== "" + ? person.member.first_name + : person.member.email, + }; + })} + multiple={true} + value={value} + onChange={onChange} + icon={} + /> + )} + /> + ); +}; + +export default SelectMembers; diff --git a/apps/app/components/project/modules/create-update-module-modal/select-status.tsx b/apps/app/components/project/modules/create-update-module-modal/select-status.tsx new file mode 100644 index 0000000000..bdc02be6c0 --- /dev/null +++ b/apps/app/components/project/modules/create-update-module-modal/select-status.tsx @@ -0,0 +1,39 @@ +// react +import React from "react"; +// react hook form +import { Controller } from "react-hook-form"; +import type { Control } from "react-hook-form"; +// ui +import { CustomListbox } from "ui"; +// icons +import { Squares2X2Icon } from "@heroicons/react/24/outline"; +// types +import type { IModule } from "types"; +import { MODULE_STATUS } from "constants/"; + +type Props = { + control: Control; +}; + +const SelectStatus: React.FC = ({ control }) => { + return ( + ( + { + return { value: status.value, display: status.label }; + })} + value={value} + optionsFontsize="sm" + onChange={onChange} + icon={} + /> + )} + /> + ); +}; + +export default SelectStatus; diff --git a/apps/app/components/project/modules/list-view/index.tsx b/apps/app/components/project/modules/list-view/index.tsx new file mode 100644 index 0000000000..8e3c84fc85 --- /dev/null +++ b/apps/app/components/project/modules/list-view/index.tsx @@ -0,0 +1,328 @@ +// react +import React from "react"; +// next +import Link from "next/link"; +// swr +import useSWR from "swr"; +// headless ui +import { Disclosure, Transition } from "@headlessui/react"; +// hooks +import useUser from "lib/hooks/useUser"; +// ui +import { CustomMenu, Spinner } from "ui"; +// icons +import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import { CalendarDaysIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types"; +// fetch keys +import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; +// constants +import { + addSpaceIfCamelCase, + findHowManyDaysLeft, + renderShortNumericDateFormat, +} from "constants/common"; +import workspaceService from "lib/services/workspace.service"; + +type Props = { + groupedByIssues: { + [key: string]: (IIssue & { bridge?: string })[]; + }; + properties: Properties; + selectedGroup: NestedKeyOf | null; + openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; + openIssuesListModal: () => void; + removeIssueFromModule: (issueId: string) => void; + handleDeleteIssue: React.Dispatch>; + setPreloadedData: React.Dispatch< + React.SetStateAction< + | (Partial & { + actionType: "createIssue" | "edit" | "delete"; + }) + | undefined + > + >; +}; + +const ModulesListView: React.FC = ({ + groupedByIssues, + selectedGroup, + openCreateIssueModal, + openIssuesListModal, + properties, + removeIssueFromModule, + handleDeleteIssue, + setPreloadedData, +}) => { + const { activeWorkspace, activeProject, states } = useUser(); + + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + + return ( +
      + {Object.keys(groupedByIssues).map((singleGroup) => { + const stateId = + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup)?.id ?? null + : null; + + return ( + + {({ open }) => ( +
      +
      + +
      + + + + {selectedGroup !== null ? ( +

      + {singleGroup === null || singleGroup === "null" + ? selectedGroup === "priority" && "No priority" + : addSpaceIfCamelCase(singleGroup)} +

      + ) : ( +

      All Issues

      + )} +

      + {groupedByIssues[singleGroup as keyof IIssue].length} +

      +
      +
      +
      + + +
      + {groupedByIssues[singleGroup] ? ( + groupedByIssues[singleGroup].length > 0 ? ( + groupedByIssues[singleGroup].map((issue) => { + const assignees = [ + ...(issue?.assignees_list ?? []), + ...(issue?.assignees ?? []), + ]?.map((assignee) => { + const tempPerson = people?.find( + (p) => p.member.id === assignee + )?.member; + + return { + avatar: tempPerson?.avatar, + first_name: tempPerson?.first_name, + email: tempPerson?.email, + }; + }); + + return ( +
      + +
      + {properties.priority && ( +
      + {/* {getPriorityIcon(issue.priority ?? "")} */} + {issue.priority ?? "None"} +
      +
      Priority
      +
      + {issue.priority ?? "None"} +
      +
      +
      + )} + {properties.state && ( +
      + + {addSpaceIfCamelCase(issue?.state_detail.name)} +
      +
      State
      +
      {issue?.state_detail.name}
      +
      +
      + )} + {properties.start_date && ( +
      + + {issue.start_date + ? renderShortNumericDateFormat(issue.start_date) + : "N/A"} +
      +
      Started at
      +
      + {renderShortNumericDateFormat(issue.start_date ?? "")} +
      +
      +
      + )} + {properties.due_date && ( +
      + + {issue.target_date + ? renderShortNumericDateFormat(issue.target_date) + : "N/A"} +
      +
      Due date
      +
      + {renderShortNumericDateFormat(issue.target_date ?? "")} +
      +
      + {issue.target_date && + (issue.target_date < new Date().toISOString() + ? `Due date has passed by ${findHowManyDaysLeft( + issue.target_date + )} days` + : findHowManyDaysLeft(issue.target_date) <= 3 + ? `Due date is in ${findHowManyDaysLeft( + issue.target_date + )} days` + : "Due date")} +
      +
      +
      + )} + + openCreateIssueModal(issue, "edit")} + > + Edit + + removeIssueFromModule(issue.bridge ?? "")} + > + Remove from module + + handleDeleteIssue(issue.id)} + > + Delete permanently + + +
      +
      + ); + }) + ) : ( +

      No issues.

      + ) + ) : ( +
      + +
      + )} +
      +
      +
      +
      + + + Add issue + + } + optionsPosition="left" + withoutBorder + > + { + openCreateIssueModal(); + if (selectedGroup !== null) { + setPreloadedData({ + state: stateId !== null ? stateId : undefined, + [selectedGroup]: singleGroup, + actionType: "createIssue", + }); + } + }} + > + Create new + + openIssuesListModal()}> + Add an existing issue + + +
      +
      + )} +
      + ); + })} +
      + ); +}; + +export default ModulesListView; diff --git a/apps/app/components/project/modules/module-detail-sidebar/index.tsx b/apps/app/components/project/modules/module-detail-sidebar/index.tsx new file mode 100644 index 0000000000..9e3482caae --- /dev/null +++ b/apps/app/components/project/modules/module-detail-sidebar/index.tsx @@ -0,0 +1,246 @@ +// react +import { useEffect } from "react"; +// swr +import useSWR, { mutate } from "swr"; +// react-hook-form +import { Controller, useForm } from "react-hook-form"; +// services +import modulesService from "lib/services/modules.service"; +// hooks +import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; +// components +import SelectMembers from "components/project/modules/module-detail-sidebar/select-members"; +import SelectStatus from "components/project/modules/module-detail-sidebar/select-status"; +// ui +import { Spinner } from "ui"; +// icons +import { + CalendarDaysIcon, + ClipboardDocumentIcon, + LinkIcon, + PlusIcon, + TrashIcon, + UserIcon, +} from "@heroicons/react/24/outline"; +// types +import { IModule } from "types"; +// fetch-keys +import { MODULE_DETAIL } from "constants/fetch-keys"; +// common +import { copyTextToClipboard } from "constants/common"; + +const defaultValues: Partial = { + members_list: [], + start_date: new Date().toString(), + target_date: new Date().toString(), + status: null, +}; + +type Props = { + module?: IModule; + isOpen: boolean; + handleDeleteModule: () => void; +}; + +const ModuleDetailSidebar: React.FC = ({ module, isOpen, handleDeleteModule }) => { + const { activeWorkspace, activeProject } = useUser(); + + const { setToastAlert } = useToast(); + + const { reset, watch, control } = useForm({ + defaultValues, + }); + + const submitChanges = (data: Partial) => { + if (!activeWorkspace || !activeProject || !module) return; + + modulesService + .patchModule(activeWorkspace.slug, activeProject.id, module.id, data) + .then((res) => { + console.log(res); + mutate(MODULE_DETAIL); + }) + .catch((e) => { + console.log(e); + }); + }; + + useEffect(() => { + if (module) + reset({ + ...module, + members_list: module.members_list ?? module.members_detail?.map((member) => member.id), + }); + }, [module, reset]); + + return ( + <> +
      + {module ? ( + <> +
      +

      {module.name}

      +
      + + + +
      +
      +
      +
      +
      +
      + +

      Lead

      +
      +
      + {module.lead_detail.first_name !== "" ? ( + <> + {module.lead_detail.first_name} {module.lead_detail.last_name} + + ) : ( + module.lead_detail.email + )} +
      +
      + +
      +
      +
      +
      + +

      Start date

      +
      +
      + ( + + )} + /> +
      +
      +
      +
      + +

      End date

      +
      +
      + ( + { + submitChanges({ target_date: e.target.value }); + onChange(e.target.value); + }} + className="hover:bg-gray-100 bg-transparent 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" + /> + )} + /> +
      +
      +
      +
      + +
      +
      +
      +

      Links

      + +
      +
      +
      +
      + +
      +
      +
      Aaryan Khandelwal
      +

      + Added 2 days ago by aaryan.khandelwal@caravel.tech +

      +
      +
      +
      +
      +
      + + ) : ( +
      + +
      + )} +
      + + ); +}; + +export default ModuleDetailSidebar; diff --git a/apps/app/components/project/modules/module-detail-sidebar/select-members.tsx b/apps/app/components/project/modules/module-detail-sidebar/select-members.tsx new file mode 100644 index 0000000000..8bae4676b4 --- /dev/null +++ b/apps/app/components/project/modules/module-detail-sidebar/select-members.tsx @@ -0,0 +1,188 @@ +// react +import React from "react"; +// next +import Image from "next/image"; +// swr +import useSWR from "swr"; +// react-hook-form +import { Control, Controller } from "react-hook-form"; +// services +import workspaceService from "lib/services/workspace.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// headless ui +import { Listbox, Transition } from "@headlessui/react"; +// ui +import { Spinner } from "ui"; +// icons +import { UserGroupIcon } from "@heroicons/react/24/outline"; +import User from "public/user.png"; +// types +import { IModule } from "types"; +// constants +import { classNames } from "constants/common"; +import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; + +type Props = { + control: Control, any>; + submitChanges: (formData: Partial) => void; +}; + +const SelectMembers: React.FC = ({ control, submitChanges }) => { + const { activeWorkspace } = useUser(); + + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + + return ( +
      +
      + +

      Assignees

      +
      +
      + ( + { + submitChanges({ members_list: value }); + }} + className="flex-shrink-0" + > + {({ open }) => ( +
      + + +
      + {value && Array.isArray(value) ? ( + <> + {value.length > 0 ? ( + value.map((assignee, index: number) => { + const person = people?.find( + (p) => p.member.id === assignee + )?.member; + + return ( +
      + {person && person.avatar && person.avatar !== "" ? ( +
      + {person.first_name} +
      + ) : ( +
      + {person?.first_name && person.first_name !== "" + ? person.first_name.charAt(0) + : person?.email.charAt(0)} +
      + )} +
      + ); + }) + ) : ( +
      + No user +
      + )} + + ) : null} +
      +
      +
      + + + +
      + {people ? ( + people.length > 0 ? ( + people.map((option) => ( + + `${ + active || selected ? "bg-indigo-50" : "" + } flex items-center gap-2 text-gray-900 cursor-pointer select-none p-2 truncate` + } + value={option.member.id} + > + {option.member.avatar && option.member.avatar !== "" ? ( +
      + avatar +
      + ) : ( +
      + {option.member.first_name && option.member.first_name !== "" + ? option.member.first_name.charAt(0) + : option.member.email.charAt(0)} +
      + )} + {option.member.first_name && option.member.first_name !== "" + ? option.member.first_name + : option.member.email} +
      + )) + ) : ( +
      No assignees found
      + ) + ) : ( + + )} +
      +
      +
      +
      + )} +
      + )} + /> +
      +
      + ); +}; + +export default SelectMembers; diff --git a/apps/app/components/project/modules/module-detail-sidebar/select-status.tsx b/apps/app/components/project/modules/module-detail-sidebar/select-status.tsx new file mode 100644 index 0000000000..a1edf38889 --- /dev/null +++ b/apps/app/components/project/modules/module-detail-sidebar/select-status.tsx @@ -0,0 +1,63 @@ +// react +import React from "react"; +// react-hook-form +import { Control, Controller, UseFormWatch } from "react-hook-form"; +// ui +import { CustomSelect } from "ui"; +// icons +import { Squares2X2Icon } from "@heroicons/react/24/outline"; +// types +import { IModule } from "types"; +// common +import { classNames } from "constants/common"; +// constants +import { MODULE_STATUS } from "constants/"; + +type Props = { + control: Control, any>; + submitChanges: (formData: Partial) => void; + watch: UseFormWatch>; +}; + +const SelectStatus: React.FC = ({ control, submitChanges, watch }) => { + return ( +
      +
      + +

      Status

      +
      +
      + ( + + {watch("status")} + + } + value={value} + onChange={(value: any) => { + submitChanges({ status: value }); + }} + > + {MODULE_STATUS.map((option) => ( + + {option.label} + + ))} + + )} + /> +
      +
      + ); +}; + +export default SelectStatus; diff --git a/apps/app/components/project/modules/single-module-card.tsx b/apps/app/components/project/modules/single-module-card.tsx new file mode 100644 index 0000000000..6c57069838 --- /dev/null +++ b/apps/app/components/project/modules/single-module-card.tsx @@ -0,0 +1,102 @@ +// next +import Image from "next/image"; +import Link from "next/link"; +// icons +import User from "public/user.png"; +// types +import { IModule } from "types"; +// common +import { renderShortNumericDateFormat } from "constants/common"; +import { CalendarDaysIcon } from "@heroicons/react/24/outline"; + +type Props = { + module: IModule; +}; + +const SingleModuleCard: React.FC = ({ module }) => { + return ( +
      + + {module.name} + +
      +
      +
      LEAD
      +
      + {module.lead_detail?.avatar && module.lead_detail.avatar !== "" ? ( +
      + {module.lead_detail.first_name} +
      + ) : ( +
      + {module.lead_detail?.first_name && module.lead_detail.first_name !== "" + ? module.lead_detail.first_name.charAt(0) + : module.lead_detail?.email.charAt(0)} +
      + )} +
      +
      +
      +
      MEMBERS
      +
      + {module.members && module.members.length > 0 ? ( + module?.members_detail?.map((member, index: number) => ( +
      + {member?.avatar && member.avatar !== "" ? ( +
      + {member?.first_name} +
      + ) : ( +
      + {member?.first_name && member.first_name !== "" + ? member.first_name.charAt(0) + : member?.email?.charAt(0)} +
      + )} +
      + )) + ) : ( +
      + No user +
      + )} +
      +
      +
      +
      END DATE
      +
      + + {renderShortNumericDateFormat(module.target_date ?? "")} +
      +
      +
      +
      STATUS
      +
      {module.status}
      +
      +
      +
      + ); +}; + +export default SingleModuleCard; diff --git a/apps/app/constants/api-routes.ts b/apps/app/constants/api-routes.ts index 7d2cfe90bf..0119e36178 100644 --- a/apps/app/constants/api-routes.ts +++ b/apps/app/constants/api-routes.ts @@ -141,6 +141,6 @@ export const MODULE_ISSUE_DETAIL = ( workspaceSlug: string, projectId: string, moduleId: string, - issueId: string + bridgeId: string ) => - `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/${issueId}/`; + `/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/${bridgeId}/`; diff --git a/apps/app/constants/global.tsx b/apps/app/constants/global.tsx index 2ce33c5429..2b470fe8cc 100644 --- a/apps/app/constants/global.tsx +++ b/apps/app/constants/global.tsx @@ -1,4 +1,6 @@ -export const getPriorityIcon = (priority: string, className: string) => { +export const getPriorityIcon = (priority: string, className?: string) => { + if (!className || className === "") className = "text-xs"; + switch (priority) { case "urgent": return error; @@ -13,6 +15,6 @@ export const getPriorityIcon = (priority: string, className: string) => { signal_cellular_alt_1_bar ); default: - return null; + return "None"; } }; diff --git a/apps/app/constants/index.ts b/apps/app/constants/index.ts index a53e6eda77..ccc6b7862e 100644 --- a/apps/app/constants/index.ts +++ b/apps/app/constants/index.ts @@ -19,6 +19,15 @@ export const GROUP_CHOICES = { cancelled: "Cancelled", }; +export const MODULE_STATUS = [ + { label: "Backlog", value: "backlog" }, + { label: "Planned", value: "planned" }, + { label: "In Progress", value: "in-progress" }, + { label: "Paused", value: "paused" }, + { label: "Completed", value: "completed" }, + { label: "Cancelled", value: "cancelled" }, +]; + export const groupByOptions: Array<{ name: string; key: NestedKeyOf | null }> = [ { name: "State", key: "state_detail.name" }, { name: "Priority", key: "priority" }, diff --git a/apps/app/contexts/user.context.tsx b/apps/app/contexts/user.context.tsx index 71f5a7ec80..f6b74dc57e 100644 --- a/apps/app/contexts/user.context.tsx +++ b/apps/app/contexts/user.context.tsx @@ -19,11 +19,13 @@ import { PROJECT_ISSUES_LIST, STATE_LIST, CYCLE_LIST, + MODULE_LIST, } from "constants/fetch-keys"; // types import type { KeyedMutator } from "swr"; -import type { IUser, IWorkspace, IProject, IssueResponse, ICycle, IState } from "types"; +import type { IUser, IWorkspace, IProject, IssueResponse, ICycle, IState, IModule } from "types"; +import modulesService from "lib/services/modules.service"; interface IUserContextProps { user?: IUser; @@ -40,6 +42,8 @@ interface IUserContextProps { mutateIssues: KeyedMutator; cycles?: ICycle[]; mutateCycles: KeyedMutator; + modules?: IModule[]; + mutateModules: KeyedMutator; states?: IState[]; mutateStates: KeyedMutator; } @@ -99,6 +103,13 @@ export const UserProvider = ({ children }: { children: ReactElement }) => { : null ); + const { data: modules, mutate: mutateModules } = useSWR( + activeWorkspace && activeProject ? MODULE_LIST(activeProject.id) : null, + activeWorkspace && activeProject + ? () => modulesService.getModules(activeWorkspace.slug, activeProject.id) + : null + ); + useEffect(() => { if (!projects) return; const activeProject = projects.find((project) => project.id === projectId); @@ -143,6 +154,8 @@ export const UserProvider = ({ children }: { children: ReactElement }) => { mutateIssues, cycles, mutateCycles, + modules, + mutateModules, states, mutateStates, setActiveProject, diff --git a/apps/app/lib/services/modules.service.ts b/apps/app/lib/services/modules.service.ts index 3e1a3e9c5c..3390f1dd33 100644 --- a/apps/app/lib/services/modules.service.ts +++ b/apps/app/lib/services/modules.service.ts @@ -50,6 +50,16 @@ class ProjectIssuesServices extends APIService { }); } + async getModuleDetails(workspaceSlug: string, projectId: string, moduleId: string): Promise { + return this.get(MODULE_DETAIL(workspaceSlug, projectId, moduleId)) + .then((response) => { + return response?.data; + }) + .catch((error) => { + throw error?.response?.data; + }); + } + async patchModule( workspaceSlug: string, projectId: string, @@ -85,11 +95,11 @@ class ProjectIssuesServices extends APIService { }); } - async addIssueToModule( + async addIssuesToModule( workspaceSlug: string, projectId: string, moduleId: string, - data: any + data: { issues: string[] } ): Promise { return this.post(MODULE_ISSUES(workspaceSlug, projectId, moduleId), data) .then((response) => { @@ -104,9 +114,9 @@ class ProjectIssuesServices extends APIService { workspaceSlug: string, projectId: string, moduleId: string, - issueId: string + bridgeId: string ): Promise { - return this.delete(MODULE_ISSUE_DETAIL(workspaceSlug, projectId, moduleId, issueId)) + return this.delete(MODULE_ISSUE_DETAIL(workspaceSlug, projectId, moduleId, bridgeId)) .then((response) => { return response?.data; }) diff --git a/apps/app/pages/me/my-issues.tsx b/apps/app/pages/me/my-issues.tsx index 3b6802ec6d..a18be4cf17 100644 --- a/apps/app/pages/me/my-issues.tsx +++ b/apps/app/pages/me/my-issues.tsx @@ -243,11 +243,11 @@ const MyIssues: NextPage = () => { {issue.project_detail.identifier}-{issue.sequence_id} )} */} - {issue.name} -
      + {issue.name} + {/*
      Name
      {issue.name}
      -
      +
      */}
      diff --git a/apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx index 7f0e908aa9..18b8ff2a4b 100644 --- a/apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx @@ -12,8 +12,8 @@ import AppLayout from "layouts/app-layout"; import CyclesListView from "components/project/cycles/list-view"; import CyclesBoardView from "components/project/cycles/board-view"; import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal"; -import CycleIssuesListModal from "components/project/cycles/cycle-issues-list-modal"; import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion"; +import ExistingIssuesListModal from "components/common/existing-issues-list-modal"; // constants import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/"; // services @@ -67,7 +67,7 @@ const SingleCycle: React.FC = () => { : null ); const cycleIssuesArray = cycleIssues?.map((issue) => { - return { bridge: issue.id, ...issue.issue_details }; + return { bridge: issue.id, ...issue.issue_detail }; }); const { data: members } = useSWR( @@ -163,6 +163,20 @@ const SingleCycle: React.FC = () => { // console.log(result); }; + const handleAddIssuesToCycle = (data: { issues: string[] }) => { + if (activeWorkspace && activeProject) { + issuesServices + .addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId as string, data) + .then((res) => { + console.log(res); + mutate(CYCLE_ISSUES(cycleId as string)); + }) + .catch((e) => { + console.log(e); + }); + } + }; + const removeIssueFromCycle = (bridgeId: string) => { if (activeWorkspace && activeProject) { mutate( @@ -191,11 +205,12 @@ const SingleCycle: React.FC = () => { setIsOpen={setIsIssueModalOpen} projectId={activeProject?.id} /> - setCycleIssuesListModal(false)} - issues={issues} - cycleId={cycleId as string} + type="cycle" + issues={issues?.results ?? []} + handleOnSubmit={handleAddIssuesToCycle} /> setDeleteIssue(undefined)} @@ -375,23 +390,22 @@ const SingleCycle: React.FC = () => { openCreateIssueModal={openCreateIssueModal} openIssuesListModal={openIssuesListModal} removeIssueFromCycle={removeIssueFromCycle} + handleDeleteIssue={setDeleteIssue} setPreloadedData={setPreloadedData} /> ) : ( -
      - -
      + )} diff --git a/apps/app/pages/projects/[projectId]/issues/index.tsx b/apps/app/pages/projects/[projectId]/issues/index.tsx index 1dd8cab21d..4fe8033d32 100644 --- a/apps/app/pages/projects/[projectId]/issues/index.tsx +++ b/apps/app/pages/projects/[projectId]/issues/index.tsx @@ -334,6 +334,7 @@ const ProjectIssues: NextPage = () => { />
      )} +
      ) : (
      diff --git a/apps/app/pages/projects/[projectId]/modules/[moduleId].tsx b/apps/app/pages/projects/[projectId]/modules/[moduleId].tsx new file mode 100644 index 0000000000..4496015d90 --- /dev/null +++ b/apps/app/pages/projects/[projectId]/modules/[moduleId].tsx @@ -0,0 +1,407 @@ +// react +import React, { useState } from "react"; +// next +import { useRouter } from "next/router"; +// swr +import useSWR, { mutate } from "swr"; +// services +import modulesService from "lib/services/modules.service"; +import projectService from "lib/services/project.service"; +import issuesService from "lib/services/issues.service"; +// hooks +import useUser from "lib/hooks/useUser"; +import useIssuesFilter from "lib/hooks/useIssuesFilter"; +import useIssuesProperties from "lib/hooks/useIssuesProperties"; +// layouts +import AppLayout from "layouts/app-layout"; +// components +import ExistingIssuesListModal from "components/common/existing-issues-list-modal"; +import ModulesBoardView from "components/project/modules/board-view"; +import ModulesListView from "components/project/modules/list-view"; +import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion"; +import ModuleDetailSidebar from "components/project/modules/module-detail-sidebar"; +import ConfirmModuleDeletion from "components/project/modules/confirm-module-deleteion"; +// headless ui +import { Popover, Transition } from "@headlessui/react"; +// ui +import { BreadcrumbItem, Breadcrumbs, CustomMenu } from "ui"; +// icons +import { + ArrowLeftIcon, + ArrowPathIcon, + ChevronDownIcon, + ListBulletIcon, +} from "@heroicons/react/24/outline"; +import { Squares2X2Icon } from "@heroicons/react/20/solid"; +// types +import { IIssue, IModule, ModuleIssueResponse, Properties, SelectModuleType } from "types"; +// fetch-keys +import { MODULE_DETAIL, MODULE_ISSUES, PROJECT_MEMBERS } from "constants/fetch-keys"; +// common +import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common"; +// constants +import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/"; + +const SingleModule = () => { + const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); + const [deleteIssue, setDeleteIssue] = useState(undefined); + const [moduleSidebar, setModuleSidebar] = useState(false); + + const [moduleDeleteModal, setModuleDeleteModal] = useState(false); + const [selectedModuleForDelete, setSelectedModuleForDelete] = useState(); + + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + const { activeWorkspace, activeProject, issues, modules } = useUser(); + + const router = useRouter(); + + const { moduleId } = router.query; + + const [properties, setProperties] = useIssuesProperties( + activeWorkspace?.slug, + activeProject?.id as string + ); + + const { data: moduleIssues } = useSWR( + activeWorkspace && activeProject && moduleId ? MODULE_ISSUES(moduleId as string) : null, + activeWorkspace && activeProject && moduleId + ? () => + modulesService.getModuleIssues( + activeWorkspace?.slug, + activeProject?.id, + moduleId as string + ) + : null + ); + const moduleIssuesArray = moduleIssues?.map((issue) => { + return { bridge: issue.id, ...issue.issue_detail }; + }); + + const { data: moduleDetail } = useSWR( + MODULE_DETAIL, + activeWorkspace && activeProject && moduleId + ? () => + modulesService.getModuleDetails( + activeWorkspace?.slug, + activeProject?.id, + moduleId as string + ) + : null + ); + + const { + issueView, + groupByProperty, + setGroupByProperty, + groupedByIssues, + setOrderBy, + setFilterIssue, + orderBy, + filterIssue, + setIssueViewToKanban, + setIssueViewToList, + } = useIssuesFilter(moduleIssuesArray ?? []); + + const { data: members } = useSWR( + activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null, + activeWorkspace && activeProject + ? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id) + : null, + { + onErrorRetry(err, _, __, revalidate, revalidateOpts) { + if (err?.status === 403) return; + setTimeout(() => revalidate(revalidateOpts), 5000); + }, + } + ); + + const handleAddIssuesToModule = (data: { issues: string[] }) => { + if (activeWorkspace && activeProject) { + modulesService + .addIssuesToModule(activeWorkspace.slug, activeProject.id, moduleId as string, data) + .then((res) => { + console.log(res); + }) + .catch((e) => console.log(e)); + } + }; + + const partialUpdateIssue = (formData: Partial, issueId: string) => { + if (!activeWorkspace || !activeProject) return; + issuesService + .patchIssue(activeWorkspace.slug, activeProject.id, issueId, formData) + .then((response) => { + mutate(MODULE_ISSUES(moduleId as string)); + }) + .catch((error) => { + console.log(error); + }); + }; + + const openCreateIssueModal = () => {}; + + const openIssuesListModal = () => { + setModuleIssuesListModal(true); + }; + + const removeIssueFromModule = (issueId: string) => { + if (!activeWorkspace || !activeProject) return; + + modulesService + .removeIssueFromModule(activeWorkspace.slug, activeProject.id, moduleId as string, issueId) + .then((res) => { + console.log(res); + mutate(MODULE_ISSUES(moduleId as string)); + }) + .catch((e) => { + console.log(e); + }); + }; + + const handleDeleteModule = () => { + if (!moduleDetail) return; + + setSelectedModuleForDelete({ ...moduleDetail, actionType: "delete" }); + setModuleDeleteModal(true); + }; + + return ( + <> + setModuleIssuesListModal(false)} + type="module" + issues={issues?.results ?? []} + handleOnSubmit={handleAddIssuesToModule} + /> + setDeleteIssue(undefined)} + isOpen={!!deleteIssue} + data={issues?.results.find((issue) => issue.id === deleteIssue)} + /> + + + + + } + left={ + + + {modules?.find((c) => c.id === moduleId)?.name} + + } + className="ml-1.5" + width="auto" + > + {modules?.map((module) => ( + + {module.name} + + ))} + + } + right={ +
      +
      + + +
      + + {({ open }) => ( + <> + + View + + + + +
      +
      +

      Group by

      + option.key === groupByProperty) + ?.name ?? "Select" + } + width="auto" + > + {groupByOptions.map((option) => ( + setGroupByProperty(option.key)} + > + {option.name} + + ))} + +
      +
      +

      Order by

      + option.key === orderBy)?.name ?? + "Select" + } + width="auto" + > + {orderByOptions.map((option) => + groupByProperty === "priority" && option.key === "priority" ? null : ( + setOrderBy(option.key)} + > + {option.name} + + ) + )} + +
      +
      +

      Issue type

      + option.key === filterIssue) + ?.name ?? "Select" + } + width="auto" + > + {filterIssueOptions.map((option) => ( + setFilterIssue(option.key)} + > + {option.name} + + ))} + +
      +
      +
      +

      Properties

      +
      + {Object.keys(properties).map((key) => ( + + ))} +
      +
      +
      +
      +
      + + )} +
      + +
      + } + > +
      + {issueView === "list" ? ( + + ) : ( + + )} +
      + +
      + + ); +}; + +export default SingleModule; diff --git a/apps/app/pages/projects/[projectId]/modules/index.tsx b/apps/app/pages/projects/[projectId]/modules/index.tsx index e95d40b79a..b01062f58f 100644 --- a/apps/app/pages/projects/[projectId]/modules/index.tsx +++ b/apps/app/pages/projects/[projectId]/modules/index.tsx @@ -10,6 +10,8 @@ import withAuth from "lib/hoc/withAuthWrapper"; import modulesService from "lib/services/modules.service"; // hooks import useUser from "lib/hooks/useUser"; +// components +import SingleModuleCard from "components/project/modules/single-module-card"; // ui import { BreadcrumbItem, Breadcrumbs, EmptySpace, EmptySpaceItem, HeaderButton, Spinner } from "ui"; // icons @@ -17,7 +19,7 @@ import { PlusIcon, RectangleGroupIcon } from "@heroicons/react/24/outline"; // types import { IModule } from "types/modules"; // fetch-keys -import { MODULE_LIST } from "constants/fetch-keys"; +import { MODULE_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; const ProjectModules: NextPage = () => { const { activeWorkspace, activeProject } = useUser(); @@ -62,12 +64,11 @@ const ProjectModules: NextPage = () => { {modules ? ( modules.length > 0 ? (
      - {modules.map((module) => ( -
      -

      {module.name}

      -

      {module.description}

      -
      - ))} +
      + {modules.map((module) => ( + + ))} +
      ) : (
      diff --git a/apps/app/pages/projects/[projectId]/settings/control.tsx b/apps/app/pages/projects/[projectId]/settings/control.tsx index 3e26c168a8..1ab9d1e9e2 100644 --- a/apps/app/pages/projects/[projectId]/settings/control.tsx +++ b/apps/app/pages/projects/[projectId]/settings/control.tsx @@ -119,150 +119,160 @@ const ControlSettings = () => {

      Control

      Set the control for the project.

      -
      -
      -

      Project Lead

      -

      Select the project leader.

      - ( - - {({ open }) => ( - <> -
      - - - {people?.find((person) => person.member.id === value)?.member - .first_name ?? "Select Lead"} - - +
      +
      +
      +

      Project Lead

      +

      Select the project leader.

      + ( + + {({ open }) => ( + <> +
      + + + {people?.find((person) => person.member.id === value)?.member + .first_name ?? "Select Lead"} + + - - - {people?.map((person) => ( - - `${ - active ? "bg-indigo-50" : "" - } text-gray-900 cursor-default select-none relative px-3 py-2` - } - value={person.member.id} - > - {({ selected, active }) => ( - <> - - {person.member.first_name !== "" - ? person.member.first_name - : person.member.email} - - - {selected ? ( + + + {people?.map((person) => ( + + `${ + active ? "bg-indigo-50" : "" + } text-gray-900 cursor-default select-none relative px-3 py-2` + } + value={person.member.id} + > + {({ selected, active }) => ( + <> - - ) : null} - - )} - - ))} - - -
      - - )} -
      - )} - /> + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
      + + )} + + )} + /> +
      -
      -

      Default Assignee

      -

      - Select the default assignee for the project. -

      - ( - - {({ open }) => ( - <> -
      - - - {people?.find((p) => p.member.id === value)?.member.first_name ?? - "Select Default Assignee"} - - +
      +
      +

      Default Assignee

      +

      + Select the default assignee for the project. +

      + ( + + {({ open }) => ( + <> +
      + + + {people?.find((p) => p.member.id === value)?.member.first_name ?? + "Select Default Assignee"} + + - - - {people?.map((person) => ( - - `${ - active ? "bg-indigo-50" : "" - } text-gray-900 cursor-default select-none relative px-3 py-2` - } - value={person.member.id} - > - {({ selected, active }) => ( - <> - - {person.member.first_name !== "" - ? person.member.first_name - : person.member.email} - - - {selected ? ( + + + {people?.map((person) => ( + + `${ + active ? "bg-indigo-50" : "" + } text-gray-900 cursor-default select-none relative px-3 py-2` + } + value={person.member.id} + > + {({ selected, active }) => ( + <> - - ) : null} - - )} - - ))} - - -
      - - )} -
      - )} - /> + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
      + + )} + + )} + /> +
      diff --git a/apps/app/pages/projects/[projectId]/settings/index.tsx b/apps/app/pages/projects/[projectId]/settings/index.tsx index aaea02d618..67e4a623d2 100644 --- a/apps/app/pages/projects/[projectId]/settings/index.tsx +++ b/apps/app/pages/projects/[projectId]/settings/index.tsx @@ -136,97 +136,101 @@ const GeneralSettings = () => { This information will be displayed to every member of the project.

      -
      -
      -

      Icon & Name

      -

      - Select an icon and a name for the project. -

      -
      - ( - - )} - /> +
      +
      +
      +

      Icon & Name

      +

      + Select an icon and a name for the project. +

      +
      + ( + + )} + /> + +
      +
      +
      +

      Identifier

      +

      + Create a 1-6 characters{"'"} identifier for the project. +

      { + if (!activeWorkspace || !e.target.value) return; + checkIdentifierAvailability(activeWorkspace.slug, e.target.value); + }} validations={{ - required: "Name is required", + 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", + }, }} />
      -
      -

      Description

      -

      Give a description to the project.

      -