forked from github/plane
feat: modules, style: kanban board, shortcut modals
This commit is contained in:
parent
9539fca585
commit
f6ca842d30
@ -1,18 +1,22 @@
|
|||||||
|
// react
|
||||||
import React, { useState, useCallback, useEffect } from "react";
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// swr
|
|
||||||
import { mutate } from "swr";
|
|
||||||
// react hook form
|
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
|
||||||
// headless ui
|
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
|
||||||
// services
|
|
||||||
import issuesServices from "lib/services/issues.service";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useTheme from "lib/hooks/useTheme";
|
import useTheme from "lib/hooks/useTheme";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
|
// components
|
||||||
|
import ShortcutsModal from "components/command-palette/shortcuts";
|
||||||
|
import CreateProjectModal from "components/project/create-project-modal";
|
||||||
|
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
|
||||||
|
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
|
||||||
|
import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal";
|
||||||
|
import BulkDeleteIssuesModal from "components/common/bulk-delete-issues-modal";
|
||||||
|
// headless ui
|
||||||
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
@ -20,25 +24,10 @@ import {
|
|||||||
ClipboardDocumentListIcon,
|
ClipboardDocumentListIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// components
|
|
||||||
import ShortcutsModal from "components/command-palette/shortcuts";
|
|
||||||
import CreateProjectModal from "components/project/create-project-modal";
|
|
||||||
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
|
|
||||||
import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal";
|
|
||||||
// ui
|
|
||||||
import { Button } from "ui";
|
|
||||||
// types
|
// types
|
||||||
import { IIssue, IssueResponse } from "types";
|
import { IIssue } from "types";
|
||||||
// fetch keys
|
// common
|
||||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
|
||||||
// constants
|
|
||||||
import { classNames, copyTextToClipboard } from "constants/common";
|
import { classNames, copyTextToClipboard } from "constants/common";
|
||||||
import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal";
|
|
||||||
|
|
||||||
type FormInput = {
|
|
||||||
issue_ids: string[];
|
|
||||||
cycleId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CommandPalette: React.FC = () => {
|
const CommandPalette: React.FC = () => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@ -49,8 +38,9 @@ const CommandPalette: React.FC = () => {
|
|||||||
const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false);
|
const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false);
|
||||||
const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false);
|
const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false);
|
||||||
const [isCreateModuleModalOpen, setisCreateModuleModalOpen] = useState(false);
|
const [isCreateModuleModalOpen, setisCreateModuleModalOpen] = useState(false);
|
||||||
|
const [isBulkDeleteIssuesModalOpen, setIsBulkDeleteIssuesModalOpen] = useState(false);
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, issues } = useUser();
|
const { activeProject, issues } = useUser();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -64,8 +54,6 @@ const CommandPalette: React.FC = () => {
|
|||||||
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
const { register, handleSubmit, reset } = useForm<FormInput>();
|
|
||||||
|
|
||||||
const quickActions = [
|
const quickActions = [
|
||||||
{
|
{
|
||||||
name: "Add new issue...",
|
name: "Add new issue...",
|
||||||
@ -88,7 +76,6 @@ const CommandPalette: React.FC = () => {
|
|||||||
const handleCommandPaletteClose = () => {
|
const handleCommandPaletteClose = () => {
|
||||||
setIsPaletteOpen(false);
|
setIsPaletteOpen(false);
|
||||||
setQuery("");
|
setQuery("");
|
||||||
reset();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
@ -114,6 +101,9 @@ const CommandPalette: React.FC = () => {
|
|||||||
} else if ((e.ctrlKey || e.metaKey) && e.key === "m") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "m") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setisCreateModuleModalOpen(true);
|
setisCreateModuleModalOpen(true);
|
||||||
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "d") {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsBulkDeleteIssuesModalOpen(true);
|
||||||
} else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") {
|
} else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -138,47 +128,6 @@ const CommandPalette: React.FC = () => {
|
|||||||
[toggleCollapsed, setToastAlert, router]
|
[toggleCollapsed, setToastAlert, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDelete: SubmitHandler<FormInput> = (data) => {
|
|
||||||
if (!data.issue_ids || data.issue_ids.length === 0) {
|
|
||||||
setToastAlert({
|
|
||||||
title: "Error",
|
|
||||||
type: "error",
|
|
||||||
message: "Please select atleast one issue",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeWorkspace && activeProject) {
|
|
||||||
issuesServices
|
|
||||||
.bulkDeleteIssues(activeWorkspace.slug, activeProject.id, data)
|
|
||||||
.then((res) => {
|
|
||||||
setToastAlert({
|
|
||||||
title: "Success",
|
|
||||||
type: "success",
|
|
||||||
message: res.message,
|
|
||||||
});
|
|
||||||
mutate<IssueResponse>(
|
|
||||||
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
|
|
||||||
(prevData) => {
|
|
||||||
return {
|
|
||||||
...(prevData as IssueResponse),
|
|
||||||
count: (prevData?.results ?? []).filter(
|
|
||||||
(p) => !data.issue_ids.some((id) => p.id === id)
|
|
||||||
).length,
|
|
||||||
results: (prevData?.results ?? []).filter(
|
|
||||||
(p) => !data.issue_ids.some((id) => p.id === id)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
@ -207,6 +156,10 @@ const CommandPalette: React.FC = () => {
|
|||||||
setIsOpen={setIsIssueModalOpen}
|
setIsOpen={setIsIssueModalOpen}
|
||||||
projectId={activeProject?.id}
|
projectId={activeProject?.id}
|
||||||
/>
|
/>
|
||||||
|
<BulkDeleteIssuesModal
|
||||||
|
isOpen={isBulkDeleteIssuesModalOpen}
|
||||||
|
setIsOpen={setIsBulkDeleteIssuesModalOpen}
|
||||||
|
/>
|
||||||
<Transition.Root
|
<Transition.Root
|
||||||
show={isPaletteOpen}
|
show={isPaletteOpen}
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
@ -283,12 +236,6 @@ const CommandPalette: React.FC = () => {
|
|||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
{...register("issue_ids")}
|
|
||||||
id={`issue-${issue.id}`}
|
|
||||||
value={issue.id}
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||||
style={{
|
style={{
|
||||||
@ -377,9 +324,6 @@ const CommandPalette: React.FC = () => {
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
|
|
||||||
<div className="flex justify-end items-center gap-2 p-3">
|
<div className="flex justify-end items-center gap-2 p-3">
|
||||||
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
|
|
||||||
Delete selected issues
|
|
||||||
</Button>
|
|
||||||
<div>
|
<div>
|
||||||
<Button type="button" size="sm" onClick={handleCommandPaletteClose}>
|
<Button type="button" size="sm" onClick={handleCommandPaletteClose}>
|
||||||
Close
|
Close
|
||||||
|
@ -30,6 +30,8 @@ const shortcuts = [
|
|||||||
{ keys: "ctrl,p", description: "To create project" },
|
{ keys: "ctrl,p", description: "To create project" },
|
||||||
{ keys: "ctrl,i", description: "To create issue" },
|
{ keys: "ctrl,i", description: "To create issue" },
|
||||||
{ keys: "ctrl,q", description: "To create cycle" },
|
{ keys: "ctrl,q", description: "To create cycle" },
|
||||||
|
{ keys: "ctrl,m", description: "To create module" },
|
||||||
|
{ keys: "ctrl,d", description: "To bulk delete issues" },
|
||||||
{ keys: "ctrl,h", description: "To open shortcuts guide" },
|
{ keys: "ctrl,h", description: "To open shortcuts guide" },
|
||||||
{
|
{
|
||||||
keys: "ctrl,alt,c",
|
keys: "ctrl,alt,c",
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
import { PRIORITIES } from "constants/";
|
import { PRIORITIES } from "constants/";
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { getPriorityIcon } from "constants/global";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
@ -95,7 +96,7 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={`rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
className={`grid place-items-center rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||||
issue.priority === "urgent"
|
issue.priority === "urgent"
|
||||||
? "bg-red-100 text-red-600"
|
? "bg-red-100 text-red-600"
|
||||||
: issue.priority === "high"
|
: issue.priority === "high"
|
||||||
@ -107,7 +108,7 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
: "bg-gray-100"
|
: "bg-gray-100"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{issue.priority ?? "None"}
|
{getPriorityIcon(issue?.priority ?? "None")}
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -124,18 +125,19 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active ? "bg-indigo-50" : "bg-white",
|
active ? "bg-indigo-50" : "bg-white",
|
||||||
"cursor-pointer capitalize select-none px-3 py-2"
|
"flex items-center gap-2 cursor-pointer capitalize select-none px-3 py-2"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={priority}
|
value={priority}
|
||||||
>
|
>
|
||||||
|
{getPriorityIcon(priority)}
|
||||||
{priority}
|
{priority}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
</Listbox.Options>
|
</Listbox.Options>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
{/* <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
|
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
|
||||||
<div
|
<div
|
||||||
className={`capitalize ${
|
className={`capitalize ${
|
||||||
@ -152,7 +154,7 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{issue.priority ?? "None"}
|
{issue.priority ?? "None"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
@ -210,10 +212,10 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
</Listbox.Options>
|
</Listbox.Options>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
{/* <div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
<h5 className="font-medium mb-1">State</h5>
|
<h5 className="font-medium mb-1">State</h5>
|
||||||
<div>{issue.state_detail.name}</div>
|
<div>{issue.state_detail.name}</div>
|
||||||
</div>
|
</div> */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
@ -222,10 +224,10 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded 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">
|
<div className="group flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded 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">
|
||||||
<CalendarDaysIcon className="h-4 w-4" />
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
{issue.start_date ? renderShortNumericDateFormat(issue.start_date) : "N/A"}
|
{issue.start_date ? renderShortNumericDateFormat(issue.start_date) : "N/A"}
|
||||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
{/* <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
<h5 className="font-medium mb-1">Started at</h5>
|
<h5 className="font-medium mb-1">Started at</h5>
|
||||||
<div>{renderShortNumericDateFormat(issue.start_date ?? "")}</div>
|
<div>{renderShortNumericDateFormat(issue.start_date ?? "")}</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.due_date && (
|
{properties.due_date && (
|
||||||
@ -240,7 +242,7 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-4 w-4" />
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
{issue.target_date ? renderShortNumericDateFormat(issue.target_date) : "N/A"}
|
{issue.target_date ? renderShortNumericDateFormat(issue.target_date) : "N/A"}
|
||||||
<div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
{/* <div className="fixed -translate-y-3/4 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
<h5 className="font-medium mb-1 text-gray-900">Target date</h5>
|
<h5 className="font-medium mb-1 text-gray-900">Target date</h5>
|
||||||
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
|
<div>{renderShortNumericDateFormat(issue.target_date ?? "")}</div>
|
||||||
<div>
|
<div>
|
||||||
@ -251,7 +253,7 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days`
|
? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days`
|
||||||
: "Due date")}
|
: "Due date")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{properties.assignee && (
|
{properties.assignee && (
|
||||||
@ -373,14 +375,14 @@ const SingleIssue: React.FC<Props> = ({
|
|||||||
</Listbox.Options>
|
</Listbox.Options>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
{/* <div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
<h5 className="font-medium mb-1">Assigned to</h5>
|
<h5 className="font-medium mb-1">Assigned to</h5>
|
||||||
<div>
|
<div>
|
||||||
{issue.assignee_details?.length > 0
|
{issue.assignee_details?.length > 0
|
||||||
? issue.assignee_details.map((assignee) => assignee.first_name).join(", ")
|
? issue.assignee_details.map((assignee) => assignee.first_name).join(", ")
|
||||||
: "No one"}
|
: "No one"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
|
230
apps/app/components/common/bulk-delete-issues-modal.tsx
Normal file
230
apps/app/components/common/bulk-delete-issues-modal.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
// swr
|
||||||
|
import { mutate } from "swr";
|
||||||
|
// react hook form
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import issuesServices from "lib/services/issues.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
|
// headless ui
|
||||||
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
// icons
|
||||||
|
import { FolderIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IIssue, IssueResponse } from "types";
|
||||||
|
// fetch keys
|
||||||
|
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
|
// common
|
||||||
|
import { classNames } from "constants/common";
|
||||||
|
|
||||||
|
type FormInput = {
|
||||||
|
issue_ids: string[];
|
||||||
|
cycleId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const filteredIssues: IIssue[] =
|
||||||
|
query === ""
|
||||||
|
? issues?.results ?? []
|
||||||
|
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
const { register, handleSubmit, reset } = useForm<FormInput>();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setQuery("");
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete: SubmitHandler<FormInput> = (data) => {
|
||||||
|
if (!data.issue_ids || data.issue_ids.length === 0) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error",
|
||||||
|
type: "error",
|
||||||
|
message: "Please select atleast one issue",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeWorkspace && activeProject) {
|
||||||
|
issuesServices
|
||||||
|
.bulkDeleteIssues(activeWorkspace.slug, activeProject.id, data)
|
||||||
|
.then((res) => {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
type: "success",
|
||||||
|
message: res.message,
|
||||||
|
});
|
||||||
|
mutate<IssueResponse>(
|
||||||
|
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
|
||||||
|
(prevData) => {
|
||||||
|
return {
|
||||||
|
...(prevData as IssueResponse),
|
||||||
|
count: (prevData?.results ?? []).filter(
|
||||||
|
(p) => !data.issue_ids.some((id) => p.id === id)
|
||||||
|
).length,
|
||||||
|
results: (prevData?.results ?? []).filter(
|
||||||
|
(p) => !data.issue_ids.some((id) => p.id === id)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||||
|
<Dialog as="div" className="relative z-10" 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-25 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
||||||
|
<form>
|
||||||
|
<Combobox>
|
||||||
|
<div className="relative m-1">
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<Combobox.Input
|
||||||
|
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
|
||||||
|
placeholder="Search..."
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Combobox.Options
|
||||||
|
static
|
||||||
|
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
||||||
|
>
|
||||||
|
{filteredIssues.length > 0 && (
|
||||||
|
<>
|
||||||
|
<li className="p-2">
|
||||||
|
{query === "" && (
|
||||||
|
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||||
|
Select issues
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<ul className="text-sm text-gray-700">
|
||||||
|
{filteredIssues.map((issue) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={issue.id}
|
||||||
|
as="label"
|
||||||
|
htmlFor={`issue-${issue.id}`}
|
||||||
|
value={{
|
||||||
|
name: issue.name,
|
||||||
|
url: `/projects/${issue.project}/issues/${issue.id}`,
|
||||||
|
}}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
"flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2",
|
||||||
|
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ active }) => (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{...register("issue_ids")}
|
||||||
|
id={`issue-${issue.id}`}
|
||||||
|
value={issue.id}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
|
</span>
|
||||||
|
<span>{issue.name}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Options>
|
||||||
|
|
||||||
|
{query !== "" && filteredIssues.length === 0 && (
|
||||||
|
<div className="py-14 px-6 text-center sm:px-14">
|
||||||
|
<FolderIcon
|
||||||
|
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<p className="mt-4 text-sm text-gray-900">
|
||||||
|
We couldn{"'"}t find any issue with that term. Please try again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
|
||||||
|
<div className="flex justify-end items-center gap-2 p-3">
|
||||||
|
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
|
||||||
|
Delete selected issues
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<Button type="button" size="sm" onClick={handleClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BulkDeleteIssuesModal;
|
@ -2,44 +2,42 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "ui";
|
import { Button } from "ui";
|
||||||
// services
|
|
||||||
import issuesServices from "lib/services/issues.service";
|
|
||||||
// hooks
|
|
||||||
import useUser from "lib/hooks/useUser";
|
|
||||||
import useToast from "lib/hooks/useToast";
|
|
||||||
// icons
|
// icons
|
||||||
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IssueResponse } from "types";
|
import { IIssue, IssueResponse } from "types";
|
||||||
// constants
|
// common
|
||||||
import { classNames } from "constants/common";
|
import { classNames } from "constants/common";
|
||||||
import { mutate } from "swr";
|
|
||||||
import { CYCLE_ISSUES } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
handleClose: () => void;
|
|
||||||
issues: IssueResponse | undefined;
|
|
||||||
cycleId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormInput = {
|
type FormInput = {
|
||||||
issues: string[];
|
issues: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const CycleIssuesListModal: React.FC<Props> = ({
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
type: string;
|
||||||
|
issues: IIssue[];
|
||||||
|
handleOnSubmit: (data: FormInput) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExistingIssuesListModal: React.FC<Props> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
handleClose: onClose,
|
handleClose: onClose,
|
||||||
issues,
|
issues,
|
||||||
cycleId,
|
handleOnSubmit,
|
||||||
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const { activeWorkspace, activeProject } = useUser();
|
const { activeProject } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -60,7 +58,7 @@ const CycleIssuesListModal: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddToCycle: SubmitHandler<FormInput> = (data) => {
|
const onSubmit: SubmitHandler<FormInput> = (data) => {
|
||||||
if (!data.issues || data.issues.length === 0) {
|
if (!data.issues || data.issues.length === 0) {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@ -70,25 +68,14 @@ const CycleIssuesListModal: React.FC<Props> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeWorkspace && activeProject) {
|
handleOnSubmit(data);
|
||||||
issuesServices
|
handleClose();
|
||||||
.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, data)
|
|
||||||
.then((res) => {
|
|
||||||
console.log(res);
|
|
||||||
mutate(CYCLE_ISSUES(cycleId));
|
|
||||||
handleClose();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredIssues: IIssue[] =
|
const filteredIssues: IIssue[] =
|
||||||
query === ""
|
query === ""
|
||||||
? issues?.results ?? []
|
? issues ?? []
|
||||||
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
: issues.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? [];
|
||||||
[];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -143,12 +130,12 @@ const CycleIssuesListModal: React.FC<Props> = ({
|
|||||||
<li className="p-2">
|
<li className="p-2">
|
||||||
{query === "" && (
|
{query === "" && (
|
||||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||||
Select issues to add to cycle
|
Select issues to add to {type}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
<ul className="text-sm text-gray-700">
|
<ul className="text-sm text-gray-700">
|
||||||
{filteredIssues.map((issue) => {
|
{filteredIssues.map((issue) => {
|
||||||
if (!issue.issue_cycle)
|
if ((type === "cycle" && !issue.issue_cycle) || type === "module")
|
||||||
return (
|
return (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
@ -206,10 +193,10 @@ const CycleIssuesListModal: React.FC<Props> = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSubmit(handleAddToCycle)}
|
onClick={handleSubmit(onSubmit)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Adding..." : "Add to Cycle"}
|
{isSubmitting ? "Adding..." : `Add to ${type}`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -222,4 +209,4 @@ const CycleIssuesListModal: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CycleIssuesListModal;
|
export default ExistingIssuesListModal;
|
@ -8,8 +8,6 @@ import workspaceService from "lib/services/workspace.service";
|
|||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// components
|
// components
|
||||||
import SingleIssue from "components/common/board-view/single-issue";
|
import SingleIssue from "components/common/board-view/single-issue";
|
||||||
// headless ui
|
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "ui";
|
import { CustomMenu } from "ui";
|
||||||
// icons
|
// icons
|
||||||
@ -19,7 +17,7 @@ import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types";
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// common
|
// common
|
||||||
import { addSpaceIfCamelCase, classNames } from "constants/common";
|
import { addSpaceIfCamelCase } from "constants/common";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
@ -46,7 +44,7 @@ type Props = {
|
|||||||
stateId: string | null;
|
stateId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SingleCycleBoard: React.FC<Props> = ({
|
const SingleModuleBoard: React.FC<Props> = ({
|
||||||
properties,
|
properties,
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
@ -207,4 +205,4 @@ const SingleCycleBoard: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SingleCycleBoard;
|
export default SingleModuleBoard;
|
||||||
|
@ -35,6 +35,7 @@ type Props = {
|
|||||||
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||||
openIssuesListModal: () => void;
|
openIssuesListModal: () => void;
|
||||||
removeIssueFromCycle: (bridgeId: string) => void;
|
removeIssueFromCycle: (bridgeId: string) => void;
|
||||||
|
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
setPreloadedData: React.Dispatch<
|
setPreloadedData: React.Dispatch<
|
||||||
React.SetStateAction<
|
React.SetStateAction<
|
||||||
| (Partial<IIssue> & {
|
| (Partial<IIssue> & {
|
||||||
@ -52,6 +53,7 @@ const CyclesListView: React.FC<Props> = ({
|
|||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
properties,
|
properties,
|
||||||
removeIssueFromCycle,
|
removeIssueFromCycle,
|
||||||
|
handleDeleteIssue,
|
||||||
setPreloadedData,
|
setPreloadedData,
|
||||||
}) => {
|
}) => {
|
||||||
const { activeWorkspace, activeProject, states } = useUser();
|
const { activeWorkspace, activeProject, states } = useUser();
|
||||||
@ -264,7 +266,11 @@ const CyclesListView: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
Remove from cycle
|
Remove from cycle
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem>Delete permanently</CustomMenu.MenuItem>
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => handleDeleteIssue(issue.id)}
|
||||||
|
>
|
||||||
|
Delete permanently
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -115,7 +115,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addIssueToCycle = async (issueId: string, cycleId: string, issueDetail: IIssue) => {
|
const addIssueToCycle = async (issueId: string, cycleId: string) => {
|
||||||
if (!activeWorkspace || !activeProject) return;
|
if (!activeWorkspace || !activeProject) return;
|
||||||
await issuesServices
|
await issuesServices
|
||||||
.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, {
|
.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, {
|
||||||
@ -169,7 +169,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
mutate<IssueResponse>(PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id));
|
mutate<IssueResponse>(PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id));
|
||||||
|
|
||||||
if (formData.sprints && formData.sprints !== null) {
|
if (formData.sprints && formData.sprints !== null) {
|
||||||
await addIssueToCycle(res.id, formData.sprints, formData);
|
await addIssueToCycle(res.id, formData.sprints);
|
||||||
}
|
}
|
||||||
handleClose();
|
handleClose();
|
||||||
resetForm();
|
resetForm();
|
||||||
@ -209,7 +209,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
if (formData.sprints && formData.sprints !== null) {
|
if (formData.sprints && formData.sprints !== null) {
|
||||||
await addIssueToCycle(res.id, formData.sprints, formData);
|
await addIssueToCycle(res.id, formData.sprints);
|
||||||
}
|
}
|
||||||
handleClose();
|
handleClose();
|
||||||
resetForm();
|
resetForm();
|
||||||
|
@ -51,7 +51,7 @@ const SelectProject: React.FC<Props> = ({ control }) => {
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
<div className="p-1">
|
<div className="py-1">
|
||||||
{projects ? (
|
{projects ? (
|
||||||
projects.length > 0 ? (
|
projects.length > 0 ? (
|
||||||
projects.map((project) => (
|
projects.map((project) => (
|
||||||
@ -59,8 +59,8 @@ const SelectProject: React.FC<Props> = ({ control }) => {
|
|||||||
key={project.id}
|
key={project.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`${
|
`${
|
||||||
active ? "text-white bg-theme" : "text-gray-900"
|
active ? "bg-indigo-50" : ""
|
||||||
} cursor-pointer select-none p-2 rounded-md`
|
} text-gray-900 cursor-pointer select-none p-2`
|
||||||
}
|
}
|
||||||
value={project.id}
|
value={project.id}
|
||||||
>
|
>
|
||||||
|
@ -15,7 +15,7 @@ import { Listbox, Transition } from "@headlessui/react";
|
|||||||
// ui
|
// ui
|
||||||
import { Spinner } from "ui";
|
import { Spinner } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { UserGroupIcon } from "@heroicons/react/24/outline";
|
||||||
import User from "public/user.png";
|
import User from "public/user.png";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
@ -39,7 +39,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center py-2 flex-wrap">
|
<div className="flex items-center py-2 flex-wrap">
|
||||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||||
<ArrowPathIcon className="flex-shrink-0 h-4 w-4" />
|
<UserGroupIcon className="flex-shrink-0 h-4 w-4" />
|
||||||
<p>Assignees</p>
|
<p>Assignees</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/2">
|
<div className="sm:basis-1/2">
|
||||||
@ -128,7 +128,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-auto bg-white shadow-lg max-h-48 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
<Listbox.Options className="absolute z-10 left-0 mt-1 w-auto bg-white shadow-lg max-h-48 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{people ? (
|
{people ? (
|
||||||
people.length > 0 ? (
|
people.length > 0 ? (
|
||||||
|
@ -79,7 +79,7 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeBlocked = (issueId: string) => {
|
const removeBlocked = (issueId: string) => {
|
||||||
|
96
apps/app/components/project/modules/board-view/index.tsx
Normal file
96
apps/app/components/project/modules/board-view/index.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// components
|
||||||
|
import SingleBoard from "components/project/modules/board-view/single-board";
|
||||||
|
// ui
|
||||||
|
import { Spinner } from "ui";
|
||||||
|
// types
|
||||||
|
import { IIssue, IProjectMember, NestedKeyOf, Properties } from "types";
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
groupedByIssues: {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
};
|
||||||
|
properties: Properties;
|
||||||
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
|
members: IProjectMember[] | undefined;
|
||||||
|
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||||
|
openIssuesListModal: () => void;
|
||||||
|
removeIssueFromModule: (issueId: string) => void;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
|
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
setPreloadedData: React.Dispatch<
|
||||||
|
React.SetStateAction<
|
||||||
|
| (Partial<IIssue> & {
|
||||||
|
actionType: "createIssue" | "edit" | "delete";
|
||||||
|
})
|
||||||
|
| undefined
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModulesBoardView: React.FC<Props> = ({
|
||||||
|
groupedByIssues,
|
||||||
|
properties,
|
||||||
|
selectedGroup,
|
||||||
|
members,
|
||||||
|
openCreateIssueModal,
|
||||||
|
openIssuesListModal,
|
||||||
|
removeIssueFromModule,
|
||||||
|
partialUpdateIssue,
|
||||||
|
handleDeleteIssue,
|
||||||
|
setPreloadedData,
|
||||||
|
}) => {
|
||||||
|
const { states } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{groupedByIssues ? (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<div className="h-full w-full overflow-hidden">
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<div className="flex gap-x-4 h-full overflow-x-auto overflow-y-hidden pb-3">
|
||||||
|
{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
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
properties={properties}
|
||||||
|
removeIssueFromModule={removeIssueFromModule}
|
||||||
|
openIssuesListModal={openIssuesListModal}
|
||||||
|
openCreateIssueModal={openCreateIssueModal}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
setPreloadedData={setPreloadedData}
|
||||||
|
stateId={
|
||||||
|
selectedGroup === "state_detail.name"
|
||||||
|
? states?.find((s) => s.name === singleGroup)?.id ?? null
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModulesBoardView;
|
208
apps/app/components/project/modules/board-view/single-board.tsx
Normal file
208
apps/app/components/project/modules/board-view/single-board.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// services
|
||||||
|
import workspaceService from "lib/services/workspace.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// components
|
||||||
|
import SingleIssue from "components/common/board-view/single-issue";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu } from "ui";
|
||||||
|
// icons
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
// common
|
||||||
|
import { addSpaceIfCamelCase } from "constants/common";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
properties: Properties;
|
||||||
|
groupedByIssues: {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
};
|
||||||
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
|
groupTitle: string;
|
||||||
|
createdBy: string | null;
|
||||||
|
bgColor?: string;
|
||||||
|
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||||
|
openIssuesListModal: () => void;
|
||||||
|
removeIssueFromModule: (bridgeId: string) => void;
|
||||||
|
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void;
|
||||||
|
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
setPreloadedData: React.Dispatch<
|
||||||
|
React.SetStateAction<
|
||||||
|
| (Partial<IIssue> & {
|
||||||
|
actionType: "createIssue" | "edit" | "delete";
|
||||||
|
})
|
||||||
|
| undefined
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
stateId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SingleCycleBoard: React.FC<Props> = ({
|
||||||
|
properties,
|
||||||
|
groupedByIssues,
|
||||||
|
selectedGroup,
|
||||||
|
groupTitle,
|
||||||
|
createdBy,
|
||||||
|
bgColor,
|
||||||
|
openCreateIssueModal,
|
||||||
|
openIssuesListModal,
|
||||||
|
removeIssueFromModule,
|
||||||
|
partialUpdateIssue,
|
||||||
|
handleDeleteIssue,
|
||||||
|
setPreloadedData,
|
||||||
|
stateId,
|
||||||
|
}) => {
|
||||||
|
// Collapse/Expand
|
||||||
|
const [show, setState] = useState(true);
|
||||||
|
|
||||||
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
|
if (selectedGroup === "priority")
|
||||||
|
groupTitle === "high"
|
||||||
|
? (bgColor = "#dc2626")
|
||||||
|
: groupTitle === "medium"
|
||||||
|
? (bgColor = "#f97316")
|
||||||
|
: groupTitle === "low"
|
||||||
|
? (bgColor = "#22c55e")
|
||||||
|
: (bgColor = "#ff0000");
|
||||||
|
|
||||||
|
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||||
|
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||||
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`rounded flex-shrink-0 h-full ${!show ? "" : "w-80 bg-gray-50 border"}`}>
|
||||||
|
<div className={`${!show ? "" : "h-full space-y-3 overflow-y-auto flex flex-col"}`}>
|
||||||
|
<div
|
||||||
|
className={`flex justify-between p-3 pb-0 ${
|
||||||
|
!show ? "flex-col bg-gray-50 rounded-md border" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-full flex justify-between items-center ${
|
||||||
|
!show ? "flex-col gap-2" : "gap-1"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-x-1 px-2 bg-slate-900 rounded-md cursor-pointer ${
|
||||||
|
!show ? "py-2 mb-2 flex-col gap-y-2" : ""
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
border: `2px solid ${bgColor}`,
|
||||||
|
backgroundColor: `${bgColor}20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className={`text-[0.9rem] font-medium capitalize`}
|
||||||
|
style={{
|
||||||
|
writingMode: !show ? "vertical-rl" : "horizontal-tb",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{groupTitle === null || groupTitle === "null"
|
||||||
|
? "None"
|
||||||
|
: createdBy
|
||||||
|
? createdBy
|
||||||
|
: addSpaceIfCamelCase(groupTitle)}
|
||||||
|
</h2>
|
||||||
|
<span className="text-gray-500 text-sm ml-0.5">
|
||||||
|
{groupedByIssues[groupTitle].length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CustomMenu width="auto" ellipsis>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`mt-3 space-y-3 h-full overflow-y-auto px-3 pb-3 ${
|
||||||
|
!show ? "hidden" : "block"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<SingleIssue
|
||||||
|
key={childIssue.id}
|
||||||
|
issue={childIssue}
|
||||||
|
properties={properties}
|
||||||
|
assignees={assignees}
|
||||||
|
people={people}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<PlusIcon className="h-3 w-3" />
|
||||||
|
Add issue
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
className="mt-1"
|
||||||
|
optionsPosition="left"
|
||||||
|
withoutBorder
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SingleCycleBoard;
|
147
apps/app/components/project/modules/confirm-module-deleteion.tsx
Normal file
147
apps/app/components/project/modules/confirm-module-deleteion.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// react
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// swr
|
||||||
|
import { mutate } from "swr";
|
||||||
|
// services
|
||||||
|
import modulesService from "lib/services/modules.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
// icons
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import type { IModule } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { MODULE_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
data?: IModule;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfirmModuleDeletion: React.FC<Props> = ({ isOpen, setIsOpen, data }) => {
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const cancelButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletion = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
if (!activeWorkspace || !data) return;
|
||||||
|
await modulesService
|
||||||
|
.deleteModule(activeWorkspace.slug, data.project, data.id)
|
||||||
|
.then(() => {
|
||||||
|
mutate(MODULE_LIST(data.project));
|
||||||
|
router.push(`/projects/${data.project}/modules`);
|
||||||
|
handleClose();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
data && setIsOpen(true);
|
||||||
|
}, [data, setIsOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
className="relative z-10"
|
||||||
|
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-10 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;
|
@ -17,6 +17,9 @@ import type { IModule } from "types";
|
|||||||
import { renderDateFormat } from "constants/common";
|
import { renderDateFormat } from "constants/common";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { MODULE_LIST } from "constants/fetch-keys";
|
import { MODULE_LIST } from "constants/fetch-keys";
|
||||||
|
import SelectLead from "./select-lead";
|
||||||
|
import SelectMembers from "./select-members";
|
||||||
|
import SelectStatus from "./select-status";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -28,6 +31,9 @@ type Props = {
|
|||||||
const defaultValues: Partial<IModule> = {
|
const defaultValues: Partial<IModule> = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
status: null,
|
||||||
|
lead: null,
|
||||||
|
members_list: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, projectId }) => {
|
const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, projectId }) => {
|
||||||
@ -45,6 +51,7 @@ const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, pro
|
|||||||
register,
|
register,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
control,
|
||||||
reset,
|
reset,
|
||||||
setError,
|
setError,
|
||||||
} = useForm<IModule>({
|
} = useForm<IModule>({
|
||||||
@ -140,7 +147,7 @@ const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, pro
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
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">
|
<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)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||||
@ -172,26 +179,6 @@ const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, pro
|
|||||||
register={register}
|
register={register}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
id="status"
|
|
||||||
name="status"
|
|
||||||
label="Status"
|
|
||||||
error={errors.status}
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "Status is required",
|
|
||||||
}}
|
|
||||||
options={[
|
|
||||||
{ label: "Backlog", value: "backlog" },
|
|
||||||
{ label: "Planned", value: "planned" },
|
|
||||||
{ label: "In Progress", value: "in-progress" },
|
|
||||||
{ label: "Paused", value: "paused" },
|
|
||||||
{ label: "Completed", value: "completed" },
|
|
||||||
{ label: "Cancelled", value: "cancelled" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Input
|
<Input
|
||||||
@ -216,6 +203,11 @@ const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, pro
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center flex-wrap gap-2">
|
||||||
|
<SelectStatus control={control} />
|
||||||
|
<SelectLead control={control} />
|
||||||
|
<SelectMembers control={control} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
|
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
|
@ -0,0 +1,61 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// react hook form
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import type { Control } from "react-hook-form";
|
||||||
|
// service
|
||||||
|
import projectServices from "lib/services/project.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// ui
|
||||||
|
import { SearchListbox } from "ui";
|
||||||
|
// icons
|
||||||
|
import { UserIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import type { IModule } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<IModule, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectLead: React.FC<Props> = ({ control }) => {
|
||||||
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
|
const { data: people } = useSWR(
|
||||||
|
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
|
||||||
|
activeWorkspace && activeProject
|
||||||
|
? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lead"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<SearchListbox
|
||||||
|
title="Lead"
|
||||||
|
optionsFontsize="sm"
|
||||||
|
options={people?.map((person) => {
|
||||||
|
return {
|
||||||
|
value: person.member.id,
|
||||||
|
display:
|
||||||
|
person.member.first_name && person.member.first_name !== ""
|
||||||
|
? person.member.first_name
|
||||||
|
: person.member.email,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
icon={<UserIcon className="h-3 w-3 text-gray-500" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectLead;
|
@ -0,0 +1,62 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// react hook form
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import type { Control } from "react-hook-form";
|
||||||
|
// service
|
||||||
|
import projectServices from "lib/services/project.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// ui
|
||||||
|
import { SearchListbox } from "ui";
|
||||||
|
// icons
|
||||||
|
import { UserIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import type { IModule } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<IModule, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectMembers: React.FC<Props> = ({ control }) => {
|
||||||
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
|
const { data: people } = useSWR(
|
||||||
|
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
|
||||||
|
activeWorkspace && activeProject
|
||||||
|
? () => projectServices.projectMembers(activeWorkspace.slug, activeProject.id)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="members_list"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<SearchListbox
|
||||||
|
title="Members"
|
||||||
|
optionsFontsize="sm"
|
||||||
|
options={people?.map((person) => {
|
||||||
|
return {
|
||||||
|
value: person.member.id,
|
||||||
|
display:
|
||||||
|
person.member.first_name && person.member.first_name !== ""
|
||||||
|
? person.member.first_name
|
||||||
|
: person.member.email,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
multiple={true}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
icon={<UserIcon className="h-3 w-3 text-gray-500" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectMembers;
|
@ -0,0 +1,39 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
// react hook form
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import type { Control } from "react-hook-form";
|
||||||
|
// ui
|
||||||
|
import { CustomListbox } from "ui";
|
||||||
|
// icons
|
||||||
|
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import type { IModule } from "types";
|
||||||
|
import { MODULE_STATUS } from "constants/";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<IModule, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectStatus: React.FC<Props> = ({ control }) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="status"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<CustomListbox
|
||||||
|
title="State"
|
||||||
|
options={MODULE_STATUS.map((status) => {
|
||||||
|
return { value: status.value, display: status.label };
|
||||||
|
})}
|
||||||
|
value={value}
|
||||||
|
optionsFontsize="sm"
|
||||||
|
onChange={onChange}
|
||||||
|
icon={<Squares2X2Icon className="h-3 w-3 text-gray-400" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectStatus;
|
328
apps/app/components/project/modules/list-view/index.tsx
Normal file
328
apps/app/components/project/modules/list-view/index.tsx
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
// next
|
||||||
|
import Link from "next/link";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// headless ui
|
||||||
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu, Spinner } from "ui";
|
||||||
|
// icons
|
||||||
|
import { PlusIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IIssue, IWorkspaceMember, NestedKeyOf, Properties } from "types";
|
||||||
|
// fetch keys
|
||||||
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import {
|
||||||
|
addSpaceIfCamelCase,
|
||||||
|
findHowManyDaysLeft,
|
||||||
|
renderShortNumericDateFormat,
|
||||||
|
} from "constants/common";
|
||||||
|
import workspaceService from "lib/services/workspace.service";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
groupedByIssues: {
|
||||||
|
[key: string]: (IIssue & { bridge?: string })[];
|
||||||
|
};
|
||||||
|
properties: Properties;
|
||||||
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
|
openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void;
|
||||||
|
openIssuesListModal: () => void;
|
||||||
|
removeIssueFromModule: (issueId: string) => void;
|
||||||
|
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
setPreloadedData: React.Dispatch<
|
||||||
|
React.SetStateAction<
|
||||||
|
| (Partial<IIssue> & {
|
||||||
|
actionType: "createIssue" | "edit" | "delete";
|
||||||
|
})
|
||||||
|
| undefined
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModulesListView: React.FC<Props> = ({
|
||||||
|
groupedByIssues,
|
||||||
|
selectedGroup,
|
||||||
|
openCreateIssueModal,
|
||||||
|
openIssuesListModal,
|
||||||
|
properties,
|
||||||
|
removeIssueFromModule,
|
||||||
|
handleDeleteIssue,
|
||||||
|
setPreloadedData,
|
||||||
|
}) => {
|
||||||
|
const { activeWorkspace, activeProject, states } = useUser();
|
||||||
|
|
||||||
|
const { data: people } = useSWR<IWorkspaceMember[]>(
|
||||||
|
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||||
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex 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="bg-white rounded-lg">
|
||||||
|
<div className="bg-gray-100 px-4 py-3 rounded-t-lg">
|
||||||
|
<Disclosure.Button>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`h-4 w-4 text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{selectedGroup !== null ? (
|
||||||
|
<h2 className="font-medium leading-5 capitalize">
|
||||||
|
{singleGroup === null || singleGroup === "null"
|
||||||
|
? selectedGroup === "priority" && "No priority"
|
||||||
|
: addSpaceIfCamelCase(singleGroup)}
|
||||||
|
</h2>
|
||||||
|
) : (
|
||||||
|
<h2 className="font-medium leading-5">All Issues</h2>
|
||||||
|
)}
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
key={issue.id}
|
||||||
|
className="px-4 py-3 text-sm rounded flex justify-between items-center gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`flex-shrink-0 h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Link href={`/projects/${activeProject?.id}/issues/${issue.id}`}>
|
||||||
|
<a className="group relative flex items-center gap-2">
|
||||||
|
{properties.key && (
|
||||||
|
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{issue.name}</span>
|
||||||
|
{/* <div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
||||||
|
<h5 className="font-medium mb-1">Name</h5>
|
||||||
|
<div>{issue.name}</div>
|
||||||
|
</div> */}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||||
|
{properties.priority && (
|
||||||
|
<div
|
||||||
|
className={`group relative flex-shrink-0 flex items-center gap-1 text-xs rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||||
|
issue.priority === "urgent"
|
||||||
|
? "bg-red-100 text-red-600"
|
||||||
|
: issue.priority === "high"
|
||||||
|
? "bg-orange-100 text-orange-500"
|
||||||
|
: issue.priority === "medium"
|
||||||
|
? "bg-yellow-100 text-yellow-500"
|
||||||
|
: issue.priority === "low"
|
||||||
|
? "bg-green-100 text-green-500"
|
||||||
|
: "bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* {getPriorityIcon(issue.priority ?? "")} */}
|
||||||
|
{issue.priority ?? "None"}
|
||||||
|
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
|
<h5 className="font-medium mb-1 text-gray-900">Priority</h5>
|
||||||
|
<div
|
||||||
|
className={`capitalize ${
|
||||||
|
issue.priority === "urgent"
|
||||||
|
? "text-red-600"
|
||||||
|
: issue.priority === "high"
|
||||||
|
? "text-orange-500"
|
||||||
|
: issue.priority === "medium"
|
||||||
|
? "text-yellow-500"
|
||||||
|
: issue.priority === "low"
|
||||||
|
? "text-green-500"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{issue.priority ?? "None"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.state && (
|
||||||
|
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded 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">
|
||||||
|
<span
|
||||||
|
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue?.state_detail?.color,
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
{addSpaceIfCamelCase(issue?.state_detail.name)}
|
||||||
|
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
|
<h5 className="font-medium mb-1">State</h5>
|
||||||
|
<div>{issue?.state_detail.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.start_date && (
|
||||||
|
<div className="group relative flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded 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">
|
||||||
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
|
{issue.start_date
|
||||||
|
? renderShortNumericDateFormat(issue.start_date)
|
||||||
|
: "N/A"}
|
||||||
|
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
|
<h5 className="font-medium mb-1">Started at</h5>
|
||||||
|
<div>
|
||||||
|
{renderShortNumericDateFormat(issue.start_date ?? "")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.due_date && (
|
||||||
|
<div
|
||||||
|
className={`group relative flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded 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 ${
|
||||||
|
issue.target_date === null
|
||||||
|
? ""
|
||||||
|
: issue.target_date < new Date().toISOString()
|
||||||
|
? "text-red-600"
|
||||||
|
: findHowManyDaysLeft(issue.target_date) <= 3 &&
|
||||||
|
"text-orange-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
|
{issue.target_date
|
||||||
|
? renderShortNumericDateFormat(issue.target_date)
|
||||||
|
: "N/A"}
|
||||||
|
<div className="absolute bottom-full right-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md whitespace-nowrap">
|
||||||
|
<h5 className="font-medium mb-1 text-gray-900">Due date</h5>
|
||||||
|
<div>
|
||||||
|
{renderShortNumericDateFormat(issue.target_date ?? "")}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{issue.target_date &&
|
||||||
|
(issue.target_date < new Date().toISOString()
|
||||||
|
? `Due date has passed by ${findHowManyDaysLeft(
|
||||||
|
issue.target_date
|
||||||
|
)} days`
|
||||||
|
: findHowManyDaysLeft(issue.target_date) <= 3
|
||||||
|
? `Due date is in ${findHowManyDaysLeft(
|
||||||
|
issue.target_date
|
||||||
|
)} days`
|
||||||
|
: "Due date")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<CustomMenu width="auto" ellipsis>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => openCreateIssueModal(issue, "edit")}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => removeIssueFromModule(issue.bridge ?? "")}
|
||||||
|
>
|
||||||
|
Remove from module
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => handleDeleteIssue(issue.id)}
|
||||||
|
>
|
||||||
|
Delete permanently
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-sm px-4 py-3 text-gray-500">No issues.</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex 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"
|
||||||
|
withoutBorder
|
||||||
|
>
|
||||||
|
<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;
|
@ -0,0 +1,246 @@
|
|||||||
|
// react
|
||||||
|
import { useEffect } from "react";
|
||||||
|
// swr
|
||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import modulesService from "lib/services/modules.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
|
// components
|
||||||
|
import SelectMembers from "components/project/modules/module-detail-sidebar/select-members";
|
||||||
|
import SelectStatus from "components/project/modules/module-detail-sidebar/select-status";
|
||||||
|
// ui
|
||||||
|
import { Spinner } from "ui";
|
||||||
|
// icons
|
||||||
|
import {
|
||||||
|
CalendarDaysIcon,
|
||||||
|
ClipboardDocumentIcon,
|
||||||
|
LinkIcon,
|
||||||
|
PlusIcon,
|
||||||
|
TrashIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IModule } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { MODULE_DETAIL } from "constants/fetch-keys";
|
||||||
|
// common
|
||||||
|
import { copyTextToClipboard } from "constants/common";
|
||||||
|
|
||||||
|
const defaultValues: Partial<IModule> = {
|
||||||
|
members_list: [],
|
||||||
|
start_date: new Date().toString(),
|
||||||
|
target_date: new Date().toString(),
|
||||||
|
status: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
module?: IModule;
|
||||||
|
isOpen: boolean;
|
||||||
|
handleDeleteModule: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModuleDetailSidebar: React.FC<Props> = ({ module, isOpen, handleDeleteModule }) => {
|
||||||
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { reset, watch, control } = useForm({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitChanges = (data: Partial<IModule>) => {
|
||||||
|
if (!activeWorkspace || !activeProject || !module) return;
|
||||||
|
|
||||||
|
modulesService
|
||||||
|
.patchModule(activeWorkspace.slug, activeProject.id, module.id, data)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
mutate(MODULE_DETAIL);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (module)
|
||||||
|
reset({
|
||||||
|
...module,
|
||||||
|
members_list: module.members_list ?? module.members_detail?.map((member) => member.id),
|
||||||
|
});
|
||||||
|
}, [module, reset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`fixed top-0 ${
|
||||||
|
isOpen ? "right-0" : "-right-[24rem]"
|
||||||
|
} z-30 bg-gray-50 border-l h-full p-5 w-[24rem] overflow-y-auto duration-300`}
|
||||||
|
>
|
||||||
|
{module ? (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between items-center pb-3">
|
||||||
|
<h4 className="text-sm font-medium">{module.name}</h4>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
|
||||||
|
onClick={() =>
|
||||||
|
copyTextToClipboard(
|
||||||
|
`https://app.plane.so/projects/${activeProject?.id}/modules/${module.id}`
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Copied to clipboard",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Some error occurred",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
|
||||||
|
onClick={() =>
|
||||||
|
copyTextToClipboard(module.id)
|
||||||
|
.then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Copied to clipboard",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Some error occurred",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-2 hover:bg-red-50 text-red-500 border border-red-500 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
|
||||||
|
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">
|
||||||
|
<div className="flex items-center py-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||||
|
<UserIcon className="flex-shrink-0 h-4 w-4" />
|
||||||
|
<p>Lead</p>
|
||||||
|
</div>
|
||||||
|
<div className="sm:basis-1/2">
|
||||||
|
{module.lead_detail.first_name !== "" ? (
|
||||||
|
<>
|
||||||
|
{module.lead_detail.first_name} {module.lead_detail.last_name}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
module.lead_detail.email
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SelectMembers control={control} submitChanges={submitChanges} />
|
||||||
|
</div>
|
||||||
|
<div className="py-1">
|
||||||
|
<div className="flex items-center py-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||||
|
<CalendarDaysIcon className="flex-shrink-0 h-4 w-4" />
|
||||||
|
<p>Start date</p>
|
||||||
|
</div>
|
||||||
|
<div className="sm:basis-1/2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="start_date"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="issueDate"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={onChange}
|
||||||
|
className="hover:bg-gray-100 bg-transparent border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center py-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||||
|
<CalendarDaysIcon className="flex-shrink-0 h-4 w-4" />
|
||||||
|
<p>End date</p>
|
||||||
|
</div>
|
||||||
|
<div className="sm:basis-1/2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="target_date"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
submitChanges({ target_date: e.target.value });
|
||||||
|
onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
className="hover:bg-gray-100 bg-transparent border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="py-1">
|
||||||
|
<SelectStatus control={control} submitChanges={submitChanges} watch={watch} />
|
||||||
|
</div>
|
||||||
|
<div className="py-1">
|
||||||
|
<div className="flex justify-between items-center gap-2">
|
||||||
|
<h4>Links</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
<div className="flex gap-2 border bg-gray-100 rounded-md p-2">
|
||||||
|
<div className="mt-0.5">
|
||||||
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>Aaryan Khandelwal</h5>
|
||||||
|
<p className="text-gray-500 mt-0.5">
|
||||||
|
Added 2 days ago by aaryan.khandelwal@caravel.tech
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex justify-center items-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModuleDetailSidebar;
|
@ -0,0 +1,188 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
// next
|
||||||
|
import Image from "next/image";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// react-hook-form
|
||||||
|
import { Control, Controller } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import workspaceService from "lib/services/workspace.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// headless ui
|
||||||
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Spinner } from "ui";
|
||||||
|
// icons
|
||||||
|
import { UserGroupIcon } from "@heroicons/react/24/outline";
|
||||||
|
import User from "public/user.png";
|
||||||
|
// types
|
||||||
|
import { IModule } from "types";
|
||||||
|
// constants
|
||||||
|
import { classNames } from "constants/common";
|
||||||
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<Partial<IModule>, any>;
|
||||||
|
submitChanges: (formData: Partial<IModule>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectMembers: React.FC<Props> = ({ control, submitChanges }) => {
|
||||||
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
|
const { data: people } = useSWR(
|
||||||
|
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||||
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center py-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||||
|
<UserGroupIcon className="flex-shrink-0 h-4 w-4" />
|
||||||
|
<p>Assignees</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="w-full flex items-center gap-1 text-xs cursor-pointer">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
value ? "" : "text-gray-900",
|
||||||
|
"hidden truncate sm:block text-left"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1 text-xs cursor-pointer">
|
||||||
|
{value && Array.isArray(value) ? (
|
||||||
|
<>
|
||||||
|
{value.length > 0 ? (
|
||||||
|
value.map((assignee, index: number) => {
|
||||||
|
const person = people?.find(
|
||||||
|
(p) => p.member.id === assignee
|
||||||
|
)?.member;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||||
|
index !== 0 ? "-ml-2.5" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{person && person.avatar && person.avatar !== "" ? (
|
||||||
|
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||||
|
<Image
|
||||||
|
src={person.avatar}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
className="rounded-full"
|
||||||
|
alt={person.first_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full capitalize`}
|
||||||
|
>
|
||||||
|
{person?.first_name && person.first_name !== ""
|
||||||
|
? person.first_name.charAt(0)
|
||||||
|
: person?.email.charAt(0)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||||
|
<Image
|
||||||
|
src={User}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
className="rounded-full"
|
||||||
|
alt="No user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : 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 z-10 left-0 mt-1 w-auto bg-white shadow-lg max-h-48 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto 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 items-center gap-2 text-gray-900 cursor-pointer select-none p-2 truncate`
|
||||||
|
}
|
||||||
|
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="flex-shrink-0 h-4 w-4 bg-gray-700 text-white grid place-items-center capitalize rounded-full">
|
||||||
|
{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 assignees found</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectMembers;
|
@ -0,0 +1,63 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
// react-hook-form
|
||||||
|
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
||||||
|
// ui
|
||||||
|
import { CustomSelect } from "ui";
|
||||||
|
// icons
|
||||||
|
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IModule } from "types";
|
||||||
|
// common
|
||||||
|
import { classNames } from "constants/common";
|
||||||
|
// constants
|
||||||
|
import { MODULE_STATUS } from "constants/";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<Partial<IModule>, any>;
|
||||||
|
submitChanges: (formData: Partial<IModule>) => void;
|
||||||
|
watch: UseFormWatch<Partial<IModule>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectStatus: React.FC<Props> = ({ control, submitChanges, watch }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center py-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||||
|
<Squares2X2Icon className="flex-shrink-0 h-4 w-4" />
|
||||||
|
<p>Status</p>
|
||||||
|
</div>
|
||||||
|
<div className="sm:basis-1/2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="status"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<CustomSelect
|
||||||
|
label={
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
value ? "" : "text-gray-900",
|
||||||
|
"text-left capitalize flex items-center gap-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{watch("status")}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={(value: any) => {
|
||||||
|
submitChanges({ status: value });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{MODULE_STATUS.map((option) => (
|
||||||
|
<CustomSelect.Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectStatus;
|
102
apps/app/components/project/modules/single-module-card.tsx
Normal file
102
apps/app/components/project/modules/single-module-card.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// next
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
// icons
|
||||||
|
import User from "public/user.png";
|
||||||
|
// types
|
||||||
|
import { IModule } from "types";
|
||||||
|
// common
|
||||||
|
import { renderShortNumericDateFormat } from "constants/common";
|
||||||
|
import { CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
module: IModule;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SingleModuleCard: React.FC<Props> = ({ module }) => {
|
||||||
|
return (
|
||||||
|
<div key={module.id} className="border bg-white p-3 rounded-md">
|
||||||
|
<Link href={`/projects/${module.project}/modules/${module.id}`}>
|
||||||
|
<a>{module.name}</a>
|
||||||
|
</Link>
|
||||||
|
<div className="grid grid-cols-4 gap-2 text-xs mt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h6 className="text-gray-500">LEAD</h6>
|
||||||
|
<div>
|
||||||
|
{module.lead_detail?.avatar && module.lead_detail.avatar !== "" ? (
|
||||||
|
<div className="h-5 w-5 border-2 border-white rounded-full">
|
||||||
|
<Image
|
||||||
|
src={module.lead_detail.avatar}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
className="rounded-full"
|
||||||
|
alt={module.lead_detail.first_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full capitalize">
|
||||||
|
{module.lead_detail?.first_name && module.lead_detail.first_name !== ""
|
||||||
|
? module.lead_detail.first_name.charAt(0)
|
||||||
|
: module.lead_detail?.email.charAt(0)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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 border-2 bg-white border-white rounded-full">
|
||||||
|
<Image
|
||||||
|
src={member.avatar}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
className="rounded-full"
|
||||||
|
alt={member?.first_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full capitalize">
|
||||||
|
{member?.first_name && member.first_name !== ""
|
||||||
|
? member.first_name.charAt(0)
|
||||||
|
: member?.email?.charAt(0)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||||
|
<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 items-center gap-1 border rounded shadow-sm px-1.5 py-0.5 cursor-pointer text-xs w-min whitespace-nowrap">
|
||||||
|
<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="capitalize">{module.status}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SingleModuleCard;
|
@ -141,6 +141,6 @@ export const MODULE_ISSUE_DETAIL = (
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
issueId: string
|
bridgeId: string
|
||||||
) =>
|
) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/${issueId}/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/${bridgeId}/`;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
export const getPriorityIcon = (priority: string, className: string) => {
|
export const getPriorityIcon = (priority: string, className?: string) => {
|
||||||
|
if (!className || className === "") className = "text-xs";
|
||||||
|
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case "urgent":
|
case "urgent":
|
||||||
return <span className={`material-symbols-rounded ${className}`}>error</span>;
|
return <span className={`material-symbols-rounded ${className}`}>error</span>;
|
||||||
@ -13,6 +15,6 @@ export const getPriorityIcon = (priority: string, className: string) => {
|
|||||||
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_1_bar</span>
|
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_1_bar</span>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return "None";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,15 @@ export const GROUP_CHOICES = {
|
|||||||
cancelled: "Cancelled",
|
cancelled: "Cancelled",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MODULE_STATUS = [
|
||||||
|
{ label: "Backlog", value: "backlog" },
|
||||||
|
{ label: "Planned", value: "planned" },
|
||||||
|
{ label: "In Progress", value: "in-progress" },
|
||||||
|
{ label: "Paused", value: "paused" },
|
||||||
|
{ label: "Completed", value: "completed" },
|
||||||
|
{ label: "Cancelled", value: "cancelled" },
|
||||||
|
];
|
||||||
|
|
||||||
export const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
export const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
||||||
{ name: "State", key: "state_detail.name" },
|
{ name: "State", key: "state_detail.name" },
|
||||||
{ name: "Priority", key: "priority" },
|
{ name: "Priority", key: "priority" },
|
||||||
|
@ -19,11 +19,13 @@ import {
|
|||||||
PROJECT_ISSUES_LIST,
|
PROJECT_ISSUES_LIST,
|
||||||
STATE_LIST,
|
STATE_LIST,
|
||||||
CYCLE_LIST,
|
CYCLE_LIST,
|
||||||
|
MODULE_LIST,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { KeyedMutator } from "swr";
|
import type { KeyedMutator } from "swr";
|
||||||
import type { IUser, IWorkspace, IProject, IssueResponse, ICycle, IState } from "types";
|
import type { IUser, IWorkspace, IProject, IssueResponse, ICycle, IState, IModule } from "types";
|
||||||
|
import modulesService from "lib/services/modules.service";
|
||||||
|
|
||||||
interface IUserContextProps {
|
interface IUserContextProps {
|
||||||
user?: IUser;
|
user?: IUser;
|
||||||
@ -40,6 +42,8 @@ interface IUserContextProps {
|
|||||||
mutateIssues: KeyedMutator<IssueResponse>;
|
mutateIssues: KeyedMutator<IssueResponse>;
|
||||||
cycles?: ICycle[];
|
cycles?: ICycle[];
|
||||||
mutateCycles: KeyedMutator<ICycle[]>;
|
mutateCycles: KeyedMutator<ICycle[]>;
|
||||||
|
modules?: IModule[];
|
||||||
|
mutateModules: KeyedMutator<IModule[]>;
|
||||||
states?: IState[];
|
states?: IState[];
|
||||||
mutateStates: KeyedMutator<IState[]>;
|
mutateStates: KeyedMutator<IState[]>;
|
||||||
}
|
}
|
||||||
@ -99,6 +103,13 @@ export const UserProvider = ({ children }: { children: ReactElement }) => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: modules, mutate: mutateModules } = useSWR<IModule[]>(
|
||||||
|
activeWorkspace && activeProject ? MODULE_LIST(activeProject.id) : null,
|
||||||
|
activeWorkspace && activeProject
|
||||||
|
? () => modulesService.getModules(activeWorkspace.slug, activeProject.id)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projects) return;
|
if (!projects) return;
|
||||||
const activeProject = projects.find((project) => project.id === projectId);
|
const activeProject = projects.find((project) => project.id === projectId);
|
||||||
@ -143,6 +154,8 @@ export const UserProvider = ({ children }: { children: ReactElement }) => {
|
|||||||
mutateIssues,
|
mutateIssues,
|
||||||
cycles,
|
cycles,
|
||||||
mutateCycles,
|
mutateCycles,
|
||||||
|
modules,
|
||||||
|
mutateModules,
|
||||||
states,
|
states,
|
||||||
mutateStates,
|
mutateStates,
|
||||||
setActiveProject,
|
setActiveProject,
|
||||||
|
@ -50,6 +50,16 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getModuleDetails(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
|
||||||
|
return this.get(MODULE_DETAIL(workspaceSlug, projectId, moduleId))
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async patchModule(
|
async patchModule(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -85,11 +95,11 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addIssueToModule(
|
async addIssuesToModule(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
data: any
|
data: { issues: string[] }
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.post(MODULE_ISSUES(workspaceSlug, projectId, moduleId), data)
|
return this.post(MODULE_ISSUES(workspaceSlug, projectId, moduleId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -104,9 +114,9 @@ class ProjectIssuesServices extends APIService {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
issueId: string
|
bridgeId: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.delete(MODULE_ISSUE_DETAIL(workspaceSlug, projectId, moduleId, issueId))
|
return this.delete(MODULE_ISSUE_DETAIL(workspaceSlug, projectId, moduleId, bridgeId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
|
@ -243,11 +243,11 @@ const MyIssues: NextPage = () => {
|
|||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
)} */}
|
)} */}
|
||||||
<span className="">{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
{/* <div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
||||||
<h5 className="font-medium mb-1">Name</h5>
|
<h5 className="font-medium mb-1">Name</h5>
|
||||||
<div>{issue.name}</div>
|
<div>{issue.name}</div>
|
||||||
</div>
|
</div> */}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,8 +12,8 @@ import AppLayout from "layouts/app-layout";
|
|||||||
import CyclesListView from "components/project/cycles/list-view";
|
import CyclesListView from "components/project/cycles/list-view";
|
||||||
import CyclesBoardView from "components/project/cycles/board-view";
|
import CyclesBoardView from "components/project/cycles/board-view";
|
||||||
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
|
import CreateUpdateIssuesModal from "components/project/issues/create-update-issue-modal";
|
||||||
import CycleIssuesListModal from "components/project/cycles/cycle-issues-list-modal";
|
|
||||||
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
|
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
|
||||||
|
import ExistingIssuesListModal from "components/common/existing-issues-list-modal";
|
||||||
// constants
|
// constants
|
||||||
import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/";
|
import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/";
|
||||||
// services
|
// services
|
||||||
@ -67,7 +67,7 @@ const SingleCycle: React.FC = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
const cycleIssuesArray = cycleIssues?.map((issue) => {
|
const cycleIssuesArray = cycleIssues?.map((issue) => {
|
||||||
return { bridge: issue.id, ...issue.issue_details };
|
return { bridge: issue.id, ...issue.issue_detail };
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
const { data: members } = useSWR(
|
||||||
@ -163,6 +163,20 @@ const SingleCycle: React.FC = () => {
|
|||||||
// console.log(result);
|
// console.log(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddIssuesToCycle = (data: { issues: string[] }) => {
|
||||||
|
if (activeWorkspace && activeProject) {
|
||||||
|
issuesServices
|
||||||
|
.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId as string, data)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
mutate(CYCLE_ISSUES(cycleId as string));
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const removeIssueFromCycle = (bridgeId: string) => {
|
const removeIssueFromCycle = (bridgeId: string) => {
|
||||||
if (activeWorkspace && activeProject) {
|
if (activeWorkspace && activeProject) {
|
||||||
mutate<CycleIssueResponse[]>(
|
mutate<CycleIssueResponse[]>(
|
||||||
@ -191,11 +205,12 @@ const SingleCycle: React.FC = () => {
|
|||||||
setIsOpen={setIsIssueModalOpen}
|
setIsOpen={setIsIssueModalOpen}
|
||||||
projectId={activeProject?.id}
|
projectId={activeProject?.id}
|
||||||
/>
|
/>
|
||||||
<CycleIssuesListModal
|
<ExistingIssuesListModal
|
||||||
isOpen={cycleIssuesListModal}
|
isOpen={cycleIssuesListModal}
|
||||||
handleClose={() => setCycleIssuesListModal(false)}
|
handleClose={() => setCycleIssuesListModal(false)}
|
||||||
issues={issues}
|
type="cycle"
|
||||||
cycleId={cycleId as string}
|
issues={issues?.results ?? []}
|
||||||
|
handleOnSubmit={handleAddIssuesToCycle}
|
||||||
/>
|
/>
|
||||||
<ConfirmIssueDeletion
|
<ConfirmIssueDeletion
|
||||||
handleClose={() => setDeleteIssue(undefined)}
|
handleClose={() => setDeleteIssue(undefined)}
|
||||||
@ -375,23 +390,22 @@ const SingleCycle: React.FC = () => {
|
|||||||
openCreateIssueModal={openCreateIssueModal}
|
openCreateIssueModal={openCreateIssueModal}
|
||||||
openIssuesListModal={openIssuesListModal}
|
openIssuesListModal={openIssuesListModal}
|
||||||
removeIssueFromCycle={removeIssueFromCycle}
|
removeIssueFromCycle={removeIssueFromCycle}
|
||||||
|
handleDeleteIssue={setDeleteIssue}
|
||||||
setPreloadedData={setPreloadedData}
|
setPreloadedData={setPreloadedData}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-screen">
|
<CyclesBoardView
|
||||||
<CyclesBoardView
|
groupedByIssues={groupedByIssues}
|
||||||
groupedByIssues={groupedByIssues}
|
properties={properties}
|
||||||
properties={properties}
|
removeIssueFromCycle={removeIssueFromCycle}
|
||||||
removeIssueFromCycle={removeIssueFromCycle}
|
selectedGroup={groupByProperty}
|
||||||
selectedGroup={groupByProperty}
|
members={members}
|
||||||
members={members}
|
openCreateIssueModal={openCreateIssueModal}
|
||||||
openCreateIssueModal={openCreateIssueModal}
|
openIssuesListModal={openIssuesListModal}
|
||||||
openIssuesListModal={openIssuesListModal}
|
handleDeleteIssue={setDeleteIssue}
|
||||||
handleDeleteIssue={setDeleteIssue}
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
setPreloadedData={setPreloadedData}
|
||||||
setPreloadedData={setPreloadedData}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</>
|
</>
|
||||||
|
@ -334,6 +334,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div></div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full grid place-items-center px-4 sm:px-0">
|
<div className="h-full w-full grid place-items-center px-4 sm:px-0">
|
||||||
|
407
apps/app/pages/projects/[projectId]/modules/[moduleId].tsx
Normal file
407
apps/app/pages/projects/[projectId]/modules/[moduleId].tsx
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// swr
|
||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
// services
|
||||||
|
import modulesService from "lib/services/modules.service";
|
||||||
|
import projectService from "lib/services/project.service";
|
||||||
|
import issuesService from "lib/services/issues.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useIssuesFilter from "lib/hooks/useIssuesFilter";
|
||||||
|
import useIssuesProperties from "lib/hooks/useIssuesProperties";
|
||||||
|
// layouts
|
||||||
|
import AppLayout from "layouts/app-layout";
|
||||||
|
// components
|
||||||
|
import ExistingIssuesListModal from "components/common/existing-issues-list-modal";
|
||||||
|
import ModulesBoardView from "components/project/modules/board-view";
|
||||||
|
import ModulesListView from "components/project/modules/list-view";
|
||||||
|
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
|
||||||
|
import ModuleDetailSidebar from "components/project/modules/module-detail-sidebar";
|
||||||
|
import ConfirmModuleDeletion from "components/project/modules/confirm-module-deleteion";
|
||||||
|
// headless ui
|
||||||
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { BreadcrumbItem, Breadcrumbs, CustomMenu } from "ui";
|
||||||
|
// icons
|
||||||
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
ArrowPathIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ListBulletIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||||
|
// types
|
||||||
|
import { IIssue, IModule, ModuleIssueResponse, Properties, SelectModuleType } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { MODULE_DETAIL, MODULE_ISSUES, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
// common
|
||||||
|
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
|
||||||
|
// constants
|
||||||
|
import { filterIssueOptions, groupByOptions, orderByOptions } from "constants/";
|
||||||
|
|
||||||
|
const SingleModule = () => {
|
||||||
|
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||||
|
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
|
||||||
|
const [moduleSidebar, setModuleSidebar] = useState(false);
|
||||||
|
|
||||||
|
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
|
||||||
|
const [selectedModuleForDelete, setSelectedModuleForDelete] = useState<SelectModuleType>();
|
||||||
|
|
||||||
|
const [preloadedData, setPreloadedData] = useState<
|
||||||
|
(Partial<IIssue> & { actionType: "createIssue" | "edit" | "delete" }) | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const { activeWorkspace, activeProject, issues, modules } = useUser();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { moduleId } = router.query;
|
||||||
|
|
||||||
|
const [properties, setProperties] = useIssuesProperties(
|
||||||
|
activeWorkspace?.slug,
|
||||||
|
activeProject?.id as string
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: moduleIssues } = useSWR<ModuleIssueResponse[]>(
|
||||||
|
activeWorkspace && activeProject && moduleId ? MODULE_ISSUES(moduleId as string) : null,
|
||||||
|
activeWorkspace && activeProject && moduleId
|
||||||
|
? () =>
|
||||||
|
modulesService.getModuleIssues(
|
||||||
|
activeWorkspace?.slug,
|
||||||
|
activeProject?.id,
|
||||||
|
moduleId as string
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
const moduleIssuesArray = moduleIssues?.map((issue) => {
|
||||||
|
return { bridge: issue.id, ...issue.issue_detail };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: moduleDetail } = useSWR<IModule>(
|
||||||
|
MODULE_DETAIL,
|
||||||
|
activeWorkspace && activeProject && moduleId
|
||||||
|
? () =>
|
||||||
|
modulesService.getModuleDetails(
|
||||||
|
activeWorkspace?.slug,
|
||||||
|
activeProject?.id,
|
||||||
|
moduleId as string
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
issueView,
|
||||||
|
groupByProperty,
|
||||||
|
setGroupByProperty,
|
||||||
|
groupedByIssues,
|
||||||
|
setOrderBy,
|
||||||
|
setFilterIssue,
|
||||||
|
orderBy,
|
||||||
|
filterIssue,
|
||||||
|
setIssueViewToKanban,
|
||||||
|
setIssueViewToList,
|
||||||
|
} = useIssuesFilter(moduleIssuesArray ?? []);
|
||||||
|
|
||||||
|
const { data: members } = useSWR(
|
||||||
|
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
|
||||||
|
activeWorkspace && activeProject
|
||||||
|
? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id)
|
||||||
|
: null,
|
||||||
|
{
|
||||||
|
onErrorRetry(err, _, __, revalidate, revalidateOpts) {
|
||||||
|
if (err?.status === 403) return;
|
||||||
|
setTimeout(() => revalidate(revalidateOpts), 5000);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddIssuesToModule = (data: { issues: string[] }) => {
|
||||||
|
if (activeWorkspace && activeProject) {
|
||||||
|
modulesService
|
||||||
|
.addIssuesToModule(activeWorkspace.slug, activeProject.id, moduleId as string, data)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => console.log(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
|
||||||
|
if (!activeWorkspace || !activeProject) return;
|
||||||
|
issuesService
|
||||||
|
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, formData)
|
||||||
|
.then((response) => {
|
||||||
|
mutate(MODULE_ISSUES(moduleId as string));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateIssueModal = () => {};
|
||||||
|
|
||||||
|
const openIssuesListModal = () => {
|
||||||
|
setModuleIssuesListModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeIssueFromModule = (issueId: string) => {
|
||||||
|
if (!activeWorkspace || !activeProject) return;
|
||||||
|
|
||||||
|
modulesService
|
||||||
|
.removeIssueFromModule(activeWorkspace.slug, activeProject.id, moduleId as string, issueId)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
mutate(MODULE_ISSUES(moduleId as string));
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteModule = () => {
|
||||||
|
if (!moduleDetail) return;
|
||||||
|
|
||||||
|
setSelectedModuleForDelete({ ...moduleDetail, actionType: "delete" });
|
||||||
|
setModuleDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExistingIssuesListModal
|
||||||
|
isOpen={moduleIssuesListModal}
|
||||||
|
handleClose={() => setModuleIssuesListModal(false)}
|
||||||
|
type="module"
|
||||||
|
issues={issues?.results ?? []}
|
||||||
|
handleOnSubmit={handleAddIssuesToModule}
|
||||||
|
/>
|
||||||
|
<ConfirmIssueDeletion
|
||||||
|
handleClose={() => setDeleteIssue(undefined)}
|
||||||
|
isOpen={!!deleteIssue}
|
||||||
|
data={issues?.results.find((issue) => issue.id === deleteIssue)}
|
||||||
|
/>
|
||||||
|
<ConfirmModuleDeletion
|
||||||
|
isOpen={
|
||||||
|
moduleDeleteModal &&
|
||||||
|
!!selectedModuleForDelete &&
|
||||||
|
selectedModuleForDelete.actionType === "delete"
|
||||||
|
}
|
||||||
|
setIsOpen={setModuleDeleteModal}
|
||||||
|
data={selectedModuleForDelete}
|
||||||
|
/>
|
||||||
|
<AppLayout
|
||||||
|
breadcrumbs={
|
||||||
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem
|
||||||
|
title={`${activeProject?.name ?? "Project"} Modules`}
|
||||||
|
link={`/projects/${activeProject?.id}/cycles`}
|
||||||
|
/>
|
||||||
|
</Breadcrumbs>
|
||||||
|
}
|
||||||
|
left={
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<ArrowPathIcon className="h-3 w-3" />
|
||||||
|
{modules?.find((c) => c.id === moduleId)?.name}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
className="ml-1.5"
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
{modules?.map((module) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={module.id}
|
||||||
|
renderAs="a"
|
||||||
|
href={`/projects/${activeProject?.id}/modules/${module.id}`}
|
||||||
|
>
|
||||||
|
{module.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 ${moduleSidebar ? "mr-[24rem]" : ""} duration-300`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-x-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none ${
|
||||||
|
issueView === "list" ? "bg-gray-100" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setIssueViewToList()}
|
||||||
|
>
|
||||||
|
<ListBulletIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none ${
|
||||||
|
issueView === "kanban" ? "bg-gray-100" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setIssueViewToKanban()}
|
||||||
|
>
|
||||||
|
<Squares2X2Icon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Popover className="relative">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Popover.Button
|
||||||
|
className={classNames(
|
||||||
|
open ? "bg-gray-100 text-gray-900" : "text-gray-500",
|
||||||
|
"group flex gap-2 items-center rounded-md bg-transparent text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none border p-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>View</span>
|
||||||
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 translate-y-1"
|
||||||
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
leave="transition ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform p-3 bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h4 className="text-sm text-gray-600">Group by</h4>
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
groupByOptions.find((option) => option.key === groupByProperty)
|
||||||
|
?.name ?? "Select"
|
||||||
|
}
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
{groupByOptions.map((option) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() => setGroupByProperty(option.key)}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h4 className="text-sm text-gray-600">Order by</h4>
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||||
|
"Select"
|
||||||
|
}
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
{orderByOptions.map((option) =>
|
||||||
|
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() => setOrderBy(option.key)}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h4 className="text-sm text-gray-600">Issue type</h4>
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
filterIssueOptions.find((option) => option.key === filterIssue)
|
||||||
|
?.name ?? "Select"
|
||||||
|
}
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
{filterIssueOptions.map((option) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() => setFilterIssue(option.key)}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div className="border-b-2"></div>
|
||||||
|
<div className="relative flex flex-col gap-1">
|
||||||
|
<h4 className="text-base text-gray-600">Properties</h4>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{Object.keys(properties).map((key) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
|
className={`px-2 py-1 capitalize rounded border border-theme text-xs ${
|
||||||
|
properties[key as keyof Properties]
|
||||||
|
? "border-theme bg-theme text-white"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
|
>
|
||||||
|
{replaceUnderscoreIfSnakeCase(key)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none ${
|
||||||
|
moduleSidebar ? "rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setModuleSidebar((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={`h-full ${moduleSidebar ? "mr-[28rem]" : ""} duration-300`}>
|
||||||
|
{issueView === "list" ? (
|
||||||
|
<ModulesListView
|
||||||
|
groupedByIssues={groupedByIssues}
|
||||||
|
selectedGroup={groupByProperty}
|
||||||
|
properties={properties}
|
||||||
|
openCreateIssueModal={openCreateIssueModal}
|
||||||
|
openIssuesListModal={openIssuesListModal}
|
||||||
|
removeIssueFromModule={removeIssueFromModule}
|
||||||
|
handleDeleteIssue={setDeleteIssue}
|
||||||
|
setPreloadedData={setPreloadedData}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ModulesBoardView
|
||||||
|
groupedByIssues={groupedByIssues}
|
||||||
|
properties={properties}
|
||||||
|
removeIssueFromModule={removeIssueFromModule}
|
||||||
|
selectedGroup={groupByProperty}
|
||||||
|
members={members}
|
||||||
|
openCreateIssueModal={openCreateIssueModal}
|
||||||
|
openIssuesListModal={openIssuesListModal}
|
||||||
|
handleDeleteIssue={setDeleteIssue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
setPreloadedData={setPreloadedData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ModuleDetailSidebar
|
||||||
|
module={moduleDetail}
|
||||||
|
isOpen={moduleSidebar}
|
||||||
|
handleDeleteModule={handleDeleteModule}
|
||||||
|
/>
|
||||||
|
</AppLayout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SingleModule;
|
@ -10,6 +10,8 @@ import withAuth from "lib/hoc/withAuthWrapper";
|
|||||||
import modulesService from "lib/services/modules.service";
|
import modulesService from "lib/services/modules.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// components
|
||||||
|
import SingleModuleCard from "components/project/modules/single-module-card";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs, EmptySpace, EmptySpaceItem, HeaderButton, Spinner } from "ui";
|
import { BreadcrumbItem, Breadcrumbs, EmptySpace, EmptySpaceItem, HeaderButton, Spinner } from "ui";
|
||||||
// icons
|
// icons
|
||||||
@ -17,7 +19,7 @@ import { PlusIcon, RectangleGroupIcon } from "@heroicons/react/24/outline";
|
|||||||
// types
|
// types
|
||||||
import { IModule } from "types/modules";
|
import { IModule } from "types/modules";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { MODULE_LIST } from "constants/fetch-keys";
|
import { MODULE_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
const ProjectModules: NextPage = () => {
|
const ProjectModules: NextPage = () => {
|
||||||
const { activeWorkspace, activeProject } = useUser();
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
@ -62,12 +64,11 @@ const ProjectModules: NextPage = () => {
|
|||||||
{modules ? (
|
{modules ? (
|
||||||
modules.length > 0 ? (
|
modules.length > 0 ? (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{modules.map((module) => (
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<div key={module.id} className="bg-white p-3 rounded-md">
|
{modules.map((module) => (
|
||||||
<h3>{module.name}</h3>
|
<SingleModuleCard key={module.id} module={module} />
|
||||||
<p className="text-gray-500 text-sm mt-2">{module.description}</p>
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex flex-col justify-center items-center px-4">
|
<div className="w-full h-full flex flex-col justify-center items-center px-4">
|
||||||
|
@ -119,150 +119,160 @@ const ControlSettings = () => {
|
|||||||
<h3 className="text-3xl font-bold leading-6 text-gray-900">Control</h3>
|
<h3 className="text-3xl font-bold leading-6 text-gray-900">Control</h3>
|
||||||
<p className="mt-4 text-sm text-gray-500">Set the control for the project.</p>
|
<p className="mt-4 text-sm text-gray-500">Set the control for the project.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-16">
|
<div className="grid grid-cols-12 gap-16">
|
||||||
<div>
|
<div className="col-span-5 space-y-16">
|
||||||
<h4 className="text-md leading-6 text-gray-900 mb-1">Project Lead</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-500 mb-3">Select the project leader.</p>
|
<h4 className="text-md leading-6 text-gray-900 mb-1">Project Lead</h4>
|
||||||
<Controller
|
<p className="text-sm text-gray-500 mb-3">Select the project leader.</p>
|
||||||
control={control}
|
<Controller
|
||||||
name="project_lead"
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
name="project_lead"
|
||||||
<Listbox value={value} onChange={onChange}>
|
render={({ field: { onChange, value } }) => (
|
||||||
{({ open }) => (
|
<Listbox value={value} onChange={onChange}>
|
||||||
<>
|
{({ open }) => (
|
||||||
<div className="relative">
|
<>
|
||||||
<Listbox.Button className="relative w-full flex justify-between items-center gap-4 border border-gray-300 rounded-md shadow-sm p-3 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
<div className="relative">
|
||||||
<span className="block truncate">
|
<Listbox.Button className="relative w-full flex justify-between items-center gap-4 border border-gray-300 rounded-md shadow-sm p-3 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
{people?.find((person) => person.member.id === value)?.member
|
<span className="block truncate">
|
||||||
.first_name ?? "Select Lead"}
|
{people?.find((person) => person.member.id === value)?.member
|
||||||
</span>
|
.first_name ?? "Select Lead"}
|
||||||
<ChevronDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
</span>
|
||||||
</Listbox.Button>
|
<ChevronDownIcon
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-20 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
<Listbox.Options className="absolute z-20 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||||
{people?.map((person) => (
|
{people?.map((person) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={person.id}
|
key={person.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`${
|
`${
|
||||||
active ? "bg-indigo-50" : ""
|
active ? "bg-indigo-50" : ""
|
||||||
} text-gray-900 cursor-default select-none relative px-3 py-2`
|
} text-gray-900 cursor-default select-none relative px-3 py-2`
|
||||||
}
|
}
|
||||||
value={person.member.id}
|
value={person.member.id}
|
||||||
>
|
>
|
||||||
{({ selected, active }) => (
|
{({ selected, active }) => (
|
||||||
<>
|
<>
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
selected ? "font-semibold" : "font-normal"
|
|
||||||
} block truncate`}
|
|
||||||
>
|
|
||||||
{person.member.first_name !== ""
|
|
||||||
? person.member.first_name
|
|
||||||
: person.member.email}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
<span
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
className={`${
|
||||||
active ? "text-white" : "text-theme"
|
selected ? "font-semibold" : "font-normal"
|
||||||
}`}
|
} block truncate`}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
{person.member.first_name !== ""
|
||||||
|
? person.member.first_name
|
||||||
|
: person.member.email}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
|
||||||
</>
|
{selected ? (
|
||||||
)}
|
<span
|
||||||
</Listbox.Option>
|
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||||
))}
|
active ? "text-white" : "text-theme"
|
||||||
</Listbox.Options>
|
}`}
|
||||||
</Transition>
|
>
|
||||||
</div>
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
</>
|
</span>
|
||||||
)}
|
) : null}
|
||||||
</Listbox>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="col-span-5 space-y-16">
|
||||||
<h4 className="text-md leading-6 text-gray-900 mb-1">Default Assignee</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-500 mb-3">
|
<h4 className="text-md leading-6 text-gray-900 mb-1">Default Assignee</h4>
|
||||||
Select the default assignee for the project.
|
<p className="text-sm text-gray-500 mb-3">
|
||||||
</p>
|
Select the default assignee for the project.
|
||||||
<Controller
|
</p>
|
||||||
control={control}
|
<Controller
|
||||||
name="default_assignee"
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
name="default_assignee"
|
||||||
<Listbox value={value} onChange={onChange}>
|
render={({ field: { value, onChange } }) => (
|
||||||
{({ open }) => (
|
<Listbox value={value} onChange={onChange}>
|
||||||
<>
|
{({ open }) => (
|
||||||
<div className="relative">
|
<>
|
||||||
<Listbox.Button className="relative w-full flex justify-between items-center gap-4 border border-gray-300 rounded-md shadow-sm p-3 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
<div className="relative">
|
||||||
<span className="block truncate">
|
<Listbox.Button className="relative w-full flex justify-between items-center gap-4 border border-gray-300 rounded-md shadow-sm p-3 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
{people?.find((p) => p.member.id === value)?.member.first_name ??
|
<span className="block truncate">
|
||||||
"Select Default Assignee"}
|
{people?.find((p) => p.member.id === value)?.member.first_name ??
|
||||||
</span>
|
"Select Default Assignee"}
|
||||||
<ChevronDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
</span>
|
||||||
</Listbox.Button>
|
<ChevronDownIcon
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute z-20 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
<Listbox.Options className="absolute z-20 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||||
{people?.map((person) => (
|
{people?.map((person) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={person.id}
|
key={person.id}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`${
|
`${
|
||||||
active ? "bg-indigo-50" : ""
|
active ? "bg-indigo-50" : ""
|
||||||
} text-gray-900 cursor-default select-none relative px-3 py-2`
|
} text-gray-900 cursor-default select-none relative px-3 py-2`
|
||||||
}
|
}
|
||||||
value={person.member.id}
|
value={person.member.id}
|
||||||
>
|
>
|
||||||
{({ selected, active }) => (
|
{({ selected, active }) => (
|
||||||
<>
|
<>
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
selected ? "font-semibold" : "font-normal"
|
|
||||||
} block truncate`}
|
|
||||||
>
|
|
||||||
{person.member.first_name !== ""
|
|
||||||
? person.member.first_name
|
|
||||||
: person.member.email}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
<span
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
className={`${
|
||||||
active ? "text-white" : "text-theme"
|
selected ? "font-semibold" : "font-normal"
|
||||||
}`}
|
} block truncate`}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
{person.member.first_name !== ""
|
||||||
|
? person.member.first_name
|
||||||
|
: person.member.email}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
|
||||||
</>
|
{selected ? (
|
||||||
)}
|
<span
|
||||||
</Listbox.Option>
|
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||||
))}
|
active ? "text-white" : "text-theme"
|
||||||
</Listbox.Options>
|
}`}
|
||||||
</Transition>
|
>
|
||||||
</div>
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
</>
|
</span>
|
||||||
)}
|
) : null}
|
||||||
</Listbox>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -136,97 +136,101 @@ const GeneralSettings = () => {
|
|||||||
This information will be displayed to every member of the project.
|
This information will be displayed to every member of the project.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-16">
|
<div className="grid grid-cols-12 gap-16">
|
||||||
<div>
|
<div className="col-span-5 space-y-16">
|
||||||
<h4 className="text-md leading-6 text-gray-900 mb-1">Icon & Name</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-500 mb-3">
|
<h4 className="text-md leading-6 text-gray-900 mb-1">Icon & Name</h4>
|
||||||
Select an icon and a name for the project.
|
<p className="text-sm text-gray-500 mb-3">
|
||||||
</p>
|
Select an icon and a name for the project.
|
||||||
<div className="flex gap-2">
|
</p>
|
||||||
<Controller
|
<div className="flex gap-2">
|
||||||
control={control}
|
<Controller
|
||||||
name="icon"
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
name="icon"
|
||||||
<EmojiIconPicker
|
render={({ field: { value, onChange } }) => (
|
||||||
label={value ? String.fromCodePoint(parseInt(value)) : "Icon"}
|
<EmojiIconPicker
|
||||||
value={value}
|
label={value ? String.fromCodePoint(parseInt(value)) : "Icon"}
|
||||||
onChange={onChange}
|
value={value}
|
||||||
/>
|
onChange={onChange}
|
||||||
)}
|
/>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
error={errors.name}
|
||||||
|
register={register}
|
||||||
|
placeholder="Project Name"
|
||||||
|
size="lg"
|
||||||
|
className="w-auto"
|
||||||
|
validations={{
|
||||||
|
required: "Name is required",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-md leading-6 text-gray-900 mb-1">Identifier</h4>
|
||||||
|
<p className="text-sm text-gray-500 mb-3">
|
||||||
|
Create a 1-6 characters{"'"} identifier for the project.
|
||||||
|
</p>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="identifier"
|
||||||
name="name"
|
name="identifier"
|
||||||
error={errors.name}
|
error={errors.identifier}
|
||||||
register={register}
|
register={register}
|
||||||
placeholder="Project Name"
|
placeholder="Enter identifier"
|
||||||
|
className="w-40"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="w-auto"
|
onChange={(e: any) => {
|
||||||
|
if (!activeWorkspace || !e.target.value) return;
|
||||||
|
checkIdentifierAvailability(activeWorkspace.slug, e.target.value);
|
||||||
|
}}
|
||||||
validations={{
|
validations={{
|
||||||
required: "Name is required",
|
required: "Identifier is required",
|
||||||
|
minLength: {
|
||||||
|
value: 1,
|
||||||
|
message: "Identifier must at least be of 1 character",
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
value: 9,
|
||||||
|
message: "Identifier must at most be of 9 characters",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="col-span-5 space-y-16">
|
||||||
<h4 className="text-md leading-6 text-gray-900 mb-1">Description</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-500 mb-3">Give a description to the project.</p>
|
<h4 className="text-md leading-6 text-gray-900 mb-1">Description</h4>
|
||||||
<TextArea
|
<p className="text-sm text-gray-500 mb-3">Give a description to the project.</p>
|
||||||
id="description"
|
<TextArea
|
||||||
name="description"
|
id="description"
|
||||||
error={errors.description}
|
name="description"
|
||||||
register={register}
|
error={errors.description}
|
||||||
placeholder="Enter project description"
|
register={register}
|
||||||
validations={{}}
|
placeholder="Enter project description"
|
||||||
/>
|
validations={{}}
|
||||||
</div>
|
/>
|
||||||
<div>
|
</div>
|
||||||
<h4 className="text-md leading-6 text-gray-900 mb-1">Identifier</h4>
|
<div>
|
||||||
<p className="text-sm text-gray-500 mb-3">
|
<h4 className="text-md leading-6 text-gray-900 mb-1">Network</h4>
|
||||||
Create a 1-6 characters{"'"} identifier for the project.
|
<p className="text-sm text-gray-500 mb-3">Select privacy type for the project.</p>
|
||||||
</p>
|
<Select
|
||||||
<Input
|
name="network"
|
||||||
id="identifier"
|
id="network"
|
||||||
name="identifier"
|
options={Object.keys(NETWORK_CHOICES).map((key) => ({
|
||||||
error={errors.identifier}
|
value: key,
|
||||||
register={register}
|
label: NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES],
|
||||||
placeholder="Enter identifier"
|
}))}
|
||||||
className="w-40"
|
size="lg"
|
||||||
size="lg"
|
register={register}
|
||||||
onChange={(e: any) => {
|
validations={{
|
||||||
if (!activeWorkspace || !e.target.value) return;
|
required: "Network is required",
|
||||||
checkIdentifierAvailability(activeWorkspace.slug, e.target.value);
|
}}
|
||||||
}}
|
className="w-40"
|
||||||
validations={{
|
/>
|
||||||
required: "Identifier is required",
|
</div>
|
||||||
minLength: {
|
|
||||||
value: 1,
|
|
||||||
message: "Identifier must at least be of 1 character",
|
|
||||||
},
|
|
||||||
maxLength: {
|
|
||||||
value: 9,
|
|
||||||
message: "Identifier must at most be of 9 characters",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-md leading-6 text-gray-900 mb-1">Network</h4>
|
|
||||||
<p className="text-sm text-gray-500 mb-3">Select privacy type for the project.</p>
|
|
||||||
<Select
|
|
||||||
name="network"
|
|
||||||
id="network"
|
|
||||||
options={Object.keys(NETWORK_CHOICES).map((key) => ({
|
|
||||||
value: key,
|
|
||||||
label: NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES],
|
|
||||||
}))}
|
|
||||||
size="lg"
|
|
||||||
register={register}
|
|
||||||
validations={{
|
|
||||||
required: "Network is required",
|
|
||||||
}}
|
|
||||||
className="w-40"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
|
import { EmptySpace, EmptySpaceItem } from "ui/empty-space";
|
||||||
|
|
||||||
const WorkspaceInvitation: NextPage = () => {
|
const WorkspaceInvitation: NextPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
// next
|
|
||||||
import type { NextPage } from "next";
|
|
||||||
import Link from "next/link";
|
|
||||||
// react
|
// react
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// layouts
|
// next
|
||||||
import AppLayout from "layouts/app-layout";
|
import Link from "next/link";
|
||||||
|
import type { NextPage } from "next";
|
||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
// services
|
||||||
|
import userService from "lib/services/user.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// hoc
|
// hoc
|
||||||
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
||||||
// fetch keys
|
// layouts
|
||||||
import { USER_ISSUE } from "constants/fetch-keys";
|
import AppLayout from "layouts/app-layout";
|
||||||
// services
|
|
||||||
import userService from "lib/services/user.service";
|
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "ui";
|
import { Spinner } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowRightIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
|
import { ArrowRightIcon, CalendarDaysIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { USER_ISSUE } from "constants/fetch-keys";
|
||||||
|
// common
|
||||||
import {
|
import {
|
||||||
addSpaceIfCamelCase,
|
addSpaceIfCamelCase,
|
||||||
findHowManyDaysLeft,
|
findHowManyDaysLeft,
|
||||||
@ -112,10 +113,6 @@ const Workspace: NextPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
)} */}
|
)} */}
|
||||||
<span className="">{issue.name}</span>
|
<span className="">{issue.name}</span>
|
||||||
<div className="absolute bottom-full left-0 mb-2 z-10 hidden group-hover:block p-2 bg-white shadow-md rounded-md max-w-sm whitespace-nowrap">
|
|
||||||
<h5 className="font-medium mb-1">Name</h5>
|
|
||||||
<div>{issue.name}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
2
apps/app/types/cycles.d.ts
vendored
2
apps/app/types/cycles.d.ts
vendored
@ -19,7 +19,7 @@ export interface ICycle {
|
|||||||
|
|
||||||
export interface CycleIssueResponse {
|
export interface CycleIssueResponse {
|
||||||
id: string;
|
id: string;
|
||||||
issue_details: IIssue;
|
issue_detail: IIssue;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
|
24
apps/app/types/modules.d.ts
vendored
24
apps/app/types/modules.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import type { IUser, IIssue, IProject } from ".";
|
import type { IUser, IUserLite, IIssue, IProject } from ".";
|
||||||
|
|
||||||
export interface IModule {
|
export interface IModule {
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@ -9,13 +9,29 @@ export interface IModule {
|
|||||||
id: string;
|
id: string;
|
||||||
lead: string | null;
|
lead: string | null;
|
||||||
lead_detail: IUserLite;
|
lead_detail: IUserLite;
|
||||||
|
members: string[];
|
||||||
members_list: string[];
|
members_list: string[];
|
||||||
|
members_detail: IUserLite[];
|
||||||
name: string;
|
name: string;
|
||||||
project: string;
|
project: string;
|
||||||
project_detail: IProject;
|
project_detail: IProject;
|
||||||
start_date: Date | null;
|
start_date: string | null;
|
||||||
status: "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled";
|
status: "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled" | null;
|
||||||
target_date: Date | null;
|
target_date: string | null;
|
||||||
|
updated_at: Date;
|
||||||
|
updated_by: string;
|
||||||
|
workspace: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModuleIssueResponse {
|
||||||
|
created_at: Date;
|
||||||
|
created_by: string;
|
||||||
|
id: string;
|
||||||
|
issue: string;
|
||||||
|
issue_detail: IIssue;
|
||||||
|
module: string;
|
||||||
|
module_detail: IModule;
|
||||||
|
project: string;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
|
@ -20,7 +20,7 @@ const CustomListbox: React.FC<Props> = ({
|
|||||||
label,
|
label,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Listbox value={value} onChange={onChange} multiple={multiple}>
|
<Listbox as="div" className="relative" value={value} onChange={onChange} multiple={multiple}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
{label && (
|
{label && (
|
||||||
@ -28,9 +28,42 @@ const CustomListbox: React.FC<Props> = ({
|
|||||||
<div className="text-gray-500 mb-2">{label}</div>
|
<div className="text-gray-500 mb-2">{label}</div>
|
||||||
</Listbox.Label>
|
</Listbox.Label>
|
||||||
)}
|
)}
|
||||||
<div className="relative">
|
<Listbox.Button
|
||||||
<Listbox.Button
|
className={`flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||||
className={`flex items-center gap-1 hover:bg-gray-100 text-xs relative border rounded-md shadow-sm cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300 ${
|
width === "sm"
|
||||||
|
? "w-32"
|
||||||
|
: width === "md"
|
||||||
|
? "w-48"
|
||||||
|
: width === "lg"
|
||||||
|
? "w-64"
|
||||||
|
: width === "xl"
|
||||||
|
? "w-80"
|
||||||
|
: width === "2xl"
|
||||||
|
? "w-96"
|
||||||
|
: width === "w-full"
|
||||||
|
? "w-full"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${className || "px-2 py-1"}`}
|
||||||
|
>
|
||||||
|
{icon ?? null}
|
||||||
|
<span className="block truncate">
|
||||||
|
{Array.isArray(value)
|
||||||
|
? value.map((v) => options?.find((o) => o.value === v)?.display).join(", ") ||
|
||||||
|
`${title}`
|
||||||
|
: options?.find((o) => o.value === value)?.display || `${title}`}
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={React.Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options
|
||||||
|
className={`absolute mt-1 bg-white shadow-lg max-h-32 overflow-auto ${
|
||||||
width === "sm"
|
width === "sm"
|
||||||
? "w-32"
|
? "w-32"
|
||||||
: width === "md"
|
: width === "md"
|
||||||
@ -44,122 +77,87 @@ const CustomListbox: React.FC<Props> = ({
|
|||||||
: width === "w-full"
|
: width === "w-full"
|
||||||
? "w-full"
|
? "w-full"
|
||||||
: ""
|
: ""
|
||||||
}
|
} ${
|
||||||
${className || "px-2 py-1"}`}
|
optionsFontsize === "sm"
|
||||||
|
? "text-xs"
|
||||||
|
: optionsFontsize === "md"
|
||||||
|
? "text-base"
|
||||||
|
: optionsFontsize === "lg"
|
||||||
|
? "text-lg"
|
||||||
|
: optionsFontsize === "xl"
|
||||||
|
? "text-xl"
|
||||||
|
: optionsFontsize === "2xl"
|
||||||
|
? "text-2xl"
|
||||||
|
: ""
|
||||||
|
} rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none z-10`}
|
||||||
>
|
>
|
||||||
{icon ?? null}
|
<div className="py-1">
|
||||||
<span className="block truncate text-xs">
|
{options ? (
|
||||||
{Array.isArray(value)
|
options.length > 0 ? (
|
||||||
? value.map((v) => options?.find((o) => o.value === v)?.display).join(", ") ||
|
options.map((option) => (
|
||||||
`${title}`
|
<Listbox.Option
|
||||||
: options?.find((o) => o.value === value)?.display || `${title}`}
|
key={option.value}
|
||||||
</span>
|
className={({ active }) =>
|
||||||
</Listbox.Button>
|
`${
|
||||||
|
active ? "bg-indigo-50" : ""
|
||||||
|
} text-gray-900 cursor-pointer select-none relative p-2`
|
||||||
|
}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
selected ||
|
||||||
|
(Array.isArray(value)
|
||||||
|
? value.includes(option.value)
|
||||||
|
: value === option.value)
|
||||||
|
? "font-semibold"
|
||||||
|
: "font-normal"
|
||||||
|
} flex items-center gap-2 truncate`}
|
||||||
|
>
|
||||||
|
{option.color && (
|
||||||
|
<span
|
||||||
|
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: option.color,
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
{option.display}
|
||||||
|
</span>
|
||||||
|
|
||||||
<Transition
|
{selected ||
|
||||||
show={open}
|
(Array.isArray(value)
|
||||||
as={React.Fragment}
|
? value.includes(option.value)
|
||||||
leave="transition ease-in duration-100"
|
: value === option.value) ? (
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options
|
|
||||||
className={`absolute mt-1 bg-white shadow-lg max-h-32 overflow-auto ${
|
|
||||||
width === "sm"
|
|
||||||
? "w-32"
|
|
||||||
: width === "md"
|
|
||||||
? "w-48"
|
|
||||||
: width === "lg"
|
|
||||||
? "w-64"
|
|
||||||
: width === "xl"
|
|
||||||
? "w-80"
|
|
||||||
: width === "2xl"
|
|
||||||
? "w-96"
|
|
||||||
: width === "w-full"
|
|
||||||
? "w-full"
|
|
||||||
: ""
|
|
||||||
} ${
|
|
||||||
optionsFontsize === "sm"
|
|
||||||
? "text-xs"
|
|
||||||
: optionsFontsize === "md"
|
|
||||||
? "text-base"
|
|
||||||
: optionsFontsize === "lg"
|
|
||||||
? "text-lg"
|
|
||||||
: optionsFontsize === "xl"
|
|
||||||
? "text-xl"
|
|
||||||
: optionsFontsize === "2xl"
|
|
||||||
? "text-2xl"
|
|
||||||
: ""
|
|
||||||
} rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none z-10`}
|
|
||||||
>
|
|
||||||
<div className="py-1">
|
|
||||||
{options ? (
|
|
||||||
options.length > 0 ? (
|
|
||||||
options.map((option) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={option.value}
|
|
||||||
className={({ active }) =>
|
|
||||||
`${
|
|
||||||
active ? "bg-indigo-50" : ""
|
|
||||||
} text-gray-900 cursor-pointer select-none relative p-2`
|
|
||||||
}
|
|
||||||
value={option.value}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||||
selected ||
|
active ||
|
||||||
(Array.isArray(value)
|
(Array.isArray(value)
|
||||||
? value.includes(option.value)
|
? value.includes(option.value)
|
||||||
: value === option.value)
|
: value === option.value)
|
||||||
? "font-semibold"
|
? "text-white"
|
||||||
: "font-normal"
|
: "text-theme"
|
||||||
} flex items-center gap-2 truncate`}
|
}`}
|
||||||
>
|
>
|
||||||
{option.color && (
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
<span
|
|
||||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: option.color,
|
|
||||||
}}
|
|
||||||
></span>
|
|
||||||
)}
|
|
||||||
{option.display}
|
|
||||||
</span>
|
</span>
|
||||||
|
) : null}
|
||||||
{selected ||
|
</>
|
||||||
(Array.isArray(value)
|
)}
|
||||||
? value.includes(option.value)
|
</Listbox.Option>
|
||||||
: value === option.value) ? (
|
))
|
||||||
<span
|
|
||||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
|
||||||
active ||
|
|
||||||
(Array.isArray(value)
|
|
||||||
? value.includes(option.value)
|
|
||||||
: value === option.value)
|
|
||||||
? "text-white"
|
|
||||||
: "text-theme"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 text-center">No options</p>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-gray-500 text-center">Loading...</p>
|
<p className="text-sm text-gray-500 text-center">No options</p>
|
||||||
)}
|
)
|
||||||
</div>
|
) : (
|
||||||
{footerOption ?? null}
|
<p className="text-sm text-gray-500 text-center">Loading...</p>
|
||||||
</Listbox.Options>
|
)}
|
||||||
</Transition>
|
</div>
|
||||||
</div>
|
{footerOption ?? null}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
|
@ -24,7 +24,7 @@ const EmptySpace: React.FC<EmptySpaceProps> = ({ title, description, children, I
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<h2 className="text-lg font-medium text-gray-900">{title}</h2>
|
<h2 className="text-lg font-medium text-gray-900">{title}</h2>
|
||||||
<p className="mt-1 text-sm text-gray-500">{description}</p>
|
<div className="mt-1 text-sm text-gray-500">{description}</div>
|
||||||
<ul role="list" className="mt-6 divide-y divide-gray-200 border-t border-b border-gray-200">
|
<ul role="list" className="mt-6 divide-y divide-gray-200 border-t border-b border-gray-200">
|
||||||
{children}
|
{children}
|
||||||
</ul>
|
</ul>
|
||||||
@ -80,7 +80,7 @@ const EmptySpaceItem: React.FC<EmptySpaceItemProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium text-gray-900">{title}</div>
|
<div className="text-sm font-medium text-gray-900">{title}</div>
|
||||||
{description ? <p className="text-sm text-gray-500">{description}</p> : null}
|
{description ? <div className="text-sm text-gray-500">{description}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon
|
@ -10,5 +10,5 @@ export { default as Tooltip } from "./Tooltip";
|
|||||||
export { default as SearchListbox } from "./search-listbox";
|
export { default as SearchListbox } from "./search-listbox";
|
||||||
export { default as HeaderButton } from "./HeaderButton";
|
export { default as HeaderButton } from "./HeaderButton";
|
||||||
export * from "./Breadcrumbs";
|
export * from "./Breadcrumbs";
|
||||||
export * from "./EmptySpace";
|
export * from "./empty-space";
|
||||||
export { default as EmojiIconPicker } from "./emoji-icon-picker";
|
export { default as EmojiIconPicker } from "./emoji-icon-picker";
|
||||||
|
@ -44,7 +44,7 @@ const SearchListbox: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<Combobox.Label className="sr-only">{title}</Combobox.Label>
|
<Combobox.Label className="sr-only">{title}</Combobox.Label>
|
||||||
<Combobox.Button
|
<Combobox.Button
|
||||||
className={`flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300 ${
|
className={`flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||||
buttonClassName || ""
|
buttonClassName || ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user