diff --git a/apps/app/.env.example b/apps/app/.env.example new file mode 100644 index 000000000..aff5c6c31 --- /dev/null +++ b/apps/app/.env.example @@ -0,0 +1,4 @@ +NEXT_PUBLIC_API_BASE_URL = "<-- endpoint goes here -->" +NEXT_PUBLIC_GOOGLE_CLIENTID = "<-- google client id goes here -->" +NEXT_PUBLIC_GITHUB_ID = "<-- github id goes here -->" +NEXT_PUBLIC_APP_ENVIRONMENT=development \ No newline at end of file diff --git a/apps/app/components/command-palette/addAsSubIssue.tsx b/apps/app/components/command-palette/addAsSubIssue.tsx index e8c585e45..aadc04574 100644 --- a/apps/app/components/command-palette/addAsSubIssue.tsx +++ b/apps/app/components/command-palette/addAsSubIssue.tsx @@ -7,7 +7,7 @@ import { useForm } from "react-hook-form"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; // icons diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index 1b5694616..32f71a3db 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -8,7 +8,7 @@ import { SubmitHandler, useForm } from "react-hook-form"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; import useTheme from "lib/hooks/useTheme"; @@ -22,7 +22,7 @@ import { } from "@heroicons/react/24/outline"; // components import ShortcutsModal from "components/command-palette/shortcuts"; -import CreateProjectModal from "components/project/CreateProjectModal"; +import CreateProjectModal from "components/project/create-project-modal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; // ui @@ -278,15 +278,15 @@ const CommandPalette: React.FC = () => { value={issue.id} /> - + {activeProject?.identifier}-{issue.sequence_id} - {issue.name} + {issue.name} {active && ( )} diff --git a/apps/app/components/project/SendProjectInvitationModal.tsx b/apps/app/components/project/SendProjectInvitationModal.tsx index a01d36658..6227c569f 100644 --- a/apps/app/components/project/SendProjectInvitationModal.tsx +++ b/apps/app/components/project/SendProjectInvitationModal.tsx @@ -20,7 +20,7 @@ import { Button, Select, TextArea } from "ui"; import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid"; // types -import { ProjectMember, WorkspaceMember } from "types"; +import { IProjectMemberInvitation } from "types"; type Props = { isOpen: boolean; @@ -28,6 +28,11 @@ type Props = { members: any[]; }; +type ProjectMember = IProjectMemberInvitation & { + member_id: string; + user_id: string; +}; + const defaultValues: Partial = { email: "", message: "", @@ -49,9 +54,15 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member const { setToastAlert } = useToast(); - const { data: people } = useSWR( - activeWorkspace ? WORKSPACE_MEMBERS : null, - activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null, + { + onErrorRetry(err, _, __, revalidate, revalidateOpts) { + if (err?.status === 403) return; + setTimeout(() => revalidate(revalidateOpts), 5000); + }, + } ); const { diff --git a/apps/app/components/project/ConfirmProjectDeletion.tsx b/apps/app/components/project/confirm-project-deletion.tsx similarity index 63% rename from apps/app/components/project/ConfirmProjectDeletion.tsx rename to apps/app/components/project/confirm-project-deletion.tsx index 0b0dde39e..159ef8db4 100644 --- a/apps/app/components/project/ConfirmProjectDeletion.tsx +++ b/apps/app/components/project/confirm-project-deletion.tsx @@ -9,19 +9,26 @@ import useToast from "lib/hooks/useToast"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // ui -import { Button } from "ui"; +import { Button, Input } from "ui"; // types import type { IProject } from "types"; type Props = { isOpen: boolean; - setIsOpen: React.Dispatch>; - data?: IProject; + onClose: () => void; + data: IProject | null; }; -const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { +const ConfirmProjectDeletion: React.FC = ({ isOpen, data, onClose }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [selectedProject, setSelectedProject] = useState(null); + + const [confirmProjectName, setConfirmProjectName] = useState(""); + const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false); + + const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject; + const { activeWorkspace, mutateProjects } = useUser(); const { setToastAlert } = useToast(); @@ -29,13 +36,18 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) => const cancelButtonRef = useRef(null); const handleClose = () => { - setIsOpen(false); setIsDeleteLoading(false); + const timer = setTimeout(() => { + setConfirmProjectName(""); + setConfirmDeleteMyProject(false); + clearTimeout(timer); + }, 350); + onClose(); }; const handleDeletion = async () => { setIsDeleteLoading(true); - if (!data || !activeWorkspace) return; + if (!data || !activeWorkspace || !canDelete) return; await projectService .deleteProject(activeWorkspace.slug, data.id) .then(() => { @@ -54,8 +66,14 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) => }; useEffect(() => { - data && setIsOpen(true); - }, [data, setIsOpen]); + if (data) setSelectedProject(data); + else { + const timer = setTimeout(() => { + setSelectedProject(null); + clearTimeout(timer); + }, 300); + } + }, [data]); return ( @@ -104,11 +122,48 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) =>

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

+
+
+

+ Enter the project name{" "} + {selectedProject?.name} to + continue: +

+ { + setConfirmProjectName(e.target.value); + }} + name="projectName" + /> +
+
+

+ To confirm, type delete my project{" "} + below: +

+ { + if (e.target.value === "delete my project") { + setConfirmDeleteMyProject(true); + } else { + setConfirmDeleteMyProject(false); + } + }} + name="typeDelete" + /> +
@@ -117,7 +172,7 @@ const ConfirmProjectDeletion: React.FC = ({ isOpen, setIsOpen, data }) => type="button" onClick={handleDeletion} theme="danger" - disabled={isDeleteLoading} + disabled={isDeleteLoading || !canDelete} className="inline-flex sm:ml-3" > {isDeleteLoading ? "Deleting..." : "Delete"} diff --git a/apps/app/components/project/CreateProjectModal.tsx b/apps/app/components/project/create-project-modal.tsx similarity index 66% rename from apps/app/components/project/CreateProjectModal.tsx rename to apps/app/components/project/create-project-modal.tsx index 2010170c7..f6ad48a78 100644 --- a/apps/app/components/project/CreateProjectModal.tsx +++ b/apps/app/components/project/create-project-modal.tsx @@ -1,21 +1,24 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect } from "react"; // swr -import { mutate } from "swr"; +import useSWR, { mutate } from "swr"; // react hook form import { useForm } from "react-hook-form"; // headless import { Dialog, Transition } from "@headlessui/react"; // services import projectServices from "lib/services/project.service"; +import workspaceService from "lib/services/workspace.service"; +// common +import { createSimilarString } from "constants/common"; +// constants +import { NETWORK_CHOICES } from "constants/"; // fetch keys -import { PROJECTS_LIST } from "constants/fetch-keys"; +import { PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; // ui import { Button, Input, TextArea, Select } from "ui"; -// common -import { debounce } from "constants/common"; // types import { IProject } from "types"; @@ -24,11 +27,28 @@ type Props = { setIsOpen: React.Dispatch>; }; -const NETWORK_CHOICES = { "0": "Secret", "2": "Public" }; - const defaultValues: Partial = { name: "", + identifier: "", description: "", + network: 0, +}; + +const IsGuestCondition: React.FC<{ + setIsOpen: React.Dispatch>; +}> = ({ setIsOpen }) => { + const { setToastAlert } = useToast(); + + useEffect(() => { + setIsOpen(false); + setToastAlert({ + title: "Error", + type: "error", + message: "You don't have permission to create project.", + }); + }, [setIsOpen, setToastAlert]); + + return null; }; const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { @@ -40,7 +60,17 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { }, 500); }; - const { activeWorkspace } = useUser(); + const { activeWorkspace, user } = useUser(); + + const { data: workspaceMembers } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null, + { + shouldRetryOnError: false, + } + ); + + const [recommendedIdentifier, setRecommendedIdentifier] = useState([]); const { setToastAlert } = useToast(); @@ -52,6 +82,7 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { handleSubmit, reset, setError, + clearErrors, watch, setValue, } = useForm({ @@ -77,6 +108,16 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { handleClose(); }) .catch((err) => { + if (err.status === 403) { + setToastAlert({ + title: "Error", + type: "error", + message: "You don't have permission to create project.", + }); + handleClose(); + return; + } + err = err.data; Object.keys(err).map((key) => { const errorMessages = err[key]; setError(key as keyof IProject, { @@ -89,22 +130,39 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { const projectName = watch("name") ?? ""; const projectIdentifier = watch("identifier") ?? ""; - const checkIdentifier = (slug: string, value: string) => { - projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => { - console.log(response); - if (response.exists) setError("identifier", { message: "Identifier already exists" }); - }); - }; + if (workspaceMembers) { + const isMember = workspaceMembers.find((member) => member.member.id === user?.id); + const isGuest = workspaceMembers.find( + (member) => member.member.id === user?.id && member.role === 5 + ); - // eslint-disable-next-line react-hooks/exhaustive-deps - const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []); + if ((!isMember || isGuest) && isOpen) return ; + } useEffect(() => { if (projectName && isChangeIdentifierRequired) { - setValue("identifier", projectName.replace(/ /g, "-").toUpperCase().substring(0, 3)); + setValue("identifier", projectName.replace(/ /g, "").toUpperCase().substring(0, 3)); } }, [projectName, projectIdentifier, setValue, isChangeIdentifierRequired]); + useEffect(() => { + if (!projectName) return; + const suggestedIdentifier = createSimilarString( + projectName.replace(/ /g, "").toUpperCase().substring(0, 3) + ); + + setRecommendedIdentifier([ + suggestedIdentifier + Math.floor(Math.random() * 101), + suggestedIdentifier + Math.floor(Math.random() * 101), + projectIdentifier.toUpperCase().substring(0, 3) + Math.floor(Math.random() * 101), + projectIdentifier.toUpperCase().substring(0, 3) + Math.floor(Math.random() * 101), + ]); + }, [errors.identifier]); + + useEffect(() => { + return () => setIsChangeIdentifierRequired(true); + }, [isOpen]); + return ( @@ -191,11 +249,7 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { placeholder="Enter Project Identifier" error={errors.identifier} register={register} - onChange={(e: any) => { - setIsChangeIdentifierRequired(false); - if (!activeWorkspace || !e.target.value) return; - checkIdentifierAvailability(activeWorkspace.slug, e.target.value); - }} + onChange={() => setIsChangeIdentifierRequired(false)} validations={{ required: "Identifier is required", minLength: { @@ -203,11 +257,32 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { message: "Identifier must at least be of 1 character", }, maxLength: { - value: 9, - message: "Identifier must at most be of 9 characters", + value: 5, + message: "Identifier must at most be of 5 characters", }, }} /> + {errors.identifier && ( +
+

Ops! Identifier is already taken. Try one of the following:

+
+ {recommendedIdentifier.map((identifier) => ( + + ))} +
+
+ )} diff --git a/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx b/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx index fb5cf565b..b3571831a 100644 --- a/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx +++ b/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx @@ -4,7 +4,7 @@ import { mutate } from "swr"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services -import cycleService from "lib/services/cycles.services"; +import cycleService from "lib/services/cycles.service"; // fetch api import { CYCLE_LIST } from "constants/fetch-keys"; // hooks diff --git a/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx b/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx index 0fd25f481..3f3127082 100644 --- a/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx +++ b/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx @@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"; // headless import { Dialog, Transition } from "@headlessui/react"; // services -import cycleService from "lib/services/cycles.services"; +import cycleService from "lib/services/cycles.service"; // fetch keys import { CYCLE_LIST } from "constants/fetch-keys"; // hooks diff --git a/apps/app/components/project/cycles/CycleIssuesListModal.tsx b/apps/app/components/project/cycles/CycleIssuesListModal.tsx index 5f29a5bfd..d25d069df 100644 --- a/apps/app/components/project/cycles/CycleIssuesListModal.tsx +++ b/apps/app/components/project/cycles/CycleIssuesListModal.tsx @@ -7,7 +7,7 @@ import { Combobox, Dialog, Transition } from "@headlessui/react"; // ui import { Button } from "ui"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; diff --git a/apps/app/components/project/cycles/CycleView.tsx b/apps/app/components/project/cycles/CycleView.tsx index f46079165..73a0d0af4 100644 --- a/apps/app/components/project/cycles/CycleView.tsx +++ b/apps/app/components/project/cycles/CycleView.tsx @@ -7,7 +7,7 @@ import useSWR, { mutate } from "swr"; // headless ui import { Disclosure, Transition, Menu } from "@headlessui/react"; // services -import cycleServices from "lib/services/cycles.services"; +import cycleServices from "lib/services/cycles.service"; // hooks import useUser from "lib/hooks/useUser"; // components @@ -22,7 +22,7 @@ import type { CycleViewProps as Props, CycleIssueResponse, IssueResponse } from import { CYCLE_ISSUES } from "constants/fetch-keys"; // constants import { renderShortNumericDateFormat } from "constants/common"; -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { Draggable } from "react-beautiful-dnd"; diff --git a/apps/app/components/project/issues/BoardView/SingleBoard.tsx b/apps/app/components/project/issues/BoardView/SingleBoard.tsx index 5f81be602..2c0426e93 100644 --- a/apps/app/components/project/issues/BoardView/SingleBoard.tsx +++ b/apps/app/components/project/issues/BoardView/SingleBoard.tsx @@ -18,11 +18,10 @@ import { ArrowsPointingOutIcon, CalendarDaysIcon, EllipsisHorizontalIcon, - PencilIcon, PlusIcon, } from "@heroicons/react/24/outline"; import Image from "next/image"; -import { divide } from "lodash"; +import { getPriorityIcon } from "constants/global"; type Props = { selectedGroup: NestedKeyOf | null; @@ -42,8 +41,8 @@ type Props = { > >; bgColor?: string; - stateId?: string; - createdBy?: string; + stateId: string | null; + createdBy: string | null; }; const SingleBoard: React.FC = ({ @@ -109,7 +108,7 @@ const SingleBoard: React.FC = ({

= ({ setIsIssueOpen(true); if (selectedGroup !== null) setPreloadedData({ - state: stateId, + state: stateId !== null ? stateId : undefined, [selectedGroup]: groupTitle, actionType: "createIssue", }); @@ -159,17 +158,6 @@ const SingleBoard: React.FC = ({ > - @@ -192,131 +180,116 @@ const SingleBoard: React.FC = ({ ref={provided.innerRef} {...provided.draggableProps} > -
- +
+ {properties.key && ( +
+ {childIssue.project_detail?.identifier}-{childIssue.sequence_id} +
+ )} +
{childIssue.name} - - {Object.keys(properties).map( - (key) => - properties[key as keyof Properties] && - !Array.isArray(childIssue[key as keyof IIssue]) && ( -
- {key === "start_date" && childIssue.start_date !== null && ( - - - {renderShortNumericDateFormat(childIssue.start_date)} - - {childIssue.target_date - ? renderShortNumericDateFormat(childIssue.target_date) - : "None"} - - )} - {key === "target_date" && ( - <> - +
+ {properties.priority && ( +
+ {/* {getPriorityIcon(childIssue.priority ?? "")} */} + {childIssue.priority} +
+ )} + {properties.state && ( +
+ + {addSpaceIfCamelCase(childIssue.state_detail.name)} +
+ )} + {properties.start_date && ( +
+ + {childIssue.start_date + ? renderShortNumericDateFormat(childIssue.start_date) + : "N/A"} +
+ )} + {properties.target_date && ( +
+ + {childIssue.target_date + ? renderShortNumericDateFormat(childIssue.target_date) + : "N/A"} + {childIssue.target_date && ( + + {childIssue.target_date < new Date().toISOString() + ? `Target date has passed by ${findHowManyDaysLeft( + childIssue.target_date + )} days` + : findHowManyDaysLeft(childIssue.target_date) <= 3 + ? `Target date is in ${findHowManyDaysLeft( + childIssue.target_date + )} days` + : "Target date"} + + )} +
+ )} + {properties.assignee && ( +
+ {childIssue?.assignee_details?.length > 0 ? ( + childIssue?.assignee_details?.map( + (assignee, index: number) => ( +
- - {childIssue.target_date - ? renderShortNumericDateFormat(childIssue.target_date) - : "N/A"} - {childIssue.target_date && ( - - {childIssue.target_date < new Date().toISOString() - ? `Target date has passed by ${findHowManyDaysLeft( - childIssue.target_date - )} days` - : findHowManyDaysLeft(childIssue.target_date) <= 3 - ? `Target date is in ${findHowManyDaysLeft( - childIssue.target_date - )} days` - : "Target date"} - + {assignee.avatar && assignee.avatar !== "" ? ( +
+ {assignee.name} +
+ ) : ( +
+ {assignee.first_name.charAt(0)} +
)} - - - )} - {key === "key" && ( - - {childIssue.project_detail?.identifier}- - {childIssue.sequence_id} - - )} - {key === "state" && ( - <>{addSpaceIfCamelCase(childIssue["state_detail"].name)} - )} - {key === "priority" && <>{childIssue.priority}} - {/* {key === "description" && <>{childIssue.description}} */} - {key === "assignee" ? ( -
- {childIssue?.assignee_details?.length > 0 ? ( - childIssue?.assignee_details?.map( - (assignee, index: number) => ( -
- {assignee.avatar && assignee.avatar !== "" ? ( -
- {assignee.name} -
- ) : ( -
- {assignee.first_name.charAt(0)} -
- )} -
- ) - ) - ) : ( - No assignee. - )} -
- ) : null} -
- ) - )} +
+ ) + ) + ) : ( + No assignee. + )} +
+ )} +
@@ -331,7 +304,7 @@ const SingleBoard: React.FC = ({ setIsIssueOpen(true); if (selectedGroup !== null) { setPreloadedData({ - state: stateId, + state: stateId !== null ? stateId : undefined, [selectedGroup]: groupTitle, actionType: "createIssue", }); diff --git a/apps/app/components/project/issues/BoardView/index.tsx b/apps/app/components/project/issues/BoardView/index.tsx index 010b9cbdc..4881f22ed 100644 --- a/apps/app/components/project/issues/BoardView/index.tsx +++ b/apps/app/components/project/issues/BoardView/index.tsx @@ -7,8 +7,8 @@ import useSWR from "swr"; import type { DropResult } from "react-beautiful-dnd"; import { DragDropContext } from "react-beautiful-dnd"; // services -import stateServices from "lib/services/state.services"; -import issuesServices from "lib/services/issues.services"; +import stateServices from "lib/services/state.service"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; // fetching keys @@ -20,7 +20,7 @@ import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssue // ui import { Spinner } from "ui"; // types -import type { IState, IIssue, Properties, NestedKeyOf, ProjectMember } from "types"; +import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types"; import ConfirmIssueDeletion from "../ConfirmIssueDeletion"; import { TrashIcon } from "@heroicons/react/24/outline"; @@ -30,7 +30,7 @@ type Props = { groupedByIssues: { [key: string]: IIssue[]; }; - members: ProjectMember[] | undefined; + members: IProjectMember[] | undefined; }; const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues, members }) => { @@ -197,9 +197,10 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues selectedGroup={selectedGroup} groupTitle={singleGroup} createdBy={ - members - ? members?.find((m) => m.member.id === singleGroup)?.member.first_name - : undefined + selectedGroup === "created_by" + ? members?.find((m) => m.member.id === singleGroup)?.member + .first_name ?? "loading..." + : null } groupedByIssues={groupedByIssues} index={index} @@ -208,8 +209,8 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues setPreloadedData={setPreloadedData} stateId={ selectedGroup === "state_detail.name" - ? states?.find((s) => s.name === singleGroup)?.id - : undefined + ? states?.find((s) => s.name === singleGroup)?.id ?? null + : null } bgColor={ selectedGroup === "state_detail.name" diff --git a/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx b/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx index b8754437a..448c50acc 100644 --- a/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx +++ b/apps/app/components/project/issues/BoardView/state/ConfirmStateDeletion.tsx @@ -4,7 +4,7 @@ import { mutate } from "swr"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // services -import stateServices from "lib/services/state.services"; +import stateServices from "lib/services/state.service"; // fetch api import { STATE_LIST } from "constants/fetch-keys"; // hooks @@ -43,7 +43,7 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { mutate( STATE_LIST(data.project), (prevData) => prevData?.filter((state) => state.id !== data?.id), - false, + false ); handleClose(); }) @@ -98,18 +98,15 @@ const ConfirmStateDeletion: React.FC = ({ isOpen, setIsOpen, data }) => { />
- + Delete State

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

diff --git a/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx b/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx index 9f3b3951b..3ba149820 100644 --- a/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx +++ b/apps/app/components/project/issues/BoardView/state/CreateUpdateStateModal.tsx @@ -8,7 +8,7 @@ import { TwitterPicker } from "react-color"; // headless import { Dialog, Popover, Transition } from "@headlessui/react"; // services -import stateService from "lib/services/state.services"; +import stateService from "lib/services/state.service"; // fetch keys import { STATE_LIST } from "constants/fetch-keys"; // hooks diff --git a/apps/app/components/project/issues/ConfirmIssueDeletion.tsx b/apps/app/components/project/issues/ConfirmIssueDeletion.tsx index 3b161ae6c..f1476c682 100644 --- a/apps/app/components/project/issues/ConfirmIssueDeletion.tsx +++ b/apps/app/components/project/issues/ConfirmIssueDeletion.tsx @@ -6,7 +6,7 @@ import { Dialog, Transition } from "@headlessui/react"; // fetching keys import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; // services -import issueServices from "lib/services/issues.services"; +import issueServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx index 376ab65e6..dc5d2b741 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectAssignee.tsx @@ -11,7 +11,7 @@ import useUser from "lib/hooks/useUser"; import { PROJECT_MEMBERS } from "constants/fetch-keys"; // types import type { Control } from "react-hook-form"; -import type { IIssue, WorkspaceMember } from "types"; +import type { IIssue } from "types"; import { UserIcon } from "@heroicons/react/24/outline"; import { SearchListbox } from "ui"; @@ -23,7 +23,7 @@ type Props = { const SelectAssignee: React.FC = ({ control }) => { const { activeWorkspace, activeProject } = useUser(); - const { data: people } = useSWR( + const { data: people } = useSWR( activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null, activeWorkspace && activeProject ? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id) diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx index a7ef75203..6fad13a44 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx @@ -6,7 +6,7 @@ import { useForm, Controller } from "react-hook-form"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; // fetching keys diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssue.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssue.tsx index e8f1648e8..d6b463ffb 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssue.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectParentIssue.tsx @@ -14,7 +14,7 @@ type Props = { control: Control; isOpen: boolean; setIsOpen: React.Dispatch>; - issues: IssueResponse | undefined; + issues: IIssue[]; }; const SelectParent: React.FC = ({ control, isOpen, setIsOpen, issues }) => { diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx index 1c0fc9d46..14032c6c6 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx @@ -16,7 +16,7 @@ import { // headless import { Dialog, Menu, Transition } from "@headlessui/react"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; @@ -392,16 +392,16 @@ const CreateUpdateIssuesModal: React.FC = ({ /> */}
- + /> */}
@@ -409,11 +409,25 @@ const CreateUpdateIssuesModal: React.FC = ({ + ( + { + onChange(e.target.value); + }} + className="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" + /> + )} + /> diff --git a/apps/app/components/project/issues/IssuesListModal.tsx b/apps/app/components/project/issues/IssuesListModal.tsx index 701da9beb..aba9a0c34 100644 --- a/apps/app/components/project/issues/IssuesListModal.tsx +++ b/apps/app/components/project/issues/IssuesListModal.tsx @@ -2,35 +2,49 @@ 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, IssueResponse } from "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: IssueResponse | undefined; + issues: IIssue[]; + title?: string; + multiple?: boolean; }; -const IssuesListModal: React.FC = ({ isOpen, handleClose: onClose, onChange, issues }) => { +const IssuesListModal: React.FC = ({ + isOpen, + handleClose: onClose, + value, + onChange, + issues, + title = "Issues", + multiple = false, +}) => { const [query, setQuery] = useState(""); + const [values, setValues] = useState([]); const { activeProject } = useUser(); const handleClose = () => { onClose(); setQuery(""); + setValues([]); }; 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 ( <> @@ -59,7 +73,14 @@ const IssuesListModal: React.FC = ({ isOpen, handleClose: onClose, onChan leaveTo="opacity-0 scale-95" > - + { + if (multiple) setValues(val); + else onChange(val); + }} + // multiple={multiple} + >
= ({ isOpen, handleClose: onClose, onChan
  • {query === "" && (

    - Issues + {title}

    )}
      @@ -95,20 +116,26 @@ const IssuesListModal: React.FC = ({ isOpen, handleClose: onClose, onChan ) } onClick={() => { - // setIssueIdFromList(issue.id); - handleClose(); + if (!multiple) handleClose(); }} > - - - {activeProject?.identifier}-{issue.sequence_id} - {" "} - {issue.name} + {({ selected }) => ( + <> + {multiple ? ( + + ) : null} + + + {activeProject?.identifier}-{issue.sequence_id} + {" "} + {issue.name} + + )} ))}
    @@ -128,6 +155,16 @@ const IssuesListModal: React.FC = ({ isOpen, handleClose: onClose, onChan
  • )}
    + {multiple ? ( +
    + + +
    + ) : null}
    diff --git a/apps/app/components/project/issues/ListView/index.tsx b/apps/app/components/project/issues/ListView/index.tsx index 5bbe2ea84..78d5ab68b 100644 --- a/apps/app/components/project/issues/ListView/index.tsx +++ b/apps/app/components/project/issues/ListView/index.tsx @@ -1,5 +1,5 @@ // react -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; // next import Link from "next/link"; import Image from "next/image"; @@ -10,23 +10,17 @@ import { Listbox, Transition } from "@headlessui/react"; // icons import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; // types -import { IIssue, IssueResponse, IState, NestedKeyOf, Properties, WorkspaceMember } from "types"; +import { IIssue, IssueResponse, NestedKeyOf, Properties } from "types"; // hooks import useUser from "lib/hooks/useUser"; // fetch keys import { PRIORITIES } from "constants/"; import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // services -import issuesServices from "lib/services/issues.services"; +import issuesServices from "lib/services/issues.service"; import workspaceService from "lib/services/workspace.service"; // constants -import { - addSpaceIfCamelCase, - classNames, - renderShortNumericDateFormat, - replaceUnderscoreIfSnakeCase, -} from "constants/common"; -import IssuePreviewModal from "../PreviewModal"; +import { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common"; // types type Props = { @@ -44,9 +38,6 @@ const ListView: React.FC = ({ setSelectedIssue, handleDeleteIssue, }) => { - const [issuePreviewModal, setIssuePreviewModal] = useState(false); - const [previewModalIssueId, setPreviewModalIssueId] = useState(null); - const { activeWorkspace, activeProject, states } = useUser(); const partialUpdateIssue = (formData: Partial, issueId: string) => { @@ -69,375 +60,355 @@ const ListView: React.FC = ({ }); }; - const { data: people } = useSWR( + const { data: people } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null ); - const handleHover = (issueId: string) => { - document.addEventListener("keydown", (e) => { - // if (e.code === "Space") { - // e.preventDefault(); - // setPreviewModalIssueId(issueId); - // setIssuePreviewModal(true); - // } - }); - }; - return ( -
    - -
    -
    -
    - - - - - {Object.keys(properties).map( - (key) => - properties[key as keyof Properties] && ( - - ) - )} - - - - - {Object.keys(groupedByIssues).map((singleGroup) => ( - - {selectedGroup !== null ? ( - - + ) : ( + + )} + + ) + )} + + + ); + }) + : null} + +
    - NAME - - {replaceUnderscoreIfSnakeCase(key)} - - ACTIONS -
    +
    + {Object.keys(groupedByIssues).map((singleGroup) => ( +
    +
    +
    + + {selectedGroup !== null ? ( + + + - - ) : null} - {groupedByIssues[singleGroup].length > 0 - ? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => { - const assignees = [ - ...(issue?.assignees_list ?? []), - ...(issue?.assignees ?? []), - ]?.map( - (assignee) => - people?.find((p) => p.member.id === assignee)?.member.email - ); + + + + + ) : ( + + + + + + )} + + {groupedByIssues[singleGroup].length > 0 + ? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => { + const assignees = [ + ...(issue?.assignees_list ?? []), + ...(issue?.assignees ?? []), + ]?.map( + (assignee) => people?.find((p) => p.member.id === assignee)?.member.email + ); - return ( - handleHover(issue.id)} - > - - {Object.keys(properties).map( - (key) => - properties[key as keyof Properties] && ( - - {(key as keyof Properties) === "key" ? ( - - ) : (key as keyof Properties) === "priority" ? ( - + {Object.keys(properties).map( + (key) => + properties[key as keyof Properties] && ( + + {(key as keyof Properties) === "key" ? ( + + ) : (key as keyof Properties) === "priority" ? ( + + ) : (key as keyof Properties) === "assignee" ? ( + - ) : (key as keyof Properties) === "assignee" ? ( - + ) : (key as keyof Properties) === "state" ? ( + - ) : (key as keyof Properties) === "state" ? ( - - ) : (key as keyof Properties) === "target_date" ? ( - - ) : ( - - )} - - ) - )} - - - ); - }) - : null} - - ))} - -
    +
    + {selectedGroup === "state_detail.name" ? ( + s.name === singleGroup)?.color, + }} + > + ) : null} {singleGroup === null || singleGroup === "null" ? selectedGroup === "priority" && "No priority" : addSpaceIfCamelCase(singleGroup)} {groupedByIssues[singleGroup as keyof IIssue].length} -
    + ALL ISSUES + + {groupedByIssues[singleGroup as keyof IIssue].length} + +
    - - {issue.name} - - - {activeProject?.identifier}-{issue.sequence_id} - - { - partialUpdateIssue({ priority: data }, issue.id); - }} - className="flex-shrink-0" - > - {({ open }) => ( - <> -
    - - - {issue.priority ?? "None"} - - - - +
    + + {issue.name} + + + {activeProject?.identifier}-{issue.sequence_id} + + { + partialUpdateIssue({ priority: data }, issue.id); + }} + className="flex-shrink-0" + > + {({ open }) => ( + <> +
    + + - - {PRIORITIES?.map((priority) => ( - - classNames( - active ? "bg-indigo-50" : "bg-white", - "cursor-pointer capitalize select-none px-3 py-2" + {issue.priority ?? "None"} + + + + + + {PRIORITIES?.map((priority) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer capitalize select-none px-3 py-2" + ) + } + value={priority} + > + {priority} + + ))} + + +
    + + )} +
    +
    + { + const newData = issue.assignees ?? []; + if (newData.includes(data)) { + newData.splice(newData.indexOf(data), 1); + } else { + newData.push(data); + } + partialUpdateIssue( + { assignees_list: newData }, + issue.id + ); + }} + className="flex-shrink-0" + > + {({ open }) => ( + <> +
    + + {() => { + if (assignees.length > 0) + return ( + <> + {assignees.map((assignee, index) => ( +
    + {assignee} +
    + ))} + + ); + else return None; + }} +
    + + + + {people?.map((person) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer select-none px-3 py-2" + ) + } + value={person.member.id} + > +
    - {priority} - - ))} - - -
    - - )} - -
    - { - const newData = issue.assignees ?? []; - if (newData.includes(data)) { - newData.splice(newData.indexOf(data), 1); - } else { - newData.push(data); - } - partialUpdateIssue( - { assignees_list: newData }, - issue.id - ); - }} - className="flex-shrink-0" - > - {({ open }) => ( - <> -
    - - {() => { - if (assignees.length > 0) - return ( - <> - {assignees.map((assignee, index) => ( -
    - {assignee} -
    - ))} - - ); - else return None; - }} -
    + {person.member.avatar && + person.member.avatar !== "" ? ( +
    + avatar +
    + ) : ( +

    + {person.member.first_name.charAt(0)} +

    + )} +

    {person.member.first_name}

    +
    + + ))} + + + + + )} +
    +
    + { + partialUpdateIssue({ state: data }, issue.id); + }} + className="flex-shrink-0" + > + {({ open }) => ( + <> +
    + + + {addSpaceIfCamelCase(issue.state_detail.name)} + + - - - {people?.map((person) => ( - - classNames( - active ? "bg-indigo-50" : "bg-white", - "cursor-pointer select-none px-3 py-2" - ) - } - value={person.member.id} - > -
    - {person.member.avatar && - person.member.avatar !== "" ? ( -
    - avatar -
    - ) : ( -

    - {person.member.first_name.charAt(0)} -

    - )} -

    {person.member.first_name}

    -
    -
    - ))} -
    -
    -
    - - )} -
    -
    - { - partialUpdateIssue({ state: data }, issue.id); - }} - className="flex-shrink-0" - > - {({ open }) => ( - <> -
    - - - {addSpaceIfCamelCase(issue.state_detail.name)} - - - - - - {states?.map((state) => ( - - classNames( - active ? "bg-indigo-50" : "bg-white", - "cursor-pointer select-none px-3 py-2" - ) - } - value={state.id} - > - {addSpaceIfCamelCase(state.name)} - - ))} - - -
    - - )} -
    -
    - {issue.target_date - ? renderShortNumericDateFormat(issue.target_date) - : "-"} - - {issue[key as keyof IIssue] ?? - (issue[key as keyof IIssue] as any)?.name ?? - "None"} - -
    - - -
    -
    + + + {states?.map((state) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer select-none px-3 py-2" + ) + } + value={state.id} + > + {addSpaceIfCamelCase(state.name)} + + ))} + + +
    + + )} + + + ) : (key as keyof Properties) === "target_date" ? ( +
    + {issue.target_date + ? renderShortNumericDateFormat(issue.target_date) + : "-"} + + {issue[key as keyof IIssue] ?? + (issue[key as keyof IIssue] as any)?.name ?? + "None"} + +
    + + +
    +
    +
    -
    + ))} ); }; -export default ListView; \ No newline at end of file +export default ListView; diff --git a/apps/app/components/project/issues/PreviewModal/index.tsx b/apps/app/components/project/issues/PreviewModal/index.tsx deleted file mode 100644 index b1c9b8e0a..000000000 --- a/apps/app/components/project/issues/PreviewModal/index.tsx +++ /dev/null @@ -1,138 +0,0 @@ -// next -import { useRouter } from "next/router"; -// react -import { Fragment } from "react"; -// headless ui -import { Dialog, Transition } from "@headlessui/react"; -// hooks -import useUser from "lib/hooks/useUser"; -// services -import issuesServices from "lib/services/issues.services"; -import projectService from "lib/services/project.service"; -// swr -import useSWR from "swr"; -// types -import { IIssue, ProjectMember } from "types"; -// constants -import { PROJECT_ISSUES_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys"; -import { Button } from "ui"; -import { ChartBarIcon, Squares2X2Icon, TagIcon, UserIcon } from "@heroicons/react/24/outline"; - -type Props = { - isOpen: boolean; - setIsOpen: React.Dispatch>; - issueId: string | null; -}; - -const IssuePreviewModal = ({ isOpen, setIsOpen, issueId }: Props) => { - const closeModal = () => { - setIsOpen(false); - }; - - const { activeWorkspace, activeProject } = useUser(); - - const router = useRouter(); - - const { data: issueDetails } = useSWR( - activeWorkspace && activeProject && issueId ? PROJECT_ISSUES_DETAILS(issueId) : null, - activeWorkspace && activeProject && issueId - ? () => issuesServices.getIssue(activeWorkspace.slug, activeProject.id, issueId) - : null - ); - - const { data: users } = useSWR( - activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null, - activeWorkspace && activeProject - ? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id) - : null - ); - - return ( - <> - - - -
    - - -
    -
    - - - - {issueDetails?.project_detail.identifier}-{issueDetails?.sequence_id}{" "} - {issueDetails?.name} - - Created by{" "} - {users?.find((u) => u.id === issueDetails?.created_by)?.member.first_name} - - -
    -

    {issueDetails?.description}

    -
    -
    - - - {issueDetails?.state_detail.name} - - - - {issueDetails?.priority} - - - - {issueDetails?.label_details && issueDetails.label_details.length > 0 - ? issueDetails.label_details.map((label) => ( - {label.name} - )) - : "None"} - - - - {issueDetails?.assignee_details && issueDetails.assignee_details.length > 0 - ? issueDetails.assignee_details.map((assignee) => ( - {assignee.first_name} - )) - : "None"} - -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - - ); -}; - -export default IssuePreviewModal; diff --git a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx index 68f2f91eb..71c84a745 100644 --- a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx +++ b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx @@ -4,13 +4,14 @@ import useSWR from "swr"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // react hook form -import { useForm, Controller } from "react-hook-form"; +import { useForm, Controller, UseFormWatch } from "react-hook-form"; // services -import stateServices from "lib/services/state.services"; -import issuesServices from "lib/services/issues.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"; // fetching keys import { PROJECT_ISSUES_LIST, @@ -20,6 +21,7 @@ import { } from "constants/fetch-keys"; // commons import { classNames, copyTextToClipboard } from "constants/common"; +import { PRIORITIES } from "constants/"; // ui import { Input, Button, Spinner } from "ui"; import { Popover } from "@headlessui/react"; @@ -38,25 +40,33 @@ import { } from "@heroicons/react/24/outline"; // types import type { Control } from "react-hook-form"; -import type { IIssue, IIssueLabels, IssueResponse, IState, WorkspaceMember } from "types"; +import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types"; import { TwitterPicker } from "react-color"; -import useToast from "lib/hooks/useToast"; +import IssuesListModal from "components/project/issues/IssuesListModal"; type Props = { control: Control; submitChanges: (formData: Partial) => void; issueDetail: IIssue | undefined; + watch: UseFormWatch; }; -const PRIORITIES = ["high", "medium", "low"]; - const defaultValues: Partial = { name: "", colour: "#ff0000", }; -const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDetail }) => { - const { activeWorkspace, activeProject, cycles } = useUser(); +const IssueDetailSidebar: React.FC = ({ + control, + watch: watchIssue, + submitChanges, + issueDetail, +}) => { + const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); + const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); + const [isParentModalOpen, setIsParentModalOpen] = useState(false); + + const { activeWorkspace, activeProject, cycles, issues } = useUser(); const { setToastAlert } = useToast(); @@ -67,20 +77,11 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta : null ); - const { data: people } = useSWR( - activeWorkspace ? WORKSPACE_MEMBERS : null, + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null ); - const { data: projectIssues } = useSWR( - activeProject && activeWorkspace - ? PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id) - : null, - activeProject && activeWorkspace - ? () => issuesServices.getIssues(activeWorkspace.slug, activeProject.id) - : null - ); - const { data: issueLabels, mutate: issueLabelMutate } = useSWR( activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null, activeProject && activeWorkspace @@ -110,7 +111,19 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta }); }; - const sidebarSections = [ + const sidebarSections: Array< + Array<{ + label: string; + name: NestedKeyOf; + canSelectMultipleOptions: boolean; + icon: (props: any) => JSX.Element; + options?: Array<{ label: string; value: any }>; + modal: boolean; + issuesList?: Array; + isOpen?: boolean; + setIsOpen?: (arg: boolean) => void; + }> + > = [ [ { label: "Status", @@ -121,6 +134,7 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta label: state.name, value: state.id, })), + modal: false, }, { label: "Assignees", @@ -131,6 +145,7 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta label: person.member.first_name, value: person.member.id, })), + modal: false, }, { label: "Priority", @@ -141,34 +156,52 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta label: property, value: property, })), + modal: false, }, ], [ { - label: "Blocker", - name: "blockers_list", - canSelectMultipleOptions: true, + label: "Parent", + name: "parent", + canSelectMultipleOptions: false, icon: UserIcon, - options: projectIssues?.results?.map((issue) => ({ - label: issue.name, - value: issue.id, - })), + issuesList: + issues?.results.filter( + (i) => + i.id !== issueDetail?.id && + i.id !== issueDetail?.parent && + i.parent !== issueDetail?.id + ) ?? [], + modal: true, + isOpen: isParentModalOpen, + setIsOpen: setIsParentModalOpen, }, + // { + // 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, + // }, { - label: "Blocked", - name: "blocked_list", - canSelectMultipleOptions: true, - icon: UserIcon, - options: projectIssues?.results?.map((issue) => ({ - label: issue.name, - value: issue.id, - })), - }, - { - label: "Due Date", + label: "Target Date", name: "target_date", canSelectMultipleOptions: true, icon: CalendarDaysIcon, + modal: false, }, ], [ @@ -181,6 +214,7 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta label: cycle.name, value: cycle.id, })), + modal: false, }, ], ]; @@ -249,12 +283,12 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta {sidebarSections.map((section, index) => (
    {section.map((item) => ( -
    -
    - +
    +
    +

    {item.label}

    -
    +
    {item.name === "target_date" ? ( = ({ control, submitChanges, issueDeta render={({ field: { value, onChange } }) => ( { submitChanges({ target_date: e.target.value }); onChange(e.target.value); }} - className="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" + className="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" /> )} /> + ) : item.modal ? ( + ( + <> + 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} + /> + + + )} + /> ) : ( = ({ control, submitChanges, issueDeta > {({ open }) => (
    - + @@ -419,12 +490,12 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta + -
    -
    +
    +

    Label

    -
    +
    = ({ control, submitChanges, issueDeta <> Label
    - + {value && value.length > 0 diff --git a/apps/app/components/project/issues/issue-detail/comment/IssueCommentCard.tsx b/apps/app/components/project/issues/issue-detail/comment/IssueCommentCard.tsx index 26ea274a1..e911ae306 100644 --- a/apps/app/components/project/issues/issue-detail/comment/IssueCommentCard.tsx +++ b/apps/app/components/project/issues/issue-detail/comment/IssueCommentCard.tsx @@ -64,14 +64,14 @@ const CommentCard: React.FC = ({ comment, onSubmit, handleCommentDeletion
    -
    +
    {comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? ( {comment.actor_detail.name} ) : (
    = ({ comment, onSubmit, handleCommentDeletion )}
    -

    - {comment.actor_detail.first_name} {comment.actor_detail.last_name} -

    -

    {timeAgo(comment.created_at)}

    -
    +
    {isEditing ? (