forked from github/plane
feat: cross-project issue linking (#1612)
* feat: cross project issue linking * fix: remove parent issue mutation * fix: build error
This commit is contained in:
parent
0e5c0fe31e
commit
6c2600efa7
@ -13,8 +13,9 @@ import useToast from "hooks/use-toast";
|
|||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
import useDebounce from "hooks/use-debounce";
|
import useDebounce from "hooks/use-debounce";
|
||||||
// ui
|
// ui
|
||||||
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
import { Loader, PrimaryButton, SecondaryButton, ToggleSwitch, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
|
import { LaunchOutlined } from "@mui/icons-material";
|
||||||
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
// types
|
// types
|
||||||
@ -45,6 +46,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [selectedIssues, setSelectedIssues] = useState<ISearchIssueResponse[]>([]);
|
const [selectedIssues, setSelectedIssues] = useState<ISearchIssueResponse[]>([]);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||||
|
|
||||||
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
||||||
|
|
||||||
@ -59,6 +61,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
onClose();
|
onClose();
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setSelectedIssues([]);
|
setSelectedIssues([]);
|
||||||
|
setIsWorkspaceLevel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@ -104,10 +107,11 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
|
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
|
||||||
search: debouncedSearchTerm,
|
search: debouncedSearchTerm,
|
||||||
...searchParams,
|
...searchParams,
|
||||||
|
workspace_search: isWorkspaceLevel,
|
||||||
})
|
})
|
||||||
.then((res) => setIssues(res))
|
.then((res) => setIssues(res))
|
||||||
.finally(() => setIsSearching(false));
|
.finally(() => setIsSearching(false));
|
||||||
}, [debouncedSearchTerm, isOpen, projectId, searchParams, workspaceSlug]);
|
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -162,7 +166,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-custom-text-200 text-[0.825rem] p-2">
|
<div className="flex flex-col-reverse sm:flex-row sm:items-center sm:justify-between gap-4 text-custom-text-200 text-[0.825rem] p-2">
|
||||||
{selectedIssues.length > 0 ? (
|
{selectedIssues.length > 0 ? (
|
||||||
<div className="flex items-center gap-2 flex-wrap mt-1">
|
<div className="flex items-center gap-2 flex-wrap mt-1">
|
||||||
{selectedIssues.map((issue) => (
|
{selectedIssues.map((issue) => (
|
||||||
@ -190,6 +194,25 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
No issues selected
|
No issues selected
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip tooltipContent="Toggle workspace level search">
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 flex items-center gap-1 text-xs cursor-pointer ${
|
||||||
|
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ToggleSwitch
|
||||||
|
value={isWorkspaceLevel}
|
||||||
|
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
workspace level
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto">
|
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto">
|
||||||
@ -242,22 +265,37 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
htmlFor={`issue-${issue.id}`}
|
htmlFor={`issue-${issue.id}`}
|
||||||
value={issue}
|
value={issue}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
`group flex items-center justify-between gap-2 w-full cursor-pointer select-none 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" : ""}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<input type="checkbox" checked={selected} readOnly />
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<input type="checkbox" checked={selected} readOnly />
|
||||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
<span
|
||||||
style={{
|
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||||
backgroundColor: issue.state__color,
|
style={{
|
||||||
}}
|
backgroundColor: issue.state__color,
|
||||||
/>
|
}}
|
||||||
<span className="flex-shrink-0 text-xs">
|
/>
|
||||||
{issue.project__identifier}-{issue.sequence_id}
|
<span className="flex-shrink-0 text-xs">
|
||||||
</span>
|
{issue.project__identifier}-{issue.sequence_id}
|
||||||
{issue.name}
|
</span>
|
||||||
|
{issue.name}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
className="group-hover:block hidden relative z-1 text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<LaunchOutlined
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -75,6 +75,7 @@ const defaultValues: Partial<IIssue> = {
|
|||||||
assignees_list: [],
|
assignees_list: [],
|
||||||
labels: [],
|
labels: [],
|
||||||
labels_list: [],
|
labels_list: [],
|
||||||
|
target_date: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IssueFormProps {
|
export interface IssueFormProps {
|
||||||
@ -271,7 +272,6 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{watch("parent") &&
|
{watch("parent") &&
|
||||||
watch("parent") !== "" &&
|
|
||||||
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
|
(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) &&
|
||||||
selectedParentIssue && (
|
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">
|
||||||
@ -476,7 +476,7 @@ export const IssueForm: FC<IssueFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
<CustomMenu ellipsis>
|
<CustomMenu ellipsis>
|
||||||
{watch("parent") && watch("parent") !== "" ? (
|
{watch("parent") ? (
|
||||||
<>
|
<>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
renderAs="button"
|
renderAs="button"
|
||||||
|
@ -53,22 +53,26 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg">
|
<div className="rounded-lg">
|
||||||
{issueDetails?.parent && issueDetails.parent !== "" ? (
|
{issueDetails?.parent ? (
|
||||||
<div className="mb-5 flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-90 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/${issueDetails.parent_detail?.project_detail.id}/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
|
||||||
className="block h-1.5 w-1.5 rounded-full"
|
className="block h-1.5 w-1.5 rounded-full"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: issueDetails?.state_detail?.color,
|
backgroundColor: issueDetails.parent_detail?.state_detail.color,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex-shrink-0">
|
<span className="flex-shrink-0">
|
||||||
{issueDetails.project_detail.identifier}-{issueDetails.parent_detail?.sequence_id}
|
{issueDetails.parent_detail?.project_detail.identifier}-
|
||||||
|
{issueDetails.parent_detail?.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{issueDetails.parent_detail?.name.substring(0, 50)}
|
{issueDetails.parent_detail?.name.substring(0, 50)}
|
||||||
@ -77,29 +81,28 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<CustomMenu position="left" ellipsis>
|
<CustomMenu position="left" ellipsis>
|
||||||
{siblingIssues && siblingIssues.sub_issues.length > 0 ? (
|
{siblingIssuesList ? (
|
||||||
<>
|
siblingIssuesList.length > 0 ? (
|
||||||
<h2 className="text-custom-text-200 px-1 mb-2">Sibling issues</h2>
|
<>
|
||||||
{siblingIssues.sub_issues.map((issue) => {
|
<h2 className="text-custom-text-200 px-1 mb-2">Sibling issues</h2>
|
||||||
if (issue.id !== issueDetails.id)
|
{siblingIssuesList.map((issue) => (
|
||||||
return (
|
<CustomMenu.MenuItem
|
||||||
<CustomMenu.MenuItem
|
key={issue.id}
|
||||||
key={issue.id}
|
renderAs="a"
|
||||||
renderAs="a"
|
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
|
||||||
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
|
issue.id
|
||||||
issue.id
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
{issueDetails.project_detail.identifier}-{issue.sequence_id}
|
||||||
{issueDetails.project_detail.identifier}-{issue.sequence_id}
|
</CustomMenu.MenuItem>
|
||||||
</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 className="flex items-center gap-2 whitespace-nowrap px-1 text-left text-xs text-custom-text-200 py-1">
|
</p>
|
||||||
No sibling issues
|
)
|
||||||
</p>
|
) : null}
|
||||||
)}
|
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
renderAs="button"
|
renderAs="button"
|
||||||
onClick={() => submitChanges({ parent: null })}
|
onClick={() => submitChanges({ parent: null })}
|
||||||
|
@ -11,8 +11,9 @@ import useDebounce from "hooks/use-debounce";
|
|||||||
// components
|
// components
|
||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "components/ui";
|
import { Loader, ToggleSwitch, Tooltip } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
|
import { LaunchOutlined } from "@mui/icons-material";
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { ISearchIssueResponse } from "types";
|
import { ISearchIssueResponse } from "types";
|
||||||
@ -37,6 +38,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||||
|
|
||||||
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
|
||||||
|
|
||||||
@ -46,6 +48,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
|
setIsWorkspaceLevel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -58,10 +61,11 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
search: debouncedSearchTerm,
|
search: debouncedSearchTerm,
|
||||||
parent: true,
|
parent: true,
|
||||||
issue_id: issueId,
|
issue_id: issueId,
|
||||||
|
workspace_search: isWorkspaceLevel,
|
||||||
})
|
})
|
||||||
.then((res) => setIssues(res))
|
.then((res) => setIssues(res))
|
||||||
.finally(() => setIsSearching(false));
|
.finally(() => setIsSearching(false));
|
||||||
}, [debouncedSearchTerm, isOpen, issueId, projectId, workspaceSlug]);
|
}, [debouncedSearchTerm, isOpen, issueId, isWorkspaceLevel, projectId, workspaceSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -115,7 +119,29 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
displayValue={() => ""}
|
displayValue={() => ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto mt-2">
|
<div className="flex sm:justify-end p-2">
|
||||||
|
<Tooltip tooltipContent="Toggle workspace level search">
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 flex items-center gap-1 text-xs cursor-pointer ${
|
||||||
|
isWorkspaceLevel ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ToggleSwitch
|
||||||
|
value={isWorkspaceLevel}
|
||||||
|
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||||
|
label="Workspace level"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
workspace level
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Combobox.Options static className="max-h-80 scroll-py-2 overflow-y-auto">
|
||||||
{searchTerm !== "" && (
|
{searchTerm !== "" && (
|
||||||
<h5 className="text-[0.825rem] text-custom-text-200 mx-2">
|
<h5 className="text-[0.825rem] text-custom-text-200 mx-2">
|
||||||
Search results for{" "}
|
Search results for{" "}
|
||||||
@ -158,12 +184,12 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
value={issue}
|
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 ${
|
`group flex items-center justify-between gap-2 cursor-pointer select-none 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" : ""}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<>
|
<div className="flex items-center gap-2">
|
||||||
<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"
|
||||||
style={{
|
style={{
|
||||||
@ -174,7 +200,20 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
{issue.project__identifier}-{issue.sequence_id}
|
{issue.project__identifier}-{issue.sequence_id}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
{issue.name}
|
{issue.name}
|
||||||
</>
|
</div>
|
||||||
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
className="group-hover:block hidden relative z-1 text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<LaunchOutlined
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { UseFormWatch } from "react-hook-form";
|
import { UseFormWatch } from "react-hook-form";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
// icons
|
// icons
|
||||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
import { BlockedIcon } from "components/icons";
|
import { BlockedIcon } from "components/icons";
|
||||||
// types
|
// types
|
||||||
import { BlockeIssue, IIssue, ISearchIssueResponse, UserAuth } from "types";
|
import { BlockeIssueDetail, IIssue, ISearchIssueResponse, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId?: string;
|
issueId?: string;
|
||||||
@ -34,10 +32,9 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { projectDetails } = useProjectDetails();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsBlockedModalOpen(false);
|
setIsBlockedModalOpen(false);
|
||||||
@ -54,11 +51,16 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedIssues: BlockeIssue[] = data.map((i) => ({
|
const selectedIssues: { blocked_issue_detail: BlockeIssueDetail }[] = data.map((i) => ({
|
||||||
blocked_issue_detail: {
|
blocked_issue_detail: {
|
||||||
id: i.id,
|
id: i.id,
|
||||||
name: i.name,
|
name: i.name,
|
||||||
sequence_id: i.sequence_id,
|
sequence_id: i.sequence_id,
|
||||||
|
project_detail: {
|
||||||
|
id: i.project_id,
|
||||||
|
identifier: i.project__identifier,
|
||||||
|
name: i.project__name,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -94,14 +96,15 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
key={issue.blocked_issue_detail?.id}
|
key={issue.blocked_issue_detail?.id}
|
||||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500/20 hover:bg-red-500/20"
|
||||||
>
|
>
|
||||||
<Link
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.blocked_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${issue.blocked_issue_detail?.project_detail.id}/issues/${issue.blocked_issue_detail?.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<a className="flex items-center gap-1">
|
<BlockedIcon height={10} width={10} />
|
||||||
<BlockedIcon height={10} width={10} />
|
{`${issue.blocked_issue_detail?.project_detail.identifier}-${issue.blocked_issue_detail?.sequence_id}`}
|
||||||
{`${projectDetails?.identifier}-${issue.blocked_issue_detail?.sequence_id}`}
|
</a>
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { UseFormWatch } from "react-hook-form";
|
import { UseFormWatch } from "react-hook-form";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useProjectDetails from "hooks/use-project-details";
|
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
// icons
|
// icons
|
||||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
import { BlockerIcon } from "components/icons";
|
import { BlockerIcon } from "components/icons";
|
||||||
// types
|
// types
|
||||||
import { BlockeIssue, IIssue, ISearchIssueResponse, UserAuth } from "types";
|
import { BlockeIssueDetail, IIssue, ISearchIssueResponse, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId?: string;
|
issueId?: string;
|
||||||
@ -34,10 +32,9 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { projectDetails } = useProjectDetails();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsBlockerModalOpen(false);
|
setIsBlockerModalOpen(false);
|
||||||
@ -54,11 +51,16 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedIssues: BlockeIssue[] = data.map((i) => ({
|
const selectedIssues: { blocker_issue_detail: BlockeIssueDetail }[] = data.map((i) => ({
|
||||||
blocker_issue_detail: {
|
blocker_issue_detail: {
|
||||||
id: i.id,
|
id: i.id,
|
||||||
name: i.name,
|
name: i.name,
|
||||||
sequence_id: i.sequence_id,
|
sequence_id: i.sequence_id,
|
||||||
|
project_detail: {
|
||||||
|
id: i.project_id,
|
||||||
|
identifier: i.project__identifier,
|
||||||
|
name: i.project__name,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -94,14 +96,15 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
key={issue.blocker_issue_detail?.id}
|
key={issue.blocker_issue_detail?.id}
|
||||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-200 px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500/20 hover:bg-yellow-500/20"
|
||||||
>
|
>
|
||||||
<Link
|
<a
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.blocker_issue_detail?.id}`}
|
href={`/${workspaceSlug}/projects/${issue.blocker_issue_detail?.project_detail.id}/issues/${issue.blocker_issue_detail?.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<a className="flex items-center gap-1">
|
<BlockerIcon height={10} width={10} />
|
||||||
<BlockerIcon height={10} width={10} />
|
{`${issue.blocker_issue_detail?.project_detail.identifier}-${issue.blocker_issue_detail?.sequence_id}`}
|
||||||
{`${projectDetails?.identifier}-${issue.blocker_issue_detail?.sequence_id}`}
|
</a>
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
|
@ -55,10 +55,10 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
|||||||
onClick={() => setIsParentModalOpen(true)}
|
onClick={() => setIsParentModalOpen(true)}
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
>
|
>
|
||||||
{selectedParentIssue ? (
|
{selectedParentIssue && issueDetails?.parent ? (
|
||||||
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
||||||
) : issueDetails?.parent ? (
|
) : !selectedParentIssue && issueDetails?.parent ? (
|
||||||
`${issueDetails.project_detail.identifier}-${issueDetails.parent_detail?.sequence_id}`
|
`${issueDetails.parent_detail?.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>
|
||||||
)}
|
)}
|
||||||
|
@ -18,12 +18,10 @@ import { CreateUpdateIssueModal } from "components/issues";
|
|||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronRightIcon, PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { ChevronRightIcon, PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
|
||||||
import { orderArrayBy } from "helpers/array.helper";
|
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, ISearchIssueResponse, ISubIssueResponse } from "types";
|
import { ICurrentUserResponse, IIssue, ISearchIssueResponse, ISubIssueResponse } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys";
|
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
parentIssue: IIssue;
|
parentIssue: IIssue;
|
||||||
@ -38,146 +36,68 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
|
|||||||
const [preloadedData, setPreloadedData] = useState<Partial<IIssue> | null>(null);
|
const [preloadedData, setPreloadedData] = useState<Partial<IIssue> | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { memberRole } = useProjectMyMembership();
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
const { data: subIssuesResponse } = useSWR<ISubIssueResponse>(
|
const { data: subIssuesResponse } = useSWR(
|
||||||
workspaceSlug && projectId && issueId ? SUB_ISSUES(issueId as string) : null,
|
workspaceSlug && parentIssue ? SUB_ISSUES(parentIssue.id) : null,
|
||||||
workspaceSlug && projectId && issueId
|
workspaceSlug && parentIssue
|
||||||
? () =>
|
? () => issuesService.subIssues(workspaceSlug as string, parentIssue.project, parentIssue.id)
|
||||||
issuesService.subIssues(workspaceSlug as string, projectId as string, issueId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: issues } = useSWR(
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const addAsSubIssue = async (data: ISearchIssueResponse[]) => {
|
const addAsSubIssue = async (data: ISearchIssueResponse[]) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !parentIssue) return;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
sub_issue_ids: data.map((i) => i.id),
|
sub_issue_ids: data.map((i) => i.id),
|
||||||
};
|
};
|
||||||
|
|
||||||
await issuesService
|
await issuesService
|
||||||
.addSubIssues(workspaceSlug as string, projectId as string, parentIssue?.id ?? "", payload)
|
.addSubIssues(workspaceSlug as string, parentIssue.project, parentIssue.id, payload)
|
||||||
.then(() => {
|
.finally(() => mutate(SUB_ISSUES(parentIssue.id)));
|
||||||
mutate<ISubIssueResponse>(
|
|
||||||
SUB_ISSUES(parentIssue?.id ?? ""),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
let newSubIssues = prevData.sub_issues as IIssue[];
|
|
||||||
|
|
||||||
const stateDistribution = { ...prevData.state_distribution };
|
|
||||||
|
|
||||||
payload.sub_issue_ids.forEach((issueId: string) => {
|
|
||||||
const issue = issues?.find((i) => i.id === issueId);
|
|
||||||
|
|
||||||
if (issue) {
|
|
||||||
newSubIssues.push(issue);
|
|
||||||
|
|
||||||
const issueGroup = issue.state_detail.group;
|
|
||||||
stateDistribution[issueGroup] = stateDistribution[issueGroup] + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newSubIssues = orderArrayBy(newSubIssues, "created_at", "descending");
|
|
||||||
return {
|
|
||||||
state_distribution: stateDistribution,
|
|
||||||
sub_issues: newSubIssues,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate<IIssue[]>(
|
|
||||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
|
||||||
(prevData) =>
|
|
||||||
(prevData ?? []).map((p) => {
|
|
||||||
if (payload.sub_issue_ids.includes(p.id))
|
|
||||||
return {
|
|
||||||
...p,
|
|
||||||
parent: parentIssue.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubIssueRemove = (issueId: string) => {
|
const handleSubIssueRemove = (issue: IIssue) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !parentIssue) return;
|
||||||
|
|
||||||
mutate<ISubIssueResponse>(
|
mutate<ISubIssueResponse>(
|
||||||
SUB_ISSUES(parentIssue.id ?? ""),
|
SUB_ISSUES(parentIssue.id),
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
const updatedArray = (prevData.sub_issues ?? []).filter((i) => i.id !== issueId);
|
|
||||||
|
|
||||||
const stateDistribution = { ...prevData.state_distribution };
|
const stateDistribution = { ...prevData.state_distribution };
|
||||||
const issueGroup = issues?.find((i) => i.id === issueId)?.state_detail.group ?? "backlog";
|
|
||||||
|
const issueGroup = issue.state_detail.group;
|
||||||
stateDistribution[issueGroup] = stateDistribution[issueGroup] - 1;
|
stateDistribution[issueGroup] = stateDistribution[issueGroup] - 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state_distribution: stateDistribution,
|
state_distribution: stateDistribution,
|
||||||
sub_issues: updatedArray,
|
sub_issues: prevData.sub_issues.filter((i) => i.id !== issue.id),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(workspaceSlug.toString(), projectId.toString(), issueId, { parent: null }, user)
|
.patchIssue(workspaceSlug.toString(), issue.project, issue.id, { parent: null }, user)
|
||||||
.then((res) => {
|
.finally(() => mutate(SUB_ISSUES(parentIssue.id)));
|
||||||
mutate(SUB_ISSUES(parentIssue.id ?? ""));
|
|
||||||
|
|
||||||
mutate<IIssue[]>(
|
|
||||||
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
|
|
||||||
(prevData) =>
|
|
||||||
(prevData ?? []).map((p) => {
|
|
||||||
if (p.id === res.id)
|
|
||||||
return {
|
|
||||||
...p,
|
|
||||||
...res,
|
|
||||||
};
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateIssueModal = () => {
|
const handleCreateIssueModal = () => {
|
||||||
setCreateIssueModal(true);
|
setCreateIssueModal(true);
|
||||||
|
|
||||||
setPreloadedData({
|
setPreloadedData({
|
||||||
parent: parentIssue.id,
|
parent: parentIssue.id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const completedSubIssues = subIssuesResponse
|
const completedSubIssues = subIssuesResponse
|
||||||
? subIssuesResponse?.state_distribution.completed +
|
? subIssuesResponse.state_distribution.completed +
|
||||||
subIssuesResponse?.state_distribution.cancelled
|
subIssuesResponse.state_distribution.cancelled
|
||||||
: 0;
|
: 0;
|
||||||
|
const totalSubIssues = subIssuesResponse ? subIssuesResponse.sub_issues.length : 0;
|
||||||
const totalSubIssues =
|
|
||||||
subIssuesResponse && subIssuesResponse.sub_issues ? subIssuesResponse?.sub_issues.length : 0;
|
|
||||||
|
|
||||||
const completionPercentage = (completedSubIssues / totalSubIssues) * 100;
|
const completionPercentage = (completedSubIssues / totalSubIssues) * 100;
|
||||||
|
|
||||||
@ -196,54 +116,50 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
|
|||||||
searchParams={{ sub_issue: true, issue_id: parentIssue?.id }}
|
searchParams={{ sub_issue: true, issue_id: parentIssue?.id }}
|
||||||
handleOnSubmit={addAsSubIssue}
|
handleOnSubmit={addAsSubIssue}
|
||||||
/>
|
/>
|
||||||
{subIssuesResponse &&
|
{subIssuesResponse && subIssuesResponse.sub_issues.length > 0 ? (
|
||||||
subIssuesResponse.sub_issues &&
|
|
||||||
subIssuesResponse.sub_issues.length > 0 ? (
|
|
||||||
<Disclosure defaultOpen={true}>
|
<Disclosure defaultOpen={true}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-start gap-3">
|
<div className="flex items-center justify-start gap-3 text-custom-text-200">
|
||||||
<Disclosure.Button className="flex items-center gap-1 rounded px-2 py-1 text-xs font-medium hover:bg-custom-background-90">
|
<Disclosure.Button className="flex items-center gap-1 rounded px-2 py-1 text-xs text-custom-text-100 hover:bg-custom-background-80">
|
||||||
<ChevronRightIcon className={`h-3 w-3 ${open ? "rotate-90" : ""}`} />
|
<ChevronRightIcon className={`h-3 w-3 ${open ? "rotate-90" : ""}`} />
|
||||||
Sub-issues{" "}
|
Sub-issues{" "}
|
||||||
<span className="ml-1 text-custom-text-200">
|
<span className="ml-1 text-custom-text-200">
|
||||||
{subIssuesResponse.sub_issues.length}
|
{subIssuesResponse.sub_issues.length}
|
||||||
</span>
|
</span>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
{subIssuesResponse.state_distribution && (
|
<div className="flex w-60 items-center gap-2">
|
||||||
<div className="flex w-60 items-center gap-2 text-custom-text-100">
|
<div className="bar relative h-1.5 w-full rounded bg-custom-background-80">
|
||||||
<div className="bar relative h-1.5 w-full rounded bg-custom-background-80">
|
<div
|
||||||
<div
|
className="absolute top-0 left-0 h-1.5 rounded bg-green-500 duration-300"
|
||||||
className="absolute top-0 left-0 h-1.5 rounded bg-green-500 duration-300"
|
style={{
|
||||||
style={{
|
width: `${
|
||||||
width: `${
|
isNaN(completionPercentage)
|
||||||
isNaN(completionPercentage)
|
? 0
|
||||||
? 0
|
: completionPercentage > 100
|
||||||
: completionPercentage > 100
|
? 100
|
||||||
? 100
|
: completionPercentage.toFixed(0)
|
||||||
: completionPercentage.toFixed(0)
|
}%`,
|
||||||
}%`,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="whitespace-nowrap text-xs">
|
|
||||||
{isNaN(completionPercentage)
|
|
||||||
? 0
|
|
||||||
: completionPercentage > 100
|
|
||||||
? 100
|
|
||||||
: completionPercentage.toFixed(0)}
|
|
||||||
% Done
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<span className="whitespace-nowrap text-xs">
|
||||||
|
{isNaN(completionPercentage)
|
||||||
|
? 0
|
||||||
|
: completionPercentage > 100
|
||||||
|
? 100
|
||||||
|
: completionPercentage.toFixed(0)}
|
||||||
|
% Done
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{open && !isNotAllowed ? (
|
{open && !isNotAllowed ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-1 rounded px-2 py-1 text-xs font-medium hover:bg-custom-background-90"
|
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80"
|
||||||
onClick={handleCreateIssueModal}
|
onClick={handleCreateIssueModal}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-3 w-3" />
|
<PlusIcon className="h-3 w-3" />
|
||||||
@ -270,7 +186,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
|
|||||||
{subIssuesResponse.sub_issues.map((issue) => (
|
{subIssuesResponse.sub_issues.map((issue) => (
|
||||||
<Link
|
<Link
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
|
||||||
>
|
>
|
||||||
<a className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-custom-background-90">
|
<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">
|
||||||
@ -293,7 +209,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSubIssueRemove(issue.id);
|
handleSubIssueRemove(issue);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XMarkIcon className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
<XMarkIcon className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||||
|
@ -50,13 +50,13 @@ const CustomMenu = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={menuButtonOnClick}
|
onClick={menuButtonOnClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none ${
|
className={`relative grid place-items-center rounded p-1 text-custom-text-200 hover:text-custom-text-100 outline-none ${
|
||||||
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
>
|
>
|
||||||
<MoreHorizOutlined
|
<MoreHorizOutlined
|
||||||
fontSize="small"
|
fontSize="small"
|
||||||
className={`${verticalEllipsis ? "rotate-90" : ""} text-custom-text-200`}
|
className={verticalEllipsis ? "rotate-90" : ""}
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
) : (
|
) : (
|
||||||
|
@ -17,7 +17,7 @@ export const ToggleSwitch: React.FC<Props> = (props) => {
|
|||||||
checked={value}
|
checked={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className={`relative inline-flex ${
|
className={`relative flex-shrink-0 inline-flex ${
|
||||||
size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11"
|
size === "sm" ? "h-3.5 w-6" : size === "md" ? "h-4 w-7" : "h-6 w-11"
|
||||||
} flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
|
} flex-shrink-0 cursor-pointer rounded-full border-2 border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
value ? "bg-green-500" : "bg-custom-background-80"
|
value ? "bg-green-500" : "bg-custom-background-80"
|
||||||
|
@ -38,7 +38,7 @@ export const Tooltip: React.FC<Props> = ({
|
|||||||
children,
|
children,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className = "",
|
className = "",
|
||||||
openDelay = 500,
|
openDelay = 200,
|
||||||
closeDelay,
|
closeDelay,
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
@ -68,7 +68,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||||
sidebarCollapse ? "w-full" : ""
|
sidebarCollapse ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -82,7 +82,7 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||||
sidebarCollapse ? "w-full" : ""
|
sidebarCollapse ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||||
@ -91,14 +91,14 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = ({ setS
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none md:hidden"
|
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none md:hidden"
|
||||||
onClick={() => setSidebarActive(false)}
|
onClick={() => setSidebarActive(false)}
|
||||||
>
|
>
|
||||||
<WestOutlined fontSize="small" />
|
<WestOutlined fontSize="small" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`hidden md:flex rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
className={`hidden md:grid place-items-center rounded-md p-1.5 text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90 outline-none ${
|
||||||
sidebarCollapse ? "w-full" : ""
|
sidebarCollapse ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleCollapsed()}
|
onClick={() => toggleCollapsed()}
|
||||||
|
13
apps/app/types/issues.d.ts
vendored
13
apps/app/types/issues.d.ts
vendored
@ -7,6 +7,7 @@ import type {
|
|||||||
IUserLite,
|
IUserLite,
|
||||||
IProjectLite,
|
IProjectLite,
|
||||||
IWorkspaceLite,
|
IWorkspaceLite,
|
||||||
|
IStateLite,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
export interface IIssueCycle {
|
export interface IIssueCycle {
|
||||||
@ -53,8 +54,10 @@ export interface IIssueParent {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
priority: string | null;
|
priority: string | null;
|
||||||
|
project_detail: IProjectLite;
|
||||||
sequence_id: number;
|
sequence_id: number;
|
||||||
start_date: string | null;
|
start_date: string | null;
|
||||||
|
state_detail: IStateLite;
|
||||||
target_date: string | null;
|
target_date: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,8 +73,8 @@ export interface IIssue {
|
|||||||
assignees_list: string[];
|
assignees_list: string[];
|
||||||
attachment_count: number;
|
attachment_count: number;
|
||||||
attachments: any[];
|
attachments: any[];
|
||||||
blocked_issues: BlockeIssue[];
|
blocked_issues: { blocked_issue_detail?: BlockeIssueDetail }[];
|
||||||
blocker_issues: BlockeIssue[];
|
blocker_issues: { blocker_issue_detail?: BlockeIssueDetail }[];
|
||||||
blockers_list: string[];
|
blockers_list: string[];
|
||||||
blocks_list: string[];
|
blocks_list: string[];
|
||||||
bridge_id?: string | null;
|
bridge_id?: string | null;
|
||||||
@ -137,15 +140,11 @@ export interface ISubIssueResponse {
|
|||||||
sub_issues: IIssue[];
|
sub_issues: IIssue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockeIssue {
|
|
||||||
blocked_issue_detail?: BlockeIssueDetail;
|
|
||||||
blocker_issue_detail?: BlockeIssueDetail;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlockeIssueDetail {
|
export interface BlockeIssueDetail {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
sequence_id: number;
|
sequence_id: number;
|
||||||
|
project_detail: IProjectLite;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueComment {
|
export interface IIssueComment {
|
||||||
|
5
apps/app/types/projects.d.ts
vendored
5
apps/app/types/projects.d.ts
vendored
@ -6,6 +6,7 @@ import type {
|
|||||||
TIssueGroupByOptions,
|
TIssueGroupByOptions,
|
||||||
TIssueOrderByOptions,
|
TIssueOrderByOptions,
|
||||||
TIssueViewOptions,
|
TIssueViewOptions,
|
||||||
|
TStateGroup,
|
||||||
} from "./";
|
} from "./";
|
||||||
|
|
||||||
export interface IProject {
|
export interface IProject {
|
||||||
@ -128,6 +129,7 @@ export type TProjectIssuesSearchParams = {
|
|||||||
module?: boolean;
|
module?: boolean;
|
||||||
sub_issue?: boolean;
|
sub_issue?: boolean;
|
||||||
issue_id?: string;
|
issue_id?: string;
|
||||||
|
workspace_search: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ISearchIssueResponse {
|
export interface ISearchIssueResponse {
|
||||||
@ -135,9 +137,10 @@ export interface ISearchIssueResponse {
|
|||||||
name: string;
|
name: string;
|
||||||
project_id: string;
|
project_id: string;
|
||||||
project__identifier: string;
|
project__identifier: string;
|
||||||
|
project__name: string;
|
||||||
sequence_id: number;
|
sequence_id: number;
|
||||||
state__color: string;
|
state__color: string;
|
||||||
state__group: string;
|
state__group: TStateGroup;
|
||||||
state__name: string;
|
state__name: string;
|
||||||
workspace__slug: string;
|
workspace__slug: string;
|
||||||
}
|
}
|
||||||
|
11
apps/app/types/state.d.ts
vendored
11
apps/app/types/state.d.ts
vendored
@ -1,5 +1,7 @@
|
|||||||
import { IProject, IProjectLite, IWorkspaceLite } from "types";
|
import { IProject, IProjectLite, IWorkspaceLite } from "types";
|
||||||
|
|
||||||
|
export type TStateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||||
|
|
||||||
export interface IState {
|
export interface IState {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
color: string;
|
color: string;
|
||||||
@ -7,7 +9,7 @@ export interface IState {
|
|||||||
readonly created_by: string;
|
readonly created_by: string;
|
||||||
default: boolean;
|
default: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
group: "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
group: TStateGroup;
|
||||||
name: string;
|
name: string;
|
||||||
project: string;
|
project: string;
|
||||||
readonly project_detail: IProjectLite;
|
readonly project_detail: IProjectLite;
|
||||||
@ -19,6 +21,13 @@ export interface IState {
|
|||||||
workspace_detail: IWorkspaceLite;
|
workspace_detail: IWorkspaceLite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IStateLite {
|
||||||
|
color: string;
|
||||||
|
group: TStateGroup;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IStateResponse {
|
export interface IStateResponse {
|
||||||
[key: string]: IState[];
|
[key: string]: IState[];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user