refactor: parent issue select (#1546)

* refactor: parent issue select

* fix: sibling issues list
This commit is contained in:
Aaryan Khandelwal 2023-07-18 15:36:03 +05:30 committed by GitHub
parent 93da220c4a
commit 059b8c793a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 122 additions and 239 deletions

View File

@ -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)}

View File

@ -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}
isOpen={parentIssueListModalOpen} name="parent"
setIsOpen={setParentIssueListModalOpen} render={({ field: { onChange } }) => (
issues={issues ?? []} <ParentIssuesListModal
isOpen={parentIssueListModalOpen}
handleClose={() => setParentIssueListModalOpen(false)}
onChange={(issue) => {
onChange(issue.id);
setSelectedParentIssue(issue);
}}
projectId={projectId}
/>
)}
/> />
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (

View File

@ -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 (
<a> <CustomMenu.MenuItem
{issueDetails.project_detail.identifier}-{issue.sequence_id} key={issue.id}
</a> renderAs="a"
</Link> href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
</CustomMenu.MenuItem> 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"> <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}

View File

@ -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}

View File

@ -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

View File

@ -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";

View File

@ -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}
/>
)}
/>
);

View File

@ -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 <ParentIssuesListModal
control={control} isOpen={isParentModalOpen}
name="parent" handleClose={() => setIsParentModalOpen(false)}
render={({ field: { value, onChange } }) => ( onChange={(issue) => {
<ParentIssuesListModal onChange(issue.id);
isOpen={isParentModalOpen} setSelectedParentIssue(issue);
handleClose={() => setIsParentModalOpen(false)} }}
onChange={(val) => { issueId={issueId as string}
submitChanges({ parent: val }); projectId={projectId as string}
onChange(val);
}}
issueId={issueId as string}
value={value}
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>
)} )}

View File

@ -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,29 +331,20 @@ 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>{" "} userAuth={memberRole}
{issueDetail.parent_detail?.name} disabled={uneditable}
<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}
/> />
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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))";

View File

@ -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/`
) )