refactor: modules and states folder structure
This commit is contained in:
parent
8b1bf53831
commit
b2eab805e9
25 changed files with 371 additions and 346 deletions
|
|
@ -1,181 +0,0 @@
|
|||
import React, { useCallback } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useIssueView from "hooks/use-issue-view";
|
||||
// components
|
||||
import SingleBoard from "components/project/modules/board-view/single-board";
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
// types
|
||||
import { IIssue, IProjectMember, ModuleIssueResponse, UserAuth } from "types";
|
||||
// constants
|
||||
import { STATE_LIST, MODULE_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issues: IIssue[];
|
||||
members: IProjectMember[] | undefined;
|
||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||
openIssuesListModal: () => void;
|
||||
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setPreloadedData: React.Dispatch<
|
||||
React.SetStateAction<
|
||||
| (Partial<IIssue> & {
|
||||
actionType: "createIssue" | "edit" | "delete";
|
||||
})
|
||||
| null
|
||||
>
|
||||
>;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
const ModulesBoardView: React.FC<Props> = ({
|
||||
issues,
|
||||
members,
|
||||
openCreateIssueModal,
|
||||
openIssuesListModal,
|
||||
handleDeleteIssue,
|
||||
setPreloadedData,
|
||||
userAuth,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
||||
|
||||
const { data: states } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleOnDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
const { source, destination } = result;
|
||||
|
||||
if (source.droppableId !== destination.droppableId) {
|
||||
const sourceGroup = source.droppableId; // source group id
|
||||
const destinationGroup = destination.droppableId; // destination group id
|
||||
if (!sourceGroup || !destinationGroup) return;
|
||||
|
||||
// removed/dragged item
|
||||
const removedItem = groupedByIssues[source.droppableId][source.index];
|
||||
|
||||
if (selectedGroup === "priority") {
|
||||
// update the removed item for mutation
|
||||
removedItem.priority = destinationGroup;
|
||||
|
||||
// patch request
|
||||
issuesService.patchIssue(workspaceSlug as string, projectId as string, removedItem.id, {
|
||||
priority: destinationGroup,
|
||||
});
|
||||
} else if (selectedGroup === "state_detail.name") {
|
||||
const destinationState = states?.find((s) => s.name === destinationGroup);
|
||||
const destinationStateId = destinationState?.id;
|
||||
|
||||
// update the removed item for mutation
|
||||
if (!destinationStateId || !destinationState) return;
|
||||
removedItem.state = destinationStateId;
|
||||
removedItem.state_detail = destinationState;
|
||||
|
||||
// patch request
|
||||
issuesService.patchIssue(workspaceSlug as string, projectId as string, removedItem.id, {
|
||||
state: destinationStateId,
|
||||
});
|
||||
|
||||
if (!moduleId) return;
|
||||
mutate<ModuleIssueResponse[]>(
|
||||
MODULE_ISSUES(moduleId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
const updatedIssues = prevData.map((issue) => {
|
||||
if (issue.issue_detail.id === removedItem.id) {
|
||||
return {
|
||||
...issue,
|
||||
issue_detail: removedItem,
|
||||
};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
return [...updatedIssues];
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// remove item from the source group
|
||||
groupedByIssues[source.droppableId].splice(source.index, 1);
|
||||
// add item to the destination group
|
||||
groupedByIssues[destination.droppableId].splice(destination.index, 0, removedItem);
|
||||
}
|
||||
},
|
||||
[workspaceSlug, groupedByIssues, projectId, selectedGroup, states, moduleId]
|
||||
);
|
||||
|
||||
if (issueView !== "kanban") return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{groupedByIssues ? (
|
||||
<div className="h-[calc(100vh-157px)] lg:h-[calc(100vh-115px)] w-full">
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full gap-x-4 overflow-x-auto overflow-y-hidden">
|
||||
{Object.keys(groupedByIssues).map((singleGroup) => (
|
||||
<SingleBoard
|
||||
key={singleGroup}
|
||||
selectedGroup={selectedGroup}
|
||||
groupTitle={singleGroup}
|
||||
createdBy={
|
||||
selectedGroup === "created_by"
|
||||
? members?.find((m) => m.member.id === singleGroup)?.member.first_name ??
|
||||
"loading..."
|
||||
: null
|
||||
}
|
||||
groupedByIssues={groupedByIssues}
|
||||
bgColor={
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)?.color
|
||||
: "#000000"
|
||||
}
|
||||
properties={properties}
|
||||
openIssuesListModal={openIssuesListModal}
|
||||
openCreateIssueModal={openCreateIssueModal}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
setPreloadedData={setPreloadedData}
|
||||
stateId={
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||
: null
|
||||
}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModulesBoardView;
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
// components
|
||||
import SingleIssue from "components/common/board-view/single-issue";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import BoardHeader from "components/common/board-view/board-header";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
properties: Properties;
|
||||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
groupTitle: string;
|
||||
createdBy: string | null;
|
||||
bgColor?: string;
|
||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||
openIssuesListModal: () => void;
|
||||
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setPreloadedData: React.Dispatch<
|
||||
React.SetStateAction<
|
||||
| (Partial<IIssue> & {
|
||||
actionType: "createIssue" | "edit" | "delete";
|
||||
})
|
||||
| null
|
||||
>
|
||||
>;
|
||||
stateId: string | null;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
const SingleModuleBoard: React.FC<Props> = ({
|
||||
properties,
|
||||
groupedByIssues,
|
||||
selectedGroup,
|
||||
groupTitle,
|
||||
createdBy,
|
||||
bgColor,
|
||||
openCreateIssueModal,
|
||||
openIssuesListModal,
|
||||
handleDeleteIssue,
|
||||
setPreloadedData,
|
||||
stateId,
|
||||
userAuth,
|
||||
}) => {
|
||||
// Collapse/Expand
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
|
||||
if (selectedGroup === "priority")
|
||||
groupTitle === "high"
|
||||
? (bgColor = "#dc2626")
|
||||
: groupTitle === "medium"
|
||||
? (bgColor = "#f97316")
|
||||
: groupTitle === "low"
|
||||
? (bgColor = "#22c55e")
|
||||
: (bgColor = "#ff0000");
|
||||
|
||||
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`h-full flex-shrink-0 rounded ${!isCollapsed ? "" : "w-80 border bg-gray-50"}`}>
|
||||
<div className={`${!isCollapsed ? "" : "flex h-full flex-col space-y-3 overflow-y-auto"}`}>
|
||||
<BoardHeader
|
||||
addIssueToState={() => {
|
||||
openCreateIssueModal();
|
||||
if (selectedGroup !== null) {
|
||||
setPreloadedData({
|
||||
state: stateId !== null ? stateId : undefined,
|
||||
[selectedGroup]: groupTitle,
|
||||
actionType: "createIssue",
|
||||
});
|
||||
}
|
||||
}}
|
||||
bgColor={bgColor ?? ""}
|
||||
createdBy={createdBy}
|
||||
groupTitle={groupTitle}
|
||||
groupedByIssues={groupedByIssues}
|
||||
isCollapsed={isCollapsed}
|
||||
setIsCollapsed={setIsCollapsed}
|
||||
selectedGroup={selectedGroup}
|
||||
/>
|
||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`mt-3 h-full space-y-3 overflow-y-auto px-3 pb-3 ${
|
||||
snapshot.isDraggingOver ? "bg-indigo-50 bg-opacity-50" : ""
|
||||
} ${!isCollapsed ? "hidden" : "block"}`}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{groupedByIssues[groupTitle].map((issue, index: number) => {
|
||||
const assignees = [
|
||||
...(issue?.assignees_list ?? []),
|
||||
...(issue?.assignees ?? []),
|
||||
]?.map((assignee) => {
|
||||
const tempPerson = people?.find((p) => p.member.id === assignee)?.member;
|
||||
|
||||
return tempPerson;
|
||||
});
|
||||
|
||||
return (
|
||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<SingleIssue
|
||||
type="module"
|
||||
typeId={moduleId as string}
|
||||
issue={issue}
|
||||
properties={properties}
|
||||
snapshot={snapshot}
|
||||
assignees={assignees}
|
||||
people={people}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
<CustomMenu
|
||||
label={
|
||||
<span className="flex items-center gap-1">
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Add issue
|
||||
</span>
|
||||
}
|
||||
className="mt-1"
|
||||
optionsPosition="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
openCreateIssueModal();
|
||||
if (selectedGroup !== null) {
|
||||
setPreloadedData({
|
||||
state: stateId !== null ? stateId : undefined,
|
||||
[selectedGroup]: groupTitle,
|
||||
actionType: "createIssue",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => openIssuesListModal()}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleModuleBoard;
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import modulesService from "services/modules.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button } from "components/ui";
|
||||
// icons
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { IModule } from "types";
|
||||
// fetch-keys
|
||||
import { MODULE_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
data?: IModule;
|
||||
};
|
||||
|
||||
const ConfirmModuleDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const {
|
||||
query: { workspaceSlug },
|
||||
} = router;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const cancelButtonRef = useRef(null);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
const handleDeletion = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
if (!workspaceSlug || !data) return;
|
||||
await modulesService
|
||||
.deleteModule(workspaceSlug as string, data.project, data.id)
|
||||
.then(() => {
|
||||
mutate(MODULE_LIST(data.project));
|
||||
router.push(`/${workspaceSlug}/projects/${data.project}/modules`);
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Module deleted successfully",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setIsDeleteLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
data && setIsOpen(true);
|
||||
}, [data, setIsOpen]);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-20"
|
||||
initialFocus={cancelButtonRef}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<ExclamationTriangleIcon
|
||||
className="h-6 w-6 text-red-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
Delete Module
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to delete module - {`"`}
|
||||
<span className="italic">{data?.name}</span>
|
||||
{`?"`} All of the data related to the module will be permanently removed.
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleDeletion}
|
||||
theme="danger"
|
||||
disabled={isDeleteLoading}
|
||||
className="inline-flex sm:ml-3"
|
||||
>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
theme="secondary"
|
||||
className="inline-flex sm:ml-3"
|
||||
onClick={handleClose}
|
||||
ref={cancelButtonRef}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmModuleDeletion;
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
import React, { useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import SelectLead from "components/project/modules/create-update-module-modal/select-lead";
|
||||
import SelectMembers from "components/project/modules/create-update-module-modal/select-members";
|
||||
import SelectStatus from "components/project/modules/create-update-module-modal/select-status";
|
||||
// ui
|
||||
import { Button, CustomDatePicker, Input, TextArea } from "components/ui";
|
||||
// services
|
||||
import modulesService from "services/modules.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { IModule } from "types";
|
||||
// fetch-keys
|
||||
import { MODULE_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
projectId: string;
|
||||
data?: IModule;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IModule> = {
|
||||
name: "",
|
||||
description: "",
|
||||
status: null,
|
||||
lead: null,
|
||||
members_list: [],
|
||||
};
|
||||
|
||||
const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, projectId }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
setError,
|
||||
} = useForm<IModule>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: IModule) => {
|
||||
if (!workspaceSlug) return;
|
||||
const payload = {
|
||||
...formData,
|
||||
start_date: formData.start_date ? renderDateFormat(formData.start_date) : null,
|
||||
target_date: formData.target_date ? renderDateFormat(formData.target_date) : null,
|
||||
};
|
||||
if (!data) {
|
||||
await modulesService
|
||||
.createModule(workspaceSlug as string, projectId, payload)
|
||||
.then(() => {
|
||||
mutate(MODULE_LIST(projectId));
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Module created successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
Object.keys(err).map((key) => {
|
||||
setError(key as keyof typeof defaultValues, {
|
||||
message: err[key].join(", "),
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await modulesService
|
||||
.updateModule(workspaceSlug as string, projectId, data.id, payload)
|
||||
.then((res) => {
|
||||
mutate<IModule[]>(
|
||||
MODULE_LIST(projectId),
|
||||
(prevData) => {
|
||||
const newData = prevData?.map((item) => {
|
||||
if (item.id === res.id) {
|
||||
return res;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return newData;
|
||||
},
|
||||
false
|
||||
);
|
||||
handleClose();
|
||||
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Module updated successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
Object.keys(err).map((key) => {
|
||||
setError(key as keyof typeof defaultValues, {
|
||||
message: err[key].join(", "),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setIsOpen(true);
|
||||
reset(data);
|
||||
} else {
|
||||
reset(defaultValues);
|
||||
}
|
||||
}, [data, setIsOpen, reset]);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
{data ? "Update" : "Create"} Module
|
||||
</Dialog.Title>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
name="name"
|
||||
type="name"
|
||||
placeholder="Enter name"
|
||||
autoComplete="off"
|
||||
error={errors.name}
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Name is required",
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Name should be less than 255 characters",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
label="Description"
|
||||
placeholder="Enter description"
|
||||
error={errors.description}
|
||||
register={register}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-x-2">
|
||||
<div className="w-full">
|
||||
<h6 className="text-gray-500">Start Date</h6>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
rules={{ required: "Start date is required" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomDatePicker
|
||||
renderAs="input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={errors.start_date ? true : false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.start_date && (
|
||||
<h6 className="text-sm text-red-500">{errors.start_date.message}</h6>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<h6 className="text-gray-500">Target Date</h6>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
rules={{ required: "Target date is required" }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomDatePicker
|
||||
renderAs="input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={errors.target_date ? true : false}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.target_date && (
|
||||
<h6 className="text-sm text-red-500">{errors.target_date.message}</h6>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<SelectStatus control={control} error={errors.status} />
|
||||
<SelectLead control={control} />
|
||||
<SelectMembers control={control} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button theme="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? "Updating Module..."
|
||||
: "Update Module"
|
||||
: isSubmitting
|
||||
? "Creating Module..."
|
||||
: "Create Module"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUpdateModuleModal;
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Controller, Control } from "react-hook-form";
|
||||
import { UserIcon } from "@heroicons/react/24/outline";
|
||||
// import type { Control } from "react-hook-form";
|
||||
// service
|
||||
import type { IModule } from "types";
|
||||
import projectServices from "services/project.service";
|
||||
// ui
|
||||
import SearchListbox from "components/search-listbox";
|
||||
// icons
|
||||
// types
|
||||
// fetch-keys
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
control: Control<IModule, any>;
|
||||
};
|
||||
|
||||
const SelectLead: React.FC<Props> = ({ control }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: people } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectServices.projectMembers(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name="lead"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SearchListbox
|
||||
title="Lead"
|
||||
optionsFontsize="sm"
|
||||
options={people?.map((person) => ({
|
||||
value: person.member.id,
|
||||
display:
|
||||
person.member.first_name && person.member.first_name !== ""
|
||||
? person.member.first_name
|
||||
: person.member.email,
|
||||
}))}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
icon={<UserIcon className="h-3 w-3 text-gray-500" />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectLead;
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Controller, Control } from "react-hook-form";
|
||||
import { UserIcon } from "@heroicons/react/24/outline";
|
||||
// import type { Control } from "react-hook-form";
|
||||
// service
|
||||
import type { IModule } from "types";
|
||||
import projectServices from "services/project.service";
|
||||
// ui
|
||||
import SearchListbox from "components/search-listbox";
|
||||
// icons
|
||||
// types
|
||||
// fetch-keys
|
||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
control: Control<IModule, any>;
|
||||
};
|
||||
|
||||
const SelectMembers: React.FC<Props> = ({ control }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: people } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectServices.projectMembers(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name="members_list"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SearchListbox
|
||||
title="Members"
|
||||
optionsFontsize="sm"
|
||||
options={people?.map((person) => ({
|
||||
value: person.member.id,
|
||||
display:
|
||||
person.member.first_name && person.member.first_name !== ""
|
||||
? person.member.first_name
|
||||
: person.member.email,
|
||||
}))}
|
||||
multiple={true}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
icon={<UserIcon className="h-3 w-3 text-gray-500" />}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectMembers;
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
// react hook form
|
||||
import { Controller, FieldError, Control } from "react-hook-form";
|
||||
// ui
|
||||
import { CustomListbox } from "components/ui";
|
||||
// icons
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { IModule } from "types";
|
||||
// constants
|
||||
import { MODULE_STATUS } from "constants/";
|
||||
|
||||
type Props = {
|
||||
control: Control<IModule, any>;
|
||||
error?: FieldError;
|
||||
};
|
||||
|
||||
const SelectStatus: React.FC<Props> = ({ control, error }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
name="status"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div>
|
||||
<CustomListbox
|
||||
className={`${
|
||||
error
|
||||
? "border-red-500 bg-red-100 hover:bg-red-100 focus:outline-none focus:ring-red-500"
|
||||
: ""
|
||||
}`}
|
||||
title="Status"
|
||||
options={MODULE_STATUS.map((status) => ({
|
||||
value: status.value,
|
||||
display: status.label,
|
||||
color: status.color,
|
||||
}))}
|
||||
value={value}
|
||||
optionsFontsize="sm"
|
||||
onChange={onChange}
|
||||
icon={<Squares2X2Icon className={`h-3 w-3 ${error ? "text-black" : "text-gray-400"}`} />}
|
||||
/>
|
||||
{error && <p className="mt-1 text-sm text-red-600">{error.message}</p>}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
export default SelectStatus;
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// icons
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
import stateService from "services/state.service";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useIssueView from "hooks/use-issue-view";
|
||||
// components
|
||||
import SingleListIssue from "components/common/list-view/single-issue";
|
||||
// ui
|
||||
import { CustomMenu, Spinner } from "components/ui";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IWorkspaceMember, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import { STATE_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issues: IIssue[];
|
||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||
openIssuesListModal: () => void;
|
||||
removeIssueFromModule: (issueId: string) => void;
|
||||
setPreloadedData: React.Dispatch<
|
||||
React.SetStateAction<
|
||||
| (Partial<IIssue> & {
|
||||
actionType: "createIssue" | "edit" | "delete";
|
||||
})
|
||||
| null
|
||||
>
|
||||
>;
|
||||
userAuth: UserAuth;
|
||||
};
|
||||
|
||||
const ModulesListView: React.FC<Props> = ({
|
||||
issues,
|
||||
openCreateIssueModal,
|
||||
openIssuesListModal,
|
||||
removeIssueFromModule,
|
||||
setPreloadedData,
|
||||
userAuth,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues);
|
||||
|
||||
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const { data: states } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
if (issueView !== "list") return <></>;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-5">
|
||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
||||
const stateId =
|
||||
selectedGroup === "state_detail.name"
|
||||
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Disclosure key={singleGroup} as="div" defaultOpen>
|
||||
{({ open }) => (
|
||||
<div className="rounded-lg bg-white">
|
||||
<div className="rounded-t-lg bg-gray-100 px-4 py-3">
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span>
|
||||
<ChevronDownIcon
|
||||
className={`h-4 w-4 text-gray-500 ${!open ? "-rotate-90 transform" : ""}`}
|
||||
/>
|
||||
</span>
|
||||
{selectedGroup !== null ? (
|
||||
<h2 className="font-medium capitalize leading-5">
|
||||
{singleGroup === null || singleGroup === "null"
|
||||
? selectedGroup === "priority" && "No priority"
|
||||
: addSpaceIfCamelCase(singleGroup)}
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="font-medium leading-5">All Issues</h2>
|
||||
)}
|
||||
<p className="text-sm text-gray-500">
|
||||
{groupedByIssues[singleGroup as keyof IIssue].length}
|
||||
</p>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="divide-y-2">
|
||||
{groupedByIssues[singleGroup] ? (
|
||||
groupedByIssues[singleGroup].length > 0 ? (
|
||||
groupedByIssues[singleGroup].map((issue) => {
|
||||
const assignees = [
|
||||
...(issue?.assignees_list ?? []),
|
||||
...(issue?.assignees ?? []),
|
||||
]?.map((assignee) => {
|
||||
const tempPerson = people?.find(
|
||||
(p) => p.member.id === assignee
|
||||
)?.member;
|
||||
|
||||
return {
|
||||
avatar: tempPerson?.avatar,
|
||||
first_name: tempPerson?.first_name,
|
||||
email: tempPerson?.email,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<SingleListIssue
|
||||
key={issue.id}
|
||||
type="module"
|
||||
typeId={moduleId as string}
|
||||
issue={issue}
|
||||
properties={properties}
|
||||
editIssue={() => openCreateIssueModal(issue, "edit")}
|
||||
removeIssue={() => removeIssueFromModule(issue.bridge ?? "")}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="px-4 py-3 text-sm text-gray-500">No issues.</p>
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
<div className="p-3">
|
||||
<CustomMenu
|
||||
label={
|
||||
<span className="flex items-center gap-1">
|
||||
<PlusIcon className="h-3 w-3" />
|
||||
Add issue
|
||||
</span>
|
||||
}
|
||||
optionsPosition="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
openCreateIssueModal();
|
||||
if (selectedGroup !== null) {
|
||||
setPreloadedData({
|
||||
state: stateId !== null ? stateId : undefined,
|
||||
[selectedGroup]: singleGroup,
|
||||
actionType: "createIssue",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => openIssuesListModal()}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModulesListView;
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// services
|
||||
import modulesService from "services/modules.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import SelectLead from "components/project/modules/module-detail-sidebar/select-lead";
|
||||
import SelectMembers from "components/project/modules/module-detail-sidebar/select-members";
|
||||
import SelectStatus from "components/project/modules/module-detail-sidebar/select-status";
|
||||
import ModuleLinkModal from "components/project/modules/module-link-modal";
|
||||
// progress-bar
|
||||
import { CircularProgressbar } from "react-circular-progressbar";
|
||||
import "react-circular-progressbar/dist/styles.css";
|
||||
// ui
|
||||
import { CustomDatePicker, Loader } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
ChartPieIcon,
|
||||
LinkIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { groupBy } from "helpers/array.helper";
|
||||
// types
|
||||
import { IModule, ModuleIssueResponse } from "types";
|
||||
// fetch-keys
|
||||
import { MODULE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
const defaultValues: Partial<IModule> = {
|
||||
lead: "",
|
||||
members_list: [],
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
status: null,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
module?: IModule;
|
||||
isOpen: boolean;
|
||||
moduleIssues: ModuleIssueResponse[] | undefined;
|
||||
handleDeleteModule: () => void;
|
||||
};
|
||||
|
||||
const ModuleDetailSidebar: React.FC<Props> = ({
|
||||
module,
|
||||
isOpen,
|
||||
moduleIssues,
|
||||
handleDeleteModule,
|
||||
}) => {
|
||||
const [moduleLinkModal, setModuleLinkModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { reset, watch, control } = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const groupedIssues = {
|
||||
backlog: [],
|
||||
unstarted: [],
|
||||
started: [],
|
||||
cancelled: [],
|
||||
completed: [],
|
||||
...groupBy(moduleIssues ?? [], "issue_detail.state_detail.group"),
|
||||
};
|
||||
|
||||
const submitChanges = (data: Partial<IModule>) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
mutate<IModule>(
|
||||
MODULE_DETAILS(moduleId as string),
|
||||
(prevData) => ({
|
||||
...(prevData as IModule),
|
||||
...data,
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
modulesService
|
||||
.patchModule(workspaceSlug as string, projectId as string, moduleId as string, data)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
mutate(MODULE_DETAILS(moduleId as string));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (module)
|
||||
reset({
|
||||
...module,
|
||||
members_list: module.members_list ?? module.members_detail?.map((m) => m.id),
|
||||
});
|
||||
}, [module, reset]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModuleLinkModal
|
||||
isOpen={moduleLinkModal}
|
||||
handleClose={() => setModuleLinkModal(false)}
|
||||
module={module}
|
||||
/>
|
||||
<div
|
||||
className={`fixed top-0 ${
|
||||
isOpen ? "right-0" : "-right-[24rem]"
|
||||
} z-20 h-full w-[24rem] overflow-y-auto border-l bg-gray-50 p-5 duration-300`}
|
||||
>
|
||||
{module ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between pb-3">
|
||||
<h4 className="text-sm font-medium">{module.name}</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border p-2 shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
onClick={() =>
|
||||
copyTextToClipboard(
|
||||
`https://app.plane.so/${workspaceSlug}/projects/${projectId}/modules/${module.id}`
|
||||
)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Module link copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
})
|
||||
}
|
||||
>
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-50 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
onClick={() => handleDeleteModule()}
|
||||
>
|
||||
<TrashIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y-2 divide-gray-100 text-xs">
|
||||
<div className="py-1">
|
||||
<SelectLead
|
||||
control={control}
|
||||
submitChanges={submitChanges}
|
||||
lead={module.lead_detail}
|
||||
/>
|
||||
<SelectMembers control={control} submitChanges={submitChanges} />
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<ChartPieIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Progress</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:basis-1/2">
|
||||
<div className="grid flex-shrink-0 place-items-center">
|
||||
<span className="h-4 w-4">
|
||||
<CircularProgressbar
|
||||
value={groupedIssues.completed.length}
|
||||
maxValue={moduleIssues?.length}
|
||||
strokeWidth={10}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{groupedIssues.completed.length}/{moduleIssues?.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Start date</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="start_date"
|
||||
render={({ field: { value } }) => (
|
||||
<CustomDatePicker
|
||||
value={value}
|
||||
onChange={(val) =>
|
||||
submitChanges({
|
||||
start_date: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>End date</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value } }) => (
|
||||
<CustomDatePicker
|
||||
value={value}
|
||||
onChange={(val) =>
|
||||
submitChanges({
|
||||
target_date: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<SelectStatus control={control} submitChanges={submitChanges} watch={watch} />
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4>Links</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-100"
|
||||
onClick={() => setModuleLinkModal(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{module.link_module && module.link_module.length > 0
|
||||
? module.link_module.map((link) => (
|
||||
<div key={link.id} className="group relative">
|
||||
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded bg-gray-100 p-1 text-red-500 outline-none duration-300 hover:bg-red-50"
|
||||
onClick={() => {
|
||||
const updatedLinks = module.link_module.filter(
|
||||
(l) => l.id !== link.id
|
||||
);
|
||||
submitChanges({ links_list: updatedLinks });
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<Link href={link.url} target="_blank">
|
||||
<a className="group relative flex gap-2 rounded-md border bg-gray-100 p-2">
|
||||
<div className="mt-0.5">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div>
|
||||
<h5>{link.title}</h5>
|
||||
<p className="mt-0.5 text-gray-500">
|
||||
Added {timeAgo(link.created_at)} ago by{" "}
|
||||
{link.created_by_detail.email}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Loader>
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="15px" width="50%" />
|
||||
<Loader.Item height="15px" width="30%" />
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleDetailSidebar;
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
// icons
|
||||
import { UserIcon } from "@heroicons/react/24/outline";
|
||||
import User from "public/user.png";
|
||||
// types
|
||||
import { IModule, IUserLite } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
control: Control<Partial<IModule>, any>;
|
||||
submitChanges: (formData: Partial<IModule>) => void;
|
||||
lead: IUserLite | null;
|
||||
};
|
||||
|
||||
const SelectLead: React.FC<Props> = ({ control, submitChanges, lead }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: people } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Lead</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="lead"
|
||||
render={({ field: { value } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
submitChanges({ lead: value });
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button className="flex w-full cursor-pointer items-center gap-1 text-xs">
|
||||
<span
|
||||
className={`hidden truncate text-left sm:block ${
|
||||
value ? "" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{lead ? (
|
||||
lead.avatar && lead.avatar !== "" ? (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-transparent">
|
||||
<Image
|
||||
src={lead.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={lead?.first_name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
|
||||
{lead?.first_name && lead.first_name !== ""
|
||||
? lead.first_name.charAt(0)
|
||||
: lead?.email.charAt(0)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="No user"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{lead
|
||||
? lead?.first_name && lead.first_name !== ""
|
||||
? lead?.first_name
|
||||
: lead?.email
|
||||
: "N/A"}
|
||||
</div>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-48 w-full overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
{people ? (
|
||||
people.length > 0 ? (
|
||||
people.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.member.id}
|
||||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "bg-indigo-50" : ""
|
||||
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
|
||||
}
|
||||
value={option.member.id}
|
||||
>
|
||||
{option.member.avatar && option.member.avatar !== "" ? (
|
||||
<div className="relative h-4 w-4">
|
||||
<Image
|
||||
src={option.member.avatar}
|
||||
alt="avatar"
|
||||
className="rounded-full"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
|
||||
{option.member.first_name && option.member.first_name !== ""
|
||||
? option.member.first_name.charAt(0)
|
||||
: option.member.email.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
{option.member.first_name && option.member.first_name !== ""
|
||||
? option.member.first_name
|
||||
: option.member.email}
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No members found</div>
|
||||
)
|
||||
) : (
|
||||
<p className="text-xs text-gray-500 px-2">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectLead;
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// services
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { UserGroupIcon } from "@heroicons/react/24/outline";
|
||||
import workspaceService from "services/workspace.service";
|
||||
// headless ui
|
||||
// ui
|
||||
import { AssigneesList } from "components/ui";
|
||||
// types
|
||||
import { IModule } from "types";
|
||||
// constants
|
||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
control: Control<Partial<IModule>, any>;
|
||||
submitChanges: (formData: Partial<IModule>) => void;
|
||||
};
|
||||
|
||||
const SelectMembers: React.FC<Props> = ({ control, submitChanges }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: people } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Members</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="members_list"
|
||||
render={({ field: { value } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
multiple={true}
|
||||
onChange={(value: any) => {
|
||||
submitChanges({ members_list: value });
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button className="flex w-full cursor-pointer items-center gap-1 text-xs">
|
||||
<span
|
||||
className={`hidden truncate text-left sm:block ${
|
||||
value ? "" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-1 text-xs">
|
||||
{value && Array.isArray(value) ? (
|
||||
<AssigneesList userIds={value} length={10} />
|
||||
) : null}
|
||||
</div>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options className="absolute left-0 z-10 mt-1 max-h-48 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none w-full">
|
||||
<div className="py-1">
|
||||
{people ? (
|
||||
people.length > 0 ? (
|
||||
people.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.member.id}
|
||||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "bg-indigo-50" : ""
|
||||
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
|
||||
}
|
||||
value={option.member.id}
|
||||
>
|
||||
{option.member.avatar && option.member.avatar !== "" ? (
|
||||
<div className="relative h-4 w-4">
|
||||
<Image
|
||||
src={option.member.avatar}
|
||||
alt="avatar"
|
||||
className="rounded-full"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
|
||||
{option.member.first_name && option.member.first_name !== ""
|
||||
? option.member.first_name.charAt(0)
|
||||
: option.member.email.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
{option.member.first_name && option.member.first_name !== ""
|
||||
? option.member.first_name
|
||||
: option.member.email}
|
||||
</Listbox.Option>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No members found</div>
|
||||
)
|
||||
) : (
|
||||
<p className="text-xs text-gray-500 px-2">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectMembers;
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
// react
|
||||
import React from "react";
|
||||
// react-hook-form
|
||||
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
||||
// ui
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { CustomSelect } from "components/ui";
|
||||
// icons
|
||||
// types
|
||||
import { IModule } from "types";
|
||||
// common
|
||||
// constants
|
||||
import { MODULE_STATUS } from "constants/";
|
||||
|
||||
type Props = {
|
||||
control: Control<Partial<IModule>, any>;
|
||||
submitChanges: (formData: Partial<IModule>) => void;
|
||||
watch: UseFormWatch<Partial<IModule>>;
|
||||
};
|
||||
|
||||
const SelectStatus: React.FC<Props> = ({ control, submitChanges, watch }) => (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Status</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="status"
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
label={
|
||||
<span
|
||||
className={`flex items-center gap-2 text-left capitalize ${
|
||||
value ? "" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: MODULE_STATUS?.find((option) => option.value === value)?.color,
|
||||
}}
|
||||
/>
|
||||
{watch("status")}
|
||||
</span>
|
||||
}
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
submitChanges({ status: value });
|
||||
}}
|
||||
>
|
||||
{MODULE_STATUS.map((option) => (
|
||||
<CustomSelect.Option key={option.value} value={option.value}>
|
||||
<>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: option.color }}
|
||||
/>
|
||||
{option.label}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SelectStatus;
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import modulesService from "services/modules.service";
|
||||
// ui
|
||||
import { Button, Input } from "components/ui";
|
||||
// types
|
||||
import type { IModule, ModuleLink } from "types";
|
||||
// fetch-keys
|
||||
import { MODULE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
module: IModule | undefined;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const defaultValues: ModuleLink = {
|
||||
title: "",
|
||||
url: "",
|
||||
};
|
||||
|
||||
const ModuleLinkModal: React.FC<Props> = ({ isOpen, module, handleClose }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
setError,
|
||||
} = useForm<ModuleLink>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: ModuleLink) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
const previousLinks = module?.link_module.map((l) => ({ title: l.title, url: l.url }));
|
||||
|
||||
const payload: Partial<IModule> = {
|
||||
links_list: [...(previousLinks ?? []), formData],
|
||||
};
|
||||
|
||||
await modulesService
|
||||
.patchModule(workspaceSlug as string, projectId as string, moduleId as string, payload)
|
||||
.then((res) => {
|
||||
mutate(MODULE_DETAILS(moduleId as string));
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
Object.keys(err).map((key) => {
|
||||
setError(key as keyof ModuleLink, {
|
||||
message: err[key].join(", "),
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
const timeout = setTimeout(() => {
|
||||
reset(defaultValues);
|
||||
clearTimeout(timeout);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
Add Link
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 space-y-3">
|
||||
<div>
|
||||
<Input
|
||||
id="title"
|
||||
label="Title"
|
||||
name="title"
|
||||
type="text"
|
||||
placeholder="Enter title"
|
||||
autoComplete="off"
|
||||
error={errors.title}
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Title is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="url"
|
||||
label="URL"
|
||||
name="url"
|
||||
type="url"
|
||||
placeholder="Enter URL"
|
||||
autoComplete="off"
|
||||
error={errors.url}
|
||||
register={register}
|
||||
validations={{
|
||||
required: "URL is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<Button theme="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Adding Link..." : "Add Link"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleLinkModal;
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import ConfirmModuleDeletion from "./confirm-module-deletion";
|
||||
// icons
|
||||
import User from "public/user.png";
|
||||
// helpers
|
||||
import { renderShortNumericDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IModule, SelectModuleType } from "types";
|
||||
// common
|
||||
import { MODULE_STATUS } from "constants/";
|
||||
|
||||
type Props = {
|
||||
module: IModule;
|
||||
};
|
||||
|
||||
const SingleModuleCard: React.FC<Props> = ({ module }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
|
||||
const [selectedModuleForDelete, setSelectedModuleForDelete] = useState<SelectModuleType>();
|
||||
const handleDeleteModule = () => {
|
||||
if (!module) return;
|
||||
|
||||
setSelectedModuleForDelete({ ...module, actionType: "delete" });
|
||||
setModuleDeleteModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group/card h-full w-full relative select-none p-2">
|
||||
<div className="absolute top-4 right-4 z-50 bg-red-200 opacity-0 group-hover/card:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center bg-white p-1 text-red-500 outline-none duration-300 hover:bg-red-50"
|
||||
onClick={() => handleDeleteModule()}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<ConfirmModuleDeletion
|
||||
isOpen={
|
||||
moduleDeleteModal &&
|
||||
!!selectedModuleForDelete &&
|
||||
selectedModuleForDelete.actionType === "delete"
|
||||
}
|
||||
setIsOpen={setModuleDeleteModal}
|
||||
data={selectedModuleForDelete}
|
||||
/>
|
||||
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
|
||||
<a className="flex flex-col cursor-pointer rounded-md border bg-white p-3 ">
|
||||
<span className="w-3/4 text-ellipsis overflow-hidden">{module.name}</span>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2 text-xs md:grid-cols-4">
|
||||
<div className="space-y-2">
|
||||
<h6 className="text-gray-500">LEAD</h6>
|
||||
<div>
|
||||
{module.lead ? (
|
||||
module.lead_detail?.avatar && module.lead_detail.avatar !== "" ? (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white">
|
||||
<Image
|
||||
src={module.lead_detail.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={module.lead_detail.first_name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
|
||||
{module.lead_detail?.first_name && module.lead_detail.first_name !== ""
|
||||
? module.lead_detail.first_name.charAt(0)
|
||||
: module.lead_detail?.email.charAt(0)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
"N/A"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h6 className="text-gray-500">MEMBERS</h6>
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{module.members && module.members.length > 0 ? (
|
||||
module?.members_detail?.map((member, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
index !== 0 ? "-ml-2.5" : ""
|
||||
}`}
|
||||
>
|
||||
{member?.avatar && member.avatar !== "" ? (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={member.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={member?.first_name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
|
||||
{member?.first_name && member.first_name !== ""
|
||||
? member.first_name.charAt(0)
|
||||
: member?.email?.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt="No user"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h6 className="text-gray-500">END DATE</h6>
|
||||
<div className="flex w-min cursor-pointer items-center gap-1 whitespace-nowrap rounded border px-1.5 py-0.5 text-xs shadow-sm">
|
||||
<CalendarDaysIcon className="h-3 w-3" />
|
||||
{renderShortNumericDateFormat(module.target_date ?? "")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h6 className="text-gray-500">STATUS</h6>
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: MODULE_STATUS.find((s) => s.value === module.status)?.color,
|
||||
}}
|
||||
/>
|
||||
{module.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleModuleCard;
|
||||
Loading…
Add table
Add a link
Reference in a new issue