forked from github/plane
Merge pull request #27 from makeplane/stage-release
dev: promote staging updates to master
This commit is contained in:
commit
54b0252dad
@ -29,7 +29,7 @@ Coming soon.
|
|||||||
|
|
||||||
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
|
The Plane community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.
|
||||||
|
|
||||||
To chat with other community members you can join the [Plane Discord](https://discord.com/invite/8SR2N9PAcJ).
|
To chat with other community members you can join the [Plane Discord](https://discord.com/invite/q9HKAdau).
|
||||||
|
|
||||||
Our Code of Conduct applies to all Plane community channels.
|
Our Code of Conduct applies to all Plane community channels.
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// react
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
// swr
|
// swr
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
@ -5,23 +6,23 @@ import { mutate } from "swr";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// icons
|
// icons
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { FolderIcon } from "@heroicons/react/24/outline";
|
|
||||||
// commons
|
// commons
|
||||||
import { classNames } from "constants/common";
|
import { classNames } from "constants/common";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IssueResponse } from "types";
|
import { IIssue, IssueResponse } from "types";
|
||||||
import { Button } from "ui";
|
// constants
|
||||||
import { PROJECT_ISSUES_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
import issuesServices from "lib/services/issues.services";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
parentId: string;
|
parent: IIssue | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormInput = {
|
type FormInput = {
|
||||||
@ -29,7 +30,7 @@ type FormInput = {
|
|||||||
cycleId: string;
|
cycleId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parent }) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, issues } = useUser();
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
@ -41,24 +42,19 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
|||||||
[];
|
[];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
reset,
|
reset,
|
||||||
setError,
|
|
||||||
} = useForm<FormInput>();
|
} = useForm<FormInput>();
|
||||||
|
|
||||||
const handleCommandPaletteClose = () => {
|
const handleCommandPaletteClose = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setQuery("");
|
setQuery("");
|
||||||
reset();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addAsSubIssue = (issueId: string) => {
|
const addAsSubIssue = (issueId: string) => {
|
||||||
if (activeWorkspace && activeProject) {
|
if (activeWorkspace && activeProject) {
|
||||||
issuesServices
|
issuesServices
|
||||||
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: parentId })
|
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: parent?.id })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate<IssueResponse>(
|
mutate<IssueResponse>(
|
||||||
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
|
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
|
||||||
@ -78,7 +74,6 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||||
<Dialog as="div" className="relative z-10" onClose={handleCommandPaletteClose}>
|
<Dialog as="div" className="relative z-10" onClose={handleCommandPaletteClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
@ -104,14 +99,7 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
|||||||
leaveTo="opacity-0 scale-95"
|
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">
|
<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">
|
||||||
<Combobox
|
<Combobox>
|
||||||
// onChange={(item: ItemType) => {
|
|
||||||
// const { url, onClick } = item;
|
|
||||||
// if (url) router.push(url);
|
|
||||||
// else if (onClick) onClick();
|
|
||||||
// handleCommandPaletteClose();
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
<div className="relative m-1">
|
<div className="relative m-1">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||||
@ -138,7 +126,11 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
|||||||
)}
|
)}
|
||||||
<ul className="text-sm text-gray-700">
|
<ul className="text-sm text-gray-700">
|
||||||
{filteredIssues.map((issue) => {
|
{filteredIssues.map((issue) => {
|
||||||
if (issue.parent === "" || issue.parent === null)
|
if (
|
||||||
|
(issue.parent === "" || issue.parent === null) && // issue does not have any other parent
|
||||||
|
issue.id !== parent?.id && // issue is not itself
|
||||||
|
issue.id !== parent?.parent // issue is not it's parent
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
@ -174,7 +166,7 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
|||||||
|
|
||||||
{query !== "" && filteredIssues.length === 0 && (
|
{query !== "" && filteredIssues.length === 0 && (
|
||||||
<div className="py-14 px-6 text-center sm:px-14">
|
<div className="py-14 px-6 text-center sm:px-14">
|
||||||
<FolderIcon
|
<RectangleStackIcon
|
||||||
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
|
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
@ -189,7 +181,6 @@ const AddAsSubIssue: React.FC<Props> = ({ isOpen, setIsOpen, parentId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,40 +4,35 @@ import { useRouter } from "next/router";
|
|||||||
// swr
|
// swr
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// services
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
// 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";
|
||||||
// icons
|
// icons
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
|
||||||
import {
|
import {
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
RectangleStackIcon,
|
RectangleStackIcon,
|
||||||
ClipboardDocumentListIcon,
|
ClipboardDocumentListIcon,
|
||||||
ArrowPathIcon,
|
MagnifyingGlassIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// commons
|
|
||||||
import { classNames, copyTextToClipboard } from "constants/common";
|
|
||||||
// components
|
// components
|
||||||
import ShortcutsModal from "components/command-palette/shortcuts";
|
import ShortcutsModal from "components/command-palette/shortcuts";
|
||||||
import CreateProjectModal from "components/project/CreateProjectModal";
|
import CreateProjectModal from "components/project/CreateProjectModal";
|
||||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||||
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IProject, IssueResponse } from "types";
|
import { IIssue, IssueResponse } from "types";
|
||||||
import { Button, SearchListbox } from "ui";
|
|
||||||
import issuesServices from "lib/services/issues.services";
|
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { PROJECTS_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
type ItemType = {
|
import { classNames, copyTextToClipboard } from "constants/common";
|
||||||
name: string;
|
|
||||||
url?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FormInput = {
|
type FormInput = {
|
||||||
issue_ids: string[];
|
issue_ids: string[];
|
||||||
@ -45,8 +40,6 @@ type FormInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CommandPalette: React.FC = () => {
|
const CommandPalette: React.FC = () => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
||||||
@ -55,7 +48,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 { activeWorkspace, activeProject, issues, cycles } = useUser();
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { toggleCollapsed } = useTheme();
|
const { toggleCollapsed } = useTheme();
|
||||||
|
|
||||||
@ -67,14 +62,7 @@ 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 {
|
const { register, handleSubmit, reset } = useForm<FormInput>();
|
||||||
register,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
reset,
|
|
||||||
setError,
|
|
||||||
} = useForm<FormInput>();
|
|
||||||
|
|
||||||
const quickActions = [
|
const quickActions = [
|
||||||
{
|
{
|
||||||
@ -103,25 +91,25 @@ const CommandPalette: React.FC = () => {
|
|||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (e.ctrlKey && e.key === "/") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "/") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsPaletteOpen(true);
|
setIsPaletteOpen(true);
|
||||||
} else if (e.ctrlKey && e.key === "i") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "i") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsIssueModalOpen(true);
|
setIsIssueModalOpen(true);
|
||||||
} else if (e.ctrlKey && e.key === "p") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "p") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsProjectModalOpen(true);
|
setIsProjectModalOpen(true);
|
||||||
} else if (e.ctrlKey && e.key === "b") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "b") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleCollapsed();
|
toggleCollapsed();
|
||||||
} else if (e.ctrlKey && e.key === "h") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "h") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsShortcutsModalOpen(true);
|
setIsShortcutsModalOpen(true);
|
||||||
} else if (e.ctrlKey && e.key === "q") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "q") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsCreateCycleModalOpen(true);
|
setIsCreateCycleModalOpen(true);
|
||||||
} else if (e.ctrlKey && e.altKey && e.key === "c") {
|
} else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!router.query.issueId) return;
|
if (!router.query.issueId) return;
|
||||||
@ -186,37 +174,6 @@ const CommandPalette: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToCycle: 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 (!data.cycleId) {
|
|
||||||
setToastAlert({
|
|
||||||
title: "Error",
|
|
||||||
type: "error",
|
|
||||||
message: "Please select a cycle",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeWorkspace && activeProject) {
|
|
||||||
issuesServices
|
|
||||||
.bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, data.cycleId, data)
|
|
||||||
.then((res) => {
|
|
||||||
console.log(res);
|
|
||||||
})
|
|
||||||
.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);
|
||||||
@ -269,14 +226,7 @@ const CommandPalette: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<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">
|
<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>
|
<form>
|
||||||
<Combobox
|
<Combobox>
|
||||||
// onChange={(item: ItemType) => {
|
|
||||||
// const { url, onClick } = item;
|
|
||||||
// if (url) router.push(url);
|
|
||||||
// else if (onClick) onClick();
|
|
||||||
// handleCommandPaletteClose();
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
<div className="relative m-1">
|
<div className="relative m-1">
|
||||||
<MagnifyingGlassIcon
|
<MagnifyingGlassIcon
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||||
@ -305,42 +255,53 @@ const CommandPalette: React.FC = () => {
|
|||||||
{filteredIssues.map((issue) => (
|
{filteredIssues.map((issue) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
|
as="label"
|
||||||
|
htmlFor={`issue-${issue.id}`}
|
||||||
value={{
|
value={{
|
||||||
name: issue.name,
|
name: issue.name,
|
||||||
url: `/projects/${issue.project}/issues/${issue.id}`,
|
url: `/projects/${issue.project}/issues/${issue.id}`,
|
||||||
}}
|
}}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
"flex cursor-pointer select-none items-center rounded-md px-3 py-2",
|
"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 ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<>
|
<>
|
||||||
{/* <FolderIcon
|
<div className="flex items-center gap-2">
|
||||||
className={classNames(
|
|
||||||
"h-6 w-6 flex-none text-gray-900 text-opacity-40",
|
|
||||||
active ? "text-opacity-100" : ""
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/> */}
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{...register("issue_ids")}
|
{...register("issue_ids")}
|
||||||
id={`issue-${issue.id}`}
|
id={`issue-${issue.id}`}
|
||||||
value={issue.id}
|
value={issue.id}
|
||||||
/>
|
/>
|
||||||
<label
|
<span
|
||||||
htmlFor={`issue-${issue.id}`}
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
className="ml-3 flex-auto truncate"
|
style={{
|
||||||
>
|
backgroundColor: issue.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
|
</span>
|
||||||
{issue.name}
|
{issue.name}
|
||||||
</label>
|
</div>
|
||||||
{active && (
|
{active && (
|
||||||
<span className="ml-3 flex-none text-gray-500">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(
|
||||||
|
`/projects/${activeProject?.id}/issues/${issue.id}`
|
||||||
|
);
|
||||||
|
handleCommandPaletteClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="justify-self-end flex-none text-gray-500">
|
||||||
Jump to...
|
Jump to...
|
||||||
</span>
|
</span>
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -405,31 +366,9 @@ const CommandPalette: React.FC = () => {
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
|
|
||||||
<div className="flex justify-between items-center gap-2 p-3">
|
<div className="flex justify-between items-center gap-2 p-3">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="cycleId"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SearchListbox
|
|
||||||
title="Cycle"
|
|
||||||
optionsFontsize="sm"
|
|
||||||
options={cycles?.map((cycle) => {
|
|
||||||
return { value: cycle.id, display: cycle.name };
|
|
||||||
})}
|
|
||||||
multiple={false}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
icon={<ArrowPathIcon className="h-4 w-4 text-gray-400" />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button onClick={handleSubmit(handleAddToCycle)} size="sm">
|
|
||||||
Add to Cycle
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
|
<Button onClick={handleSubmit(handleDelete)} theme="danger" size="sm">
|
||||||
Delete
|
Delete selected
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Button type="button" size="sm" onClick={handleCommandPaletteClose}>
|
<Button type="button" size="sm" onClick={handleCommandPaletteClose}>
|
||||||
Close
|
Close
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { EditorState, LexicalEditor, $getRoot, $getSelection } from "lexical";
|
import {
|
||||||
|
EditorState,
|
||||||
|
$getRoot,
|
||||||
|
$getSelection,
|
||||||
|
SerializedEditorState,
|
||||||
|
LexicalEditor,
|
||||||
|
} from "lexical";
|
||||||
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
||||||
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
||||||
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
||||||
@ -21,7 +27,7 @@ import { getValidatedValue } from "./helpers/editor";
|
|||||||
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
||||||
|
|
||||||
export interface RichTextEditorProps {
|
export interface RichTextEditorProps {
|
||||||
onChange: (state: string) => void;
|
onChange: (state: SerializedEditorState) => void;
|
||||||
id: string;
|
id: string;
|
||||||
value: string;
|
value: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@ -33,11 +39,18 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||||||
value,
|
value,
|
||||||
placeholder = "Enter some text...",
|
placeholder = "Enter some text...",
|
||||||
}) => {
|
}) => {
|
||||||
function handleChange(state: EditorState, editor: LexicalEditor) {
|
const handleChange = (editorState: EditorState) => {
|
||||||
state.read(() => {
|
editorState.read(() => {
|
||||||
onChange(JSON.stringify(state.toJSON()));
|
let editorData = editorState.toJSON();
|
||||||
|
if (onChange) onChange(editorData);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// function handleChange(state: EditorState, editor: LexicalEditor) {
|
||||||
|
// state.read(() => {
|
||||||
|
// onChange(state.toJSON());
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LexicalComposer
|
<LexicalComposer
|
||||||
|
@ -6,9 +6,7 @@ export const positionEditorElement = (editor: any, rect: any) => {
|
|||||||
editor.style.left = "-1000px";
|
editor.style.left = "-1000px";
|
||||||
} else {
|
} else {
|
||||||
editor.style.opacity = "1";
|
editor.style.opacity = "1";
|
||||||
editor.style.top = `${
|
editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`;
|
||||||
rect.top + rect.height + window.pageYOffset + 10
|
|
||||||
}px`;
|
|
||||||
editor.style.left = `${
|
editor.style.left = `${
|
||||||
rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2
|
rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2
|
||||||
}px`;
|
}px`;
|
||||||
@ -22,9 +20,9 @@ export const getValidatedValue = (value: string) => {
|
|||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(value);
|
console.log(value);
|
||||||
return JSON.stringify(json);
|
return value;
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
||||||
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
||||||
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
||||||
@ -17,14 +16,11 @@ import { getValidatedValue } from "./helpers/editor";
|
|||||||
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
||||||
|
|
||||||
export interface RichTextViewerProps {
|
export interface RichTextViewerProps {
|
||||||
id: string;
|
|
||||||
value: string;
|
value: string;
|
||||||
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RichTextViewer: FC<RichTextViewerProps> = (props) => {
|
const RichTextViewer: React.FC<RichTextViewerProps> = ({ value, id }) => {
|
||||||
// props
|
|
||||||
const { value, id } = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LexicalComposer
|
<LexicalComposer
|
||||||
initialConfig={{
|
initialConfig={{
|
||||||
@ -37,7 +33,7 @@ const RichTextViewer: FC<RichTextViewerProps> = (props) => {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
<ContentEditable className='className="h-[450px] outline-none py-[15px] resize-none overflow-hidden text-ellipsis' />
|
<ContentEditable className='className="h-[450px] outline-none resize-none overflow-hidden text-ellipsis' />
|
||||||
}
|
}
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
placeholder={
|
placeholder={
|
||||||
|
116
apps/app/components/project/ConfirmProjectMemberRemove.tsx
Normal file
116
apps/app/components/project/ConfirmProjectMemberRemove.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleDelete: () => void;
|
||||||
|
data?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfirmProjectMemberRemove: React.FC<Props> = ({ isOpen, onClose, data, handleDelete }) => {
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const cancelButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletion = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
handleDelete();
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
Remove {data?.email}?
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Are you sure you want to remove member - {`"`}
|
||||||
|
<span className="italic">{data?.email}</span>
|
||||||
|
{`"`} ? They will no longer have access to this project. 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 ? "Removing..." : "Remove"}
|
||||||
|
</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 ConfirmProjectMemberRemove;
|
@ -12,6 +12,7 @@ import useToast from "lib/hooks/useToast";
|
|||||||
import projectService from "lib/services/project.service";
|
import projectService from "lib/services/project.service";
|
||||||
import workspaceService from "lib/services/workspace.service";
|
import workspaceService from "lib/services/workspace.service";
|
||||||
// constants
|
// constants
|
||||||
|
import { ROLE } from "constants/";
|
||||||
import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Select, TextArea } from "ui";
|
import { Button, Select, TextArea } from "ui";
|
||||||
@ -30,13 +31,9 @@ type Props = {
|
|||||||
const defaultValues: Partial<ProjectMember> = {
|
const defaultValues: Partial<ProjectMember> = {
|
||||||
email: "",
|
email: "",
|
||||||
message: "",
|
message: "",
|
||||||
};
|
role: 5,
|
||||||
|
member_id: "",
|
||||||
const ROLE = {
|
user_id: "",
|
||||||
5: "Guest",
|
|
||||||
10: "Viewer",
|
|
||||||
15: "Member",
|
|
||||||
20: "Admin",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, members }) => {
|
const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, members }) => {
|
||||||
|
208
apps/app/components/project/cycles/CycleIssuesListModal.tsx
Normal file
208
apps/app/components/project/cycles/CycleIssuesListModal.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
// headless ui
|
||||||
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
// services
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
|
// icons
|
||||||
|
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IIssue, IssueResponse } from "types";
|
||||||
|
// constants
|
||||||
|
import { classNames } from "constants/common";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
issues: IssueResponse | undefined;
|
||||||
|
cycleId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormInput = {
|
||||||
|
issue_ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CycleIssuesListModal: React.FC<Props> = ({
|
||||||
|
isOpen,
|
||||||
|
handleClose: onClose,
|
||||||
|
issues,
|
||||||
|
cycleId,
|
||||||
|
}) => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setQuery("");
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { handleSubmit, reset, control } = useForm<FormInput>({
|
||||||
|
defaultValues: {
|
||||||
|
issue_ids: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddToCycle: 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
|
||||||
|
.bulkAddIssuesToCycle(activeWorkspace.slug, activeProject.id, cycleId, data)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredIssues: IIssue[] =
|
||||||
|
query === ""
|
||||||
|
? issues?.results ?? []
|
||||||
|
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
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>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="issue_ids"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Combobox as="div" {...field} multiple>
|
||||||
|
<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={(e) => setQuery(e.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 to add to cycle
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<ul className="text-sm text-gray-700">
|
||||||
|
{filteredIssues.map((issue) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={issue.id}
|
||||||
|
as="label"
|
||||||
|
htmlFor={`issue-${issue.id}`}
|
||||||
|
value={issue.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
"flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2",
|
||||||
|
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
<input type="checkbox" checked={selected} readOnly />
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
|
</span>
|
||||||
|
{issue.name}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</Combobox.Options>
|
||||||
|
|
||||||
|
{query !== "" && filteredIssues.length === 0 && (
|
||||||
|
<div className="py-14 px-6 text-center sm:px-14">
|
||||||
|
<RectangleStackIcon
|
||||||
|
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 type="button" theme="danger" size="sm" onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" onClick={handleSubmit(handleAddToCycle)}>
|
||||||
|
Add to Cycle
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CycleIssuesListModal;
|
@ -1,50 +1,77 @@
|
|||||||
import React from "react";
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import Link from "next/link";
|
||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Disclosure, Transition, Menu, Listbox } from "@headlessui/react";
|
import { Disclosure, Transition, Menu } from "@headlessui/react";
|
||||||
// fetch keys
|
|
||||||
import { PROJECT_ISSUES_LIST, CYCLE_ISSUES } from "constants/fetch-keys";
|
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
|
||||||
import cycleServices from "lib/services/cycles.services";
|
import cycleServices from "lib/services/cycles.services";
|
||||||
// commons
|
// hooks
|
||||||
import { classNames, renderShortNumericDateFormat } from "constants/common";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// components
|
||||||
|
import CycleIssuesListModal from "./CycleIssuesListModal";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "ui";
|
import { Spinner } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||||
// types
|
// types
|
||||||
import type { ICycle, SprintViewProps as Props, SprintIssueResponse, IssueResponse } from "types";
|
import type { CycleViewProps as Props, CycleIssueResponse, IssueResponse } from "types";
|
||||||
|
// fetch keys
|
||||||
|
import { CYCLE_ISSUES } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { renderShortNumericDateFormat } from "constants/common";
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
|
|
||||||
const SprintView: React.FC<Props> = ({
|
const CycleView: React.FC<Props> = ({
|
||||||
sprint,
|
cycle,
|
||||||
selectSprint,
|
selectSprint,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
openIssueModal,
|
openIssueModal,
|
||||||
addIssueToSprint,
|
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||||
|
|
||||||
const { data: sprintIssues } = useSWR<SprintIssueResponse[]>(CYCLE_ISSUES(sprint.id), () =>
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
cycleServices.getCycleIssues(workspaceSlug, projectId, sprint.id)
|
|
||||||
|
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(CYCLE_ISSUES(cycle.id), () =>
|
||||||
|
cycleServices.getCycleIssues(workspaceSlug, projectId, cycle.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projectIssues } = useSWR<IssueResponse>(
|
const removeIssueFromCycle = (cycleId: string, bridgeId: string) => {
|
||||||
projectId && workspaceSlug ? PROJECT_ISSUES_LIST(workspaceSlug, projectId) : null,
|
if (activeWorkspace && activeProject && cycleIssues) {
|
||||||
workspaceSlug ? () => issuesServices.getIssues(workspaceSlug, projectId) : null
|
mutate<CycleIssueResponse[]>(
|
||||||
|
CYCLE_ISSUES(cycleId),
|
||||||
|
(prevData) => prevData?.filter((p) => p.id !== bridgeId),
|
||||||
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
issuesServices
|
||||||
|
.removeIssueFromCycle(activeWorkspace.slug, activeProject.id, cycleId, bridgeId)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-y-4 pb-5 relative">
|
<>
|
||||||
<Disclosure defaultOpen>
|
<CycleIssuesListModal
|
||||||
|
isOpen={cycleIssuesListModal}
|
||||||
|
handleClose={() => setCycleIssuesListModal(false)}
|
||||||
|
issues={issues}
|
||||||
|
cycleId={cycle.id}
|
||||||
|
/>
|
||||||
|
<Disclosure as="div" defaultOpen>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="bg-gray-50 py-5 px-5 rounded">
|
<div className="bg-white px-4 py-2 rounded-lg space-y-3">
|
||||||
<div className="w-full h-full space-y-6 overflow-auto pb-10">
|
<div className="flex items-center">
|
||||||
<div className="w-full flex items-center">
|
|
||||||
<Disclosure.Button className="w-full">
|
<Disclosure.Button className="w-full">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<span>
|
<span>
|
||||||
@ -53,51 +80,48 @@ const SprintView: React.FC<Props> = ({
|
|||||||
className={`text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
|
className={`text-gray-500 ${!open ? "transform -rotate-90" : ""}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<h2 className="text-xl">{sprint.name}</h2>
|
<h2 className="font-medium leading-5">{cycle.name}</h2>
|
||||||
<p className="font-light text-gray-500">
|
<p className="flex gap-2 text-xs text-gray-500">
|
||||||
{sprint.status === "started"
|
<span>
|
||||||
? sprint.start_date
|
{cycle.status === "started"
|
||||||
? `${renderShortNumericDateFormat(sprint.start_date)} - `
|
? cycle.start_date
|
||||||
|
? `${renderShortNumericDateFormat(cycle.start_date)} - `
|
||||||
: ""
|
: ""
|
||||||
: sprint.status}
|
: cycle.status}
|
||||||
{sprint.end_date ? renderShortNumericDateFormat(sprint.end_date) : ""}
|
</span>
|
||||||
|
<span>
|
||||||
|
{cycle.end_date ? renderShortNumericDateFormat(cycle.end_date) : ""}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
|
|
||||||
<div className="relative">
|
<Menu as="div" className="relative inline-block">
|
||||||
<Menu>
|
<Menu.Button className="grid place-items-center rounded p-1 hover:bg-gray-100 focus:outline-none">
|
||||||
<Menu.Button>
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
<EllipsisHorizontalIcon width="16" height="16" />
|
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Menu.Items className="absolute z-20 w-28 bg-white rounded border cursor-pointer -left-24">
|
<Menu.Items className="absolute origin-top-right right-0 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
<button
|
||||||
className="w-full text-left py-2 pl-2"
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectSprint({ ...sprint, actionType: "edit" })}
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
onClick={() => selectSprint({ ...cycle, actionType: "edit" })}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
<button
|
||||||
className="w-full text-left py-2 pl-2"
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectSprint({ ...sprint, actionType: "delete" })}
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
onClick={() => selectSprint({ ...cycle, actionType: "delete" })}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
enter="transition duration-100 ease-out"
|
enter="transition duration-100 ease-out"
|
||||||
@ -108,63 +132,111 @@ const SprintView: React.FC<Props> = ({
|
|||||||
leaveTo="transform opacity-0"
|
leaveTo="transform opacity-0"
|
||||||
>
|
>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div className="space-y-3">
|
<StrictModeDroppable droppableId={cycle.id}>
|
||||||
{sprintIssues ? (
|
{(provided) => (
|
||||||
sprintIssues.length > 0 ? (
|
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
sprintIssues.map((issue) => (
|
{cycleIssues ? (
|
||||||
<div
|
cycleIssues.length > 0 ? (
|
||||||
|
cycleIssues.map((issue, index) => (
|
||||||
|
<Draggable
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
className="p-4 bg-white border border-gray-200 rounded flex items-center justify-between"
|
draggableId={`${issue.id},${issue.issue}`} // bridge id, issue id
|
||||||
|
index={index}
|
||||||
>
|
>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
className={`group p-2 hover:bg-gray-100 text-sm rounded flex items-center justify-between ${
|
||||||
|
snapshot.isDragging
|
||||||
|
? "bg-gray-100 shadow-lg border border-theme"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 rotate-90 outline-none`}
|
||||||
router.push(
|
{...provided.dragHandleProps}
|
||||||
`/projects/${projectId}/issues/${issue.issue_details.id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<p>{issue.issue_details.name}</p>
|
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-x-4">
|
|
||||||
<span
|
<span
|
||||||
className="text-black rounded px-2 py-0.5 text-sm border"
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.issue_details.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
href={`/projects/${projectId}/issues/${issue.issue_details.id}`}
|
||||||
|
>
|
||||||
|
<a className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-
|
||||||
|
{issue.issue_details.sequence_id}
|
||||||
|
</span>
|
||||||
|
{issue.issue_details.name}
|
||||||
|
{/* {cycle.id} */}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="text-black rounded-md px-2 py-0.5 text-sm"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${issue.issue_details.state_detail?.color}20`,
|
backgroundColor: `${issue.issue_details.state_detail?.color}20`,
|
||||||
borderColor: issue.issue_details.state_detail?.color,
|
border: `2px solid ${issue.issue_details.state_detail?.color}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{issue.issue_details.state_detail?.name}
|
{issue.issue_details.state_detail?.name}
|
||||||
</span>
|
</span>
|
||||||
<div className="relative">
|
<Menu as="div" className="relative">
|
||||||
<Menu>
|
<Menu.Button
|
||||||
<Menu.Button>
|
as="button"
|
||||||
<EllipsisHorizontalIcon width="16" height="16" />
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none`}
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Menu.Items className="absolute z-20 w-28 bg-white rounded border cursor-pointer -left-24">
|
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
<button
|
||||||
className="w-full text-left py-2 pl-2"
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openIssueModal(sprint.id, issue.issue_details, "edit")
|
openIssueModal(cycle.id, issue.issue_details, "edit")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
|
<button
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
removeIssueFromCycle(issue.cycle, issue.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Remove from cycle
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
<button
|
<button
|
||||||
className="w-full text-left py-2 pl-2"
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openIssueModal(sprint.id, issue.issue_details, "delete")
|
openIssueModal(
|
||||||
|
cycle.id,
|
||||||
|
issue.issue_details,
|
||||||
|
"delete"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Delete
|
Delete permanently
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@ -172,43 +244,28 @@ const SprintView: React.FC<Props> = ({
|
|||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</Draggable>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-gray-500">This cycle has no issues.</p>
|
<p className="text-sm text-gray-500">This cycle has no issue.</p>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</StrictModeDroppable>
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div className="flex flex-col gap-y-2">
|
<Menu as="div" className="relative inline-block">
|
||||||
<button
|
<Menu.Button className="flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 text-xs font-medium">
|
||||||
className="text-indigo-600 flex items-center gap-x-2"
|
<PlusIcon className="h-3 w-3" />
|
||||||
onClick={() => openIssueModal(sprint.id)}
|
Add issue
|
||||||
>
|
|
||||||
<div className="bg-theme text-white rounded-full p-0.5">
|
|
||||||
<PlusIcon width="18" height="18" />
|
|
||||||
</div>
|
|
||||||
<p>Add Issue</p>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="ml-1">
|
|
||||||
<Menu as="div" className="inline-block text-left">
|
|
||||||
<div>
|
|
||||||
<Menu.Button className="inline-flex w-full items-center justify-center rounded-md text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-100">
|
|
||||||
<div className="text-indigo-600 flex items-center gap-x-2">
|
|
||||||
<p>Add Existing Issue</p>
|
|
||||||
</div>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="-mr-1 ml-2 h-5 w-5 text-indigo-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
@ -219,40 +276,39 @@ const SprintView: React.FC<Props> = ({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute left-5 z-20 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
<Menu.Items className="absolute left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
<div className="py-1">
|
<div className="p-1">
|
||||||
{projectIssues?.results.map((issue) => (
|
<Menu.Item as="div">
|
||||||
<Menu.Item
|
{(active) => (
|
||||||
key={issue.id}
|
<button
|
||||||
as="div"
|
type="button"
|
||||||
onClick={() => {
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
addIssueToSprint(sprint.id, issue.id);
|
onClick={() => openIssueModal(cycle.id)}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{({ active }) => (
|
Create new
|
||||||
<p
|
</button>
|
||||||
className={classNames(
|
)}
|
||||||
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
|
</Menu.Item>
|
||||||
"block px-4 py-2 text-sm"
|
<Menu.Item as="div">
|
||||||
)}
|
{(active) => (
|
||||||
>
|
<button
|
||||||
{issue.name}
|
type="button"
|
||||||
</p>
|
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||||
|
onClick={() => setCycleIssuesListModal(true)}
|
||||||
|
>
|
||||||
|
Add an existing issue
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SprintView;
|
export default CycleView;
|
||||||
|
@ -5,7 +5,11 @@ import Link from "next/link";
|
|||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
// common
|
// common
|
||||||
import { addSpaceIfCamelCase, renderShortNumericDateFormat } from "constants/common";
|
import {
|
||||||
|
addSpaceIfCamelCase,
|
||||||
|
findHowManyDaysLeft,
|
||||||
|
renderShortNumericDateFormat,
|
||||||
|
} from "constants/common";
|
||||||
// types
|
// types
|
||||||
import { IIssue, Properties, NestedKeyOf } from "types";
|
import { IIssue, Properties, NestedKeyOf } from "types";
|
||||||
// icons
|
// icons
|
||||||
@ -23,7 +27,9 @@ import { divide } from "lodash";
|
|||||||
type Props = {
|
type Props = {
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
groupedByIssues: any;
|
groupedByIssues: {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
};
|
||||||
index: number;
|
index: number;
|
||||||
setIsIssueOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsIssueOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
@ -69,7 +75,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className={`rounded flex-shrink-0 h-full ${
|
className={`rounded flex-shrink-0 h-full ${
|
||||||
snapshot.isDragging ? "border-indigo-600 shadow-lg" : ""
|
snapshot.isDragging ? "border-theme shadow-lg" : ""
|
||||||
} ${!show ? "" : "w-80 bg-gray-50 border"}`}
|
} ${!show ? "" : "w-80 bg-gray-50 border"}`}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
@ -158,25 +164,12 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
|
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setPreloadedData({
|
setPreloadedData({
|
||||||
// ...state,
|
|
||||||
actionType: "edit",
|
actionType: "edit",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PencilIcon className="h-4 w-4" />
|
<PencilIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
{/* <button
|
|
||||||
type="button"
|
|
||||||
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300"
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedState({
|
|
||||||
...state,
|
|
||||||
actionType: "delete",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
|
||||||
</button> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||||
@ -188,7 +181,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{groupedByIssues[groupTitle].map((childIssue: any, index: number) => (
|
{groupedByIssues[groupTitle].map((childIssue, index: number) => (
|
||||||
<Draggable key={childIssue.id} draggableId={childIssue.id} index={index}>
|
<Draggable key={childIssue.id} draggableId={childIssue.id} index={index}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<Link href={`/projects/${childIssue.project}/issues/${childIssue.id}`}>
|
<Link href={`/projects/${childIssue.project}/issues/${childIssue.id}`}>
|
||||||
@ -203,6 +196,9 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
className="px-2 py-3 space-y-1.5 select-none"
|
className="px-2 py-3 space-y-1.5 select-none"
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
>
|
>
|
||||||
|
<span className="group-hover:text-theme text-sm break-all">
|
||||||
|
{childIssue.name}
|
||||||
|
</span>
|
||||||
{Object.keys(properties).map(
|
{Object.keys(properties).map(
|
||||||
(key) =>
|
(key) =>
|
||||||
properties[key as keyof Properties] &&
|
properties[key as keyof Properties] &&
|
||||||
@ -227,34 +223,66 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
: key === "target_date"
|
: key === "target_date"
|
||||||
? "text-xs bg-indigo-50 px-2 py-1 mt-2 flex items-center gap-x-1 rounded w-min whitespace-nowrap"
|
? "text-xs bg-indigo-50 px-2 py-1 mt-2 flex items-center gap-x-1 rounded w-min whitespace-nowrap"
|
||||||
: "text-sm text-gray-500"
|
: "text-sm text-gray-500"
|
||||||
} gap-1
|
} gap-1 relative
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{key === "target_date" ? (
|
{key === "start_date" && childIssue.start_date !== null && (
|
||||||
|
<span className="text-sm">
|
||||||
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
|
{renderShortNumericDateFormat(childIssue.start_date)} -
|
||||||
|
{childIssue.target_date
|
||||||
|
? renderShortNumericDateFormat(childIssue.target_date)
|
||||||
|
: "None"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{key === "target_date" && (
|
||||||
<>
|
<>
|
||||||
<CalendarDaysIcon className="h-4 w-4" />{" "}
|
<span
|
||||||
|
className={`flex items-center gap-x-1 group ${
|
||||||
|
childIssue.target_date === null
|
||||||
|
? ""
|
||||||
|
: childIssue.target_date < new Date().toISOString()
|
||||||
|
? "text-red-600"
|
||||||
|
: findHowManyDaysLeft(childIssue.target_date) <=
|
||||||
|
3 && "text-orange-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
{childIssue.target_date
|
{childIssue.target_date
|
||||||
? renderShortNumericDateFormat(childIssue.target_date)
|
? renderShortNumericDateFormat(childIssue.target_date)
|
||||||
: "N/A"}
|
: "N/A"}
|
||||||
</>
|
{childIssue.target_date && (
|
||||||
) : (
|
<span className="absolute -top-full mb-2 left-4 border transition-opacity opacity-0 group-hover:opacity-100 bg-white rounded px-2 py-1">
|
||||||
""
|
{childIssue.target_date < new Date().toISOString()
|
||||||
|
? `Target date has passed by ${findHowManyDaysLeft(
|
||||||
|
childIssue.target_date
|
||||||
|
)} days`
|
||||||
|
: findHowManyDaysLeft(childIssue.target_date) <= 3
|
||||||
|
? `Target date is in ${findHowManyDaysLeft(
|
||||||
|
childIssue.target_date
|
||||||
|
)} days`
|
||||||
|
: "Target date"}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{key === "name" && (
|
</span>
|
||||||
<span className="group-hover:text-theme">
|
</>
|
||||||
{childIssue.name}
|
)}
|
||||||
|
{key === "key" && (
|
||||||
|
<span className="text-xs">
|
||||||
|
{childIssue.project_detail?.identifier}-
|
||||||
|
{childIssue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{key === "state" && (
|
{key === "state" && (
|
||||||
<>{addSpaceIfCamelCase(childIssue["state_detail"].name)}</>
|
<>{addSpaceIfCamelCase(childIssue["state_detail"].name)}</>
|
||||||
)}
|
)}
|
||||||
{key === "priority" && <>{childIssue.priority}</>}
|
{key === "priority" && <>{childIssue.priority}</>}
|
||||||
{key === "description" && <>{childIssue.description}</>}
|
{/* {key === "description" && <>{childIssue.description}</>} */}
|
||||||
{key === "assignee" ? (
|
{key === "assignee" ? (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
{childIssue?.assignee_details?.length > 0 ? (
|
{childIssue?.assignee_details?.length > 0 ? (
|
||||||
childIssue?.assignee_details?.map(
|
childIssue?.assignee_details?.map(
|
||||||
(assignee: any, index: number) => (
|
(assignee, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||||
@ -282,7 +310,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<span>None</span>
|
<span>No assignee.</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@ -290,29 +318,6 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div
|
|
||||||
className={`p-2 bg-indigo-50 flex items-center justify-between ${
|
|
||||||
snapshot.isDragging ? "bg-indigo-200" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex flex-col"
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
>
|
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
|
|
||||||
</button>
|
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
<button type="button">
|
|
||||||
<HeartIcon className="h-4 w-4 text-yellow-500" />
|
|
||||||
</button>
|
|
||||||
<button type="button">
|
|
||||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
@ -67,8 +67,6 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||||||
|
|
||||||
setIssueDeletionData(removedItem);
|
setIssueDeletionData(removedItem);
|
||||||
setIsIssueDeletionOpen(true);
|
setIsIssueDeletionOpen(true);
|
||||||
|
|
||||||
console.log(removedItem);
|
|
||||||
} else {
|
} else {
|
||||||
if (type === "state") {
|
if (type === "state") {
|
||||||
const newStates = Array.from(states ?? []);
|
const newStates = Array.from(states ?? []);
|
||||||
@ -168,21 +166,6 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <CreateUpdateStateModal
|
|
||||||
isOpen={
|
|
||||||
isOpen &&
|
|
||||||
preloadedData?.actionType !== "delete" &&
|
|
||||||
preloadedData?.actionType !== "createIssue"
|
|
||||||
}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
data={preloadedData as Partial<IIssue>}
|
|
||||||
projectId={projectId as string}
|
|
||||||
/> */}
|
|
||||||
{/* <ConfirmStateDeletion
|
|
||||||
isOpen={isOpen && preloadedData?.actionType === "delete"}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
data={preloadedData as Partial<IIssue>}
|
|
||||||
/> */}
|
|
||||||
<ConfirmIssueDeletion
|
<ConfirmIssueDeletion
|
||||||
isOpen={isIssueDeletionOpen}
|
isOpen={isIssueDeletionOpen}
|
||||||
handleClose={() => setIsIssueDeletionOpen(false)}
|
handleClose={() => setIsIssueDeletionOpen(false)}
|
||||||
@ -199,21 +182,6 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||||||
{groupedByIssues ? (
|
{groupedByIssues ? (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
{/* <StrictModeDroppable droppableId="trashBox">
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`fixed bottom-2 right-8 z-10 px-2 py-1 flex items-center gap-2 rounded-lg mb-5 text-red-600 text-sm bg-red-100 border-2 border-transparent ${
|
|
||||||
snapshot.isDraggingOver ? "border-red-600" : ""
|
|
||||||
}`}
|
|
||||||
{...provided.droppableProps}
|
|
||||||
ref={provided.innerRef}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-3 w-3" />
|
|
||||||
Drop to delete
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</StrictModeDroppable> */}
|
|
||||||
<div className="h-full w-full overflow-hidden">
|
<div className="h-full w-full overflow-hidden">
|
||||||
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
|
@ -44,7 +44,7 @@ const SelectAssignee: React.FC<Props> = ({ control }) => {
|
|||||||
multiple={true}
|
multiple={true}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
icon={<UserIcon className="h-4 w-4 text-gray-400" />}
|
icon={<UserIcon className="h-3 w-3 text-gray-500" />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -33,7 +33,7 @@ const SelectSprint: React.FC<Props> = ({ control, setIsOpen }) => {
|
|||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative 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">
|
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative 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">
|
||||||
<ArrowPathIcon className="h-3 w-3" />
|
<ArrowPathIcon className="h-3 w-3 text-gray-500" />
|
||||||
<span className="block truncate">
|
<span className="block truncate">
|
||||||
{cycles?.find((i) => i.id.toString() === value?.toString())?.name ?? "Cycle"}
|
{cycles?.find((i) => i.id.toString() === value?.toString())?.name ?? "Cycle"}
|
||||||
</span>
|
</span>
|
||||||
|
@ -83,7 +83,7 @@ const SelectLabels: React.FC<Props> = ({ control }) => {
|
|||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative 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">
|
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative 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">
|
||||||
<TagIcon className="h-3 w-3" />
|
<TagIcon className="h-3 w-3 text-gray-500" />
|
||||||
<span className="block truncate">
|
<span className="block truncate">
|
||||||
{value && value.length > 0
|
{value && value.length > 0
|
||||||
? value.map((id) => issueLabels?.find((i) => i.id === id)?.name).join(", ")
|
? value.map((id) => issueLabels?.find((i) => i.id === id)?.name).join(", ")
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
// react hook form
|
||||||
|
import { Controller, Control } from "react-hook-form";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// types
|
||||||
|
import type { IIssue, IssueResponse } from "types";
|
||||||
|
// icons
|
||||||
|
import { UserIcon } from "@heroicons/react/24/outline";
|
||||||
|
// components
|
||||||
|
import IssuesListModal from "components/project/issues/IssuesListModal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<IIssue, any>;
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
issues: IssueResponse | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectParent: React.FC<Props> = ({ control, isOpen, setIsOpen, issues }) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="parent"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<IssuesListModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
handleClose={() => setIsOpen(false)}
|
||||||
|
onChange={onChange}
|
||||||
|
issues={issues}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectParent;
|
@ -1,66 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
// react hook form
|
|
||||||
import { Controller } from "react-hook-form";
|
|
||||||
// hooks
|
|
||||||
import useUser from "lib/hooks/useUser";
|
|
||||||
// types
|
|
||||||
import type { IIssue } from "types";
|
|
||||||
import type { Control } from "react-hook-form";
|
|
||||||
import { UserIcon } from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
control: Control<IIssue, any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
import { SearchListbox } from "ui";
|
|
||||||
|
|
||||||
const SelectParent: React.FC<Props> = ({ control }) => {
|
|
||||||
const { issues: projectIssues } = useUser();
|
|
||||||
|
|
||||||
const getSelectedIssueKey = (issueId: string | undefined) => {
|
|
||||||
const identifier = projectIssues?.results?.find((i) => i.id.toString() === issueId?.toString())
|
|
||||||
?.project_detail?.identifier;
|
|
||||||
|
|
||||||
const sequenceId = projectIssues?.results?.find(
|
|
||||||
(i) => i.id.toString() === issueId?.toString()
|
|
||||||
)?.sequence_id;
|
|
||||||
|
|
||||||
if (issueId) return `${identifier}-${sequenceId}`;
|
|
||||||
else return "Parent issue";
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="parent"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SearchListbox
|
|
||||||
title="Parent issue"
|
|
||||||
optionsFontsize="sm"
|
|
||||||
options={projectIssues?.results?.map((issue) => {
|
|
||||||
return {
|
|
||||||
value: issue.id,
|
|
||||||
display: issue.name,
|
|
||||||
element: (
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="block truncate">
|
|
||||||
<span className="block truncate">{`${getSelectedIssueKey(issue.id)}`}</span>
|
|
||||||
<span className="block truncate text-gray-400">{issue.name}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
value={value}
|
|
||||||
width="xs"
|
|
||||||
buttonClassName="max-h-30 overflow-y-scroll"
|
|
||||||
optionsClassName="max-h-30 overflow-y-scroll"
|
|
||||||
onChange={onChange}
|
|
||||||
icon={<UserIcon className="h-4 w-4 text-gray-400" />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectParent;
|
|
@ -5,6 +5,8 @@ import { Controller } from "react-hook-form";
|
|||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// icons
|
// icons
|
||||||
import { CheckIcon } from "@heroicons/react/20/solid";
|
import { CheckIcon } from "@heroicons/react/20/solid";
|
||||||
|
// constants
|
||||||
|
import { PRIORITIES } from "constants/";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
@ -15,8 +17,6 @@ type Props = {
|
|||||||
control: Control<IIssue, any>;
|
control: Control<IIssue, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRIORITIES = ["high", "medium", "low"];
|
|
||||||
|
|
||||||
const SelectPriority: React.FC<Props> = ({ control }) => {
|
const SelectPriority: React.FC<Props> = ({ control }) => {
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
@ -28,8 +28,10 @@ const SelectPriority: React.FC<Props> = ({ control }) => {
|
|||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative 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">
|
<Listbox.Button className="flex items-center gap-1 hover:bg-gray-100 relative 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">
|
||||||
<ChartBarIcon className="h-3 w-3" />
|
<ChartBarIcon className="h-3 w-3 text-gray-500" />
|
||||||
<span className="block capitalize">{value ?? "Priority"}</span>
|
<span className="block capitalize">
|
||||||
|
{value && value !== "" ? value : "Priority"}
|
||||||
|
</span>
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
|
@ -21,7 +21,6 @@ const SelectState: React.FC<Props> = ({ control, setIsOpen }) => {
|
|||||||
const { states } = useUser();
|
const { states } = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="state"
|
name="state"
|
||||||
@ -51,8 +50,7 @@ const SelectState: React.FC<Props> = ({ control, setIsOpen }) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
></Controller>
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// next
|
// next
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
// swr
|
// swr
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// fetching keys
|
// fetching keys
|
||||||
import {
|
import {
|
||||||
PROJECT_ISSUES_DETAILS,
|
PROJECT_ISSUES_DETAILS,
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
USER_ISSUE,
|
USER_ISSUE,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
// headless
|
// headless
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.services";
|
||||||
// hooks
|
// hooks
|
||||||
@ -31,12 +31,13 @@ import SelectLabels from "./SelectLabels";
|
|||||||
import SelectProject from "./SelectProject";
|
import SelectProject from "./SelectProject";
|
||||||
import SelectPriority from "./SelectPriority";
|
import SelectPriority from "./SelectPriority";
|
||||||
import SelectAssignee from "./SelectAssignee";
|
import SelectAssignee from "./SelectAssignee";
|
||||||
import SelectParent from "./SelectParentIssues";
|
import SelectParent from "./SelectParentIssue";
|
||||||
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
|
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
|
||||||
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IIssue, IssueResponse, SprintIssueResponse } from "types";
|
import type { IIssue, IssueResponse, CycleIssueResponse } from "types";
|
||||||
|
import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -48,8 +49,13 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IIssue> = {
|
const defaultValues: Partial<IIssue> = {
|
||||||
|
project: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
// description: "",
|
||||||
|
state: "",
|
||||||
|
sprints: null,
|
||||||
|
priority: null,
|
||||||
|
labels_list: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateUpdateIssuesModal: React.FC<Props> = ({
|
const CreateUpdateIssuesModal: React.FC<Props> = ({
|
||||||
@ -62,9 +68,20 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isCycleModalOpen, setIsCycleModalOpen] = useState(false);
|
const [isCycleModalOpen, setIsCycleModalOpen] = useState(false);
|
||||||
const [isStateModalOpen, setIsStateModalOpen] = useState(false);
|
const [isStateModalOpen, setIsStateModalOpen] = useState(false);
|
||||||
|
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||||
|
|
||||||
const [mostSimilarIssue, setMostSimilarIssue] = useState<string | undefined>();
|
const [mostSimilarIssue, setMostSimilarIssue] = useState<string | undefined>();
|
||||||
|
|
||||||
|
// const [issueDescriptionValue, setIssueDescriptionValue] = useState("");
|
||||||
|
// const handleDescriptionChange: any = (value: any) => {
|
||||||
|
// console.log(value);
|
||||||
|
// setIssueDescriptionValue(value);
|
||||||
|
// };
|
||||||
|
|
||||||
|
const RichTextEditor = dynamic(() => import("components/lexical/editor"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -74,13 +91,6 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
reset(defaultValues);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, user, issues } = useUser();
|
const { activeWorkspace, activeProject, user, issues } = useUser();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -97,6 +107,13 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reset(defaultValues);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
const addIssueToSprint = async (issueId: string, sprintId: string, issueDetail: IIssue) => {
|
const addIssueToSprint = async (issueId: string, sprintId: string, issueDetail: IIssue) => {
|
||||||
if (!activeWorkspace || !activeProject) return;
|
if (!activeWorkspace || !activeProject) return;
|
||||||
await issuesServices
|
await issuesServices
|
||||||
@ -104,8 +121,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
issue: issueId,
|
issue: issueId,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log("add to sprint", res);
|
mutate<CycleIssueResponse[]>(
|
||||||
mutate<SprintIssueResponse[]>(
|
|
||||||
CYCLE_ISSUES(sprintId),
|
CYCLE_ISSUES(sprintId),
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
const targetResponse = prevData?.find((t) => t.cycle === sprintId);
|
const targetResponse = prevData?.find((t) => t.cycle === sprintId);
|
||||||
@ -118,7 +134,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
{
|
{
|
||||||
cycle: sprintId,
|
cycle: sprintId,
|
||||||
issue_details: issueDetail,
|
issue_details: issueDetail,
|
||||||
} as SprintIssueResponse,
|
} as CycleIssueResponse,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -166,17 +182,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
.createIssues(activeWorkspace.slug, activeProject.id, payload)
|
.createIssues(activeWorkspace.slug, activeProject.id, payload)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
console.log(res);
|
console.log(res);
|
||||||
mutate<IssueResponse>(
|
mutate<IssueResponse>(PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id));
|
||||||
PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id),
|
|
||||||
(prevData) => {
|
|
||||||
return {
|
|
||||||
...(prevData as IssueResponse),
|
|
||||||
results: [res, ...(prevData?.results ?? [])],
|
|
||||||
count: (prevData?.count ?? 0) + 1,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (formData.sprints && formData.sprints !== null) {
|
if (formData.sprints && formData.sprints !== null) {
|
||||||
await addIssueToSprint(res.id, formData.sprints, formData);
|
await addIssueToSprint(res.id, formData.sprints, formData);
|
||||||
@ -189,13 +195,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
message: `Issue ${data ? "updated" : "created"} successfully`,
|
message: `Issue ${data ? "updated" : "created"} successfully`,
|
||||||
});
|
});
|
||||||
if (formData.assignees_list.some((assignee) => assignee === user?.id)) {
|
if (formData.assignees_list.some((assignee) => assignee === user?.id)) {
|
||||||
mutate<IIssue[]>(
|
mutate<IIssue[]>(USER_ISSUE);
|
||||||
USER_ISSUE,
|
|
||||||
(prevData) => {
|
|
||||||
return [res, ...(prevData ?? [])];
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -261,6 +261,8 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
return () => setMostSimilarIssue(undefined);
|
return () => setMostSimilarIssue(undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// console.log(watch("parent"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{activeProject && (
|
{activeProject && (
|
||||||
@ -381,6 +383,13 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
error={errors.description}
|
error={errors.description}
|
||||||
register={register}
|
register={register}
|
||||||
/>
|
/>
|
||||||
|
{/* <Controller
|
||||||
|
name="description"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<RichTextEditor {...field} id="issueDescriptionEditor" />
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
@ -398,9 +407,48 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
<SelectState control={control} setIsOpen={setIsStateModalOpen} />
|
<SelectState control={control} setIsOpen={setIsStateModalOpen} />
|
||||||
<SelectCycles control={control} setIsOpen={setIsCycleModalOpen} />
|
<SelectCycles control={control} setIsOpen={setIsCycleModalOpen} />
|
||||||
<SelectPriority control={control} />
|
<SelectPriority control={control} />
|
||||||
<SelectLabels control={control} />
|
|
||||||
<SelectAssignee control={control} />
|
<SelectAssignee control={control} />
|
||||||
<SelectParent control={control} />
|
<SelectLabels control={control} />
|
||||||
|
<SelectParent
|
||||||
|
control={control}
|
||||||
|
isOpen={parentIssueListModalOpen}
|
||||||
|
setIsOpen={setParentIssueListModalOpen}
|
||||||
|
issues={issues}
|
||||||
|
/>
|
||||||
|
<Menu as="div" className="relative inline-block">
|
||||||
|
<Menu.Button className="grid place-items-center p-1 hover:bg-gray-100 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">
|
||||||
|
<EllipsisHorizontalIcon className="h-5 w-5" />
|
||||||
|
</Menu.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
||||||
|
<div className="p-1">
|
||||||
|
<Menu.Item as="div">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap "
|
||||||
|
onClick={() => setParentIssueListModalOpen(true)}
|
||||||
|
>
|
||||||
|
{watch("parent") && watch("parent") !== ""
|
||||||
|
? `${activeProject?.identifier}-${
|
||||||
|
issues?.results.find((i) => i.id === watch("parent"))
|
||||||
|
?.sequence_id
|
||||||
|
}`
|
||||||
|
: "Select Parent Issue"}
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
140
apps/app/components/project/issues/IssuesListModal.tsx
Normal file
140
apps/app/components/project/issues/IssuesListModal.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
// headless ui
|
||||||
|
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IIssue, IssueResponse } from "types";
|
||||||
|
import { classNames } from "constants/common";
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onChange: (...event: any[]) => void;
|
||||||
|
issues: IssueResponse | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChange, issues }) => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const { activeProject } = useUser();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setQuery("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredIssues: IIssue[] =
|
||||||
|
query === ""
|
||||||
|
? issues?.results ?? []
|
||||||
|
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
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">
|
||||||
|
<Combobox onChange={onChange}>
|
||||||
|
<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={(e) => setQuery(e.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">
|
||||||
|
Issues
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<ul className="text-sm text-gray-700">
|
||||||
|
{filteredIssues.map((issue) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={issue.id}
|
||||||
|
value={issue.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
"flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2",
|
||||||
|
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
// setIssueIdFromList(issue.id);
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
|
</span>{" "}
|
||||||
|
{issue.name}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</Combobox.Options>
|
||||||
|
|
||||||
|
{query !== "" && filteredIssues.length === 0 && (
|
||||||
|
<div className="py-14 px-6 text-center sm:px-14">
|
||||||
|
<RectangleStackIcon
|
||||||
|
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>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IssuesListModal;
|
@ -14,6 +14,7 @@ import { IIssue, IssueResponse, IState, NestedKeyOf, Properties, WorkspaceMember
|
|||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
|
import { PRIORITIES } from "constants/";
|
||||||
import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.services";
|
||||||
@ -36,8 +37,6 @@ type Props = {
|
|||||||
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRIORITIES = ["high", "medium", "low"];
|
|
||||||
|
|
||||||
const ListView: React.FC<Props> = ({
|
const ListView: React.FC<Props> = ({
|
||||||
properties,
|
properties,
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
@ -175,10 +174,6 @@ const ListView: React.FC<Props> = ({
|
|||||||
<td className="px-3 py-4 font-medium text-gray-900 text-xs whitespace-nowrap">
|
<td className="px-3 py-4 font-medium text-gray-900 text-xs whitespace-nowrap">
|
||||||
{activeProject?.identifier}-{issue.sequence_id}
|
{activeProject?.identifier}-{issue.sequence_id}
|
||||||
</td>
|
</td>
|
||||||
) : (key as keyof Properties) === "description" ? (
|
|
||||||
<td className="px-3 py-4 font-medium text-gray-900 truncate text-xs max-w-[15rem]">
|
|
||||||
{issue.description}
|
|
||||||
</td>
|
|
||||||
) : (key as keyof Properties) === "priority" ? (
|
) : (key as keyof Properties) === "priority" ? (
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||||
<Listbox
|
<Listbox
|
||||||
@ -389,10 +384,6 @@ const ListView: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</td>
|
</td>
|
||||||
) : (key as keyof Properties) === "children" ? (
|
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900">
|
|
||||||
No children.
|
|
||||||
</td>
|
|
||||||
) : (key as keyof Properties) === "target_date" ? (
|
) : (key as keyof Properties) === "target_date" ? (
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap">
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap">
|
||||||
{issue.target_date
|
{issue.target_date
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
// next
|
// next
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
// ui
|
||||||
|
import { Spinner } from "ui";
|
||||||
|
// icons
|
||||||
import {
|
import {
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
ChartBarIcon,
|
ChartBarIcon,
|
||||||
ChatBubbleBottomCenterTextIcon,
|
ChatBubbleBottomCenterTextIcon,
|
||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
|
UserIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IssueResponse, IState } from "types";
|
||||||
|
// constants
|
||||||
import { addSpaceIfCamelCase, timeAgo } from "constants/common";
|
import { addSpaceIfCamelCase, timeAgo } from "constants/common";
|
||||||
import { IIssue, IState } from "types";
|
|
||||||
import { Spinner } from "ui";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueActivities: any[] | undefined;
|
issueActivities: any[] | undefined;
|
||||||
states: IState[] | undefined;
|
states: IState[] | undefined;
|
||||||
|
issues: IssueResponse | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const activityIcons: {
|
const activityIcons: {
|
||||||
@ -23,9 +29,10 @@ const activityIcons: {
|
|||||||
name: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
|
name: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
|
||||||
description: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
|
description: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
|
||||||
target_date: <CalendarDaysIcon className="h-4 w-4" />,
|
target_date: <CalendarDaysIcon className="h-4 w-4" />,
|
||||||
|
parent: <UserIcon className="h-4 w-4" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const IssueActivitySection: React.FC<Props> = ({ issueActivities, states }) => {
|
const IssueActivitySection: React.FC<Props> = ({ issueActivities, states, issues }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issueActivities ? (
|
{issueActivities ? (
|
||||||
@ -92,6 +99,10 @@ const IssueActivitySection: React.FC<Props> = ({ issueActivities, states }) => {
|
|||||||
states?.find((s) => s.id === activity.old_value)?.name ?? ""
|
states?.find((s) => s.id === activity.old_value)?.name ?? ""
|
||||||
)
|
)
|
||||||
: "None"
|
: "None"
|
||||||
|
: activity.field === "parent"
|
||||||
|
? activity.old_value
|
||||||
|
? issues?.results.find((i) => i.id === activity.old_value)?.name
|
||||||
|
: "None"
|
||||||
: activity.old_value ?? "None"}
|
: activity.old_value ?? "None"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -102,6 +113,10 @@ const IssueActivitySection: React.FC<Props> = ({ issueActivities, states }) => {
|
|||||||
states?.find((s) => s.id === activity.new_value)?.name ?? ""
|
states?.find((s) => s.id === activity.new_value)?.name ?? ""
|
||||||
)
|
)
|
||||||
: "None"
|
: "None"
|
||||||
|
: activity.field === "parent"
|
||||||
|
? activity.new_value
|
||||||
|
? issues?.results.find((i) => i.id === activity.new_value)?.name
|
||||||
|
: "None"
|
||||||
: activity.new_value ?? "None"}
|
: activity.new_value ?? "None"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,7 +73,7 @@ const ChangeStateDropdown: React.FC<Props> = ({ issue, updateIssues }) => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
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="fixed 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">
|
||||||
{states?.map((state) => (
|
{states?.map((state) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={state.id}
|
key={state.id}
|
||||||
|
197
apps/app/components/project/settings/ControlSettings.tsx
Normal file
197
apps/app/components/project/settings/ControlSettings.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
// 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 { Button } from "ui";
|
||||||
|
// icons
|
||||||
|
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IProject, WorkspaceMember } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<IProject, any>;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ControlSettings: React.FC<Props> = ({ control, isSubmitting }) => {
|
||||||
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
|
const { data: people } = useSWR<WorkspaceMember[]>(
|
||||||
|
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||||
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">Control</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Set the control for the project.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between gap-3">
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="project_lead"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Listbox value={value} onChange={onChange}>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Listbox.Label>
|
||||||
|
<div className="text-gray-500 mb-2">Project Lead</div>
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span className="block truncate">
|
||||||
|
{people?.find((person) => person.member.id === value)?.member
|
||||||
|
.first_name ?? "Select Lead"}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<ChevronDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</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 z-10 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) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={person.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
`${
|
||||||
|
active ? "text-white bg-theme" : "text-gray-900"
|
||||||
|
} cursor-default select-none relative py-2 pl-3 pr-9`
|
||||||
|
}
|
||||||
|
value={person.member.id}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
selected ? "font-semibold" : "font-normal"
|
||||||
|
} block truncate`}
|
||||||
|
>
|
||||||
|
{person.member.first_name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||||
|
active ? "text-white" : "text-indigo-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="default_assignee"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Listbox value={value} onChange={onChange}>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Listbox.Label>
|
||||||
|
<div className="text-gray-500 mb-2">Default Assignee</div>
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span className="block truncate">
|
||||||
|
{people?.find((p) => p.member.id === value)?.member.first_name ??
|
||||||
|
"Select Default Assignee"}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<ChevronDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</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 z-10 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) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={person.id}
|
||||||
|
className={({ active }) =>
|
||||||
|
`${
|
||||||
|
active ? "text-white bg-theme" : "text-gray-900"
|
||||||
|
} cursor-default select-none relative py-2 pl-3 pr-9`
|
||||||
|
}
|
||||||
|
value={person.member.id}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
selected ? "font-semibold" : "font-normal"
|
||||||
|
} block truncate`}
|
||||||
|
>
|
||||||
|
{person.member.first_name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||||
|
active ? "text-white" : "text-indigo-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ControlSettings;
|
119
apps/app/components/project/settings/GeneralSettings.tsx
Normal file
119
apps/app/components/project/settings/GeneralSettings.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// react
|
||||||
|
import { useCallback } from "react";
|
||||||
|
// react-hook-form
|
||||||
|
import { UseFormRegister, UseFormSetError } from "react-hook-form";
|
||||||
|
// services
|
||||||
|
import projectServices from "lib/services/project.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// ui
|
||||||
|
import { Input, Select, TextArea } from "ui";
|
||||||
|
// types
|
||||||
|
import { IProject } from "types";
|
||||||
|
// constants
|
||||||
|
import { debounce } from "constants/common";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
register: UseFormRegister<IProject>;
|
||||||
|
errors: any;
|
||||||
|
setError: UseFormSetError<IProject>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||||
|
|
||||||
|
const GeneralSettings: React.FC<Props> = ({ register, errors, setError }) => {
|
||||||
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
|
const checkIdentifier = (slug: string, value: string) => {
|
||||||
|
projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => {
|
||||||
|
console.log(response);
|
||||||
|
if (response.exists) setError("identifier", { message: "Identifier already exists" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">General</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
This information will be displayed to every member of the project.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
error={errors.name}
|
||||||
|
register={register}
|
||||||
|
placeholder="Project Name"
|
||||||
|
label="Name"
|
||||||
|
validations={{
|
||||||
|
required: "Name is required",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
name="network"
|
||||||
|
id="network"
|
||||||
|
options={Object.keys(NETWORK_CHOICES).map((key) => ({
|
||||||
|
value: key,
|
||||||
|
label: NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES],
|
||||||
|
}))}
|
||||||
|
label="Network"
|
||||||
|
register={register}
|
||||||
|
validations={{
|
||||||
|
required: "Network is required",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="identifier"
|
||||||
|
name="identifier"
|
||||||
|
error={errors.identifier}
|
||||||
|
register={register}
|
||||||
|
placeholder="Enter identifier"
|
||||||
|
label="Identifier"
|
||||||
|
onChange={(e: any) => {
|
||||||
|
if (!activeWorkspace || !e.target.value) return;
|
||||||
|
checkIdentifierAvailability(activeWorkspace.slug, e.target.value);
|
||||||
|
}}
|
||||||
|
validations={{
|
||||||
|
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>
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
error={errors.description}
|
||||||
|
register={register}
|
||||||
|
label="Description"
|
||||||
|
placeholder="Enter project description"
|
||||||
|
validations={{
|
||||||
|
required: "Description is required",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GeneralSettings;
|
275
apps/app/components/project/settings/LabelsSettings.tsx
Normal file
275
apps/app/components/project/settings/LabelsSettings.tsx
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
// react-hook-form
|
||||||
|
import { Controller, SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
// react-color
|
||||||
|
import { TwitterPicker } from "react-color";
|
||||||
|
// services
|
||||||
|
import issuesServices from "lib/services/issues.services";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// headless ui
|
||||||
|
import { Popover, Transition, Menu } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
|
import { Button, Input, Spinner } from "ui";
|
||||||
|
// icons
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
EllipsisHorizontalIcon,
|
||||||
|
PencilIcon,
|
||||||
|
PlusIcon,
|
||||||
|
RectangleGroupIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
// types
|
||||||
|
import { IIssueLabels } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
|
name: "",
|
||||||
|
colour: "#ff0000",
|
||||||
|
};
|
||||||
|
|
||||||
|
const LabelsSettings: React.FC = () => {
|
||||||
|
const [newLabelForm, setNewLabelForm] = useState(false);
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [labelIdForUpdate, setLabelidForUpdate] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
watch,
|
||||||
|
} = useForm<IIssueLabels>({ defaultValues });
|
||||||
|
|
||||||
|
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>(
|
||||||
|
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
|
||||||
|
activeProject && activeWorkspace
|
||||||
|
? () => issuesServices.getIssueLabels(activeWorkspace.slug, activeProject.id)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNewLabel: SubmitHandler<IIssueLabels> = (formData) => {
|
||||||
|
if (!activeWorkspace || !activeProject || isSubmitting) return;
|
||||||
|
issuesServices
|
||||||
|
.createIssueLabel(activeWorkspace.slug, activeProject.id, formData)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
reset(defaultValues);
|
||||||
|
mutate((prevData) => [...(prevData ?? []), res], false);
|
||||||
|
setNewLabelForm(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelUpdate: SubmitHandler<IIssueLabels> = (formData) => {
|
||||||
|
if (!activeWorkspace || !activeProject || isSubmitting) return;
|
||||||
|
issuesServices
|
||||||
|
.patchIssueLabel(activeWorkspace.slug, activeProject.id, labelIdForUpdate ?? "", formData)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
reset(defaultValues);
|
||||||
|
mutate(
|
||||||
|
(prevData) =>
|
||||||
|
prevData?.map((p) => (p.id === labelIdForUpdate ? { ...p, ...formData } : p)),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
setNewLabelForm(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelDelete = (labelId: string) => {
|
||||||
|
if (activeWorkspace && activeProject) {
|
||||||
|
mutate((prevData) => prevData?.filter((p) => p.id !== labelId), false);
|
||||||
|
issuesServices
|
||||||
|
.deleteIssueLabel(activeWorkspace.slug, activeProject.id, labelId)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLabelChildren = (labelId: string) => {
|
||||||
|
return issueLabels?.filter((l) => l.parent === labelId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">Labels</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Manage the labels of this project.</p>
|
||||||
|
</div>
|
||||||
|
<Button className="flex items-center gap-x-1" onClick={() => setNewLabelForm(true)}>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
New label
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<form
|
||||||
|
className={`bg-white px-4 py-2 flex items-center gap-2 ${newLabelForm ? "" : "hidden"}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Popover className="relative">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Popover.Button
|
||||||
|
className={`bg-white flex items-center gap-1 rounded-md p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
|
||||||
|
>
|
||||||
|
{watch("colour") && watch("colour") !== "" && (
|
||||||
|
<span
|
||||||
|
className="w-6 h-6 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: watch("colour") ?? "green",
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</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 z-20 transform left-0 mt-1 px-2 max-w-xs sm:px-0">
|
||||||
|
<Controller
|
||||||
|
name="colour"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<TwitterPicker
|
||||||
|
color={value}
|
||||||
|
onChange={(value) => onChange(value.hex)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="labelName"
|
||||||
|
name="name"
|
||||||
|
register={register}
|
||||||
|
placeholder="Lable title"
|
||||||
|
/>
|
||||||
|
<Button type="button" theme="secondary" onClick={() => setNewLabelForm(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{isUpdating ? (
|
||||||
|
<Button type="button" onClick={handleSubmit(handleLabelUpdate)}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button type="button" onClick={handleSubmit(handleNewLabel)}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
{issueLabels ? (
|
||||||
|
issueLabels.map((label) => {
|
||||||
|
const children = getLabelChildren(label.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={label.id}>
|
||||||
|
{children && children.length === 0 ? (
|
||||||
|
<div className="bg-white p-2 flex items-center justify-between text-gray-900 rounded-md">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: label.colour,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="text-sm">{label.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<Menu.Button
|
||||||
|
as="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-100 duration-300 outline-none`}
|
||||||
|
>
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute origin-top-right right-0.5 mt-1 p-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
onClick={() => {
|
||||||
|
setNewLabelForm(true);
|
||||||
|
setValue("colour", label.colour);
|
||||||
|
setValue("name", label.name);
|
||||||
|
setIsUpdating(true);
|
||||||
|
setLabelidForUpdate(label.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-left p-2 text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap w-full"
|
||||||
|
onClick={() => handleLabelDelete(label.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white p-4 text-gray-900 rounded-md">
|
||||||
|
<h3 className="font-medium leading-5 flex items-center gap-2">
|
||||||
|
<RectangleGroupIcon className="h-5 w-5" />
|
||||||
|
This is the label group title
|
||||||
|
</h3>
|
||||||
|
<div className="pl-5 mt-4">
|
||||||
|
<div className="group text-sm flex justify-between items-center p-2 hover:bg-gray-100 rounded">
|
||||||
|
<h5 className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-red-600 rounded-full"></div>
|
||||||
|
This is the label title
|
||||||
|
</h5>
|
||||||
|
<button type="button" className="hidden group-hover:block">
|
||||||
|
<PencilIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LabelsSettings;
|
78
apps/app/components/project/settings/StatesSettings.tsx
Normal file
78
apps/app/components/project/settings/StatesSettings.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// react
|
||||||
|
import { useState } from "react";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// components
|
||||||
|
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
// icons
|
||||||
|
import { PencilSquareIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
|
// constants
|
||||||
|
import { addSpaceIfCamelCase } from "constants/common";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectId: string | string[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatesSettings: React.FC<Props> = ({ projectId }) => {
|
||||||
|
const [isCreateStateModal, setIsCreateStateModal] = useState(false);
|
||||||
|
const [selectedState, setSelectedState] = useState<string | undefined>();
|
||||||
|
|
||||||
|
const { states } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateUpdateStateModal
|
||||||
|
isOpen={isCreateStateModal || Boolean(selectedState)}
|
||||||
|
handleClose={() => {
|
||||||
|
setSelectedState(undefined);
|
||||||
|
setIsCreateStateModal(false);
|
||||||
|
}}
|
||||||
|
projectId={projectId as string}
|
||||||
|
data={selectedState ? states?.find((state) => state.id === selectedState) : undefined}
|
||||||
|
/>
|
||||||
|
<section className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900">State</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Manage the state of this project.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between gap-3">
|
||||||
|
<div className="w-full space-y-5">
|
||||||
|
{states?.map((state) => (
|
||||||
|
<div
|
||||||
|
key={state.id}
|
||||||
|
className="bg-white px-4 py-2 rounded flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: state.color,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<h4>{addSpaceIfCamelCase(state.name)}</h4>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" onClick={() => setSelectedState(state.id)}>
|
||||||
|
<PencilSquareIcon className="h-5 w-5 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-x-1"
|
||||||
|
onClick={() => setIsCreateStateModal(true)}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
<span>Add State</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatesSettings;
|
116
apps/app/components/workspace/ConfirmWorkspaceMemberRemove.tsx
Normal file
116
apps/app/components/workspace/ConfirmWorkspaceMemberRemove.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
|
// ui
|
||||||
|
import { Button } from "ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
handleDelete: () => void;
|
||||||
|
data?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfirmWorkspaceMemberRemove: React.FC<Props> = ({ isOpen, onClose, data, handleDelete }) => {
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const cancelButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletion = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
handleDelete();
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
Remove {data?.email}?
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Are you sure you want to remove member - {`"`}
|
||||||
|
<span className="italic">{data?.email}</span>
|
||||||
|
{`"`} ? They will no longer have access to this workspace. 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 ? "Removing..." : "Remove"}
|
||||||
|
</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 ConfirmWorkspaceMemberRemove;
|
@ -100,6 +100,8 @@ export const ISSUE_ACTIVITIES = (workspaceSlug: string, projectId: string, issue
|
|||||||
|
|
||||||
export const ISSUE_LABELS = (workspaceSlug: string, projectId: string) =>
|
export const ISSUE_LABELS = (workspaceSlug: string, projectId: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`;
|
||||||
|
export const ISSUE_LABEL_DETAILS = (workspaceSlug: string, projectId: string, labelId: string) =>
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/${labelId}/`;
|
||||||
|
|
||||||
export const FILTER_STATE_ISSUES = (workspaceSlug: string, projectId: string, state: string) =>
|
export const FILTER_STATE_ISSUES = (workspaceSlug: string, projectId: string, state: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${state}`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/?state=${state}`;
|
||||||
@ -122,3 +124,10 @@ export const CYCLES_ENDPOINT = (workspaceSlug: string, projectId: string) =>
|
|||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`;
|
||||||
export const CYCLE_DETAIL = (workspaceSlug: string, projectId: string, cycleId: string) =>
|
export const CYCLE_DETAIL = (workspaceSlug: string, projectId: string, cycleId: string) =>
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`;
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`;
|
||||||
|
export const REMOVE_ISSUE_FROM_CYCLE = (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
bridgeId: string
|
||||||
|
) =>
|
||||||
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/${bridgeId}/`;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { NestedKeyOf } from "types";
|
||||||
|
|
||||||
export const classNames = (...classes: string[]) => {
|
export const classNames = (...classes: string[]) => {
|
||||||
return classes.filter(Boolean).join(" ");
|
return classes.filter(Boolean).join(" ");
|
||||||
};
|
};
|
||||||
@ -30,6 +32,32 @@ export const groupBy = (array: any[], key: string) => {
|
|||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const orderArrayBy = (
|
||||||
|
array: any[],
|
||||||
|
key: string,
|
||||||
|
ordering: "ascending" | "descending" = "ascending"
|
||||||
|
) => {
|
||||||
|
const innerKey = key.split("."); // split the key by dot
|
||||||
|
return array.sort((a, b) => {
|
||||||
|
const keyA = innerKey.reduce((obj, i) => obj[i], a); // get the value of the inner key
|
||||||
|
const keyB = innerKey.reduce((obj, i) => obj[i], b); // get the value of the inner key
|
||||||
|
if (keyA < keyB) {
|
||||||
|
return ordering === "ascending" ? -1 : 1;
|
||||||
|
}
|
||||||
|
if (keyA > keyB) {
|
||||||
|
return ordering === "ascending" ? 1 : -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findHowManyDaysLeft = (date: string | Date) => {
|
||||||
|
const today = new Date();
|
||||||
|
const eventDate = new Date(date);
|
||||||
|
const timeDiff = Math.abs(eventDate.getTime() - today.getTime());
|
||||||
|
return Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||||
|
};
|
||||||
|
|
||||||
export const timeAgo = (time: any) => {
|
export const timeAgo = (time: any) => {
|
||||||
switch (typeof time) {
|
switch (typeof time) {
|
||||||
case "number":
|
case "number":
|
||||||
|
8
apps/app/constants/index.ts
Normal file
8
apps/app/constants/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export const PRIORITIES = ["urgent", "high", "medium", "low"];
|
||||||
|
|
||||||
|
export const ROLE = {
|
||||||
|
5: "Guest",
|
||||||
|
10: "Viewer",
|
||||||
|
15: "Member",
|
||||||
|
20: "Admin",
|
||||||
|
};
|
@ -87,7 +87,7 @@ export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
removeAlert(id);
|
removeAlert(id);
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}, 5000);
|
}, 3000);
|
||||||
},
|
},
|
||||||
[removeAlert]
|
[removeAlert]
|
||||||
);
|
);
|
||||||
|
@ -211,7 +211,7 @@ const Sidebar: React.FC = () => {
|
|||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
<div
|
<div
|
||||||
className={`relative ${
|
className={`relative ${
|
||||||
sidebarCollapse ? "flex" : "grid grid-cols-5 gap-1 items-center"
|
sidebarCollapse ? "flex" : "grid grid-cols-5 gap-2 items-center"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Menu as="div" className="col-span-4 inline-block text-left w-full">
|
<Menu as="div" className="col-span-4 inline-block text-left w-full">
|
||||||
@ -224,7 +224,7 @@ const Sidebar: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-x-1 items-center">
|
<div className="flex gap-x-1 items-center">
|
||||||
<div className="h-5 w-5 p-4 flex items-center justify-center bg-gray-500 text-white rounded uppercase relative">
|
<div className="h-5 w-5 p-4 flex items-center justify-center bg-gray-700 text-white rounded uppercase relative">
|
||||||
{activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
|
{activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
|
||||||
<Image
|
<Image
|
||||||
src={activeWorkspace.logo}
|
src={activeWorkspace.logo}
|
||||||
@ -259,7 +259,7 @@ const Sidebar: React.FC = () => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-left fixed max-w-[15rem] ml-2 left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
<Menu.Items className="origin-top-left fixed max-w-[15rem] ml-2 left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{workspaces ? (
|
{workspaces ? (
|
||||||
<>
|
<>
|
||||||
@ -298,14 +298,22 @@ const Sidebar: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p>No workspace found!</p>
|
<p>No workspace found!</p>
|
||||||
)}
|
)}
|
||||||
<Menu.Item>
|
<Menu.Item
|
||||||
{(active) => (
|
as="button"
|
||||||
<Link href="/create-workspace">
|
onClick={() => {
|
||||||
<a className="flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm">
|
router.push("/create-workspace");
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
className={`flex items-center gap-x-1 p-2 w-full text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-sm ${
|
||||||
|
active ? "bg-theme text-white" : "text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<PlusIcon className="w-5 h-5" />
|
<PlusIcon className="w-5 h-5" />
|
||||||
<span>Create Workspace</span>
|
<span>Create Workspace</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</>
|
</>
|
||||||
@ -319,16 +327,15 @@ const Sidebar: React.FC = () => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
{!sidebarCollapse && (
|
{!sidebarCollapse && (
|
||||||
<Menu as="div" className="inline-block text-left w-full">
|
<Menu as="div" className="inline-block text-left flex-shrink-0 w-full">
|
||||||
<div className="h-10 w-10">
|
<div className="h-10 w-10">
|
||||||
<Menu.Button className="grid relative place-items-center h-full w-full rounded-md shadow-sm px-2 py-2 bg-white text-gray-700 hover:bg-gray-50 focus:outline-none">
|
<Menu.Button className="grid relative place-items-center h-full w-full rounded-md shadow-sm bg-white text-gray-700 hover:bg-gray-50 focus:outline-none">
|
||||||
{user?.avatar && user.avatar !== "" ? (
|
{user?.avatar && user.avatar !== "" ? (
|
||||||
<Image
|
<Image
|
||||||
src={user.avatar}
|
src={user.avatar}
|
||||||
alt="User Avatar"
|
alt="User Avatar"
|
||||||
layout="fill"
|
layout="fill"
|
||||||
objectFit="cover"
|
className="rounded-md"
|
||||||
className="rounded-full"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserIcon className="h-5 w-5" />
|
<UserIcon className="h-5 w-5" />
|
||||||
@ -345,7 +352,7 @@ const Sidebar: React.FC = () => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-right absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
<Menu.Items className="origin-top-right absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{userLinks.map((item) => (
|
{userLinks.map((item) => (
|
||||||
<Menu.Item key={item.name} as="div">
|
<Menu.Item key={item.name} as="div">
|
||||||
@ -462,7 +469,7 @@ const Sidebar: React.FC = () => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
<Menu.Items className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-20">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
{(active) => (
|
{(active) => (
|
||||||
@ -553,11 +560,14 @@ const Sidebar: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-2 bg-gray-50 w-full self-baseline flex items-center gap-x-2">
|
<div
|
||||||
<Tooltip content="Click to toggle sidebar" position="right">
|
className={`px-2 py-2 bg-gray-50 w-full self-baseline flex items-center ${
|
||||||
|
sidebarCollapse ? "flex-col-reverse" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 outline-none ${
|
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 outline-none ${
|
||||||
sidebarCollapse ? "justify-center w-full" : ""
|
sidebarCollapse ? "justify-center w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleCollapsed()}
|
onClick={() => toggleCollapsed()}
|
||||||
@ -568,9 +578,11 @@ const Sidebar: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 outline-none ${
|
||||||
|
sidebarCollapse ? "justify-center w-full" : ""
|
||||||
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const e = new KeyboardEvent("keydown", {
|
const e = new KeyboardEvent("keydown", {
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
|
94
apps/app/lib/hooks/useIssuesFilter.tsx
Normal file
94
apps/app/lib/hooks/useIssuesFilter.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
// hooks
|
||||||
|
import useTheme from "./useTheme";
|
||||||
|
import useUser from "./useUser";
|
||||||
|
// commons
|
||||||
|
import { groupBy, orderArrayBy } from "constants/common";
|
||||||
|
// constants
|
||||||
|
import { PRIORITIES } from "constants/";
|
||||||
|
// types
|
||||||
|
import type { IssueResponse, IIssue, NestedKeyOf } from "types";
|
||||||
|
|
||||||
|
const useIssuesFilter = (projectIssues?: IssueResponse) => {
|
||||||
|
const { issueView, setIssueView, groupByProperty, setGroupByProperty } = useTheme();
|
||||||
|
|
||||||
|
const [orderBy, setOrderBy] = useState<NestedKeyOf<IIssue> | null>(null);
|
||||||
|
|
||||||
|
const [filterIssue, setFilterIssue] = useState<"activeIssue" | "backlogIssue" | null>(null);
|
||||||
|
|
||||||
|
const { states } = useUser();
|
||||||
|
|
||||||
|
let groupedByIssues: {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
} = {
|
||||||
|
...(groupByProperty === "state_detail.name"
|
||||||
|
? Object.fromEntries(
|
||||||
|
states
|
||||||
|
?.sort((a, b) => a.sequence - b.sequence)
|
||||||
|
?.map((state) => [
|
||||||
|
state.name,
|
||||||
|
projectIssues?.results.filter((issue) => issue.state === state.name) ?? [],
|
||||||
|
]) ?? []
|
||||||
|
)
|
||||||
|
: groupByProperty === "priority"
|
||||||
|
? Object.fromEntries(
|
||||||
|
PRIORITIES.map((priority) => [
|
||||||
|
priority,
|
||||||
|
projectIssues?.results.filter((issue) => issue.priority === priority) ?? [],
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: {}),
|
||||||
|
...groupBy(projectIssues?.results ?? [], groupByProperty ?? ""),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (orderBy !== null) {
|
||||||
|
groupedByIssues = Object.fromEntries(
|
||||||
|
Object.entries(groupedByIssues).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
orderArrayBy(value, orderBy, "descending"),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterIssue !== null) {
|
||||||
|
if (filterIssue === "activeIssue") {
|
||||||
|
groupedByIssues = Object.keys(groupedByIssues).reduce((acc, key) => {
|
||||||
|
const value = groupedByIssues[key];
|
||||||
|
const filteredValue = value.filter(
|
||||||
|
(issue) =>
|
||||||
|
issue.state_detail.group === "started" || issue.state_detail.group === "unstarted"
|
||||||
|
);
|
||||||
|
if (filteredValue.length > 0) {
|
||||||
|
acc[key] = filteredValue;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as typeof groupedByIssues);
|
||||||
|
} else if (filterIssue === "backlogIssue") {
|
||||||
|
groupedByIssues = Object.keys(groupedByIssues).reduce((acc, key) => {
|
||||||
|
const value = groupedByIssues[key];
|
||||||
|
const filteredValue = value.filter(
|
||||||
|
(issue) =>
|
||||||
|
issue.state_detail.group === "backlog" || issue.state_detail.group === "cancelled"
|
||||||
|
);
|
||||||
|
if (filteredValue.length > 0) {
|
||||||
|
acc[key] = filteredValue;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as typeof groupedByIssues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupedByIssues,
|
||||||
|
issueView,
|
||||||
|
setIssueView,
|
||||||
|
groupByProperty,
|
||||||
|
setGroupByProperty,
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
filterIssue,
|
||||||
|
setFilterIssue,
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIssuesFilter;
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useContext, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// api routes
|
// api routes
|
||||||
@ -11,19 +11,12 @@ import useUser from "./useUser";
|
|||||||
import { IssuePriorities, Properties } from "types";
|
import { IssuePriorities, Properties } from "types";
|
||||||
|
|
||||||
const initialValues: Properties = {
|
const initialValues: Properties = {
|
||||||
name: true,
|
|
||||||
key: true,
|
key: true,
|
||||||
parent: false,
|
|
||||||
project: false,
|
|
||||||
state: true,
|
state: true,
|
||||||
assignee: true,
|
assignee: true,
|
||||||
description: false,
|
|
||||||
priority: false,
|
priority: false,
|
||||||
start_date: false,
|
start_date: false,
|
||||||
target_date: false,
|
target_date: false,
|
||||||
sequence_id: false,
|
|
||||||
attachments: false,
|
|
||||||
children: false,
|
|
||||||
cycle: false,
|
cycle: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,7 +73,22 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
|||||||
[workspaceSlug, projectId, issueProperties, user]
|
[workspaceSlug, projectId, issueProperties, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
return [properties, updateIssueProperties] as const;
|
const newProperties = Object.keys(properties).reduce((obj: any, key) => {
|
||||||
|
if (
|
||||||
|
key !== "children" &&
|
||||||
|
key !== "name" &&
|
||||||
|
key !== "parent" &&
|
||||||
|
key !== "project" &&
|
||||||
|
key !== "description" &&
|
||||||
|
key !== "attachments" &&
|
||||||
|
key !== "sequence_id"
|
||||||
|
) {
|
||||||
|
obj[key] = properties[key as keyof Properties];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return [newProperties, updateIssueProperties] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useIssuesProperties;
|
export default useIssuesProperties;
|
||||||
|
@ -10,8 +10,8 @@ class ProjectCycleServices extends APIService {
|
|||||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCycle(workspace_slug: string, projectId: string, data: any): Promise<any> {
|
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||||
return this.post(CYCLES_ENDPOINT(workspace_slug, projectId), data)
|
return this.post(CYCLES_ENDPOINT(workspaceSlug, projectId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -20,8 +20,8 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCycles(workspace_slug: string, projectId: string): Promise<any> {
|
async getCycles(workspaceSlug: string, projectId: string): Promise<any> {
|
||||||
return this.get(CYCLES_ENDPOINT(workspace_slug, projectId))
|
return this.get(CYCLES_ENDPOINT(workspaceSlug, projectId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -30,18 +30,8 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCycleIssues(workspace_slug: string, projectId: string, cycleId: string): Promise<any> {
|
async getCycleIssues(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
||||||
return this.get(CYCLE_DETAIL(workspace_slug, projectId, cycleId))
|
return this.get(CYCLE_DETAIL(workspaceSlug, projectId, cycleId))
|
||||||
.then((response) => {
|
|
||||||
return response?.data;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCycle(workspace_slug: string, projectId: string, cycleId: string): Promise<any> {
|
|
||||||
return this.get(CYCLE_DETAIL(workspace_slug, projectId, cycleId))
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -51,13 +41,13 @@ class ProjectCycleServices extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateCycle(
|
async updateCycle(
|
||||||
workspace_slug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
cycleId: string,
|
cycleId: string,
|
||||||
data: any
|
data: any
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.put(
|
return this.put(
|
||||||
CYCLE_DETAIL(workspace_slug, projectId, cycleId).replace("cycle-issues/", ""),
|
CYCLE_DETAIL(workspaceSlug, projectId, cycleId).replace("cycle-issues/", ""),
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -68,10 +58,8 @@ class ProjectCycleServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCycle(workspace_slug: string, projectId: string, cycleId: string): Promise<any> {
|
async deleteCycle(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
||||||
return this.delete(
|
return this.delete(CYCLE_DETAIL(workspaceSlug, projectId, cycleId).replace("cycle-issues/", ""))
|
||||||
CYCLE_DETAIL(workspace_slug, projectId, cycleId).replace("cycle-issues/", "")
|
|
||||||
)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
|
@ -10,6 +10,8 @@ import {
|
|||||||
ISSUE_LABELS,
|
ISSUE_LABELS,
|
||||||
BULK_DELETE_ISSUES,
|
BULK_DELETE_ISSUES,
|
||||||
BULK_ADD_ISSUES_TO_CYCLE,
|
BULK_ADD_ISSUES_TO_CYCLE,
|
||||||
|
REMOVE_ISSUE_FROM_CYCLE,
|
||||||
|
ISSUE_LABEL_DETAILS,
|
||||||
} from "constants/api-routes";
|
} from "constants/api-routes";
|
||||||
// services
|
// services
|
||||||
import APIService from "lib/services/api.service";
|
import APIService from "lib/services/api.service";
|
||||||
@ -103,6 +105,21 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeIssueFromCycle(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string,
|
||||||
|
bridgeId: string
|
||||||
|
) {
|
||||||
|
return this.delete(REMOVE_ISSUE_FROM_CYCLE(workspaceSlug, projectId, cycleId, bridgeId))
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createIssueProperties(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
async createIssueProperties(workspaceSlug: string, projectId: string, data: any): Promise<any> {
|
||||||
return this.post(ISSUE_PROPERTIES_ENDPOINT(workspaceSlug, projectId), data)
|
return this.post(ISSUE_PROPERTIES_ENDPOINT(workspaceSlug, projectId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -198,6 +215,31 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async patchIssueLabel(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
labelId: string,
|
||||||
|
data: any
|
||||||
|
): Promise<any> {
|
||||||
|
return this.patch(ISSUE_LABEL_DETAILS(workspaceSlug, projectId, labelId), data)
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteIssueLabel(workspaceSlug: string, projectId: string, labelId: string): Promise<any> {
|
||||||
|
return this.delete(ISSUE_LABEL_DETAILS(workspaceSlug, projectId, labelId))
|
||||||
|
.then((response) => {
|
||||||
|
return response?.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateIssue(
|
async updateIssue(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
|
@ -124,12 +124,14 @@ class ProjectServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMember(
|
async updateProjectMember(
|
||||||
workspace_slug: string,
|
workspace_slug: string,
|
||||||
project_id: string,
|
project_id: string,
|
||||||
memberId: string
|
memberId: string,
|
||||||
|
data: any
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.put(PROJECT_MEMBER_DETAIL(workspace_slug, project_id, memberId))
|
return this.put(PROJECT_MEMBER_DETAIL(workspace_slug, project_id, memberId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -137,6 +139,7 @@ class ProjectServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteProjectMember(
|
async deleteProjectMember(
|
||||||
workspace_slug: string,
|
workspace_slug: string,
|
||||||
project_id: string,
|
project_id: string,
|
||||||
|
@ -111,8 +111,9 @@ class WorkspaceService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async updateWorkspaceMember(workspace_slug: string, memberId: string): Promise<any> {
|
|
||||||
return this.put(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId))
|
async updateWorkspaceMember(workspace_slug: string, memberId: string, data: any): Promise<any> {
|
||||||
|
return this.put(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId), data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response?.data;
|
return response?.data;
|
||||||
})
|
})
|
||||||
@ -120,6 +121,7 @@ class WorkspaceService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteWorkspaceMember(workspace_slug: string, memberId: string): Promise<any> {
|
async deleteWorkspaceMember(workspace_slug: string, memberId: string): Promise<any> {
|
||||||
return this.delete(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId))
|
return this.delete(WORKSPACE_MEMBER_DETAIL(workspace_slug, memberId))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
const RichTextEditor = dynamic(() => import("../components/lexical/editor"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const LexicalViewer = dynamic(() => import("../components/lexical/viewer"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Home = () => {
|
|
||||||
const [value, setValue] = React.useState("");
|
|
||||||
const onChange: any = (value: any) => {
|
|
||||||
console.log(value);
|
|
||||||
setValue(value);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<RichTextEditor onChange={onChange} value={value} id="editor" />
|
|
||||||
<LexicalViewer id="institution_viewer" value={value} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Home;
|
|
@ -1,5 +1,5 @@
|
|||||||
// react
|
// react
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
// next
|
// next
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// swr
|
// swr
|
||||||
@ -22,15 +22,18 @@ import issuesServices from "lib/services/issues.services";
|
|||||||
// components
|
// components
|
||||||
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
|
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon, PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
const MyIssues: NextPage = () => {
|
const MyIssues: NextPage = () => {
|
||||||
const { user } = useUser();
|
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: myIssues, mutate: mutateMyIssue } = useSWR<IIssue[]>(
|
const { user, workspaces } = useUser();
|
||||||
|
|
||||||
|
const { data: myIssues, mutate: mutateMyIssues } = useSWR<IIssue[]>(
|
||||||
user ? USER_ISSUE : null,
|
user ? USER_ISSUE : null,
|
||||||
user ? () => userService.userIssues() : null
|
user ? () => userService.userIssues() : null
|
||||||
);
|
);
|
||||||
@ -41,7 +44,7 @@ const MyIssues: NextPage = () => {
|
|||||||
issueId: string,
|
issueId: string,
|
||||||
issue: Partial<IIssue>
|
issue: Partial<IIssue>
|
||||||
) => {
|
) => {
|
||||||
mutateMyIssue((prevData) => {
|
mutateMyIssues((prevData) => {
|
||||||
return prevData?.map((prevIssue) => {
|
return prevData?.map((prevIssue) => {
|
||||||
if (prevIssue.id === issueId) {
|
if (prevIssue.id === issueId) {
|
||||||
return {
|
return {
|
||||||
@ -66,6 +69,10 @@ const MyIssues: NextPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleWorkspaceChange = (workspaceId: string | null) => {
|
||||||
|
setSelectedWorkspace(workspaceId);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div className="w-full h-full flex flex-col space-y-5">
|
<div className="w-full h-full flex flex-col space-y-5">
|
||||||
@ -79,6 +86,63 @@ const MyIssues: NextPage = () => {
|
|||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
<div className="flex items-center justify-between cursor-pointer w-full">
|
||||||
<h2 className="text-2xl font-medium">My Issues</h2>
|
<h2 className="text-2xl font-medium">My Issues</h2>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center gap-x-3">
|
||||||
|
<Menu as="div" className="relative inline-block w-40">
|
||||||
|
<div className="w-full">
|
||||||
|
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md shadow-sm p-2 border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-100 focus:outline-none">
|
||||||
|
<span className="flex gap-x-1 items-center">
|
||||||
|
{workspaces?.find((w) => w.id === selectedWorkspace)?.name ??
|
||||||
|
"All workspaces"}
|
||||||
|
</span>
|
||||||
|
<div className="flex-grow flex justify-end">
|
||||||
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Menu.Items className="origin-top-left absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
||||||
|
<div className="p-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${
|
||||||
|
active ? "bg-theme text-white" : "text-gray-900"
|
||||||
|
} group flex w-full items-center rounded-md p-2 text-xs`}
|
||||||
|
onClick={() => handleWorkspaceChange(null)}
|
||||||
|
>
|
||||||
|
All workspaces
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
{workspaces &&
|
||||||
|
workspaces.map((workspace) => (
|
||||||
|
<Menu.Item key={workspace.id}>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${
|
||||||
|
active ? "bg-theme text-white" : "text-gray-900"
|
||||||
|
} group flex w-full items-center rounded-md p-2 text-xs`}
|
||||||
|
onClick={() => handleWorkspaceChange(workspace.id)}
|
||||||
|
>
|
||||||
|
{workspace.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
Icon={PlusIcon}
|
Icon={PlusIcon}
|
||||||
label="Add Issue"
|
label="Add Issue"
|
||||||
@ -132,7 +196,11 @@ const MyIssues: NextPage = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white">
|
<tbody className="bg-white">
|
||||||
{myIssues.map((myIssue, index) => (
|
{myIssues
|
||||||
|
.filter((i) =>
|
||||||
|
selectedWorkspace ? i.workspace === selectedWorkspace : true
|
||||||
|
)
|
||||||
|
.map((myIssue, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={myIssue.id}
|
key={myIssue.id}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -141,12 +209,14 @@ const MyIssues: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
|
<td className="px-3 py-4 text-sm font-medium text-gray-900 hover:text-theme max-w-[15rem] duration-300">
|
||||||
<Link href={`/projects/${myIssue.project}/issues/${myIssue.id}`}>
|
<Link
|
||||||
|
href={`/projects/${myIssue.project}/issues/${myIssue.id}`}
|
||||||
|
>
|
||||||
<a>{myIssue.name}</a>
|
<a>{myIssue.name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4 max-w-[15rem] truncate">
|
<td className="px-3 py-4 max-w-[15rem] truncate">
|
||||||
{myIssue.description}
|
{/* {myIssue.description} */}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4">
|
<td className="px-3 py-4">
|
||||||
{myIssue.project_detail?.name}
|
{myIssue.project_detail?.name}
|
||||||
|
@ -25,7 +25,8 @@ import { BreadcrumbItem, Breadcrumbs, HeaderButton, Spinner, EmptySpace, EmptySp
|
|||||||
import { PlusIcon } from "@heroicons/react/20/solid";
|
import { PlusIcon } from "@heroicons/react/20/solid";
|
||||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue, ICycle, SelectSprintType, SelectIssue } from "types";
|
import { IIssue, ICycle, SelectSprintType, SelectIssue, CycleIssueResponse } from "types";
|
||||||
|
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||||
|
|
||||||
const ProjectSprints: NextPage = () => {
|
const ProjectSprints: NextPage = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@ -35,7 +36,7 @@ const ProjectSprints: NextPage = () => {
|
|||||||
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
|
const [selectedIssues, setSelectedIssues] = useState<SelectIssue>();
|
||||||
const [deleteIssue, setDeleteIssue] = useState<string | undefined>();
|
const [deleteIssue, setDeleteIssue] = useState<string | undefined>();
|
||||||
|
|
||||||
const { activeWorkspace, activeProject } = useUser();
|
const { activeWorkspace, activeProject, issues } = useUser();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -80,6 +81,75 @@ const ProjectSprints: NextPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
const { source, destination } = result;
|
||||||
|
|
||||||
|
if (source.droppableId === destination.droppableId) return;
|
||||||
|
|
||||||
|
if (activeWorkspace && activeProject) {
|
||||||
|
// remove issue from the source cycle
|
||||||
|
mutate<CycleIssueResponse[]>(
|
||||||
|
CYCLE_ISSUES(source.droppableId),
|
||||||
|
(prevData) => prevData?.filter((p) => p.id !== result.draggableId.split(",")[0]),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// add issue to the destination cycle
|
||||||
|
mutate(CYCLE_ISSUES(destination.droppableId));
|
||||||
|
|
||||||
|
// mutate<CycleIssueResponse[]>(
|
||||||
|
// CYCLE_ISSUES(destination.droppableId),
|
||||||
|
// (prevData) => {
|
||||||
|
// const issueDetails = issues?.results.find(
|
||||||
|
// (i) => i.id === result.draggableId.split(",")[1]
|
||||||
|
// );
|
||||||
|
// const targetResponse = prevData?.find((t) => t.cycle === destination.droppableId);
|
||||||
|
// console.log(issueDetails, targetResponse, prevData);
|
||||||
|
// if (targetResponse) {
|
||||||
|
// console.log("if");
|
||||||
|
// targetResponse.issue_details = issueDetails as IIssue;
|
||||||
|
// return prevData;
|
||||||
|
// } else {
|
||||||
|
// console.log("else");
|
||||||
|
// return [
|
||||||
|
// ...(prevData ?? []),
|
||||||
|
// {
|
||||||
|
// cycle: destination.droppableId,
|
||||||
|
// issue_details: issueDetails,
|
||||||
|
// } as CycleIssueResponse,
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// false
|
||||||
|
// );
|
||||||
|
|
||||||
|
issuesServices
|
||||||
|
.removeIssueFromCycle(
|
||||||
|
activeWorkspace.slug,
|
||||||
|
activeProject.id,
|
||||||
|
source.droppableId,
|
||||||
|
result.draggableId.split(",")[0]
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
issuesServices
|
||||||
|
.addIssueToSprint(activeWorkspace.slug, activeProject.id, destination.droppableId, {
|
||||||
|
issue: result.draggableId.split(",")[1],
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// console.log(result);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) return;
|
if (isOpen) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -142,11 +212,12 @@ const ProjectSprints: NextPage = () => {
|
|||||||
<h2 className="text-2xl font-medium">Project Cycle</h2>
|
<h2 className="text-2xl font-medium">Project Cycle</h2>
|
||||||
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
|
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full">
|
<div className="space-y-5">
|
||||||
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
{cycles.map((cycle) => (
|
{cycles.map((cycle) => (
|
||||||
<CycleView
|
<CycleView
|
||||||
key={cycle.id}
|
key={cycle.id}
|
||||||
sprint={cycle}
|
cycle={cycle}
|
||||||
selectSprint={setSelectedSprint}
|
selectSprint={setSelectedSprint}
|
||||||
projectId={projectId as string}
|
projectId={projectId as string}
|
||||||
workspaceSlug={activeWorkspace?.slug as string}
|
workspaceSlug={activeWorkspace?.slug as string}
|
||||||
@ -154,6 +225,7 @@ const ProjectSprints: NextPage = () => {
|
|||||||
addIssueToSprint={addIssueToSprint}
|
addIssueToSprint={addIssueToSprint}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useState } from "react";
|
|||||||
// swr
|
// swr
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// react hook form
|
// react hook form
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
|
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
@ -17,7 +17,6 @@ import stateServices from "lib/services/state.services";
|
|||||||
import {
|
import {
|
||||||
PROJECT_ISSUES_ACTIVITY,
|
PROJECT_ISSUES_ACTIVITY,
|
||||||
PROJECT_ISSUES_COMMENTS,
|
PROJECT_ISSUES_COMMENTS,
|
||||||
PROJECT_ISSUES_DETAILS,
|
|
||||||
PROJECT_ISSUES_LIST,
|
PROJECT_ISSUES_LIST,
|
||||||
STATE_LIST,
|
STATE_LIST,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
@ -55,7 +54,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
|
|
||||||
const { issueId, projectId } = router.query;
|
const { issueId, projectId } = router.query;
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, issues, mutateIssues } = useUser();
|
const { activeWorkspace, activeProject, issues, mutateIssues, states } = useUser();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false);
|
const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false);
|
||||||
@ -76,12 +75,17 @@ const IssueDetail: NextPage = () => {
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const LexicalViewer = dynamic(() => import("components/lexical/viewer"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
control,
|
control,
|
||||||
|
watch,
|
||||||
} = useForm<IIssue>({
|
} = useForm<IIssue>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
@ -93,6 +97,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
blocked_list: [],
|
blocked_list: [],
|
||||||
target_date: new Date().toString(),
|
target_date: new Date().toString(),
|
||||||
cycle: "",
|
cycle: "",
|
||||||
|
labels_list: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -120,16 +125,10 @@ const IssueDetail: NextPage = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: states } = useSWR<IState[]>(
|
|
||||||
activeWorkspace && activeProject ? STATE_LIST(activeProject.id) : null,
|
|
||||||
activeWorkspace && activeProject
|
|
||||||
? () => stateServices.getStates(activeWorkspace.slug, activeProject.id)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const submitChanges = useCallback(
|
const submitChanges = useCallback(
|
||||||
(formData: Partial<IIssue>) => {
|
(formData: Partial<IIssue>) => {
|
||||||
if (!activeWorkspace || !activeProject || !issueId) return;
|
if (!activeWorkspace || !activeProject || !issueId) return;
|
||||||
|
|
||||||
mutateIssues(
|
mutateIssues(
|
||||||
(prevData) => ({
|
(prevData) => ({
|
||||||
...(prevData as IssueResponse),
|
...(prevData as IssueResponse),
|
||||||
@ -142,6 +141,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
}),
|
}),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
issuesServices
|
issuesServices
|
||||||
.patchIssue(activeWorkspace.slug, projectId as string, issueId as string, formData)
|
.patchIssue(activeWorkspace.slug, projectId as string, issueId as string, formData)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -181,8 +181,11 @@ const IssueDetail: NextPage = () => {
|
|||||||
const nextIssue = issues?.results[issues?.results.findIndex((issue) => issue.id === issueId) + 1];
|
const nextIssue = issues?.results[issues?.results.findIndex((issue) => issue.id === issueId) + 1];
|
||||||
|
|
||||||
const subIssues = (issues && issues.results.filter((i) => i.parent === issueDetail?.id)) ?? [];
|
const subIssues = (issues && issues.results.filter((i) => i.parent === issueDetail?.id)) ?? [];
|
||||||
|
const siblingIssues =
|
||||||
|
issueDetail &&
|
||||||
|
issues?.results.filter((i) => i.parent === issueDetail.parent && i.id !== issueDetail.id);
|
||||||
|
|
||||||
const handleRemove = (issueId: string) => {
|
const handleSubIssueRemove = (issueId: string) => {
|
||||||
if (activeWorkspace && activeProject) {
|
if (activeWorkspace && activeProject) {
|
||||||
issuesServices
|
issuesServices
|
||||||
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: null })
|
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: null })
|
||||||
@ -217,7 +220,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
<AddAsSubIssue
|
<AddAsSubIssue
|
||||||
isOpen={isAddAsSubIssueOpen}
|
isOpen={isAddAsSubIssueOpen}
|
||||||
setIsOpen={setIsAddAsSubIssueOpen}
|
setIsOpen={setIsAddAsSubIssueOpen}
|
||||||
parentId={issueDetail?.id ?? ""}
|
parent={issueDetail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-between w-full mb-5">
|
<div className="flex items-center justify-between w-full mb-5">
|
||||||
@ -259,20 +262,66 @@ const IssueDetail: NextPage = () => {
|
|||||||
<div className="grid grid-cols-4 gap-5">
|
<div className="grid grid-cols-4 gap-5">
|
||||||
<div className="col-span-3 space-y-5">
|
<div className="col-span-3 space-y-5">
|
||||||
<div className="bg-secondary rounded-lg p-4">
|
<div className="bg-secondary rounded-lg p-4">
|
||||||
{/* <Controller
|
{issueDetail.parent !== null && issueDetail.parent !== "" ? (
|
||||||
control={control}
|
<div className="bg-gray-100 flex items-center gap-2 p-2 text-xs rounded mb-5 w-min whitespace-nowrap">
|
||||||
name="description"
|
<Link href={`/projects/${activeProject.id}/issues/${issueDetail.parent}`}>
|
||||||
render={({ field: { value, onChange } }) => (
|
<a className="flex items-center gap-2">
|
||||||
<RichTextEditor
|
<span
|
||||||
onChange={(state: string) => {
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
handleDescriptionChange(state);
|
style={{
|
||||||
onChange(issueDescriptionValue);
|
backgroundColor: issueDetail.state_detail.color,
|
||||||
}}
|
}}
|
||||||
value={issueDescriptionValue}
|
|
||||||
id="editor"
|
|
||||||
/>
|
/>
|
||||||
|
<span className="flex-shrink-0 text-gray-600">
|
||||||
|
{activeProject.identifier}-{issueDetail.sequence_id}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium truncate">
|
||||||
|
{issues?.results
|
||||||
|
.find((i) => i.id === issueDetail.parent)
|
||||||
|
?.name.substring(0, 50)}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Menu as="div" className="relative inline-block">
|
||||||
|
<Menu.Button className="grid relative place-items-center hover:bg-gray-200 rounded p-1 focus:outline-none">
|
||||||
|
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||||
|
</Menu.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute left-0 mt-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
||||||
|
<div className="p-1">
|
||||||
|
{siblingIssues && siblingIssues.length > 0 ? (
|
||||||
|
siblingIssues.map((issue) => (
|
||||||
|
<Menu.Item as="div" key={issue.id}>
|
||||||
|
<Link href={`/projects/${activeProject.id}/issues/${issue.id}`}>
|
||||||
|
<a className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap">
|
||||||
|
{activeProject.identifier}-{issue.sequence_id}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Menu.Item
|
||||||
|
as="div"
|
||||||
|
className="flex items-center gap-2 p-2 text-left text-gray-900 text-xs whitespace-nowrap"
|
||||||
|
>
|
||||||
|
No other sub-issues
|
||||||
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
/> */}
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
<TextArea
|
<TextArea
|
||||||
id="name"
|
id="name"
|
||||||
@ -301,6 +350,18 @@ const IssueDetail: NextPage = () => {
|
|||||||
mode="transparent"
|
mode="transparent"
|
||||||
register={register}
|
register={register}
|
||||||
/>
|
/>
|
||||||
|
{/* <Controller
|
||||||
|
name="description"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<RichTextEditor
|
||||||
|
{...field}
|
||||||
|
id="issueDescriptionEditor"
|
||||||
|
value={JSON.parse(issueDetail.description)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
{/* <LexicalViewer id="descriptionViewer" value={JSON.parse(issueDetail.description)} /> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{subIssues && subIssues.length > 0 ? (
|
{subIssues && subIssues.length > 0 ? (
|
||||||
@ -327,7 +388,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-3 w-3" />
|
<PlusIcon className="h-3 w-3" />
|
||||||
Add new
|
Create new
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Menu as="div" className="relative inline-block">
|
<Menu as="div" className="relative inline-block">
|
||||||
@ -349,6 +410,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
{(active) => (
|
{(active) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||||
onClick={() => setIsAddAsSubIssueOpen(true)}
|
onClick={() => setIsAddAsSubIssueOpen(true)}
|
||||||
>
|
>
|
||||||
@ -373,23 +435,26 @@ const IssueDetail: NextPage = () => {
|
|||||||
>
|
>
|
||||||
<Disclosure.Panel className="flex flex-col gap-y-1 mt-3">
|
<Disclosure.Panel className="flex flex-col gap-y-1 mt-3">
|
||||||
{subIssues.map((subIssue) => (
|
{subIssues.map((subIssue) => (
|
||||||
<Link
|
<div
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
href={`/projects/${activeProject.id}/issues/${subIssue.id}`}
|
className="flex justify-between items-center gap-2 p-2 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
<a className="p-2 flex justify-between items-center rounded text-xs hover:bg-gray-100">
|
<Link href={`/projects/${activeProject.id}/issues/${subIssue.id}`}>
|
||||||
<div className="flex items-center gap-2">
|
<a className="flex items-center gap-2 rounded text-xs">
|
||||||
<span
|
<span
|
||||||
className={`h-1.5 w-1.5 block rounded-full`}
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: subIssue.state_detail.color,
|
backgroundColor: subIssue.state_detail.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-gray-600">
|
<span className="flex-shrink-0 text-gray-600">
|
||||||
{activeProject.identifier}-{subIssue.sequence_id}
|
{activeProject.identifier}-{subIssue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium">{subIssue.name}</span>
|
<span className="font-medium max-w-sm break-all">
|
||||||
</div>
|
{subIssue.name}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<Menu as="div" className="relative inline-block">
|
<Menu as="div" className="relative inline-block">
|
||||||
<Menu.Button className="grid relative place-items-center focus:outline-none">
|
<Menu.Button className="grid relative place-items-center focus:outline-none">
|
||||||
@ -411,7 +476,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
{(active) => (
|
{(active) => (
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
className="flex items-center gap-2 p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||||
onClick={() => handleRemove(subIssue.id)}
|
onClick={() => handleSubIssueRemove(subIssue.id)}
|
||||||
>
|
>
|
||||||
Remove as sub-issue
|
Remove as sub-issue
|
||||||
</button>
|
</button>
|
||||||
@ -422,8 +487,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
|
||||||
))}
|
))}
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
@ -446,7 +510,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="origin-top-right absolute left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
<Menu.Items className="absolute origin-top-right left-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
{(active) => (
|
{(active) => (
|
||||||
@ -461,7 +525,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add new
|
Create new
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@ -515,7 +579,11 @@ const IssueDetail: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<IssueActivitySection issueActivities={issueActivities} states={states} />
|
<IssueActivitySection
|
||||||
|
issueActivities={issueActivities}
|
||||||
|
states={states}
|
||||||
|
issues={issues}
|
||||||
|
/>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// react
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// next
|
// next
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
@ -6,52 +5,77 @@ import { useRouter } from "next/router";
|
|||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Menu, Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// services
|
// hoc
|
||||||
import stateServices from "lib/services/state.services";
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
import issuesServices from "lib/services/issues.services";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useTheme from "lib/hooks/useTheme";
|
|
||||||
import useIssuesProperties from "lib/hooks/useIssuesProperties";
|
import useIssuesProperties from "lib/hooks/useIssuesProperties";
|
||||||
// fetching keys
|
// api routes
|
||||||
import { PROJECT_ISSUES_LIST, STATE_LIST } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS } from "constants/api-routes";
|
||||||
|
// services
|
||||||
|
import projectService from "lib/services/project.service";
|
||||||
// commons
|
// commons
|
||||||
import { groupBy } from "constants/common";
|
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AdminLayout from "layouts/AdminLayout";
|
||||||
|
// hooks
|
||||||
|
import useIssuesFilter from "lib/hooks/useIssuesFilter";
|
||||||
// components
|
// components
|
||||||
import ListView from "components/project/issues/ListView";
|
import ListView from "components/project/issues/ListView";
|
||||||
import BoardView from "components/project/issues/BoardView";
|
import BoardView from "components/project/issues/BoardView";
|
||||||
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
|
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
|
||||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "ui";
|
import { Spinner, CustomMenu, BreadcrumbItem, Breadcrumbs } from "ui";
|
||||||
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
|
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
|
||||||
import HeaderButton from "ui/HeaderButton";
|
import HeaderButton from "ui/HeaderButton";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "ui";
|
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
import { PlusIcon, EyeIcon, EyeSlashIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||||
// types
|
// types
|
||||||
import type { IIssue, IssueResponse, Properties, IState, NestedKeyOf, ProjectMember } from "types";
|
import type { IIssue, Properties, NestedKeyOf, ProjectMember } from "types";
|
||||||
import { PROJECT_MEMBERS } from "constants/api-routes";
|
|
||||||
import projectService from "lib/services/project.service";
|
|
||||||
|
|
||||||
const PRIORITIES = ["high", "medium", "low"];
|
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> | null }> = [
|
||||||
|
{ name: "State", key: "state_detail.name" },
|
||||||
|
{ name: "Priority", key: "priority" },
|
||||||
|
{ name: "Created By", key: "created_by" },
|
||||||
|
{ name: "None", key: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
const orderByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
|
||||||
|
{ name: "Created", key: "created_at" },
|
||||||
|
{ name: "Update", key: "updated_at" },
|
||||||
|
{ name: "Priority", key: "priority" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const filterIssueOptions: Array<{
|
||||||
|
name: string;
|
||||||
|
key: "activeIssue" | "backlogIssue" | null;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
name: "All",
|
||||||
|
key: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Active Issues",
|
||||||
|
key: "activeIssue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Backlog Issues",
|
||||||
|
key: "backlogIssue",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const ProjectIssues: NextPage = () => {
|
const ProjectIssues: NextPage = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { issueView, setIssueView, groupByProperty, setGroupByProperty } = useTheme();
|
|
||||||
|
|
||||||
const [selectedIssue, setSelectedIssue] = useState<
|
const [selectedIssue, setSelectedIssue] = useState<
|
||||||
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const [editIssue, setEditIssue] = useState<string | undefined>();
|
|
||||||
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
|
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, issues } = useUser();
|
const { activeWorkspace, activeProject, issues: projectIssues } = useUser();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -62,22 +86,6 @@ const ProjectIssues: NextPage = () => {
|
|||||||
projectId as string
|
projectId as string
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projectIssues } = useSWR<IssueResponse>(
|
|
||||||
projectId && activeWorkspace
|
|
||||||
? PROJECT_ISSUES_LIST(activeWorkspace.slug, projectId as string)
|
|
||||||
: null,
|
|
||||||
activeWorkspace && projectId
|
|
||||||
? () => issuesServices.getIssues(activeWorkspace.slug, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: states } = useSWR<IState[]>(
|
|
||||||
activeWorkspace && activeProject ? STATE_LIST(activeProject.id) : null,
|
|
||||||
activeWorkspace && activeProject
|
|
||||||
? () => stateServices.getStates(activeWorkspace.slug, activeProject.id)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: members } = useSWR<ProjectMember[]>(
|
const { data: members } = useSWR<ProjectMember[]>(
|
||||||
activeWorkspace && activeProject ? PROJECT_MEMBERS : null,
|
activeWorkspace && activeProject ? PROJECT_MEMBERS : null,
|
||||||
activeWorkspace && activeProject
|
activeWorkspace && activeProject
|
||||||
@ -85,6 +93,18 @@ const ProjectIssues: NextPage = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
issueView,
|
||||||
|
setIssueView,
|
||||||
|
groupByProperty,
|
||||||
|
setGroupByProperty,
|
||||||
|
groupedByIssues,
|
||||||
|
setOrderBy,
|
||||||
|
setFilterIssue,
|
||||||
|
orderBy,
|
||||||
|
filterIssue,
|
||||||
|
} = useIssuesFilter(projectIssues);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -94,35 +114,6 @@ const ProjectIssues: NextPage = () => {
|
|||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const groupedByIssues: {
|
|
||||||
[key: string]: IIssue[];
|
|
||||||
} = {
|
|
||||||
...(groupByProperty === "state_detail.name"
|
|
||||||
? Object.fromEntries(
|
|
||||||
states
|
|
||||||
?.sort((a, b) => a.sequence - b.sequence)
|
|
||||||
?.map((state) => [
|
|
||||||
state.name,
|
|
||||||
projectIssues?.results.filter((issue) => issue.state === state.name) ?? [],
|
|
||||||
]) ?? []
|
|
||||||
)
|
|
||||||
: groupByProperty === "priority"
|
|
||||||
? Object.fromEntries(
|
|
||||||
PRIORITIES.map((priority) => [
|
|
||||||
priority,
|
|
||||||
projectIssues?.results.filter((issue) => issue.priority === priority) ?? [],
|
|
||||||
])
|
|
||||||
)
|
|
||||||
: {}),
|
|
||||||
...groupBy(projectIssues?.results ?? [], groupByProperty ?? ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
|
|
||||||
{ name: "State", key: "state_detail.name" },
|
|
||||||
{ name: "Priority", key: "priority" },
|
|
||||||
{ name: "Created By", key: "created_by" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<CreateUpdateIssuesModal
|
<CreateUpdateIssuesModal
|
||||||
@ -149,7 +140,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<h2 className="text-2xl font-medium">Project Issues</h2>
|
<h2 className="text-2xl font-medium">Project Issues</h2>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center md:gap-x-6 sm:gap-x-3">
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -176,70 +167,23 @@ const ProjectIssues: NextPage = () => {
|
|||||||
<Squares2X2Icon className="h-4 w-4" />
|
<Squares2X2Icon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Menu as="div" className="relative inline-block w-40">
|
|
||||||
<div className="w-full">
|
|
||||||
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md shadow-sm p-2 border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-100 focus:outline-none">
|
|
||||||
<span className="flex gap-x-1 items-center">
|
|
||||||
{groupByOptions.find((option) => option.key === groupByProperty)?.name ??
|
|
||||||
"No Grouping"}
|
|
||||||
</span>
|
|
||||||
<div className="flex-grow flex justify-end">
|
|
||||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Menu.Items className="origin-top-left absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
|
||||||
<div className="p-1">
|
|
||||||
{groupByOptions.map((option) => (
|
|
||||||
<Menu.Item key={option.key}>
|
|
||||||
{({ active }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${
|
|
||||||
active ? "bg-theme text-white" : "text-gray-900"
|
|
||||||
} group flex w-full items-center rounded-md p-2 text-xs`}
|
|
||||||
onClick={() => setGroupByProperty(option.key)}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
{issueView === "list" ? (
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`hover:bg-theme hover:text-white ${
|
|
||||||
active ? "bg-theme text-white" : "text-gray-900"
|
|
||||||
} group flex w-full items-center rounded-md p-2 text-xs`}
|
|
||||||
onClick={() => setGroupByProperty(null)}
|
|
||||||
>
|
|
||||||
No grouping
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button className="inline-flex justify-between items-center rounded-md shadow-sm p-2 border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-100 focus:outline-none w-40">
|
<Popover.Button
|
||||||
<span>Properties</span>
|
className={classNames(
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
open ? "text-gray-900" : "text-gray-500",
|
||||||
|
"group inline-flex items-center rounded-md bg-transparent text-base font-medium hover:text-gray-900 focus:outline-none border border-gray-300 px-3 py-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>View</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={classNames(
|
||||||
|
open ? "text-gray-600" : "text-gray-400",
|
||||||
|
"ml-2 h-4 w-4 group-hover:text-gray-500"
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -251,27 +195,88 @@ const ProjectIssues: NextPage = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute left-1/2 z-10 mt-1 -translate-x-1/2 transform px-2 sm:px-0 w-full">
|
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-3 w-screen max-w-xs translate-x-1/2 transform px-2 sm:px-0 bg-gray-0 backdrop-filter backdrop-blur-xl bg-opacity-100 rounded-lg shadow-lg overflow-hidden">
|
||||||
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
|
<div className="overflow-hidden py-8 px-4">
|
||||||
<div className="relative grid bg-white p-1">
|
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h4 className="text-base text-gray-600">Group by</h4>
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
groupByOptions.find((option) => option.key === groupByProperty)
|
||||||
|
?.name ?? "Select"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{groupByOptions.map((option) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={option.key}
|
||||||
|
onClick={() => setGroupByProperty(option.key)}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h4 className="text-base text-gray-600">Order by</h4>
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||||
|
"Select"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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">
|
||||||
|
<h4 className="text-base text-gray-600">Issue type</h4>
|
||||||
|
<CustomMenu
|
||||||
|
label={
|
||||||
|
filterIssueOptions.find((option) => option.key === filterIssue)
|
||||||
|
?.name ?? "Select"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
{Object.keys(properties).map((key) => (
|
{Object.keys(properties).map((key) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
className={`text-gray-900 hover:bg-theme hover:text-white flex justify-between w-full items-center rounded-md p-2 text-xs`}
|
type="button"
|
||||||
|
className={`px-2 py-1 inline capitalize rounded border border-indigo-600 text-sm m-1 ${
|
||||||
|
properties[key as keyof Properties]
|
||||||
|
? "border-indigo-600 bg-indigo-600 text-white"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
onClick={() => setProperties(key as keyof Properties)}
|
||||||
>
|
>
|
||||||
<p className="capitalize">{key.replace("_", " ")}</p>
|
{replaceUnderscoreIfSnakeCase(key)}
|
||||||
<span className="self-end">
|
|
||||||
{properties[key as keyof Properties] ? (
|
|
||||||
<EyeIcon width="18" height="18" />
|
|
||||||
) : (
|
|
||||||
<EyeSlashIcon width="18" height="18" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
@ -335,4 +340,4 @@ const ProjectIssues: NextPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectIssues;
|
export default withAuth(ProjectIssues);
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import Image from "next/image";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// headless ui
|
// headless ui
|
||||||
@ -10,19 +11,20 @@ import { Menu } from "@headlessui/react";
|
|||||||
import projectService from "lib/services/project.service";
|
import projectService from "lib/services/project.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
// fetching keys
|
// fetching keys
|
||||||
import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AdminLayout from "layouts/AdminLayout";
|
||||||
// components
|
// components
|
||||||
import SendProjectInvitationModal from "components/project/SendProjectInvitationModal";
|
import SendProjectInvitationModal from "components/project/SendProjectInvitationModal";
|
||||||
|
import ConfirmProjectMemberRemove from "components/project/ConfirmProjectMemberRemove";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, Button } from "ui";
|
import { Spinner, CustomListbox } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, EllipsisHorizontalIcon } from "@heroicons/react/20/solid";
|
import { PlusIcon, EllipsisHorizontalIcon } from "@heroicons/react/20/solid";
|
||||||
import HeaderButton from "ui/HeaderButton";
|
import HeaderButton from "ui/HeaderButton";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
const ROLE = {
|
const ROLE = {
|
||||||
5: "Guest",
|
5: "Guest",
|
||||||
@ -33,8 +35,16 @@ const ROLE = {
|
|||||||
|
|
||||||
const ProjectMembers: NextPage = () => {
|
const ProjectMembers: NextPage = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const [selectedMember, setSelectedMember] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
|
||||||
|
const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState<string | null>(null);
|
||||||
|
|
||||||
const { activeWorkspace, activeProject } = useUser();
|
const { activeWorkspace, activeProject } = useUser();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
@ -75,6 +85,48 @@ const ProjectMembers: NextPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
|
<ConfirmProjectMemberRemove
|
||||||
|
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedRemoveMember(null);
|
||||||
|
setSelectedInviteRemoveMember(null);
|
||||||
|
}}
|
||||||
|
data={members.find(
|
||||||
|
(item) => item.id === selectedRemoveMember || item.id === selectedInviteRemoveMember
|
||||||
|
)}
|
||||||
|
handleDelete={async () => {
|
||||||
|
if (!activeWorkspace || !projectId) return;
|
||||||
|
if (selectedRemoveMember) {
|
||||||
|
await projectService.deleteProjectMember(
|
||||||
|
activeWorkspace.slug,
|
||||||
|
projectId as string,
|
||||||
|
selectedRemoveMember
|
||||||
|
);
|
||||||
|
mutateMembers(
|
||||||
|
(prevData: any[]) =>
|
||||||
|
prevData?.filter((item: any) => item.id !== selectedRemoveMember),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (selectedInviteRemoveMember) {
|
||||||
|
await projectService.deleteProjectInvitation(
|
||||||
|
activeWorkspace.slug,
|
||||||
|
projectId as string,
|
||||||
|
selectedInviteRemoveMember
|
||||||
|
);
|
||||||
|
mutateInvitations(
|
||||||
|
(prevData: any[]) =>
|
||||||
|
prevData?.filter((item: any) => item.id !== selectedInviteRemoveMember),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
message: "Member removed successfully",
|
||||||
|
title: "Success",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<SendProjectInvitationModal isOpen={isOpen} setIsOpen={setIsOpen} members={members} />
|
<SendProjectInvitationModal isOpen={isOpen} setIsOpen={setIsOpen} members={members} />
|
||||||
{!projectMembers || !projectInvitations ? (
|
{!projectMembers || !projectInvitations ? (
|
||||||
<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">
|
||||||
@ -137,14 +189,55 @@ const ProjectMembers: NextPage = () => {
|
|||||||
{member.email ?? "No email has been added."}
|
{member.email ?? "No email has been added."}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||||
{ROLE[member.role as keyof typeof ROLE] ?? "None"}
|
{selectedMember === member.id ? (
|
||||||
|
<CustomListbox
|
||||||
|
options={Object.keys(ROLE).map((key) => ({
|
||||||
|
value: key,
|
||||||
|
display: ROLE[parseInt(key) as keyof typeof ROLE],
|
||||||
|
}))}
|
||||||
|
title={ROLE[member.role as keyof typeof ROLE] ?? "Select Role"}
|
||||||
|
value={member.role}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!activeWorkspace || !projectId) return;
|
||||||
|
projectService
|
||||||
|
.updateProjectMember(
|
||||||
|
activeWorkspace.slug,
|
||||||
|
projectId as string,
|
||||||
|
member.id,
|
||||||
|
{
|
||||||
|
role: value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
message: "Member role updated successfully.",
|
||||||
|
title: "Success",
|
||||||
|
});
|
||||||
|
mutateMembers(
|
||||||
|
(prevData: any) =>
|
||||||
|
prevData.map((m: any) => {
|
||||||
|
return m.id === selectedMember
|
||||||
|
? { ...m, ...res, role: value }
|
||||||
|
: m;
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
setSelectedMember(null);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
ROLE[member.role as keyof typeof ROLE] ?? "None"
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
||||||
{member?.member ? (
|
{member.status ? (
|
||||||
"Member"
|
|
||||||
) : member.status ? (
|
|
||||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||||
Accepted
|
Active
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
|
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
|
||||||
@ -167,7 +260,17 @@ const ProjectMembers: NextPage = () => {
|
|||||||
<button
|
<button
|
||||||
className="w-full text-left py-2 pl-2"
|
className="w-full text-left py-2 pl-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {}}
|
onClick={() => {
|
||||||
|
if (!member.status) {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
message: "You can't edit a pending invitation.",
|
||||||
|
title: "Error",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedMember(member.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@ -178,20 +281,12 @@ const ProjectMembers: NextPage = () => {
|
|||||||
<button
|
<button
|
||||||
className="w-full text-left py-2 pl-2"
|
className="w-full text-left py-2 pl-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
member.member
|
if (member.status) {
|
||||||
? (await projectService.deleteProjectMember(
|
setSelectedRemoveMember(member.id);
|
||||||
activeWorkspace?.slug as string,
|
} else {
|
||||||
projectId as any,
|
setSelectedInviteRemoveMember(member.id);
|
||||||
member.id
|
}
|
||||||
),
|
|
||||||
await mutateMembers())
|
|
||||||
: (await projectService.deleteProjectInvitation(
|
|
||||||
activeWorkspace?.slug as string,
|
|
||||||
projectId as any,
|
|
||||||
member.id
|
|
||||||
),
|
|
||||||
await mutateInvitations());
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
@ -252,6 +252,11 @@ const ProjectSettings: NextPage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
|
@ -14,7 +14,7 @@ import useUser from "lib/hooks/useUser";
|
|||||||
// layouts
|
// layouts
|
||||||
import DefaultLayout from "layouts/DefaultLayout";
|
import DefaultLayout from "layouts/DefaultLayout";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "ui";
|
import { Button, Spinner } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ChartBarIcon,
|
ChartBarIcon,
|
||||||
@ -35,8 +35,9 @@ const WorkspaceInvitation: NextPage = () => {
|
|||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const { data: invitationDetail, error } = useSWR(WORKSPACE_INVITATION, () =>
|
const { data: invitationDetail, error } = useSWR(
|
||||||
workspaceService.getWorkspaceInvitation(invitationId as string)
|
invitationId && WORKSPACE_INVITATION,
|
||||||
|
() => invitationId && workspaceService.getWorkspaceInvitation(invitationId as string)
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAccept = () => {
|
const handleAccept = () => {
|
||||||
@ -93,7 +94,7 @@ const WorkspaceInvitation: NextPage = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : error ? (
|
||||||
<EmptySpace
|
<EmptySpace
|
||||||
title="This invitation link is not active anymore."
|
title="This invitation link is not active anymore."
|
||||||
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
|
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
|
||||||
@ -131,6 +132,10 @@ const WorkspaceInvitation: NextPage = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</EmptySpace>
|
</EmptySpace>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
|
@ -103,7 +103,9 @@ const Workspace: NextPage = () => {
|
|||||||
<a className="hover:text-theme duration-300">{issue.name}</a>
|
<a className="hover:text-theme duration-300">{issue.name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4">{issue.sequence_id}</td>
|
<td className="px-3 py-4">
|
||||||
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
|
</td>
|
||||||
<td className="px-3 py-4">
|
<td className="px-3 py-4">
|
||||||
<span
|
<span
|
||||||
className="rounded px-2 py-1 text-xs font-medium"
|
className="rounded px-2 py-1 text-xs font-medium"
|
||||||
|
@ -8,9 +8,11 @@ import useSWR from "swr";
|
|||||||
import { Menu } from "@headlessui/react";
|
import { Menu } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
|
import useToast from "lib/hooks/useToast";
|
||||||
// services
|
// services
|
||||||
import workspaceService from "lib/services/workspace.service";
|
import workspaceService from "lib/services/workspace.service";
|
||||||
// constants
|
// constants
|
||||||
|
import { ROLE } from "constants/";
|
||||||
import { WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// hoc
|
// hoc
|
||||||
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
||||||
@ -18,26 +20,26 @@ import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
|||||||
import AdminLayout from "layouts/AdminLayout";
|
import AdminLayout from "layouts/AdminLayout";
|
||||||
// components
|
// components
|
||||||
import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal";
|
import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal";
|
||||||
|
import ConfirmWorkspaceMemberRemove from "components/workspace/ConfirmWorkspaceMemberRemove";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner, Button } from "ui";
|
import { Spinner, CustomListbox } from "ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon, EllipsisHorizontalIcon } from "@heroicons/react/20/solid";
|
import { PlusIcon, EllipsisHorizontalIcon } from "@heroicons/react/20/solid";
|
||||||
import HeaderButton from "ui/HeaderButton";
|
import HeaderButton from "ui/HeaderButton";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
||||||
// types
|
|
||||||
|
|
||||||
const ROLE = {
|
|
||||||
5: "Guest",
|
|
||||||
10: "Viewer",
|
|
||||||
15: "Member",
|
|
||||||
20: "Admin",
|
|
||||||
};
|
|
||||||
|
|
||||||
const WorkspaceInvite: NextPage = () => {
|
const WorkspaceInvite: NextPage = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const [selectedMember, setSelectedMember] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
|
||||||
|
const [selectedInviteRemoveMember, setSelectedInviteRemoveMember] = useState<string | null>(null);
|
||||||
|
|
||||||
const { activeWorkspace } = useUser();
|
const { activeWorkspace } = useUser();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { data: workspaceMembers, mutate: mutateMembers } = useSWR<any[]>(
|
const { data: workspaceMembers, mutate: mutateMembers } = useSWR<any[]>(
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
@ -74,6 +76,50 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
title: "Plane - Workspace Invite",
|
title: "Plane - Workspace Invite",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<ConfirmWorkspaceMemberRemove
|
||||||
|
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedRemoveMember(null);
|
||||||
|
setSelectedInviteRemoveMember(null);
|
||||||
|
}}
|
||||||
|
data={
|
||||||
|
selectedRemoveMember
|
||||||
|
? members.find((item) => item.id === selectedRemoveMember)
|
||||||
|
: selectedInviteRemoveMember
|
||||||
|
? members.find((item) => item.id === selectedInviteRemoveMember)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
handleDelete={async () => {
|
||||||
|
if (!activeWorkspace) return;
|
||||||
|
if (selectedRemoveMember) {
|
||||||
|
await workspaceService.deleteWorkspaceMember(
|
||||||
|
activeWorkspace.slug,
|
||||||
|
selectedRemoveMember
|
||||||
|
);
|
||||||
|
mutateMembers(
|
||||||
|
(prevData) => prevData?.filter((item) => item.id !== selectedRemoveMember),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (selectedInviteRemoveMember) {
|
||||||
|
await workspaceService.deleteWorkspaceInvitations(
|
||||||
|
activeWorkspace.slug,
|
||||||
|
selectedInviteRemoveMember
|
||||||
|
);
|
||||||
|
mutateInvitations(
|
||||||
|
(prevData) => prevData?.filter((item) => item.id !== selectedInviteRemoveMember),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
message: "Member removed successfully",
|
||||||
|
});
|
||||||
|
setSelectedRemoveMember(null);
|
||||||
|
setSelectedInviteRemoveMember(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<SendWorkspaceInvitationModal
|
<SendWorkspaceInvitationModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
setIsOpen={setIsOpen}
|
setIsOpen={setIsOpen}
|
||||||
@ -141,16 +187,51 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
{member.email ?? "No email has been added."}
|
{member.email ?? "No email has been added."}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||||
{ROLE[member.role as keyof typeof ROLE] ?? "None"}
|
{selectedMember === member.id ? (
|
||||||
|
<CustomListbox
|
||||||
|
options={Object.keys(ROLE).map((key) => ({
|
||||||
|
display: ROLE[parseInt(key) as keyof typeof ROLE],
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
title={ROLE[member.role as keyof typeof ROLE] ?? "None"}
|
||||||
|
value={member.role}
|
||||||
|
onChange={(value) => {
|
||||||
|
workspaceService
|
||||||
|
.updateWorkspaceMember(activeWorkspace?.slug as string, member.id, {
|
||||||
|
role: value,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
mutateMembers(
|
||||||
|
(prevData) =>
|
||||||
|
prevData?.map((m) => {
|
||||||
|
return m.id === selectedMember ? { ...m, role: value } : m;
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
type: "success",
|
||||||
|
message: "Member role updated successfully.",
|
||||||
|
});
|
||||||
|
setSelectedMember(null);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Error",
|
||||||
|
type: "error",
|
||||||
|
message: "An error occurred while updating member role.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
ROLE[member.role as keyof typeof ROLE] ?? "None"
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
||||||
{member?.member ? (
|
{member.status ? (
|
||||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||||
Accepted
|
Active
|
||||||
</span>
|
|
||||||
) : member.status ? (
|
|
||||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
|
||||||
Accepted
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
|
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
|
||||||
@ -173,7 +254,18 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
<button
|
<button
|
||||||
className="w-full text-left py-2 pl-2"
|
className="w-full text-left py-2 pl-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {}}
|
onClick={() => {
|
||||||
|
if (!member.status || !member.member) {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error",
|
||||||
|
message: "You can't edit this member.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setSelectedMember(member.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@ -184,26 +276,12 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
<button
|
<button
|
||||||
className="w-full text-left py-2 pl-2"
|
className="w-full text-left py-2 pl-2"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
member.member
|
if (member.status) {
|
||||||
? (await workspaceService.deleteWorkspaceMember(
|
setSelectedRemoveMember(member.id);
|
||||||
activeWorkspace?.slug as string,
|
} else {
|
||||||
member.id
|
setSelectedInviteRemoveMember(member.id);
|
||||||
),
|
}
|
||||||
await mutateMembers((prevData) => [
|
|
||||||
...(prevData ?? [])?.filter(
|
|
||||||
(m: any) => m.id !== member.id
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
false)
|
|
||||||
: (await workspaceService.deleteWorkspaceInvitations(
|
|
||||||
activeWorkspace?.slug as string,
|
|
||||||
member.id
|
|
||||||
),
|
|
||||||
await mutateInvitations((prevData) => [
|
|
||||||
...(prevData ?? []).filter((m) => m.id !== member.id),
|
|
||||||
false,
|
|
||||||
]));
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
|
7
apps/app/types/issues.d.ts
vendored
7
apps/app/types/issues.d.ts
vendored
@ -98,18 +98,11 @@ export type IssuePriorities = {
|
|||||||
|
|
||||||
export type Properties = {
|
export type Properties = {
|
||||||
key: boolean;
|
key: boolean;
|
||||||
name: boolean;
|
|
||||||
parent: boolean;
|
|
||||||
project: boolean;
|
|
||||||
state: boolean;
|
state: boolean;
|
||||||
assignee: boolean;
|
assignee: boolean;
|
||||||
description: boolean;
|
|
||||||
priority: boolean;
|
priority: boolean;
|
||||||
start_date: boolean;
|
start_date: boolean;
|
||||||
target_date: boolean;
|
target_date: boolean;
|
||||||
sequence_id: boolean;
|
|
||||||
attachments: boolean;
|
|
||||||
children: boolean;
|
|
||||||
cycle: boolean;
|
cycle: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
6
apps/app/types/sprints.d.ts
vendored
6
apps/app/types/sprints.d.ts
vendored
@ -17,7 +17,7 @@ export interface ICycle {
|
|||||||
issue: string;
|
issue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SprintIssueResponse {
|
export interface CycleIssueResponse {
|
||||||
id: string;
|
id: string;
|
||||||
issue_details: IIssue;
|
issue_details: IIssue;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@ -30,8 +30,8 @@ export interface SprintIssueResponse {
|
|||||||
cycle: string;
|
cycle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SprintViewProps = {
|
export type CycleViewProps = {
|
||||||
sprint: ICycle;
|
cycle: ICycle;
|
||||||
selectSprint: React.Dispatch<React.SetStateAction<SelectSprintType>>;
|
selectSprint: React.Dispatch<React.SetStateAction<SelectSprintType>>;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
1
apps/app/types/state.d.ts
vendored
1
apps/app/types/state.d.ts
vendored
@ -11,4 +11,5 @@ export interface IState {
|
|||||||
project: string;
|
project: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
sequence: number;
|
sequence: number;
|
||||||
|
group: "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { CheckIcon } from "@heroicons/react/20/solid";
|
|||||||
import { Props } from "./types";
|
import { Props } from "./types";
|
||||||
|
|
||||||
const CustomListbox: React.FC<Props> = ({
|
const CustomListbox: React.FC<Props> = ({
|
||||||
title,
|
title = "",
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@ -90,8 +90,6 @@ const CustomListbox: React.FC<Props> = ({
|
|||||||
: optionsFontsize === "2xl"
|
: optionsFontsize === "2xl"
|
||||||
? "text-2xl"
|
? "text-2xl"
|
||||||
: ""
|
: ""
|
||||||
} ${
|
|
||||||
className || ""
|
|
||||||
} rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none z-10`}
|
} rounded-md py-1 ring-1 ring-black ring-opacity-5 focus:outline-none z-10`}
|
||||||
>
|
>
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
|
2
apps/app/ui/CustomListbox/types.d.ts
vendored
2
apps/app/ui/CustomListbox/types.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
export type Props = {
|
export type Props = {
|
||||||
title: string;
|
title?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
options?: Array<{ display: string; value: any }>;
|
options?: Array<{ display: string; value: any }>;
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
|
82
apps/app/ui/CustomMenu/index.tsx
Normal file
82
apps/app/ui/CustomMenu/index.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React from "react";
|
||||||
|
// next
|
||||||
|
import Link from "next/link";
|
||||||
|
// headless ui
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||||
|
// commons
|
||||||
|
import { classNames } from "constants/common";
|
||||||
|
// types
|
||||||
|
import type { MenuItemProps, Props } from "./types";
|
||||||
|
|
||||||
|
const CustomMenu = ({ children, label, textAlignment }: Props) => {
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<Menu.Button
|
||||||
|
className={`inline-flex w-32 justify-between gap-x-4 rounded-md border border-gray-300 bg-white px-4 py-1 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-100 ${
|
||||||
|
textAlignment === "right"
|
||||||
|
? "text-right"
|
||||||
|
: textAlignment === "center"
|
||||||
|
? "text-center"
|
||||||
|
: "text-left"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="truncate w-20">{label}</span>
|
||||||
|
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<div className="py-1">{children}</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuItem: React.FC<MenuItemProps> = ({ children, renderAs, href, onClick }) => {
|
||||||
|
return (
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) =>
|
||||||
|
renderAs === "a" ? (
|
||||||
|
<Link href={href ?? ""}>
|
||||||
|
<a
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
|
||||||
|
"block px-4 py-2 text-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
|
||||||
|
"block w-full px-4 py-2 text-left text-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomMenu.MenuItem = MenuItem;
|
||||||
|
|
||||||
|
export default CustomMenu;
|
12
apps/app/ui/CustomMenu/types.d.ts
vendored
Normal file
12
apps/app/ui/CustomMenu/types.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
textAlignment?: "left" | "center" | "right";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MenuItemProps = {
|
||||||
|
children: string;
|
||||||
|
renderAs?: "button" | "a";
|
||||||
|
href?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
@ -45,24 +45,14 @@ 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 sm:text-sm duration-300 ${
|
||||||
width === "sm"
|
buttonClassName || ""
|
||||||
? "w-32"
|
}`}
|
||||||
: width === "md"
|
|
||||||
? "w-48"
|
|
||||||
: width === "lg"
|
|
||||||
? "w-64"
|
|
||||||
: width === "xl"
|
|
||||||
? "w-80"
|
|
||||||
: width === "2xl"
|
|
||||||
? "w-96"
|
|
||||||
: ""
|
|
||||||
} ${buttonClassName || ""}`}
|
|
||||||
>
|
>
|
||||||
{icon ?? null}
|
{icon ?? null}
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
value === null || value === undefined ? "" : "text-gray-900",
|
value === null || value === undefined ? "" : "text-gray-900",
|
||||||
"hidden truncate sm:ml-2 sm:block"
|
"hidden truncate sm:block"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{Array.isArray(value)
|
{Array.isArray(value)
|
||||||
|
@ -6,7 +6,7 @@ const Select: React.FC<Props> = ({
|
|||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
className,
|
className = "",
|
||||||
name,
|
name,
|
||||||
register,
|
register,
|
||||||
disabled,
|
disabled,
|
||||||
@ -27,7 +27,7 @@ const Select: React.FC<Props> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
{...(register && register(name, validations))}
|
{...(register && register(name, validations))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="mt-1 block w-full px-3 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md bg-transparent"
|
className={`mt-1 block w-full px-3 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md bg-transparent ${className}`}
|
||||||
>
|
>
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<option value={option.value} key={index}>
|
<option value={option.value} key={index}>
|
||||||
|
@ -3,6 +3,7 @@ export { default as Input } from "./Input";
|
|||||||
export { default as Select } from "./Select";
|
export { default as Select } from "./Select";
|
||||||
export { default as TextArea } from "./TextArea";
|
export { default as TextArea } from "./TextArea";
|
||||||
export { default as CustomListbox } from "./CustomListbox";
|
export { default as CustomListbox } from "./CustomListbox";
|
||||||
|
export { default as CustomMenu } from "./CustomMenu";
|
||||||
export { default as Spinner } from "./Spinner";
|
export { default as Spinner } from "./Spinner";
|
||||||
export { default as Tooltip } from "./Tooltip";
|
export { default as Tooltip } from "./Tooltip";
|
||||||
export { default as SearchListbox } from "./SearchListbox";
|
export { default as SearchListbox } from "./SearchListbox";
|
||||||
|
Loading…
Reference in New Issue
Block a user