diff --git a/apps/app/components/forms/EmailPasswordForm.tsx b/apps/app/components/forms/EmailPasswordForm.tsx index a73c8b040..384b77598 100644 --- a/apps/app/components/forms/EmailPasswordForm.tsx +++ b/apps/app/components/forms/EmailPasswordForm.tsx @@ -88,9 +88,7 @@ const EmailPasswordForm = ({ onSuccess }: any) => {
- - Forgot your password? - + Forgot your password?
diff --git a/apps/app/components/lexical/editor.tsx b/apps/app/components/lexical/editor.tsx index 510c03e94..71fbb03cf 100644 --- a/apps/app/components/lexical/editor.tsx +++ b/apps/app/components/lexical/editor.tsx @@ -27,7 +27,7 @@ import { getValidatedValue } from "./helpers/editor"; import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; export interface RichTextEditorProps { - onChange: (state: SerializedEditorState) => void; + onChange: (state: string) => void; id: string; value: string; placeholder?: string; @@ -41,8 +41,7 @@ const RichTextEditor: React.FC = ({ }) => { const handleChange = (editorState: EditorState) => { editorState.read(() => { - let editorData = editorState.toJSON(); - if (onChange) onChange(editorData); + onChange(JSON.stringify(editorState.toJSON())); }); }; diff --git a/apps/app/components/lexical/helpers/editor.ts b/apps/app/components/lexical/helpers/editor.ts index 702269cd5..56f2ffe20 100644 --- a/apps/app/components/lexical/helpers/editor.ts +++ b/apps/app/components/lexical/helpers/editor.ts @@ -18,10 +18,12 @@ export const getValidatedValue = (value: string) => { const defaultValue = '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'; + console.log("Value: ", value); + if (value) { try { - console.log(value); - return value; + const data = JSON.parse(value); + return JSON.stringify(data); } catch (e) { return defaultValue; } diff --git a/apps/app/components/project/SendProjectInvitationModal.tsx b/apps/app/components/project/SendProjectInvitationModal.tsx index 6227c569f..ef77b65f9 100644 --- a/apps/app/components/project/SendProjectInvitationModal.tsx +++ b/apps/app/components/project/SendProjectInvitationModal.tsx @@ -216,7 +216,7 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member {selected ? ( = ({ isOpen, setIsOpen }) => { const projectName = watch("name") ?? ""; const projectIdentifier = watch("identifier") ?? ""; - 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 - ); - - if ((!isMember || isGuest) && isOpen) return ; - } - useEffect(() => { if (projectName && isChangeIdentifierRequired) { setValue("identifier", projectName.replace(/ /g, "").toUpperCase().substring(0, 3)); @@ -157,12 +148,21 @@ const CreateProjectModal: React.FC = ({ isOpen, setIsOpen }) => { projectIdentifier.toUpperCase().substring(0, 3) + Math.floor(Math.random() * 101), projectIdentifier.toUpperCase().substring(0, 3) + Math.floor(Math.random() * 101), ]); - }, [errors.identifier]); + }, [errors.identifier, projectIdentifier, projectName]); useEffect(() => { return () => setIsChangeIdentifierRequired(true); }, [isOpen]); + 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 + ); + + if ((!isMember || isGuest) && isOpen) return ; + } + return ( diff --git a/apps/app/components/project/cycles/BoardView/index.tsx b/apps/app/components/project/cycles/BoardView/index.tsx new file mode 100644 index 000000000..8026786c4 --- /dev/null +++ b/apps/app/components/project/cycles/BoardView/index.tsx @@ -0,0 +1,79 @@ +// components +import SingleBoard from "components/project/cycles/BoardView/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: ( + sprintId: string, + issue?: IIssue, + actionType?: "create" | "edit" | "delete" + ) => void; + openIssuesListModal: (cycleId: string) => void; + removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; +}; + +const CyclesBoardView: React.FC = ({ + groupedByIssues, + properties, + selectedGroup, + members, + openCreateIssueModal, + openIssuesListModal, + removeIssueFromCycle, +}) => { + 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} + removeIssueFromCycle={removeIssueFromCycle} + openIssuesListModal={openIssuesListModal} + openCreateIssueModal={openCreateIssueModal} + /> + ))} +
+
+
+
+ ) : ( +
+ +
+ )} + + ); +}; + +export default CyclesBoardView; diff --git a/apps/app/components/project/cycles/BoardView/single-board.tsx b/apps/app/components/project/cycles/BoardView/single-board.tsx new file mode 100644 index 000000000..92ff37110 --- /dev/null +++ b/apps/app/components/project/cycles/BoardView/single-board.tsx @@ -0,0 +1,662 @@ +// react +import React, { useState } from "react"; +// next +import Link from "next/link"; +import Image from "next/image"; +// swr +import useSWR from "swr"; +// services +import cycleServices from "lib/services/cycles.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// ui +import { Spinner } from "ui"; +// icons +import { + ArrowsPointingInIcon, + ArrowsPointingOutIcon, + CalendarDaysIcon, + PlusIcon, + EllipsisHorizontalIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; +import User from "public/user.png"; +// types +import { + CycleIssueResponse, + ICycle, + IIssue, + IWorkspaceMember, + NestedKeyOf, + Properties, +} from "types"; +// constants +import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys"; +import { + addSpaceIfCamelCase, + findHowManyDaysLeft, + renderShortNumericDateFormat, +} from "constants/common"; +import { Menu, Transition } from "@headlessui/react"; +import workspaceService from "lib/services/workspace.service"; + +type Props = { + properties: Properties; + groupedByIssues: { + [key: string]: IIssue[]; + }; + selectedGroup: NestedKeyOf | null; + groupTitle: string; + createdBy: string | null; + bgColor?: string; + openCreateIssueModal: ( + sprintId: string, + issue?: IIssue, + actionType?: "create" | "edit" | "delete" + ) => void; + openIssuesListModal: (cycleId: string) => void; + removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; +}; + +const SingleCycleBoard: React.FC = ({ + properties, + groupedByIssues, + selectedGroup, + groupTitle, + createdBy, + bgColor, + openCreateIssueModal, + openIssuesListModal, + removeIssueFromCycle, +}) => { + // Collapse/Expand + const [show, setState] = useState(true); + + const { activeWorkspace, activeProject } = 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} + +
+
+
+
+ {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 ( +
+
+
+ +
+ + + {properties.key && ( +
+ {activeProject?.identifier}-{childIssue.sequence_id} +
+ )} +
+ {childIssue.name} +
+
+ +
+ {properties.priority && ( +
+ {/* {getPriorityIcon(childIssue.priority ?? "")} */} + {childIssue.priority ?? "None"} +
+
Priority
+
+ {childIssue.priority ?? "None"} +
+
+
+ )} + {properties.state && ( +
+ + {addSpaceIfCamelCase(childIssue.state_detail.name)} +
+
State
+
{childIssue.state_detail.name}
+
+
+ )} + {properties.start_date && ( +
+ + {childIssue.start_date + ? renderShortNumericDateFormat(childIssue.start_date) + : "N/A"} +
+
Started at
+
{renderShortNumericDateFormat(childIssue.start_date ?? "")}
+
+
+ )} + {properties.target_date && ( +
+ + {childIssue.target_date + ? renderShortNumericDateFormat(childIssue.target_date) + : "N/A"} +
+
Target date
+
{renderShortNumericDateFormat(childIssue.target_date ?? "")}
+
+ {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) => ( +
+ {assignee.avatar && assignee.avatar !== "" ? ( +
+ {assignee.name} +
+ ) : ( +
+ {assignee.first_name.charAt(0)} +
+ )} +
+ )) + ) : ( +
+ No user +
+ )} +
+
Assigned to
+
+ {childIssue.assignee_details?.length > 0 + ? childIssue.assignee_details + .map((assignee) => assignee.first_name) + .join(", ") + : "No one"} +
+
+
+ )} +
+
+
+ ); + })} + +
+
+
+ ); + + // return ( + //
+ //
+ //
+ //
+ //
+ //

+ // {cycle.name} + //

+ // {cycleIssues?.length} + //
+ //
+ + //
+ // + // + // + // + // + + // + // + //
+ // + // {(active) => ( + // + // )} + // + // + // {(active) => ( + // + // )} + // + //
+ //
+ //
+ //
+ //
+ //
+ //
+ // {cycleIssues ? ( + // cycleIssues.map((issue, index: number) => ( + //
+ //
+ //
+ // + // + // + // + // + // + //
+ // + //
+ //
+ // + //
+ // + //
+ //
+ //
+ //
+ //
+ // + // + // {properties.key && ( + //
+ // {activeProject?.identifier}-{childIssue.sequence_id} + //
+ // )} + //
+ // {childIssue.name} + //
+ //
+ // + //
+ // {properties.priority && ( + //
+ // {/* {getPriorityIcon(childIssue.priority ?? "")} */} + // {childIssue.priority ?? "None"} + //
+ //
Priority
+ //
+ // {childIssue.priority ?? "None"} + //
+ //
+ //
+ // )} + // {properties.state && ( + //
+ // + // {addSpaceIfCamelCase(childIssue.state_detail.name)} + //
+ //
State
+ //
{childIssue.state_detail.name}
+ //
+ //
+ // )} + // {properties.start_date && ( + //
+ // + // {childIssue.start_date + // ? renderShortNumericDateFormat(childIssue.start_date) + // : "N/A"} + //
+ //
Started at
+ //
+ // {renderShortNumericDateFormat(childIssue.start_date ?? "")} + //
+ //
+ //
+ // )} + // {properties.target_date && ( + //
+ // + // {childIssue.target_date + // ? renderShortNumericDateFormat(childIssue.target_date) + // : "N/A"} + //
+ //
Target date
+ //
+ // {renderShortNumericDateFormat(childIssue.target_date ?? "")} + //
+ //
+ // {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) => ( + //
+ // {assignee.avatar && assignee.avatar !== "" ? ( + //
+ // {assignee.name} + //
+ // ) : ( + //
+ // {assignee.first_name.charAt(0)} + //
+ // )} + //
+ // )) + // ) : ( + //
+ // No user + //
+ // )} + //
+ //
Assigned to
+ //
+ // {childIssue.assignee_details?.length > 0 + // ? childIssue.assignee_details + // .map((assignee) => assignee.first_name) + // .join(", ") + // : "No one"} + //
+ //
+ //
+ // )} + //
+ //
+ //
+ // )) + // ) : ( + //
+ // + //
+ // )} + // + //
+ //
+ //
+ // ); +}; + +export default SingleCycleBoard; diff --git a/apps/app/components/project/cycles/CycleIssuesListModal.tsx b/apps/app/components/project/cycles/CycleIssuesListModal.tsx index d25d069df..d9f3226c0 100644 --- a/apps/app/components/project/cycles/CycleIssuesListModal.tsx +++ b/apps/app/components/project/cycles/CycleIssuesListModal.tsx @@ -47,7 +47,12 @@ const CycleIssuesListModal: React.FC = ({ reset(); }; - const { handleSubmit, reset, control } = useForm({ + const { + handleSubmit, + reset, + control, + formState: { isSubmitting }, + } = useForm({ defaultValues: { issue_ids: [], }, @@ -68,6 +73,7 @@ const CycleIssuesListModal: React.FC = ({ .bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, cycleId, data) .then((res) => { console.log(res); + handleClose(); }) .catch((e) => { console.log(e); @@ -138,36 +144,39 @@ const CycleIssuesListModal: React.FC = ({ )}
    - {filteredIssues.map((issue) => ( - - classNames( - "flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2", - active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" - ) - } - > - {({ selected }) => ( - <> - - - - {activeProject?.identifier}-{issue.sequence_id} - - {issue.name} - - )} - - ))} + {filteredIssues.map((issue) => { + // if (issue.cycle !== cycleId) + return ( + + classNames( + "flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + > + {({ selected }) => ( + <> + + + + {activeProject?.identifier}-{issue.sequence_id} + + {issue.name} + + )} + + ); + })}
)} @@ -191,8 +200,13 @@ const CycleIssuesListModal: React.FC = ({ - diff --git a/apps/app/components/project/cycles/CycleView.tsx b/apps/app/components/project/cycles/CycleView.tsx deleted file mode 100644 index 73a0d0af4..000000000 --- a/apps/app/components/project/cycles/CycleView.tsx +++ /dev/null @@ -1,314 +0,0 @@ -// react -import React, { useState } from "react"; -// next -import Link from "next/link"; -// swr -import useSWR, { mutate } from "swr"; -// headless ui -import { Disclosure, Transition, Menu } from "@headlessui/react"; -// services -import cycleServices from "lib/services/cycles.service"; -// hooks -import useUser from "lib/hooks/useUser"; -// components -import CycleIssuesListModal from "./CycleIssuesListModal"; -// ui -import { Spinner } from "ui"; -// icons -import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; -// types -import type { CycleViewProps as Props, CycleIssueResponse, IssueResponse } from "types"; -// fetch keys -import { CYCLE_ISSUES } from "constants/fetch-keys"; -// constants -import { renderShortNumericDateFormat } from "constants/common"; -import issuesServices from "lib/services/issues.service"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; -import { Draggable } from "react-beautiful-dnd"; - -const CycleView: React.FC = ({ - cycle, - selectSprint, - workspaceSlug, - projectId, - openIssueModal, -}) => { - const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); - - const { activeWorkspace, activeProject, issues } = useUser(); - - const { data: cycleIssues } = useSWR(CYCLE_ISSUES(cycle.id), () => - cycleServices.getCycleIssues(workspaceSlug, projectId, cycle.id) - ); - - const removeIssueFromCycle = (cycleId: string, bridgeId: string) => { - if (activeWorkspace && activeProject && cycleIssues) { - mutate( - CYCLE_ISSUES(cycleId), - (prevData) => prevData?.filter((p) => p.id !== bridgeId), - false - ); - - issuesServices - .removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId, bridgeId) - .then((res) => { - console.log(res); - }) - .catch((e) => { - console.log(e); - }); - } - }; - - return ( - <> - setCycleIssuesListModal(false)} - issues={issues} - cycleId={cycle.id} - /> - - {({ open }) => ( -
-
- -
- - - -

{cycle.name}

-

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

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

This cycle has no issue.

- ) - ) : ( -
- -
- )} - {provided.placeholder} -
- )} -
-
-
- - - - Add issue - - - - -
- - {(active) => ( - - )} - - - {(active) => ( - - )} - -
-
-
-
-
- )} -
- - ); -}; - -export default CycleView; diff --git a/apps/app/components/project/cycles/ListView/index.tsx b/apps/app/components/project/cycles/ListView/index.tsx new file mode 100644 index 000000000..5add9687a --- /dev/null +++ b/apps/app/components/project/cycles/ListView/index.tsx @@ -0,0 +1,714 @@ +// react +import React from "react"; +// next +import Link from "next/link"; +// swr +import useSWR from "swr"; +// headless ui +import { Disclosure, Transition, Menu } from "@headlessui/react"; +// services +import cycleServices from "lib/services/cycles.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// ui +import { Spinner } from "ui"; +// icons +import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import { CalendarDaysIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, SelectSprintType } from "types"; +// fetch keys +import { CYCLE_ISSUES, 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[]; + }; + properties: Properties; + selectedGroup: NestedKeyOf | null; + openCreateIssueModal: ( + sprintId: string, + issue?: IIssue, + actionType?: "create" | "edit" | "delete" + ) => void; + openIssuesListModal: (cycleId: string) => void; + removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; +}; + +const CyclesListView: React.FC = ({ + groupedByIssues, + selectedGroup, + openCreateIssueModal, + openIssuesListModal, + properties, + removeIssueFromCycle, +}) => { + const { activeWorkspace, activeProject } = useUser(); + + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + + return ( +
+ {Object.keys(groupedByIssues).map((singleGroup) => ( + + {({ 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: IIssue) => { + 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.target_date && ( +
+ + {issue.target_date + ? renderShortNumericDateFormat(issue.target_date) + : "N/A"} +
+
+ Target date +
+
+ {renderShortNumericDateFormat(issue.target_date ?? "")} +
+
+ {issue.target_date && + (issue.target_date < new Date().toISOString() + ? `Target date has passed by ${findHowManyDaysLeft( + issue.target_date + )} days` + : findHowManyDaysLeft(issue.target_date) <= 3 + ? `Target date is in ${findHowManyDaysLeft( + issue.target_date + )} days` + : "Target date")} +
+
+
+ )} + + + + + + + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+ ); + }) + ) : ( +

No issues.

+ ) + ) : ( +
+ +
+ )} +
+
+
+
+ +
+
+ )} +
+ ))} +
+ ); + + // return ( + // <> + // + // {({ open }) => ( + //
+ //
+ // + + // + // + // + // + // + // + // + // + // + // + // + // + // + //
+ // + // + // + // {(provided) => ( + //
+ // {cycleIssues ? ( + // cycleIssues.length > 0 ? ( + // cycleIssues.map((issue, index) => ( + // + // {(provided, snapshot) => ( + //
+ // + //
+ // {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.target_date && ( + //
+ // + // {issue.target_date + // ? renderShortNumericDateFormat( + // issue.target_date + // ) + // : "N/A"} + //
+ //
+ // Target date + //
+ //
+ // {renderShortNumericDateFormat( + // issue.target_date ?? "" + // )} + //
+ //
+ // {issue.target_date && + // (issue.target_date < + // new Date().toISOString() + // ? `Target date has passed by ${findHowManyDaysLeft( + // issue.target_date + // )} days` + // : findHowManyDaysLeft( + // issue.target_date + // ) <= 3 + // ? `Target date is in ${findHowManyDaysLeft( + // issue.target_date + // )} days` + // : "Target date")} + //
+ //
+ //
+ // )} + // + // + // + // + // + // + // + // + // + //
+ // + //
+ //
+ // + //
+ // + //
+ //
+ //
+ //
+ //
+ //
+ // )} + //
+ // )) + // ) : ( + //

+ // This cycle has no issue. + //

+ // ) + // ) : ( + //
+ // + //
+ // )} + // {provided.placeholder} + //
+ // )} + //
+ //
+ //
+ //
+ // + // + // + // Add issue + // + + // + // + //
+ // + // {(active) => ( + // + // )} + // + // + // {(active) => ( + // + // )} + // + //
+ //
+ //
+ //
+ //
+ //
+ // )} + //
+ // + // ); +}; + +export default CyclesListView; diff --git a/apps/app/components/project/issues/BoardView/SingleBoard.tsx b/apps/app/components/project/issues/BoardView/SingleBoard.tsx deleted file mode 100644 index 2c0426e93..000000000 --- a/apps/app/components/project/issues/BoardView/SingleBoard.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import React, { useState } from "react"; -// Next imports -import Link from "next/link"; -// React beautiful dnd -import { Draggable } from "react-beautiful-dnd"; -import StrictModeDroppable from "components/dnd/StrictModeDroppable"; -// common -import { - addSpaceIfCamelCase, - findHowManyDaysLeft, - renderShortNumericDateFormat, -} from "constants/common"; -// types -import { IIssue, Properties, NestedKeyOf } from "types"; -// icons -import { - ArrowsPointingInIcon, - ArrowsPointingOutIcon, - CalendarDaysIcon, - EllipsisHorizontalIcon, - PlusIcon, -} from "@heroicons/react/24/outline"; -import Image from "next/image"; -import { getPriorityIcon } from "constants/global"; - -type Props = { - selectedGroup: NestedKeyOf | null; - groupTitle: string; - groupedByIssues: { - [key: string]: IIssue[]; - }; - index: number; - setIsIssueOpen: React.Dispatch>; - properties: Properties; - setPreloadedData: React.Dispatch< - React.SetStateAction< - | (Partial & { - actionType: "createIssue" | "edit" | "delete"; - }) - | undefined - > - >; - bgColor?: string; - stateId: string | null; - createdBy: string | null; -}; - -const SingleBoard: React.FC = ({ - selectedGroup, - groupTitle, - groupedByIssues, - index, - setIsIssueOpen, - properties, - setPreloadedData, - bgColor = "#0f2b16", - stateId, - createdBy, -}) => { - // Collapse/Expand - const [show, setState] = useState(true); - - if (selectedGroup === "priority") - groupTitle === "high" - ? (bgColor = "#dc2626") - : groupTitle === "medium" - ? (bgColor = "#f97316") - : groupTitle === "low" - ? (bgColor = "#22c55e") - : (bgColor = "#ff0000"); - - return ( - - {(provided, snapshot) => ( -
-
-
-
- -
- -

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

- - {groupedByIssues[groupTitle].length} - -
-
- -
- - -
-
- - {(provided, snapshot) => ( -
- {groupedByIssues[groupTitle].map((childIssue, index: number) => ( - - {(provided, snapshot) => ( - - -
- {properties.key && ( -
- {childIssue.project_detail?.identifier}-{childIssue.sequence_id} -
- )} -
- {childIssue.name} -
-
- {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) => ( -
- {assignee.avatar && assignee.avatar !== "" ? ( -
- {assignee.name} -
- ) : ( -
- {assignee.first_name.charAt(0)} -
- )} -
- ) - ) - ) : ( - No assignee. - )} -
- )} -
-
-
- - )} -
- ))} - {provided.placeholder} - -
- )} -
-
-
- )} -
- ); -}; - -export default SingleBoard; diff --git a/apps/app/components/project/issues/BoardView/index.tsx b/apps/app/components/project/issues/BoardView/index.tsx index 4881f22ed..86d54c526 100644 --- a/apps/app/components/project/issues/BoardView/index.tsx +++ b/apps/app/components/project/issues/BoardView/index.tsx @@ -14,15 +14,14 @@ import useUser from "lib/hooks/useUser"; // fetching keys import { STATE_LIST } from "constants/fetch-keys"; // components -import SingleBoard from "components/project/issues/BoardView/SingleBoard"; +import SingleBoard from "components/project/issues/BoardView/single-board"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; // ui import { Spinner } from "ui"; // types import type { IState, IIssue, Properties, NestedKeyOf, IProjectMember } from "types"; -import ConfirmIssueDeletion from "../ConfirmIssueDeletion"; -import { TrashIcon } from "@heroicons/react/24/outline"; +import ConfirmIssueDeletion from "../confirm-issue-deletion"; type Props = { properties: Properties; @@ -31,9 +30,18 @@ type Props = { [key: string]: IIssue[]; }; members: IProjectMember[] | undefined; + handleDeleteIssue: React.Dispatch>; + partialUpdateIssue: (formData: Partial, issueId: string) => void; }; -const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues, members }) => { +const BoardView: React.FC = ({ + properties, + selectedGroup, + groupedByIssues, + members, + handleDeleteIssue, + partialUpdateIssue, +}) => { const [isOpen, setIsOpen] = useState(false); const [isIssueOpen, setIsIssueOpen] = useState(false); @@ -217,6 +225,8 @@ const BoardView: React.FC = ({ properties, selectedGroup, groupedByIssues ? states?.find((s) => s.name === singleGroup)?.color : undefined } + handleDeleteIssue={handleDeleteIssue} + partialUpdateIssue={partialUpdateIssue} /> ))} diff --git a/apps/app/components/project/issues/BoardView/single-board.tsx b/apps/app/components/project/issues/BoardView/single-board.tsx new file mode 100644 index 000000000..7e843a8ca --- /dev/null +++ b/apps/app/components/project/issues/BoardView/single-board.tsx @@ -0,0 +1,609 @@ +// react +import React, { useState } from "react"; +// next +import Link from "next/link"; +import Image from "next/image"; +// swr +import useSWR from "swr"; +// react-beautiful-dnd +import { Draggable } from "react-beautiful-dnd"; +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +// services +import workspaceService from "lib/services/workspace.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// headless ui +import { Listbox, Transition } from "@headlessui/react"; +// icons +import { + ArrowsPointingInIcon, + ArrowsPointingOutIcon, + CalendarDaysIcon, + EllipsisHorizontalIcon, + PlusIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; +import User from "public/user.png"; +// common +import { PRIORITIES } from "constants/"; +import { + addSpaceIfCamelCase, + classNames, + findHowManyDaysLeft, + renderShortNumericDateFormat, +} from "constants/common"; +import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; +import { getPriorityIcon } from "constants/global"; +// types +import { IIssue, Properties, NestedKeyOf, IWorkspaceMember } from "types"; + +type Props = { + selectedGroup: NestedKeyOf | null; + groupTitle: string; + groupedByIssues: { + [key: string]: IIssue[]; + }; + index: number; + setIsIssueOpen: React.Dispatch>; + properties: Properties; + setPreloadedData: React.Dispatch< + React.SetStateAction< + | (Partial & { + actionType: "createIssue" | "edit" | "delete"; + }) + | undefined + > + >; + bgColor?: string; + stateId: string | null; + createdBy: string | null; + handleDeleteIssue: React.Dispatch>; + partialUpdateIssue: (formData: Partial, childIssueId: string) => void; +}; + +const SingleBoard: React.FC = ({ + selectedGroup, + groupTitle, + groupedByIssues, + index, + setIsIssueOpen, + properties, + setPreloadedData, + bgColor = "#0f2b16", + stateId, + createdBy, + handleDeleteIssue, + partialUpdateIssue, +}) => { + // Collapse/Expand + const [show, setShow] = useState(true); + + const { activeProject, activeWorkspace, states } = 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 ( + + {(provided, snapshot) => ( +
+
+
+
+ +
+ +

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

+ + {groupedByIssues[groupTitle].length} + +
+
+ +
+ + +
+
+ + {(provided, snapshot) => ( +
+ {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 ( + + {(provided, snapshot) => ( +
+
+
+ +
+ + + {properties.key && ( +
+ {activeProject?.identifier}-{childIssue.sequence_id} +
+ )} +
+ {childIssue.name} +
+
+ +
+ {properties.priority && ( + { + partialUpdateIssue({ priority: data }, childIssue.id); + }} + className="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ + {childIssue.priority ?? "None"} + + + + + {PRIORITIES?.map((priority) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer capitalize select-none px-3 py-2" + ) + } + value={priority} + > + {priority} + + ))} + + +
+
+
+ Priority +
+
+ {childIssue.priority ?? "None"} +
+
+ + )} +
+ )} + {properties.state && ( + { + partialUpdateIssue({ state: data }, childIssue.id); + }} + className="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ + + {addSpaceIfCamelCase(childIssue.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)} + + ))} + + +
+
+
State
+
{childIssue.state_detail.name}
+
+ + )} +
+ )} + {properties.start_date && ( +
+ + {childIssue.start_date + ? renderShortNumericDateFormat(childIssue.start_date) + : "N/A"} +
+
Started at
+
+ {renderShortNumericDateFormat(childIssue.start_date ?? "")} +
+
+
+ )} + {properties.target_date && ( +
+ + {childIssue.target_date + ? renderShortNumericDateFormat(childIssue.target_date) + : "N/A"} +
+
+ Target date +
+
+ {renderShortNumericDateFormat(childIssue.target_date ?? "")} +
+
+ {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 && ( + { + const newData = childIssue.assignees ?? []; + if (newData.includes(data)) { + newData.splice(newData.indexOf(data), 1); + } else { + newData.push(data); + } + partialUpdateIssue( + { assignees_list: newData }, + childIssue.id + ); + }} + className="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ +
+ {assignees.length > 0 ? ( + assignees.map((assignee, index: number) => ( +
+ {assignee.avatar && assignee.avatar !== "" ? ( +
+ {assignee?.first_name} +
+ ) : ( +
+ {assignee.first_name?.charAt(0)} +
+ )} +
+ )) + ) : ( +
+ No user +
+ )} +
+
+ + + + {people?.map((person) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer select-none p-2" + ) + } + value={person.member.id} + > +
+ {person.member.avatar && + person.member.avatar !== "" ? ( +
+ avatar +
+ ) : ( +
+ {person.member.first_name && + person.member.first_name !== "" + ? person.member.first_name.charAt(0) + : person.member.email.charAt(0)} +
+ )} +

+ {person.member.first_name && + person.member.first_name !== "" + ? person.member.first_name + : person.member.email} +

+
+
+ ))} +
+
+
+
+
Assigned to
+
+ {childIssue.assignee_details?.length > 0 + ? childIssue.assignee_details + .map((assignee) => assignee.first_name) + .join(", ") + : "No one"} +
+
+ + )} +
+ )} +
+
+
+ )} +
+ ); + })} + {provided.placeholder} + +
+ )} +
+
+
+ )} +
+ ); +}; + +export default SingleBoard; diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx index 6fad13a44..a8ca2ee74 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/SelectLabels.tsx @@ -106,12 +106,16 @@ const SelectLabels: React.FC = ({ control }) => { className={({ active }) => `${ active ? "text-white bg-theme" : "text-gray-900" - } cursor-pointer select-none w-full p-2 rounded-md` + } flex items-center gap-2 cursor-pointer select-none w-full p-2 rounded-md` } value={label.id} > {({ selected, active }) => ( <> + i === label.id) diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx index 6f029eb6a..41abbd348 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx @@ -34,11 +34,14 @@ import SelectAssignee from "./SelectAssignee"; import SelectParent from "./SelectParentIssue"; import CreateUpdateStateModal from "components/project/issues/BoardView/state/create-update-state-modal"; import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; - // types import type { IIssue, IssueResponse, CycleIssueResponse } from "types"; import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline"; +const RichTextEditor = dynamic(() => import("components/lexical/editor"), { + ssr: false, +}); + type Props = { isOpen: boolean; setIsOpen: React.Dispatch>; @@ -78,10 +81,6 @@ const CreateUpdateIssuesModal: React.FC = ({ // setIssueDescriptionValue(value); // }; - const RichTextEditor = dynamic(() => import("components/lexical/editor"), { - ssr: false, - }); - const router = useRouter(); const handleClose = () => { @@ -117,7 +116,7 @@ const CreateUpdateIssuesModal: React.FC = ({ const addIssueToSprint = async (issueId: string, sprintId: string, issueDetail: IIssue) => { if (!activeWorkspace || !activeProject) return; await issuesServices - .addIssueToSprint(activeWorkspace.slug, activeProject.id, sprintId, { + .addIssueToCycle(activeWorkspace.slug, activeProject.id, sprintId, { issue: issueId, }) .then((res) => { @@ -176,6 +175,7 @@ const CreateUpdateIssuesModal: React.FC = ({ const payload: Partial = { ...formData, target_date: formData.target_date ? renderDateFormat(formData.target_date ?? "") : null, + // description: formData.description ? JSON.parse(formData.description) : null, }; if (!data) { await issuesServices diff --git a/apps/app/components/project/issues/ListView/index.tsx b/apps/app/components/project/issues/ListView/index.tsx index 78d5ab68b..a95c83606 100644 --- a/apps/app/components/project/issues/ListView/index.tsx +++ b/apps/app/components/project/issues/ListView/index.tsx @@ -4,23 +4,37 @@ import React, { useState } from "react"; import Link from "next/link"; import Image from "next/image"; // swr -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; +// headless ui +import { Disclosure, Listbox, Menu, Transition } from "@headlessui/react"; // ui -import { Listbox, Transition } from "@headlessui/react"; +import { Spinner } from "ui"; // icons -import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { + ChevronDownIcon, + PlusIcon, + CalendarDaysIcon, + EllipsisHorizontalIcon, +} from "@heroicons/react/24/outline"; +import User from "public/user.png"; +// components +import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; // types -import { IIssue, IssueResponse, NestedKeyOf, Properties } from "types"; +import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types"; +// services +import workspaceService from "lib/services/workspace.service"; // 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.service"; -import workspaceService from "lib/services/workspace.service"; +import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; // constants -import { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common"; +import { + addSpaceIfCamelCase, + classNames, + findHowManyDaysLeft, + renderShortNumericDateFormat, +} from "constants/common"; // types type Props = { @@ -29,6 +43,7 @@ type Props = { selectedGroup: NestedKeyOf | null; setSelectedIssue: any; handleDeleteIssue: React.Dispatch>; + partialUpdateIssue: (formData: Partial, issueId: string) => void; }; const ListView: React.FC = ({ @@ -37,377 +52,515 @@ const ListView: React.FC = ({ selectedGroup, setSelectedIssue, handleDeleteIssue, + partialUpdateIssue, }) => { + const [isCreateIssuesModalOpen, setIsCreateIssuesModalOpen] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + const { activeWorkspace, activeProject, states } = useUser(); - const partialUpdateIssue = (formData: Partial, issueId: string) => { - if (!activeWorkspace || !activeProject) return; - issuesServices - .patchIssue(activeWorkspace.slug, activeProject.id, issueId, formData) - .then((response) => { - mutate( - PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), - (prevData) => ({ - ...(prevData as IssueResponse), - results: - prevData?.results.map((issue) => (issue.id === response.id ? response : issue)) ?? [], - }), - false - ); - }) - .catch((error) => { - console.log(error); - }); - }; - - const { data: people } = useSWR( + const { data: people } = useSWR( activeWorkspace ? WORKSPACE_MEMBERS : null, activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null ); return ( -
- {Object.keys(groupedByIssues).map((singleGroup) => ( -
-
-
- - {selectedGroup !== 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 - ); + + ) : ( +

All Issues

+ )} +

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

+ + + + + +
+ {groupedByIssues[singleGroup] ? ( + groupedByIssues[singleGroup].length > 0 ? ( + groupedByIssues[singleGroup].map((issue: IIssue) => { + const assignees = [ + ...(issue?.assignees_list ?? []), + ...(issue?.assignees ?? []), + ]?.map((assignee) => { + const tempPerson = people?.find( + (p) => p.member.id === assignee + )?.member; - return ( -
- - {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) === "state" ? ( - - ) : (key as keyof Properties) === "target_date" ? ( - - ) : ( - - )} - - ) - )} - - - ); - }) - : null} - -
-
- {selectedGroup === "state_detail.name" ? ( - s.name === singleGroup)?.color, - }} - > - ) : null} + <> + +
+ {Object.keys(groupedByIssues).map((singleGroup) => ( + + {({ open }) => ( +
+
+ +
+ + + + {selectedGroup !== 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 }) => ( - <> -
- - + +
+ {properties.priority && ( + { + partialUpdateIssue({ priority: data }, issue.id); + }} + className="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ + {issue.priority ?? "None"} + + + + + {PRIORITIES?.map((priority) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer capitalize select-none px-3 py-2" + ) + } + value={priority} > - {issue.priority ?? "None"} - - + {priority} + + ))} + + +
+
+
+ Priority +
+
+ {issue.priority ?? "None"} +
+
+ + )} +
+ )} + {properties.state && ( + { + partialUpdateIssue({ state: data }, issue.id); + }} + className="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ + + {addSpaceIfCamelCase(issue.state_detail.name)} + - - - {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} - > -
- {person.member.avatar && - person.member.avatar !== "" ? ( -
- avatar -
- ) : ( -

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

- )} -

{person.member.first_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)} + + ))} + + +
+
+
State
+
{issue.state_detail.name}
+
+ + )} + + )} + {properties.start_date && ( +
+ + {issue.start_date + ? renderShortNumericDateFormat(issue.start_date) + : "N/A"} +
+
Started at
+
+ {renderShortNumericDateFormat(issue.start_date ?? "")} +
+
+
+ )} + {properties.target_date && ( +
+ + {issue.target_date + ? renderShortNumericDateFormat(issue.target_date) + : "N/A"} +
+
+ Target date +
+
+ {renderShortNumericDateFormat(issue.target_date ?? "")} +
+
+ {issue.target_date && + (issue.target_date < new Date().toISOString() + ? `Target date has passed by ${findHowManyDaysLeft( + issue.target_date + )} days` + : findHowManyDaysLeft(issue.target_date) <= 3 + ? `Target date is in ${findHowManyDaysLeft( + issue.target_date + )} days` + : "Target date")} +
+
+
+ )} + {properties.assignee && ( + { + 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="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ +
+ {assignees.length > 0 ? ( + assignees.map((assignee, index: number) => ( +
+ {assignee.avatar && assignee.avatar !== "" ? ( +
+ {assignee?.first_name}
- - ))} - - + ) : ( +
+ {assignee.first_name?.charAt(0)} +
+ )} +
+ )) + ) : ( +
+ No user +
+ )}
- - )} - -
- { - 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"} - -
- - + + + {people?.map((person) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer select-none p-2" + ) + } + value={person.member.id} + > +
+ {person.member.avatar && + person.member.avatar !== "" ? ( +
+ avatar +
+ ) : ( +
+ {person.member.first_name && + person.member.first_name !== "" + ? person.member.first_name.charAt(0) + : person.member.email.charAt(0)} +
+ )} +

+ {person.member.first_name && + person.member.first_name !== "" + ? person.member.first_name + : person.member.email} +

+
+
+ ))} +
+
+
+
+
Assigned to
+
+ {issue.assignee_details?.length > 0 + ? issue.assignee_details + .map((assignee) => assignee.first_name) + .join(", ") + : "No one"} +
+
+ + )} + + )} + + + + + + + + + +
+ +
+
+
+
+ -
-
-
-
- ))} -
+ ); + }) + ) : ( +

No issues.

+ ) + ) : ( +
+ +
+ )} + + + +
+ +
+ + )} + + ))} + + ); }; diff --git a/apps/app/components/project/issues/ConfirmIssueDeletion.tsx b/apps/app/components/project/issues/confirm-issue-deletion.tsx similarity index 81% rename from apps/app/components/project/issues/ConfirmIssueDeletion.tsx rename to apps/app/components/project/issues/confirm-issue-deletion.tsx index f1476c682..7be85390e 100644 --- a/apps/app/components/project/issues/ConfirmIssueDeletion.tsx +++ b/apps/app/components/project/issues/confirm-issue-deletion.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useRef, useState } from "react"; // swr import { mutate } from "swr"; // headless ui @@ -26,7 +26,7 @@ type Props = { const ConfirmIssueDeletion: React.FC = ({ isOpen, handleClose, data }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const { activeWorkspace } = useUser(); + const { activeWorkspace, activeProject } = useUser(); const { setToastAlert } = useToast(); @@ -70,7 +70,7 @@ const ConfirmIssueDeletion: React.FC = ({ isOpen, handleClose, data }) => return ( - + = ({ isOpen, handleClose, data }) =>
-
-
-
- - Delete Issue +
+
+
+ + Are you sure you want to delete {`"`} + {activeProject?.identifier}-{data?.sequence_id} - {data?.name}?{`"`}

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

diff --git a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx index 71c84a745..c6e1eb573 100644 --- a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx +++ b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx @@ -12,6 +12,8 @@ import workspaceService from "lib/services/workspace.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; +// components +import IssuesListModal from "components/project/issues/IssuesListModal"; // fetching keys import { PROJECT_ISSUES_LIST, @@ -37,18 +39,22 @@ import { LinkIcon, ArrowPathIcon, CalendarDaysIcon, + TrashIcon, + PlusIcon, + XMarkIcon, } from "@heroicons/react/24/outline"; // types import type { Control } from "react-hook-form"; import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types"; import { TwitterPicker } from "react-color"; -import IssuesListModal from "components/project/issues/IssuesListModal"; +import { positionEditorElement } from "components/lexical/helpers/editor"; type Props = { control: Control; submitChanges: (formData: Partial) => void; issueDetail: IIssue | undefined; watch: UseFormWatch; + setDeleteIssueModal: React.Dispatch>; }; const defaultValues: Partial = { @@ -58,13 +64,15 @@ const defaultValues: Partial = { const IssueDetailSidebar: React.FC = ({ control, - watch: watchIssue, submitChanges, issueDetail, + watch: watchIssue, + setDeleteIssueModal, }) => { const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); const [isParentModalOpen, setIsParentModalOpen] = useState(false); + const [createLabelForm, setCreateLabelForm] = useState(false); const { activeWorkspace, activeProject, cycles, issues } = useUser(); @@ -117,7 +125,7 @@ const IssueDetailSidebar: React.FC = ({ name: NestedKeyOf; canSelectMultipleOptions: boolean; icon: (props: any) => JSX.Element; - options?: Array<{ label: string; value: any }>; + options?: Array<{ label: string; value: any; color?: string }>; modal: boolean; issuesList?: Array; isOpen?: boolean; @@ -133,6 +141,7 @@ const IssueDetailSidebar: React.FC = ({ options: states?.map((state) => ({ label: state.name, value: state.id, + color: state.color, })), modal: false, }, @@ -221,364 +230,411 @@ const IssueDetailSidebar: React.FC = ({ const handleCycleChange = (cycleId: string) => { if (activeWorkspace && activeProject && issueDetail) - issuesServices.addIssueToSprint(activeWorkspace.slug, activeProject.id, cycleId, { + issuesServices.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, { issue: issueDetail.id, }); }; return ( -
-
-

- {activeProject?.identifier}-{issueDetail?.sequence_id} -

-
- - + <> +
+
+

+ {activeProject?.identifier}-{issueDetail?.sequence_id} +

+
+ + + +
-
-
- {sidebarSections.map((section, index) => ( -
- {section.map((item) => ( -
-
- -

{item.label}

-
-
- {item.name === "target_date" ? ( - ( - { - 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 w-full" - /> - )} - /> - ) : item.modal ? ( - ( - <> - item.setIsOpen && item.setIsOpen(false)} - onChange={(val) => { - console.log(val); - // submitChanges({ [item.name]: val }); - onChange(val); +
+ {sidebarSections.map((section, index) => ( +
+ {section.map((item) => ( +
+
+ +

{item.label}

+
+
+ {item.name === "target_date" ? ( + ( + { + submitChanges({ target_date: e.target.value }); + onChange(e.target.value); }} - issues={item?.issuesList ?? []} - title={`Select ${item.label}`} - multiple={item.canSelectMultipleOptions} - value={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 w-full" /> - - - )} - /> - ) : ( - ( - { - if (item.name === "cycle") handleCycleChange(value); - else submitChanges({ [item.name]: value }); - }} - className="flex-shrink-0" - > - {({ open }) => ( -
- - - {value - ? Array.isArray(value) - ? value - .map( - (i: any) => - item.options?.find((option) => option.value === i) - ?.label - ) - .join(", ") || item.label - : item.options?.find((option) => option.value === value) - ?.label - : "None"} - - - - - - -
- {item.options ? ( - item.options.length > 0 ? ( - item.options.map((option) => ( - - `${ - active || selected - ? "text-white bg-theme" - : "text-gray-900" - } ${ - item.label === "Priority" && "capitalize" - } cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={option.value} - > - {option.label} - - )) - ) : ( -
No {item.label}s found
- ) - ) : ( - - )} -
-
-
-
- )} -
- )} - /> - )} -
-
- ))} -
- ))} -
-
-
Add new label
-
-
- - {({ open }) => ( - <> - - {watch("colour") && watch("colour") !== "" && ( - - )} - - - - - - ( - onChange(value.hex)} /> + : `Select ${item.label}`} + + )} /> - - + ) : ( + ( + { + if (item.name === "cycle") handleCycleChange(value); + else submitChanges({ [item.name]: value }); + }} + className="flex-shrink-0" + > + {({ open }) => ( +
+ + + {value + ? Array.isArray(value) + ? value + .map( + (i: any) => + item.options?.find((option) => option.value === i) + ?.label + ) + .join(", ") || item.label + : item.options?.find((option) => option.value === value) + ?.label + : "None"} + + + + + + +
+ {item.options ? ( + item.options.length > 0 ? ( + item.options.map((option) => ( + + `${ + active || selected + ? "text-white bg-theme" + : "text-gray-900" + } ${ + item.label === "Priority" && "capitalize" + } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + } + value={option.value} + > + {option.color && ( + + )} + {option.label} + + )) + ) : ( +
No {item.label}s found
+ ) + ) : ( + + )} +
+
+
+
+ )} +
+ )} + /> + )} +
+
+ ))} +
+ ))} +
+
+
+
+ +

Label

+
+
+
+ {issueDetail?.label_details.map((label) => ( + + // submitChanges({ + // labels_list: issueDetail?.labels_list.filter((l) => l !== label.id), + // }) + // } + > + + {label.name} + + ))} + ( + submitChanges({ labels_list: value })} + className="flex-shrink-0" + > + {({ open }) => ( + <> + Label +
+ + + Select Label + + + + + +
+ {issueLabels ? ( + issueLabels.length > 0 ? ( + issueLabels.map((label: IIssueLabels) => ( + + `${ + active || selected + ? "text-white bg-theme" + : "text-gray-900" + } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + } + value={label.id} + > + + {label.name} + + )) + ) : ( +
No labels found
+ ) + ) : ( + + )} +
+
+
+
+ + )} +
+ )} + /> +
+
+
+
+
- - - -
-
- -

Label

-
-
- ( - submitChanges({ labels_list: value })} - className="flex-shrink-0" - > + {createLabelForm && ( +
+
+ {({ open }) => ( <> - Label -
- + + {watch("colour") && watch("colour") !== "" && ( - {value && value.length > 0 - ? value - .map( - (i: string) => - issueLabels?.find((option) => option.id === i)?.name - ) - .join(", ") - : "None"} - - - + className="w-5 h-5 rounded" + style={{ + backgroundColor: watch("colour") ?? "green", + }} + > + )} + + - - -
- {issueLabels ? ( - issueLabels.length > 0 ? ( - issueLabels.map((label: IIssueLabels) => ( - - `${ - active || selected - ? "text-white bg-theme" - : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={label.id} - > - - {label.name} - - )) - ) : ( -
No labels found
- ) - ) : ( - - )} -
-
-
-
+ + + ( + onChange(value.hex)} + /> + )} + /> + + )} - - )} - /> -
+ +
+ + + + )}
-
+ ); }; diff --git a/apps/app/components/project/issues/issue-detail/activity/index.tsx b/apps/app/components/project/issues/issue-detail/activity/index.tsx index 2df8c5092..84085d7b1 100644 --- a/apps/app/components/project/issues/issue-detail/activity/index.tsx +++ b/apps/app/components/project/issues/issue-detail/activity/index.tsx @@ -24,12 +24,12 @@ type Props = { const activityIcons: { [key: string]: JSX.Element; } = { - state: , - priority: , - name: , - description: , - target_date: , - parent: , + state: , + priority: , + name: , + description: , + target_date: , + parent: , }; const IssueActivitySection: React.FC = ({ issueActivities, states, issues }) => { diff --git a/apps/app/components/project/issues/issue-detail/comment/IssueCommentSection.tsx b/apps/app/components/project/issues/issue-detail/comment/IssueCommentSection.tsx index d70196b28..fe55bdbf5 100644 --- a/apps/app/components/project/issues/issue-detail/comment/IssueCommentSection.tsx +++ b/apps/app/components/project/issues/issue-detail/comment/IssueCommentSection.tsx @@ -77,42 +77,6 @@ const IssueCommentSection: React.FC = ({ comments, issueId, projectId, wo return (
-
-
-
-