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}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isIssueModalOpen}
|
||||
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 { useRouter } from "next/router";
|
||||
|
||||
@ -12,12 +11,12 @@ import aiService from "services/ai.service";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { GptAssistantModal } from "components/core";
|
||||
import { ParentIssuesListModal } from "components/issues";
|
||||
import {
|
||||
IssueAssigneeSelect,
|
||||
IssueDateSelect,
|
||||
IssueEstimateSelect,
|
||||
IssueLabelSelect,
|
||||
IssueParentSelect,
|
||||
IssuePrioritySelect,
|
||||
IssueProjectSelect,
|
||||
IssueStateSelect,
|
||||
@ -35,10 +34,8 @@ import {
|
||||
} from "components/ui";
|
||||
// icons
|
||||
import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { cosineSimilarity } from "helpers/string.helper";
|
||||
// types
|
||||
import type { ICurrentUserResponse, IIssue } from "types";
|
||||
import type { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
|
||||
// rich-text-editor
|
||||
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
|
||||
ssr: false,
|
||||
@ -72,6 +69,7 @@ const defaultValues: Partial<IIssue> = {
|
||||
description_html: "<p></p>",
|
||||
estimate_point: null,
|
||||
state: "",
|
||||
parent: null,
|
||||
priority: null,
|
||||
assignees: [],
|
||||
assignees_list: [],
|
||||
@ -82,7 +80,6 @@ const defaultValues: Partial<IIssue> = {
|
||||
export interface IssueFormProps {
|
||||
handleFormSubmit: (values: Partial<IIssue>) => Promise<void>;
|
||||
initialData?: Partial<IIssue>;
|
||||
issues: IIssue[];
|
||||
projectId: string;
|
||||
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
createMore: boolean;
|
||||
@ -108,7 +105,6 @@ export interface IssueFormProps {
|
||||
export const IssueForm: FC<IssueFormProps> = ({
|
||||
handleFormSubmit,
|
||||
initialData,
|
||||
issues = [],
|
||||
projectId,
|
||||
setActiveProject,
|
||||
createMore,
|
||||
@ -118,11 +114,10 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
user,
|
||||
fieldsToShow,
|
||||
}) => {
|
||||
// states
|
||||
const [mostSimilarIssue, setMostSimilarIssue] = useState<IIssue | undefined>();
|
||||
const [stateModal, setStateModal] = useState(false);
|
||||
const [labelModal, setLabelModal] = useState(false);
|
||||
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||
|
||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
@ -151,12 +146,6 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
|
||||
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>) => {
|
||||
await handleFormSubmit(formData);
|
||||
|
||||
@ -283,26 +272,28 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
</div>
|
||||
{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 items-center gap-2">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issues.find((i) => i.id === watch("parent"))?.state_detail
|
||||
.color,
|
||||
backgroundColor: selectedParentIssue.state__color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-custom-text-200">
|
||||
{/* {projects?.find((p) => p.id === projectId)?.identifier}- */}
|
||||
{issues.find((i) => i.id === watch("parent"))?.sequence_id}
|
||||
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
|
||||
</span>
|
||||
<span className="truncate font-medium">
|
||||
{issues.find((i) => i.id === watch("parent"))?.name.substring(0, 50)}
|
||||
{selectedParentIssue.name.substring(0, 50)}
|
||||
</span>
|
||||
<XMarkIcon
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => setValue("parent", null)}
|
||||
onClick={() => {
|
||||
setValue("parent", null);
|
||||
setSelectedParentIssue(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -314,7 +305,6 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
onChange={handleTitleChange}
|
||||
className="resize-none text-xl"
|
||||
placeholder="Title"
|
||||
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>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
|
||||
<div className="relative">
|
||||
<div className="-mb-2 flex justify-end">
|
||||
<div className="flex justify-end">
|
||||
{issueName && issueName !== "" && (
|
||||
<button
|
||||
type="button"
|
||||
@ -495,11 +458,20 @@ export const IssueForm: FC<IssueFormProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
<IssueParentSelect
|
||||
<Controller
|
||||
control={control}
|
||||
isOpen={parentIssueListModalOpen}
|
||||
setIsOpen={setParentIssueListModalOpen}
|
||||
issues={issues ?? []}
|
||||
name="parent"
|
||||
render={({ field: { onChange } }) => (
|
||||
<ParentIssuesListModal
|
||||
isOpen={parentIssueListModalOpen}
|
||||
handleClose={() => setParentIssueListModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
onChange(issue.id);
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
|
@ -58,7 +58,7 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
<>
|
||||
<div className="rounded-lg">
|
||||
{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}`}>
|
||||
<a className="flex items-center gap-2 text-custom-text-200">
|
||||
<span
|
||||
@ -76,24 +76,36 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<CustomMenu ellipsis position="left">
|
||||
{siblingIssues && siblingIssues.length > 0 ? (
|
||||
siblingIssues.map((issue: IIssue) => (
|
||||
<CustomMenu.MenuItem key={issue.id}>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id}`}
|
||||
>
|
||||
<a>
|
||||
{issueDetails.project_detail.identifier}-{issue.sequence_id}
|
||||
</a>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
))
|
||||
<CustomMenu position="left" ellipsis>
|
||||
{siblingIssues && siblingIssues.sub_issues.length > 0 ? (
|
||||
<>
|
||||
<h2 className="text-custom-text-200 px-1 mb-2">Sibling issues</h2>
|
||||
{siblingIssues.sub_issues.map((issue) => {
|
||||
if (issue.id !== issueDetails.id)
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={issue.id}
|
||||
renderAs="a"
|
||||
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
|
||||
issue.id
|
||||
}`}
|
||||
>
|
||||
{issueDetails.project_detail.identifier}-{issue.sequence_id}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<CustomMenu.MenuItem className="flex items-center gap-2 whitespace-nowrap p-2 text-left text-xs text-custom-text-200">
|
||||
No other sibling issues
|
||||
</CustomMenu.MenuItem>
|
||||
<p className="flex items-center gap-2 whitespace-nowrap px-1 text-left text-xs text-custom-text-200 py-1">
|
||||
No sibling issues
|
||||
</p>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
renderAs="button"
|
||||
onClick={() => submitChanges({ parent: null })}
|
||||
>
|
||||
Remove parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
@ -94,15 +94,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
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(() => {
|
||||
if (projects && projects.length > 0)
|
||||
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);
|
||||
};
|
||||
|
||||
if (!projects || projects.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<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">
|
||||
<IssueForm
|
||||
issues={issues ?? []}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
initialData={data ?? prePopulateData}
|
||||
createMore={createMore}
|
||||
|
@ -21,7 +21,8 @@ type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
value?: any;
|
||||
onChange: (...event: any[]) => void;
|
||||
onChange: (issue: ISearchIssueResponse) => void;
|
||||
projectId: string;
|
||||
issueId?: string;
|
||||
customDisplay?: JSX.Element;
|
||||
};
|
||||
@ -31,6 +32,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
||||
handleClose: onClose,
|
||||
value,
|
||||
onChange,
|
||||
projectId,
|
||||
issueId,
|
||||
customDisplay,
|
||||
}) => {
|
||||
@ -42,7 +44,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
||||
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
@ -109,7 +111,13 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
||||
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">
|
||||
<Combobox value={value} onChange={onChange}>
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<div className="relative m-1">
|
||||
<MagnifyingGlassIcon
|
||||
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) => (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
value={issue.id}
|
||||
value={issue}
|
||||
className={({ active, selected }) =>
|
||||
`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" : ""
|
||||
} ${selected ? "text-custom-text-100" : ""}`
|
||||
}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<>
|
||||
<span
|
||||
|
@ -1,8 +1,7 @@
|
||||
export * from "./assignee";
|
||||
export * from "./date";
|
||||
export * from "./estimate"
|
||||
export * from "./estimate";
|
||||
export * from "./label";
|
||||
export * from "./parent";
|
||||
export * from "./priority";
|
||||
export * from "./project";
|
||||
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 useSWR from "swr";
|
||||
|
||||
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
||||
// fetch keys
|
||||
// icons
|
||||
import { UserIcon } from "@heroicons/react/24/outline";
|
||||
// services
|
||||
import issuesServices from "services/issues.service";
|
||||
// components
|
||||
import { ParentIssuesListModal } from "components/issues";
|
||||
// icons
|
||||
// types
|
||||
import { IIssue, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
import { IIssue, ISearchIssueResponse, UserAuth } from "types";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
customDisplay: JSX.Element;
|
||||
watch: UseFormWatch<IIssue>;
|
||||
onChange: (value: string) => void;
|
||||
issueDetails: IIssue | undefined;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarParentSelect: React.FC<Props> = ({
|
||||
control,
|
||||
submitChanges,
|
||||
customDisplay,
|
||||
watch,
|
||||
onChange,
|
||||
issueDetails,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, 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 { projectId, issueId } = router.query;
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
@ -57,22 +37,15 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
||||
<p>Parent</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ParentIssuesListModal
|
||||
isOpen={isParentModalOpen}
|
||||
handleClose={() => setIsParentModalOpen(false)}
|
||||
onChange={(val) => {
|
||||
submitChanges({ parent: val });
|
||||
onChange(val);
|
||||
}}
|
||||
issueId={issueId as string}
|
||||
value={value}
|
||||
customDisplay={customDisplay}
|
||||
/>
|
||||
)}
|
||||
<ParentIssuesListModal
|
||||
isOpen={isParentModalOpen}
|
||||
handleClose={() => setIsParentModalOpen(false)}
|
||||
onChange={(issue) => {
|
||||
onChange(issue.id);
|
||||
setSelectedParentIssue(issue);
|
||||
}}
|
||||
issueId={issueId as string}
|
||||
projectId={projectId as string}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@ -82,10 +55,10 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
||||
onClick={() => setIsParentModalOpen(true)}
|
||||
disabled={isNotAllowed}
|
||||
>
|
||||
{watch("parent") && watch("parent") !== "" ? (
|
||||
`${issues?.find((i) => i.id === watch("parent"))?.project_detail?.identifier}-${
|
||||
issues?.find((i) => i.id === watch("parent"))?.sequence_id
|
||||
}`
|
||||
{selectedParentIssue ? (
|
||||
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
||||
) : issueDetails?.parent ? (
|
||||
`${issueDetails.project_detail.identifier}-${issueDetails.parent_detail?.sequence_id}`
|
||||
) : (
|
||||
<span className="text-custom-text-200">Select issue</span>
|
||||
)}
|
||||
|
@ -33,13 +33,7 @@ import {
|
||||
// ui
|
||||
import { CustomDatePicker, Icon } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
LinkIcon,
|
||||
CalendarDaysIcon,
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -337,29 +331,20 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
{showSecondSection && (
|
||||
<div className="py-1">
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
<SidebarParentSelect
|
||||
<Controller
|
||||
control={control}
|
||||
submitChanges={submitChanges}
|
||||
customDisplay={
|
||||
issueDetail?.parent_detail ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 rounded bg-custom-background-80 px-3 py-2 text-xs"
|
||||
onClick={() => submitChanges({ parent: null })}
|
||||
>
|
||||
<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}
|
||||
disabled={uneditable}
|
||||
name="parent"
|
||||
render={({ field: { onChange } }) => (
|
||||
<SidebarParentSelect
|
||||
onChange={(val: string) => {
|
||||
submitChanges({ parent: val });
|
||||
onChange(val);
|
||||
}}
|
||||
issueDetails={issueDetail}
|
||||
userAuth={memberRole}
|
||||
disabled={uneditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
||||
|
@ -272,7 +272,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
|
||||
key={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">
|
||||
<span
|
||||
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
|
||||
</>
|
||||
}
|
||||
buttonClassName="whitespace-nowrap"
|
||||
position="left"
|
||||
noBorder
|
||||
noChevron
|
||||
|
@ -175,7 +175,11 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading || !canDelete}>
|
||||
<DangerButton
|
||||
onClick={handleDeletion}
|
||||
disabled={!canDelete}
|
||||
loading={isDeleteLoading}
|
||||
>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete Project"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
|
@ -106,7 +106,7 @@ const CustomMenu = ({
|
||||
);
|
||||
|
||||
type MenuItemProps = {
|
||||
children: JSX.Element | string;
|
||||
children: React.ReactNode;
|
||||
renderAs?: "button" | "a";
|
||||
href?: string;
|
||||
onClick?: (args?: any) => void;
|
||||
|
@ -49,45 +49,6 @@ export const copyTextToClipboard = async (text: string) => {
|
||||
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 => {
|
||||
if (!string) return "rgb(var(--color-primary-100))";
|
||||
|
||||
|
@ -9,6 +9,7 @@ import type {
|
||||
IIssueComment,
|
||||
IIssueLabels,
|
||||
IIssueViewOptions,
|
||||
ISubIssueResponse,
|
||||
} from "types";
|
||||
|
||||
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(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/sub-issues/`
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user