forked from github/plane
refactor: parent issue select (#1546)
* refactor: parent issue select * fix: sibling issues list
This commit is contained in:
parent
93da220c4a
commit
059b8c793a
@ -380,7 +380,6 @@ export const CommandPalette: React.FC = () => {
|
|||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={isIssueModalOpen}
|
isOpen={isIssueModalOpen}
|
||||||
handleClose={() => setIsIssueModalOpen(false)}
|
handleClose={() => setIsIssueModalOpen(false)}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { ChangeEvent, FC, useState, useEffect, useRef } from "react";
|
import React, { FC, useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
@ -12,12 +11,12 @@ import aiService from "services/ai.service";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { GptAssistantModal } from "components/core";
|
import { GptAssistantModal } from "components/core";
|
||||||
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
import {
|
import {
|
||||||
IssueAssigneeSelect,
|
IssueAssigneeSelect,
|
||||||
IssueDateSelect,
|
IssueDateSelect,
|
||||||
IssueEstimateSelect,
|
IssueEstimateSelect,
|
||||||
IssueLabelSelect,
|
IssueLabelSelect,
|
||||||
IssueParentSelect,
|
|
||||||
IssuePrioritySelect,
|
IssuePrioritySelect,
|
||||||
IssueProjectSelect,
|
IssueProjectSelect,
|
||||||
IssueStateSelect,
|
IssueStateSelect,
|
||||||
@ -35,10 +34,8 @@ import {
|
|||||||
} from "components/ui";
|
} from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
|
||||||
import { cosineSimilarity } from "helpers/string.helper";
|
|
||||||
// types
|
// types
|
||||||
import type { ICurrentUserResponse, IIssue } from "types";
|
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
|
||||||
// rich-text-editor
|
// rich-text-editor
|
||||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -72,6 +69,7 @@ const defaultValues: Partial<IIssue> = {
|
|||||||
description_html: "<p></p>",
|
description_html: "<p></p>",
|
||||||
estimate_point: null,
|
estimate_point: null,
|
||||||
state: "",
|
state: "",
|
||||||
|
parent: null,
|
||||||
priority: null,
|
priority: null,
|
||||||
assignees: [],
|
assignees: [],
|
||||||
assignees_list: [],
|
assignees_list: [],
|
||||||
@ -82,7 +80,6 @@ const defaultValues: Partial<IIssue> = {
|
|||||||
export interface IssueFormProps {
|
export interface IssueFormProps {
|
||||||
handleFormSubmit: (values: Partial<IIssue>) => Promise<void>;
|
handleFormSubmit: (values: Partial<IIssue>) => Promise<void>;
|
||||||
initialData?: Partial<IIssue>;
|
initialData?: Partial<IIssue>;
|
||||||
issues: IIssue[];
|
|
||||||
projectId: string;
|
projectId: string;
|
||||||
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
createMore: boolean;
|
createMore: boolean;
|
||||||
@ -108,7 +105,6 @@ export interface IssueFormProps {
|
|||||||
export const IssueForm: FC<IssueFormProps> = ({
|
export const IssueForm: FC<IssueFormProps> = ({
|
||||||
handleFormSubmit,
|
handleFormSubmit,
|
||||||
initialData,
|
initialData,
|
||||||
issues = [],
|
|
||||||
projectId,
|
projectId,
|
||||||
setActiveProject,
|
setActiveProject,
|
||||||
createMore,
|
createMore,
|
||||||
@ -118,11 +114,10 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
user,
|
user,
|
||||||
fieldsToShow,
|
fieldsToShow,
|
||||||
}) => {
|
}) => {
|
||||||
// states
|
|
||||||
const [mostSimilarIssue, setMostSimilarIssue] = useState<IIssue | undefined>();
|
|
||||||
const [stateModal, setStateModal] = useState(false);
|
const [stateModal, setStateModal] = useState(false);
|
||||||
const [labelModal, setLabelModal] = useState(false);
|
const [labelModal, setLabelModal] = useState(false);
|
||||||
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||||
|
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||||
|
|
||||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||||
@ -151,12 +146,6 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
|
|
||||||
const issueName = watch("name");
|
const issueName = watch("name");
|
||||||
|
|
||||||
const handleTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
const similarIssue = issues?.find((i: IIssue) => cosineSimilarity(i.name, value) > 0.7);
|
|
||||||
setMostSimilarIssue(similarIssue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
const handleCreateUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
@ -283,26 +272,28 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
{watch("parent") &&
|
{watch("parent") &&
|
||||||
watch("parent") !== "" &&
|
watch("parent") !== "" &&
|
||||||
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
|
||||||
|
selectedParentIssue && (
|
||||||
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
|
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className="block h-1.5 w-1.5 rounded-full"
|
className="block h-1.5 w-1.5 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: issues.find((i) => i.id === watch("parent"))?.state_detail
|
backgroundColor: selectedParentIssue.state__color,
|
||||||
.color,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex-shrink-0 text-custom-text-200">
|
<span className="flex-shrink-0 text-custom-text-200">
|
||||||
{/* {projects?.find((p) => p.id === projectId)?.identifier}- */}
|
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
|
||||||
{issues.find((i) => i.id === watch("parent"))?.sequence_id}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate font-medium">
|
<span className="truncate font-medium">
|
||||||
{issues.find((i) => i.id === watch("parent"))?.name.substring(0, 50)}
|
{selectedParentIssue.name.substring(0, 50)}
|
||||||
</span>
|
</span>
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
className="h-3 w-3 cursor-pointer"
|
className="h-3 w-3 cursor-pointer"
|
||||||
onClick={() => setValue("parent", null)}
|
onClick={() => {
|
||||||
|
setValue("parent", null);
|
||||||
|
setSelectedParentIssue(null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -314,7 +305,6 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
onChange={handleTitleChange}
|
|
||||||
className="resize-none text-xl"
|
className="resize-none text-xl"
|
||||||
placeholder="Title"
|
placeholder="Title"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -328,38 +318,11 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{mostSimilarIssue && (
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<p className="text-sm text-custom-text-200">
|
|
||||||
<Link
|
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${mostSimilarIssue.id}`}
|
|
||||||
>
|
|
||||||
<a target="_blank" type="button" className="inline text-left">
|
|
||||||
<span>Did you mean </span>
|
|
||||||
<span className="italic">
|
|
||||||
{mostSimilarIssue.project_detail.identifier}-
|
|
||||||
{mostSimilarIssue.sequence_id}: {mostSimilarIssue.name}{" "}
|
|
||||||
</span>
|
|
||||||
?
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-sm text-custom-primary"
|
|
||||||
onClick={() => {
|
|
||||||
setMostSimilarIssue(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
No
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="-mb-2 flex justify-end">
|
<div className="flex justify-end">
|
||||||
{issueName && issueName !== "" && (
|
{issueName && issueName !== "" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -495,11 +458,20 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
<IssueParentSelect
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
name="parent"
|
||||||
|
render={({ field: { onChange } }) => (
|
||||||
|
<ParentIssuesListModal
|
||||||
isOpen={parentIssueListModalOpen}
|
isOpen={parentIssueListModalOpen}
|
||||||
setIsOpen={setParentIssueListModalOpen}
|
handleClose={() => setParentIssueListModalOpen(false)}
|
||||||
issues={issues ?? []}
|
onChange={(issue) => {
|
||||||
|
onChange(issue.id);
|
||||||
|
setSelectedParentIssue(issue);
|
||||||
|
}}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
|
@ -58,7 +58,7 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<div className="rounded-lg">
|
<div className="rounded-lg">
|
||||||
{issueDetails?.parent && issueDetails.parent !== "" ? (
|
{issueDetails?.parent && issueDetails.parent !== "" ? (
|
||||||
<div className="mb-5 flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
|
<div className="mb-5 flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-90 p-2 text-xs">
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${issueDetails.parent}`}>
|
<Link href={`/${workspaceSlug}/projects/${projectId}/issues/${issueDetails.parent}`}>
|
||||||
<a className="flex items-center gap-2 text-custom-text-200">
|
<a className="flex items-center gap-2 text-custom-text-200">
|
||||||
<span
|
<span
|
||||||
@ -76,24 +76,36 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<CustomMenu ellipsis position="left">
|
<CustomMenu position="left" ellipsis>
|
||||||
{siblingIssues && siblingIssues.length > 0 ? (
|
{siblingIssues && siblingIssues.sub_issues.length > 0 ? (
|
||||||
siblingIssues.map((issue: IIssue) => (
|
<>
|
||||||
<CustomMenu.MenuItem key={issue.id}>
|
<h2 className="text-custom-text-200 px-1 mb-2">Sibling issues</h2>
|
||||||
<Link
|
{siblingIssues.sub_issues.map((issue) => {
|
||||||
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id}`}
|
if (issue.id !== issueDetails.id)
|
||||||
|
return (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={issue.id}
|
||||||
|
renderAs="a"
|
||||||
|
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
|
||||||
|
issue.id
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<a>
|
|
||||||
{issueDetails.project_detail.identifier}-{issue.sequence_id}
|
{issueDetails.project_detail.identifier}-{issue.sequence_id}
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
))
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<CustomMenu.MenuItem className="flex items-center gap-2 whitespace-nowrap p-2 text-left text-xs text-custom-text-200">
|
<p className="flex items-center gap-2 whitespace-nowrap px-1 text-left text-xs text-custom-text-200 py-1">
|
||||||
No other sibling issues
|
No sibling issues
|
||||||
</CustomMenu.MenuItem>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
renderAs="button"
|
||||||
|
onClick={() => submitChanges({ parent: null })}
|
||||||
|
>
|
||||||
|
Remove parent issue
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
@ -94,15 +94,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: issues } = useSWR(
|
|
||||||
workspaceSlug && activeProject
|
|
||||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? "")
|
|
||||||
: null,
|
|
||||||
workspaceSlug && activeProject
|
|
||||||
? () => issuesService.getIssues(workspaceSlug as string, activeProject ?? "")
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projects && projects.length > 0)
|
if (projects && projects.length > 0)
|
||||||
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||||
@ -317,6 +308,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
else await updateIssue(payload);
|
else await updateIssue(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!projects || projects.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
|
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
|
||||||
@ -345,7 +338,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
|
||||||
<IssueForm
|
<IssueForm
|
||||||
issues={issues ?? []}
|
|
||||||
handleFormSubmit={handleFormSubmit}
|
handleFormSubmit={handleFormSubmit}
|
||||||
initialData={data ?? prePopulateData}
|
initialData={data ?? prePopulateData}
|
||||||
createMore={createMore}
|
createMore={createMore}
|
||||||
|
@ -21,7 +21,8 @@ type Props = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
value?: any;
|
value?: any;
|
||||||
onChange: (...event: any[]) => void;
|
onChange: (issue: ISearchIssueResponse) => void;
|
||||||
|
projectId: string;
|
||||||
issueId?: string;
|
issueId?: string;
|
||||||
customDisplay?: JSX.Element;
|
customDisplay?: JSX.Element;
|
||||||
};
|
};
|
||||||
@ -31,6 +32,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
handleClose: onClose,
|
handleClose: onClose,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
projectId,
|
||||||
issueId,
|
issueId,
|
||||||
customDisplay,
|
customDisplay,
|
||||||
}) => {
|
}) => {
|
||||||
@ -42,7 +44,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
@ -109,7 +111,13 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
<Dialog.Panel className="relative mx-auto max-w-2xl transform rounded-xl border border-custom-border-200 bg-custom-background-100 shadow-2xl transition-all">
|
||||||
<Combobox value={value} onChange={onChange}>
|
<Combobox
|
||||||
|
value={value}
|
||||||
|
onChange={(val) => {
|
||||||
|
onChange(val);
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<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-custom-text-100 text-opacity-40"
|
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-100 text-opacity-40"
|
||||||
@ -165,13 +173,12 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
{issues.map((issue) => (
|
{issues.map((issue) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
value={issue.id}
|
value={issue}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
`flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||||
} ${selected ? "text-custom-text-100" : ""}`
|
} ${selected ? "text-custom-text-100" : ""}`
|
||||||
}
|
}
|
||||||
onClick={handleClose}
|
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
export * from "./assignee";
|
export * from "./assignee";
|
||||||
export * from "./date";
|
export * from "./date";
|
||||||
export * from "./estimate"
|
export * from "./estimate";
|
||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./parent";
|
|
||||||
export * from "./priority";
|
export * from "./priority";
|
||||||
export * from "./project";
|
export * from "./project";
|
||||||
export * from "./state";
|
export * from "./state";
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Controller, Control } from "react-hook-form";
|
|
||||||
// components
|
|
||||||
import { ParentIssuesListModal } from "components/issues";
|
|
||||||
// types
|
|
||||||
import type { IIssue } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
control: Control<IIssue, any>;
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
issues: IIssue[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IssueParentSelect: React.FC<Props> = ({ control, isOpen, setIsOpen, issues }) => (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="parent"
|
|
||||||
render={({ field: { onChange } }) => (
|
|
||||||
<ParentIssuesListModal
|
|
||||||
isOpen={isOpen}
|
|
||||||
handleClose={() => setIsOpen(false)}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
@ -2,51 +2,31 @@ import React, { useState } from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
// icons
|
||||||
|
|
||||||
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
|
||||||
// fetch keys
|
|
||||||
import { UserIcon } from "@heroicons/react/24/outline";
|
import { UserIcon } from "@heroicons/react/24/outline";
|
||||||
// services
|
|
||||||
import issuesServices from "services/issues.service";
|
|
||||||
// components
|
// components
|
||||||
import { ParentIssuesListModal } from "components/issues";
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
// icons
|
|
||||||
// types
|
// types
|
||||||
import { IIssue, UserAuth } from "types";
|
import { IIssue, ISearchIssueResponse, UserAuth } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Control<IIssue, any>;
|
onChange: (value: string) => void;
|
||||||
submitChanges: (formData: Partial<IIssue>) => void;
|
issueDetails: IIssue | undefined;
|
||||||
customDisplay: JSX.Element;
|
|
||||||
watch: UseFormWatch<IIssue>;
|
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarParentSelect: React.FC<Props> = ({
|
export const SidebarParentSelect: React.FC<Props> = ({
|
||||||
control,
|
onChange,
|
||||||
submitChanges,
|
issueDetails,
|
||||||
customDisplay,
|
|
||||||
watch,
|
|
||||||
userAuth,
|
userAuth,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||||
|
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { projectId, issueId } = router.query;
|
||||||
|
|
||||||
const { data: issues } = useSWR(
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
@ -57,22 +37,15 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
|||||||
<p>Parent</p>
|
<p>Parent</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/2">
|
<div className="sm:basis-1/2">
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="parent"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<ParentIssuesListModal
|
<ParentIssuesListModal
|
||||||
isOpen={isParentModalOpen}
|
isOpen={isParentModalOpen}
|
||||||
handleClose={() => setIsParentModalOpen(false)}
|
handleClose={() => setIsParentModalOpen(false)}
|
||||||
onChange={(val) => {
|
onChange={(issue) => {
|
||||||
submitChanges({ parent: val });
|
onChange(issue.id);
|
||||||
onChange(val);
|
setSelectedParentIssue(issue);
|
||||||
}}
|
}}
|
||||||
issueId={issueId as string}
|
issueId={issueId as string}
|
||||||
value={value}
|
projectId={projectId as string}
|
||||||
customDisplay={customDisplay}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -82,10 +55,10 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
|||||||
onClick={() => setIsParentModalOpen(true)}
|
onClick={() => setIsParentModalOpen(true)}
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
>
|
>
|
||||||
{watch("parent") && watch("parent") !== "" ? (
|
{selectedParentIssue ? (
|
||||||
`${issues?.find((i) => i.id === watch("parent"))?.project_detail?.identifier}-${
|
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
||||||
issues?.find((i) => i.id === watch("parent"))?.sequence_id
|
) : issueDetails?.parent ? (
|
||||||
}`
|
`${issueDetails.project_detail.identifier}-${issueDetails.parent_detail?.sequence_id}`
|
||||||
) : (
|
) : (
|
||||||
<span className="text-custom-text-200">Select issue</span>
|
<span className="text-custom-text-200">Select issue</span>
|
||||||
)}
|
)}
|
||||||
|
@ -33,13 +33,7 @@ import {
|
|||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker, Icon } from "components/ui";
|
import { CustomDatePicker, Icon } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||||
LinkIcon,
|
|
||||||
CalendarDaysIcon,
|
|
||||||
TrashIcon,
|
|
||||||
PlusIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -337,31 +331,22 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
{showSecondSection && (
|
{showSecondSection && (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
<SidebarParentSelect
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
submitChanges={submitChanges}
|
name="parent"
|
||||||
customDisplay={
|
render={({ field: { onChange } }) => (
|
||||||
issueDetail?.parent_detail ? (
|
<SidebarParentSelect
|
||||||
<button
|
onChange={(val: string) => {
|
||||||
type="button"
|
submitChanges({ parent: val });
|
||||||
className="flex items-center gap-2 rounded bg-custom-background-80 px-3 py-2 text-xs"
|
onChange(val);
|
||||||
onClick={() => submitChanges({ parent: null })}
|
}}
|
||||||
>
|
issueDetails={issueDetail}
|
||||||
<span className="text-custom-text-200">Selected:</span>{" "}
|
|
||||||
{issueDetail.parent_detail?.name}
|
|
||||||
<XMarkIcon className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="inline-block rounded bg-custom-background-90 px-3 py-2 text-xs text-custom-text-200">
|
|
||||||
No parent selected
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
watch={watchIssue}
|
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
disabled={uneditable}
|
disabled={uneditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
||||||
<SidebarBlockerSelect
|
<SidebarBlockerSelect
|
||||||
issueId={issueId as string}
|
issueId={issueId as string}
|
||||||
|
@ -272,7 +272,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||||
>
|
>
|
||||||
<a className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-custom-background-100">
|
<a className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-custom-background-90">
|
||||||
<div className="flex items-center gap-2 rounded text-xs">
|
<div className="flex items-center gap-2 rounded text-xs">
|
||||||
<span
|
<span
|
||||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
@ -316,6 +316,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
|
|||||||
Add sub-issue
|
Add sub-issue
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
buttonClassName="whitespace-nowrap"
|
||||||
position="left"
|
position="left"
|
||||||
noBorder
|
noBorder
|
||||||
noChevron
|
noChevron
|
||||||
|
@ -175,7 +175,11 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading || !canDelete}>
|
<DangerButton
|
||||||
|
onClick={handleDeletion}
|
||||||
|
disabled={!canDelete}
|
||||||
|
loading={isDeleteLoading}
|
||||||
|
>
|
||||||
{isDeleteLoading ? "Deleting..." : "Delete Project"}
|
{isDeleteLoading ? "Deleting..." : "Delete Project"}
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,7 +106,7 @@ const CustomMenu = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
type MenuItemProps = {
|
type MenuItemProps = {
|
||||||
children: JSX.Element | string;
|
children: React.ReactNode;
|
||||||
renderAs?: "button" | "a";
|
renderAs?: "button" | "a";
|
||||||
href?: string;
|
href?: string;
|
||||||
onClick?: (args?: any) => void;
|
onClick?: (args?: any) => void;
|
||||||
|
@ -49,45 +49,6 @@ export const copyTextToClipboard = async (text: string) => {
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wordsVector = (str: string) => {
|
|
||||||
const words = str.split(" ");
|
|
||||||
const vector: any = {};
|
|
||||||
for (let i = 0; i < words.length; i++) {
|
|
||||||
const word = words[i];
|
|
||||||
if (vector[word]) {
|
|
||||||
vector[word] += 1;
|
|
||||||
} else {
|
|
||||||
vector[word] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vector;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cosineSimilarity = (a: string, b: string) => {
|
|
||||||
const vectorA = wordsVector(a.trim());
|
|
||||||
const vectorB = wordsVector(b.trim());
|
|
||||||
|
|
||||||
const vectorAKeys = Object.keys(vectorA);
|
|
||||||
const vectorBKeys = Object.keys(vectorB);
|
|
||||||
|
|
||||||
const union = vectorAKeys.concat(vectorBKeys);
|
|
||||||
|
|
||||||
let dotProduct = 0;
|
|
||||||
let magnitudeA = 0;
|
|
||||||
let magnitudeB = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < union.length; i++) {
|
|
||||||
const key = union[i];
|
|
||||||
const valueA = vectorA[key] || 0;
|
|
||||||
const valueB = vectorB[key] || 0;
|
|
||||||
dotProduct += valueA * valueB;
|
|
||||||
magnitudeA += valueA * valueA;
|
|
||||||
magnitudeB += valueB * valueB;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dotProduct / Math.sqrt(magnitudeA * magnitudeB);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateRandomColor = (string: string): string => {
|
export const generateRandomColor = (string: string): string => {
|
||||||
if (!string) return "rgb(var(--color-primary-100))";
|
if (!string) return "rgb(var(--color-primary-100))";
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import type {
|
|||||||
IIssueComment,
|
IIssueComment,
|
||||||
IIssueLabels,
|
IIssueLabels,
|
||||||
IIssueViewOptions,
|
IIssueViewOptions,
|
||||||
|
ISubIssueResponse,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
@ -420,7 +421,11 @@ class ProjectIssuesServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async subIssues(workspaceSlug: string, projectId: string, issueId: string) {
|
async subIssues(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string
|
||||||
|
): Promise<ISubIssueResponse> {
|
||||||
return this.get(
|
return this.get(
|
||||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`
|
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user