forked from github/plane
feat: lexical integrated
This commit is contained in:
parent
3e5e1ab403
commit
945a75e18b
@ -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,18 +6,18 @@ 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;
|
||||||
@ -41,18 +42,13 @@ 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) => {
|
||||||
@ -78,118 +74,112 @@ 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
|
||||||
|
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
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100 scale-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity" />
|
<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">
|
||||||
</Transition.Child>
|
<Combobox>
|
||||||
|
<div className="relative m-1">
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<Combobox.Input
|
||||||
|
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
|
||||||
|
placeholder="Search..."
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
<Combobox.Options
|
||||||
<Transition.Child
|
static
|
||||||
as={React.Fragment}
|
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
||||||
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={(item: ItemType) => {
|
|
||||||
// const { url, onClick } = item;
|
|
||||||
// if (url) router.push(url);
|
|
||||||
// else if (onClick) onClick();
|
|
||||||
// handleCommandPaletteClose();
|
|
||||||
// }}
|
|
||||||
>
|
>
|
||||||
<div className="relative m-1">
|
{filteredIssues.length > 0 && (
|
||||||
<MagnifyingGlassIcon
|
<>
|
||||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
<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) => {
|
||||||
|
if (
|
||||||
|
(issue.parent === "" || issue.parent === null) &&
|
||||||
|
issue.id !== parentId
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Combobox.Option
|
||||||
|
key={issue.id}
|
||||||
|
value={{
|
||||||
|
name: issue.name,
|
||||||
|
}}
|
||||||
|
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={() => {
|
||||||
|
addAsSubIssue(issue.id);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 block rounded-full`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: issue.state_detail.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{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"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<Combobox.Input
|
<p className="mt-4 text-sm text-gray-900">
|
||||||
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"
|
We couldn{"'"}t find any issue with that term. Please try again.
|
||||||
placeholder="Search..."
|
</p>
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<Combobox.Options
|
</Combobox>
|
||||||
static
|
</Dialog.Panel>
|
||||||
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
</Transition.Child>
|
||||||
>
|
</div>
|
||||||
{filteredIssues.length > 0 && (
|
</Dialog>
|
||||||
<>
|
</Transition.Root>
|
||||||
<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) => {
|
|
||||||
if (issue.parent === "" || issue.parent === null)
|
|
||||||
return (
|
|
||||||
<Combobox.Option
|
|
||||||
key={issue.id}
|
|
||||||
value={{
|
|
||||||
name: issue.name,
|
|
||||||
}}
|
|
||||||
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={() => {
|
|
||||||
addAsSubIssue(issue.id);
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`h-1.5 w-1.5 block rounded-full`}
|
|
||||||
style={{
|
|
||||||
backgroundColor: issue.state_detail.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{issue.name}
|
|
||||||
</Combobox.Option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Combobox.Options>
|
|
||||||
|
|
||||||
{query !== "" && filteredIssues.length === 0 && (
|
|
||||||
<div className="py-14 px-6 text-center sm:px-14">
|
|
||||||
<FolderIcon
|
|
||||||
className="mx-auto h-6 w-6 text-gray-900 text-opacity-40"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<p className="mt-4 text-sm text-gray-900">
|
|
||||||
We couldn{"'"}t find any issue with that term. Please try again.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Combobox>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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={
|
||||||
|
@ -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,47 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
// react hook form
|
||||||
|
import { Controller, Control } from "react-hook-form";
|
||||||
|
// hooks
|
||||||
|
import useUser from "lib/hooks/useUser";
|
||||||
|
// types
|
||||||
|
import type { IIssue } from "types";
|
||||||
|
// icons
|
||||||
|
import { UserIcon } from "@heroicons/react/24/outline";
|
||||||
|
// components
|
||||||
|
import IssuesListModal from "components/project/issues/IssuesListModal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
control: Control<IIssue, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectParent: React.FC<Props> = ({ control }) => {
|
||||||
|
const [isIssueListModalOpen, setIsIssueListModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { issues } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="parent"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<>
|
||||||
|
<IssuesListModal
|
||||||
|
isOpen={isIssueListModalOpen}
|
||||||
|
handleClose={() => setIsIssueListModalOpen(false)}
|
||||||
|
onChange={onChange}
|
||||||
|
issues={issues}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-2 text-left text-gray-900 hover:bg-theme hover:text-white rounded-md text-xs whitespace-nowrap"
|
||||||
|
onClick={() => setIsIssueListModalOpen(true)}
|
||||||
|
>
|
||||||
|
{value ? issues?.results.find((i) => i.id === value)?.name : "Select Parent Issue"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
|
@ -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
|
||||||
|
@ -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,14 +14,14 @@ import {
|
|||||||
USER_ISSUE,
|
USER_ISSUE,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
// headless
|
// headless
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Menu, Popover, Transition } from "@headlessui/react";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.services";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useToast from "lib/hooks/useToast";
|
import useToast from "lib/hooks/useToast";
|
||||||
// ui
|
// ui
|
||||||
import { Button, Input, TextArea } from "ui";
|
import { Button, CustomListbox, Input, TextArea } from "ui";
|
||||||
// commons
|
// commons
|
||||||
import { renderDateFormat, cosineSimilarity } from "constants/common";
|
import { renderDateFormat, cosineSimilarity } from "constants/common";
|
||||||
// components
|
// components
|
||||||
@ -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, SprintIssueResponse } 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: "",
|
||||||
|
priority: "",
|
||||||
|
labels_list: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateUpdateIssuesModal: React.FC<Props> = ({
|
const CreateUpdateIssuesModal: React.FC<Props> = ({
|
||||||
@ -65,6 +71,16 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
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 = () => {
|
||||||
@ -93,6 +109,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
setError,
|
setError,
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
} = useForm<IIssue>({
|
} = useForm<IIssue>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
@ -159,6 +176,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
if (!activeWorkspace || !activeProject) return;
|
if (!activeWorkspace || !activeProject) return;
|
||||||
const payload: Partial<IIssue> = {
|
const payload: Partial<IIssue> = {
|
||||||
...formData,
|
...formData,
|
||||||
|
description: JSON.stringify(formData.description),
|
||||||
target_date: formData.target_date ? renderDateFormat(formData.target_date ?? "") : null,
|
target_date: formData.target_date ? renderDateFormat(formData.target_date ?? "") : null,
|
||||||
};
|
};
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -261,6 +279,8 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
return () => setMostSimilarIssue(undefined);
|
return () => setMostSimilarIssue(undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// console.log(watch("parent"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{activeProject && (
|
{activeProject && (
|
||||||
@ -373,13 +393,20 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<TextArea
|
{/* <TextArea
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
placeholder="Enter description"
|
placeholder="Enter description"
|
||||||
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>
|
||||||
@ -398,9 +425,31 @@ 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} />
|
||||||
|
<Menu as="div" className="relative inline-block">
|
||||||
|
<Menu.Button className="grid relative place-items-center rounded p-1 hover:bg-gray-100 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="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">
|
||||||
|
{(active) => <SelectParent control={control} />}
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
134
apps/app/components/project/issues/IssuesListModal.tsx
Normal file
134
apps/app/components/project/issues/IssuesListModal.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// 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";
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{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;
|
@ -1,8 +1,9 @@
|
|||||||
// react
|
// react
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
// next
|
// next
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
// swr
|
// swr
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
// ui
|
// ui
|
||||||
@ -10,7 +11,7 @@ import { Listbox, Transition } from "@headlessui/react";
|
|||||||
// icons
|
// icons
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IssueResponse, IState, NestedKeyOf, Properties, WorkspaceMember } from "types";
|
import { IIssue, IssueResponse, NestedKeyOf, Properties, WorkspaceMember } from "types";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
@ -48,7 +49,7 @@ const ListView: React.FC<Props> = ({
|
|||||||
const [issuePreviewModal, setIssuePreviewModal] = useState(false);
|
const [issuePreviewModal, setIssuePreviewModal] = useState(false);
|
||||||
const [previewModalIssueId, setPreviewModalIssueId] = useState<string | null>(null);
|
const [previewModalIssueId, setPreviewModalIssueId] = useState<string | null>(null);
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, states } = useUser();
|
const { activeWorkspace, activeProject, states, issues } = useUser();
|
||||||
|
|
||||||
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
|
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
|
||||||
if (!activeWorkspace || !activeProject) return;
|
if (!activeWorkspace || !activeProject) return;
|
||||||
@ -70,6 +71,10 @@ const ListView: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LexicalViewer = dynamic(() => import("components/lexical/viewer"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
const { data: people } = useSWR<WorkspaceMember[]>(
|
||||||
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
activeWorkspace ? WORKSPACE_MEMBERS : null,
|
||||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||||
@ -177,6 +182,10 @@ const ListView: React.FC<Props> = ({
|
|||||||
</td>
|
</td>
|
||||||
) : (key as keyof Properties) === "description" ? (
|
) : (key as keyof Properties) === "description" ? (
|
||||||
<td className="px-3 py-4 font-medium text-gray-900 truncate text-xs max-w-[15rem]">
|
<td className="px-3 py-4 font-medium text-gray-900 truncate text-xs max-w-[15rem]">
|
||||||
|
{/* <LexicalViewer
|
||||||
|
id={`descriptionViewer-${issue.id}`}
|
||||||
|
value={JSON.parse(issue.description)}
|
||||||
|
/> */}
|
||||||
{issue.description}
|
{issue.description}
|
||||||
</td>
|
</td>
|
||||||
) : (key as keyof Properties) === "priority" ? (
|
) : (key as keyof Properties) === "priority" ? (
|
||||||
|
@ -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,9 +99,14 @@ 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>
|
||||||
|
{console.log(activity)}
|
||||||
<span className="text-gray-500">To: </span>
|
<span className="text-gray-500">To: </span>
|
||||||
{activity.field === "state"
|
{activity.field === "state"
|
||||||
? activity.new_value
|
? activity.new_value
|
||||||
@ -102,6 +114,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>
|
||||||
|
@ -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";
|
||||||
@ -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: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -130,6 +135,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
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 +148,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) => {
|
||||||
@ -259,20 +266,6 @@ 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
|
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<RichTextEditor
|
|
||||||
onChange={(state: string) => {
|
|
||||||
handleDescriptionChange(state);
|
|
||||||
onChange(issueDescriptionValue);
|
|
||||||
}}
|
|
||||||
value={issueDescriptionValue}
|
|
||||||
id="editor"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/> */}
|
|
||||||
<div>
|
<div>
|
||||||
<TextArea
|
<TextArea
|
||||||
id="name"
|
id="name"
|
||||||
@ -287,7 +280,7 @@ const IssueDetail: NextPage = () => {
|
|||||||
mode="transparent"
|
mode="transparent"
|
||||||
className="text-xl font-medium"
|
className="text-xl font-medium"
|
||||||
/>
|
/>
|
||||||
<TextArea
|
{/* <TextArea
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
error={errors.description}
|
error={errors.description}
|
||||||
@ -300,7 +293,19 @@ const IssueDetail: NextPage = () => {
|
|||||||
placeholder="Enter issue description"
|
placeholder="Enter issue description"
|
||||||
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 ? (
|
||||||
@ -349,6 +354,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)}
|
||||||
>
|
>
|
||||||
@ -515,7 +521,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>
|
||||||
|
@ -144,7 +144,7 @@ const ProjectMembers: NextPage = () => {
|
|||||||
"Member"
|
"Member"
|
||||||
) : member.status ? (
|
) : 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">
|
||||||
|
@ -146,11 +146,11 @@ const WorkspaceInvite: NextPage = () => {
|
|||||||
<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?.member ? (
|
||||||
<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>
|
||||||
) : member.status ? (
|
) : 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">
|
||||||
|
@ -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,
|
||||||
|
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;
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user