mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: Error Handling and Validation Updates (#3351)
* fix: handled undefined issue_id in list layout * chore: updated label select dropdown in the issue detail * fix: peekoverview issue is resolved * chore: user role validation for issue details. * fix: Link, Attachement, parent mutation * build-error: build error resolved in peekoverview * chore: user role validation for issue details. * chore: user role validation for `issue description`, `parent`, `relation` and `subscription`. * chore: issue subscription mutation * chore: user role validation for `labels` in issue details. --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
parent
3c9926d383
commit
57d5ff7646
@ -13,16 +13,20 @@ import { getFileIcon } from "components/icons";
|
|||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper";
|
import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper";
|
||||||
// type
|
// types
|
||||||
import { TIssueAttachmentsList } from "./attachments-list";
|
import { TAttachmentOperations } from "./root";
|
||||||
|
|
||||||
export type TIssueAttachmentsDetail = TIssueAttachmentsList & {
|
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
||||||
|
|
||||||
|
type TIssueAttachmentsDetail = {
|
||||||
attachmentId: string;
|
attachmentId: string;
|
||||||
|
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
|
export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { attachmentId, handleAttachmentOperations } = props;
|
const { attachmentId, handleAttachmentOperations, disabled } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const {
|
const {
|
||||||
@ -75,13 +79,15 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<button
|
{!disabled && (
|
||||||
onClick={() => {
|
<button
|
||||||
setAttachmentDeleteModal(true);
|
onClick={() => {
|
||||||
}}
|
setAttachmentDeleteModal(true);
|
||||||
>
|
}}
|
||||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
>
|
||||||
</button>
|
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -7,25 +7,35 @@ import { IssueAttachmentsDetail } from "./attachment-detail";
|
|||||||
// types
|
// types
|
||||||
import { TAttachmentOperations } from "./root";
|
import { TAttachmentOperations } from "./root";
|
||||||
|
|
||||||
export type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
||||||
|
|
||||||
export type TIssueAttachmentsList = {
|
type TIssueAttachmentsList = {
|
||||||
|
issueId: string;
|
||||||
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props) => {
|
export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props) => {
|
||||||
const { handleAttachmentOperations } = props;
|
const { issueId, handleAttachmentOperations, disabled } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
attachment: { issueAttachments },
|
attachment: { getAttachmentsByIssueId },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
|
||||||
|
const issueAttachments = getAttachmentsByIssueId(issueId);
|
||||||
|
|
||||||
|
if (!issueAttachments) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issueAttachments &&
|
{issueAttachments &&
|
||||||
issueAttachments.length > 0 &&
|
issueAttachments.length > 0 &&
|
||||||
issueAttachments.map((attachmentId) => (
|
issueAttachments.map((attachmentId) => (
|
||||||
<IssueAttachmentsDetail attachmentId={attachmentId} handleAttachmentOperations={handleAttachmentOperations} />
|
<IssueAttachmentsDetail
|
||||||
|
attachmentId={attachmentId}
|
||||||
|
disabled={disabled}
|
||||||
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -8,12 +8,15 @@ import { Button } from "@plane/ui";
|
|||||||
import { getFileName } from "helpers/attachment.helper";
|
import { getFileName } from "helpers/attachment.helper";
|
||||||
// types
|
// types
|
||||||
import type { TIssueAttachment } from "@plane/types";
|
import type { TIssueAttachment } from "@plane/types";
|
||||||
import { TIssueAttachmentsList } from "./attachments-list";
|
import { TAttachmentOperations } from "./root";
|
||||||
|
|
||||||
type Props = TIssueAttachmentsList & {
|
export type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
data: TIssueAttachment;
|
data: TIssueAttachment;
|
||||||
|
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
||||||
|
@ -10,8 +10,7 @@ export type TIssueAttachmentRoot = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
is_archived: boolean;
|
disabled?: boolean;
|
||||||
is_editable: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAttachmentOperations = {
|
export type TAttachmentOperations = {
|
||||||
@ -21,7 +20,7 @@ export type TAttachmentOperations = {
|
|||||||
|
|
||||||
export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { workspaceSlug, projectId, issueId, is_archived, is_editable } = props;
|
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { createAttachment, removeAttachment } = useIssueDetail();
|
const { createAttachment, removeAttachment } = useIssueDetail();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -72,10 +71,14 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
<IssueAttachmentUpload
|
<IssueAttachmentUpload
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
disabled={is_editable}
|
disabled={disabled}
|
||||||
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
|
/>
|
||||||
|
<IssueAttachmentsList
|
||||||
|
issueId={issueId}
|
||||||
|
disabled={disabled}
|
||||||
handleAttachmentOperations={handleAttachmentOperations}
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
/>
|
/>
|
||||||
<IssueAttachmentsList handleAttachmentOperations={handleAttachmentOperations} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,7 @@ import useReloadConfirmations from "hooks/use-reload-confirmation";
|
|||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
// components
|
// components
|
||||||
import { TextArea } from "@plane/ui";
|
import { TextArea } from "@plane/ui";
|
||||||
import { RichTextEditor } from "@plane/rich-text-editor";
|
import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
import { TIssueOperations } from "./issue-detail";
|
import { TIssueOperations } from "./issue-detail";
|
||||||
@ -29,7 +29,7 @@ export interface IssueDetailsProps {
|
|||||||
project_id?: string;
|
project_id?: string;
|
||||||
};
|
};
|
||||||
issueOperations: TIssueOperations;
|
issueOperations: TIssueOperations;
|
||||||
isAllowed: boolean;
|
disabled: boolean;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ export interface IssueDetailsProps {
|
|||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||||
const { workspaceSlug, projectId, issueId, issue, issueOperations, isAllowed, isSubmitting, setIsSubmitting } = props;
|
const { workspaceSlug, projectId, issueId, issue, issueOperations, disabled, isSubmitting, setIsSubmitting } = props;
|
||||||
// states
|
// states
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{isAllowed ? (
|
{!disabled ? (
|
||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -141,14 +141,13 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
||||||
hasError={Boolean(errors?.name)}
|
hasError={Boolean(errors?.name)}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
disabled={!isAllowed}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
|
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
|
||||||
)}
|
)}
|
||||||
{characterLimit && isAllowed && (
|
{characterLimit && !disabled && (
|
||||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200">
|
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200">
|
||||||
<span className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""}`}>
|
<span className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""}`}>
|
||||||
{watch("name").length}
|
{watch("name").length}
|
||||||
@ -162,29 +161,37 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="description_html"
|
name="description_html"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange } }) => (
|
render={({ field: { onChange } }) =>
|
||||||
<RichTextEditor
|
!disabled ? (
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
<RichTextEditor
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
deleteFile={fileService.deleteImage}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
restoreFile={fileService.restoreImage}
|
deleteFile={fileService.deleteImage}
|
||||||
value={localIssueDescription.description_html}
|
restoreFile={fileService.restoreImage}
|
||||||
rerenderOnPropsChange={localIssueDescription}
|
value={localIssueDescription.description_html}
|
||||||
setShouldShowAlert={setShowAlert}
|
rerenderOnPropsChange={localIssueDescription}
|
||||||
setIsSubmitting={setIsSubmitting}
|
setShouldShowAlert={setShowAlert}
|
||||||
dragDropEnabled
|
setIsSubmitting={setIsSubmitting}
|
||||||
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
dragDropEnabled
|
||||||
noBorder={!isAllowed}
|
customClassName="min-h-[150px] shadow-sm"
|
||||||
onChange={(description: Object, description_html: string) => {
|
onChange={(description: Object, description_html: string) => {
|
||||||
setShowAlert(true);
|
setShowAlert(true);
|
||||||
setIsSubmitting("submitting");
|
setIsSubmitting("submitting");
|
||||||
onChange(description_html);
|
onChange(description_html);
|
||||||
debouncedFormSave();
|
debouncedFormSave();
|
||||||
}}
|
}}
|
||||||
mentionSuggestions={mentionSuggestions}
|
mentionSuggestions={mentionSuggestions}
|
||||||
mentionHighlights={mentionHighlights}
|
mentionHighlights={mentionHighlights}
|
||||||
/>
|
/>
|
||||||
)}
|
) : (
|
||||||
|
<RichReadOnlyEditor
|
||||||
|
value={localIssueDescription.description_html}
|
||||||
|
customClassName="!p-0 !pt-2 text-custom-text-200"
|
||||||
|
noBorder={disabled}
|
||||||
|
mentionHighlights={mentionHighlights}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,15 +74,11 @@ export const LabelCreate: FC<ILabelCreate> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 transition-all relative flex items-center gap-1 cursor-pointer border border-custom-border-100 rounded-full text-xs p-0.5 px-2 group hover:border-red-500/50 hover:bg-red-500/20"
|
className="flex-shrink-0 transition-all relative flex items-center gap-1 cursor-pointer border border-custom-border-100 rounded-full text-xs p-0.5 px-2 hover:bg-custom-background-90 text-custom-text-300 hover:text-custom-text-200"
|
||||||
onClick={handleIsCreateToggle}
|
onClick={handleIsCreateToggle}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{isCreateToggle ? (
|
{isCreateToggle ? <X className="h-2.5 w-2.5" /> : <Plus className="h-2.5 w-2.5" />}
|
||||||
<X className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
|
||||||
) : (
|
|
||||||
<Plus className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">{isCreateToggle ? "Cancel" : "New"}</div>
|
<div className="flex-shrink-0">{isCreateToggle ? "Cancel" : "New"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,3 +3,5 @@ export * from "./root";
|
|||||||
export * from "./label-list";
|
export * from "./label-list";
|
||||||
export * from "./label-list-item";
|
export * from "./label-list-item";
|
||||||
export * from "./create-label";
|
export * from "./create-label";
|
||||||
|
export * from "./select/root";
|
||||||
|
export * from "./select/label-select";
|
||||||
|
@ -10,10 +10,11 @@ type TLabelListItem = {
|
|||||||
issueId: string;
|
issueId: string;
|
||||||
labelId: string;
|
labelId: string;
|
||||||
labelOperations: TLabelOperations;
|
labelOperations: TLabelOperations;
|
||||||
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LabelListItem: FC<TLabelListItem> = (props) => {
|
export const LabelListItem: FC<TLabelListItem> = (props) => {
|
||||||
const { workspaceSlug, projectId, issueId, labelId, labelOperations } = props;
|
const { workspaceSlug, projectId, issueId, labelId, labelOperations, disabled } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
@ -34,7 +35,9 @@ export const LabelListItem: FC<TLabelListItem> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={labelId}
|
key={labelId}
|
||||||
className="transition-all relative flex items-center gap-1 cursor-pointer border border-custom-border-100 rounded-full text-xs p-0.5 px-1 group hover:border-red-500/50 hover:bg-red-500/20"
|
className={`transition-all relative flex items-center gap-1 border border-custom-border-100 rounded-full text-xs p-0.5 px-1 group ${
|
||||||
|
!disabled ? "cursor-pointer hover:border-red-500/50 hover:bg-red-500/20" : "cursor-not-allowed"
|
||||||
|
} `}
|
||||||
onClick={handleLabel}
|
onClick={handleLabel}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -44,9 +47,11 @@ export const LabelListItem: FC<TLabelListItem> = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex-shrink-0">{label.name}</div>
|
<div className="flex-shrink-0">{label.name}</div>
|
||||||
<div className="flex-shrink-0">
|
{!disabled && (
|
||||||
<X className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
<div className="flex-shrink-0">
|
||||||
</div>
|
<X className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,10 +11,11 @@ type TLabelList = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
labelOperations: TLabelOperations;
|
labelOperations: TLabelOperations;
|
||||||
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LabelList: FC<TLabelList> = (props) => {
|
export const LabelList: FC<TLabelList> = (props) => {
|
||||||
const { workspaceSlug, projectId, issueId, labelOperations } = props;
|
const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
@ -33,6 +34,7 @@ export const LabelList: FC<TLabelList> = (props) => {
|
|||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
labelId={labelId}
|
labelId={labelId}
|
||||||
labelOperations={labelOperations}
|
labelOperations={labelOperations}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { FC, useMemo } from "react";
|
import { FC, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { LabelList, LabelCreate } from "./";
|
import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useLabel } from "hooks/store";
|
import { useIssueDetail, useLabel } from "hooks/store";
|
||||||
// types
|
// types
|
||||||
@ -77,16 +76,26 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
labelOperations={labelOperations}
|
labelOperations={labelOperations}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div>select existing labels</div> */}
|
{!disabled && (
|
||||||
|
<IssueLabelSelectRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
labelOperations={labelOperations}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<LabelCreate
|
{!disabled && (
|
||||||
workspaceSlug={workspaceSlug}
|
<LabelCreate
|
||||||
projectId={projectId}
|
workspaceSlug={workspaceSlug}
|
||||||
issueId={issueId}
|
projectId={projectId}
|
||||||
labelOperations={labelOperations}
|
issueId={issueId}
|
||||||
/>
|
labelOperations={labelOperations}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
type TLabelExistingSelect = {};
|
|
||||||
|
|
||||||
export const LabelExistingSelect: FC<TLabelExistingSelect> = (props) => {
|
|
||||||
const {} = props;
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
|
159
web/components/issues/issue-detail/label/select/label-select.tsx
Normal file
159
web/components/issues/issue-detail/label/select/label-select.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { Fragment, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
|
import { Check, Search, Tag } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail, useLabel } from "hooks/store";
|
||||||
|
// components
|
||||||
|
import { Combobox } from "@headlessui/react";
|
||||||
|
|
||||||
|
export interface IIssueLabelSelect {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
onSelect: (_labelIds: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, onSelect } = props;
|
||||||
|
// store hooks
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
const {
|
||||||
|
project: { fetchProjectLabels, projectLabels },
|
||||||
|
} = useLabel();
|
||||||
|
// states
|
||||||
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<Boolean>(false);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
|
const fetchLabels = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = (projectLabels ?? []).map((label) => ({
|
||||||
|
value: label.id,
|
||||||
|
query: label.name,
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center justify-start gap-2 overflow-hidden">
|
||||||
|
<span
|
||||||
|
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: label.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="line-clamp-1 inline-block truncate">{label.name}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filteredOptions =
|
||||||
|
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: "bottom-start",
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "preventOverflow",
|
||||||
|
options: {
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const issueLabels = issue?.label_ids ?? [];
|
||||||
|
|
||||||
|
const label = (
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 transition-all relative flex items-center gap-1 cursor-pointer rounded-full text-xs p-0.5 px-2 hover:bg-custom-background-90 py-0.5 text-custom-text-300 hover:text-custom-text-200 border border-custom-border-100`}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Tag className="h-2.5 w-2.5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">Select Label</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!issue) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Combobox
|
||||||
|
as="div"
|
||||||
|
className={`w-auto max-w-full flex-shrink-0 text-left`}
|
||||||
|
value={issueLabels}
|
||||||
|
onChange={(value) => onSelect(value)}
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<Combobox.Button as={Fragment}>
|
||||||
|
<button
|
||||||
|
ref={setReferenceElement}
|
||||||
|
type="button"
|
||||||
|
className="rounded cursor-pointer"
|
||||||
|
onClick={() => !projectLabels && fetchLabels()}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
</Combobox.Button>
|
||||||
|
|
||||||
|
<Combobox.Options className="fixed z-10">
|
||||||
|
<div
|
||||||
|
className={`z-10 my-1 w-48 whitespace-nowrap rounded border border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none`}
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.popper}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||||
|
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
|
<Combobox.Input
|
||||||
|
className="w-full bg-transparent px-2 py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-center text-custom-text-200">Loading...</p>
|
||||||
|
) : filteredOptions.length > 0 ? (
|
||||||
|
filteredOptions.map((option) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
className={({ selected }) =>
|
||||||
|
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 hover:bg-custom-background-80 ${
|
||||||
|
selected ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
{option.content}
|
||||||
|
{selected && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Check className={`h-3.5 w-3.5`} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-2 p-1">
|
||||||
|
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
24
web/components/issues/issue-detail/label/select/root.tsx
Normal file
24
web/components/issues/issue-detail/label/select/root.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
// components
|
||||||
|
import { IssueLabelSelect } from "./label-select";
|
||||||
|
// types
|
||||||
|
import { TLabelOperations } from "../root";
|
||||||
|
|
||||||
|
type TIssueLabelSelectRoot = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
labelOperations: TLabelOperations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueLabelSelectRoot: FC<TIssueLabelSelectRoot> = (props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, labelOperations } = props;
|
||||||
|
|
||||||
|
const handleLabel = async (_labelIds: string[]) => {
|
||||||
|
await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: _labelIds });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IssueLabelSelect workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} onSelect={handleLabel} />
|
||||||
|
);
|
||||||
|
};
|
@ -9,20 +9,25 @@ import { TLinkOperations } from "./root";
|
|||||||
export type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
|
export type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
|
||||||
|
|
||||||
export type TIssueLinkList = {
|
export type TIssueLinkList = {
|
||||||
|
issueId: string;
|
||||||
linkOperations: TLinkOperationsModal;
|
linkOperations: TLinkOperationsModal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueLinkList: FC<TIssueLinkList> = observer((props) => {
|
export const IssueLinkList: FC<TIssueLinkList> = observer((props) => {
|
||||||
// props
|
// props
|
||||||
const { linkOperations } = props;
|
const { issueId, linkOperations } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
link: { issueLinks },
|
link: { getLinksByIssueId },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
|
const issueLinks = getLinksByIssueId(issueId);
|
||||||
|
|
||||||
|
if (!issueLinks) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{issueLinks &&
|
{issueLinks &&
|
||||||
|
@ -19,13 +19,12 @@ export type TIssueLinkRoot = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
is_editable: boolean;
|
disabled?: boolean;
|
||||||
is_archived: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { workspaceSlug, projectId, issueId, is_editable, is_archived } = props;
|
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { toggleIssueLinkModal: toggleIssueLinkModalStore, createLink, updateLink, removeLink } = useIssueDetail();
|
const { toggleIssueLinkModal: toggleIssueLinkModalStore, createLink, updateLink, removeLink } = useIssueDetail();
|
||||||
// state
|
// state
|
||||||
@ -108,17 +107,17 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
|||||||
linkOperations={handleLinkOperations}
|
linkOperations={handleLinkOperations}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={`py-1 text-xs ${is_archived ? "opacity-60" : ""}`}>
|
<div className={`py-1 text-xs ${disabled ? "opacity-60" : ""}`}>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h4>Links</h4>
|
<h4>Links</h4>
|
||||||
{is_editable && (
|
{!disabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
||||||
is_archived ? "cursor-not-allowed" : "cursor-pointer"
|
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleIssueLinkModal(true)}
|
onClick={() => toggleIssueLinkModal(true)}
|
||||||
disabled={is_archived}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -126,7 +125,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<IssueLinkList linkOperations={handleLinkOperations} />
|
<IssueLinkList issueId={issueId} linkOperations={handleLinkOperations} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -11,8 +11,6 @@ import { SubIssuesRoot } from "../sub-issues";
|
|||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { TIssueOperations } from "./root";
|
import { TIssueOperations } from "./root";
|
||||||
// constants
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -28,10 +26,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const { currentUser } = useUser();
|
||||||
currentUser,
|
|
||||||
membership: { currentProjectRole },
|
|
||||||
} = useUser();
|
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const {
|
const {
|
||||||
@ -44,8 +39,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const projectDetails = projectId ? getProjectById(projectId) : null;
|
const projectDetails = projectId ? getProjectById(projectId) : null;
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||||
|
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg space-y-4">
|
<div className="rounded-lg space-y-4">
|
||||||
@ -78,7 +71,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
isAllowed={isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
@ -107,8 +100,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
is_archived={is_archived}
|
disabled={!is_editable}
|
||||||
is_editable={is_editable}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div className="space-y-5 pt-3">
|
{/* <div className="space-y-5 pt-3">
|
||||||
|
@ -30,16 +30,20 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer(
|
|||||||
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
const parentIssue = issue && issue.parent_id ? getIssueById(issue.parent_id) : undefined;
|
const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined;
|
||||||
const parentIssueProjectDetails =
|
const parentIssueProjectDetails =
|
||||||
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
|
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
|
||||||
|
|
||||||
const handleParentIssue = async (_issueId: string | null = null) => {
|
const handleParentIssue = async (_issueId: string | null = null) => {
|
||||||
setUpdating(true);
|
setUpdating(true);
|
||||||
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId }).finally(() => {
|
try {
|
||||||
|
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
||||||
|
await issueOperations.fetch(workspaceSlug, projectId, issueId);
|
||||||
toggleParentIssueModal(false);
|
toggleParentIssueModal(false);
|
||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error("something went wrong while fetching the issue");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!issue) return <></>;
|
if (!issue) return <></>;
|
||||||
@ -61,14 +65,14 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer(
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div onClick={() => toggleParentIssueModal(true)}>
|
<div onClick={() => toggleParentIssueModal(true)}>
|
||||||
{parentIssue ? (
|
{issue?.parent_id && parentIssue ? (
|
||||||
`${parentIssueProjectDetails?.identifier}-${parentIssue.sequence_id}`
|
`${parentIssueProjectDetails?.identifier}-${parentIssue.sequence_id}`
|
||||||
) : (
|
) : (
|
||||||
<span className="text-custom-text-200">Select issue</span>
|
<span className="text-custom-text-200">Select issue</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{parentIssue && (
|
{issue?.parent_id && parentIssue && !disabled && (
|
||||||
<div onClick={() => handleParentIssue(null)}>
|
<div onClick={() => handleParentIssue(null)}>
|
||||||
<X className="h-2.5 w-2.5" />
|
<X className="h-2.5 w-2.5" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,22 +126,24 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||||||
{issueRelationObject[relationKey].icon(10)}
|
{issueRelationObject[relationKey].icon(10)}
|
||||||
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
|
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
{!disabled && (
|
||||||
type="button"
|
<button
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
type="button"
|
||||||
onClick={() => {
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
if (!currentUser) return;
|
onClick={() => {
|
||||||
removeRelation(
|
if (!currentUser) return;
|
||||||
workspaceSlug as string,
|
removeRelation(
|
||||||
projectId as string,
|
workspaceSlug as string,
|
||||||
issueId,
|
projectId as string,
|
||||||
relationKey,
|
issueId,
|
||||||
relationIssueId
|
relationKey,
|
||||||
);
|
relationIssueId
|
||||||
}}
|
);
|
||||||
>
|
}}
|
||||||
<X className="h-2 w-2" />
|
>
|
||||||
</button>
|
<X className="h-2 w-2" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -8,12 +8,15 @@ import { EmptyState } from "components/common";
|
|||||||
// images
|
// images
|
||||||
import emptyIssue from "public/empty-state/issue.svg";
|
import emptyIssue from "public/empty-state/issue.svg";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
|
||||||
export type TIssueOperations = {
|
export type TIssueOperations = {
|
||||||
|
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
@ -27,16 +30,16 @@ export type TIssueDetailRoot = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
is_editable?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
||||||
const { workspaceSlug, projectId, issueId, is_archived = false, is_editable = true } = props;
|
const { workspaceSlug, projectId, issueId, is_archived = false } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
|
fetchIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
addIssueToCycle,
|
addIssueToCycle,
|
||||||
@ -45,9 +48,19 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
removeIssueFromModule,
|
removeIssueFromModule,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
const {
|
||||||
|
membership: { currentProjectRole },
|
||||||
|
} = useUser();
|
||||||
|
|
||||||
const issueOperations: TIssueOperations = useMemo(
|
const issueOperations: TIssueOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
|
try {
|
||||||
|
await fetchIssue(workspaceSlug, projectId, issueId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching the parent issue");
|
||||||
|
}
|
||||||
|
},
|
||||||
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
try {
|
try {
|
||||||
await updateIssue(workspaceSlug, projectId, issueId, data);
|
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
@ -146,6 +159,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
fetchIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
addIssueToCycle,
|
addIssueToCycle,
|
||||||
@ -156,7 +170,10 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Issue details
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
// Check if issue is editable, based on user role
|
||||||
|
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -189,7 +206,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
is_archived={is_archived}
|
is_archived={is_archived}
|
||||||
is_editable={true}
|
is_editable={is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,8 +25,6 @@ import { ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon
|
|||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import type { TIssueOperations } from "./root";
|
import type { TIssueOperations } from "./root";
|
||||||
// fetch-keys
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -72,10 +70,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const { inboxIssueId } = router.query;
|
const { inboxIssueId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const {
|
const { currentUser } = useUser();
|
||||||
currentUser,
|
|
||||||
membership: { currentProjectRole },
|
|
||||||
} = useUser();
|
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -124,8 +119,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
||||||
maxDate?.setDate(maxDate.getDate());
|
maxDate?.setDate(maxDate.getDate());
|
||||||
|
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
|
||||||
|
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -166,7 +159,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
currentUserId={currentUser?.id}
|
currentUserId={currentUser?.id}
|
||||||
disabled={!isAllowed || !is_editable}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -193,7 +185,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full w-full overflow-y-auto px-5">
|
<div className="h-full w-full overflow-y-auto px-5">
|
||||||
<div className={`divide-y-2 divide-custom-border-200 ${is_editable ? "opacity-60" : ""}`}>
|
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}>
|
||||||
{showFirstSection && (
|
{showFirstSection && (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||||
@ -208,7 +200,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
value={issue?.state_id ?? undefined}
|
value={issue?.state_id ?? undefined}
|
||||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -228,7 +220,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })
|
issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })
|
||||||
}
|
}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
placeholder="Assignees"
|
placeholder="Assignees"
|
||||||
multiple
|
multiple
|
||||||
@ -252,7 +244,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<PriorityDropdown
|
<PriorityDropdown
|
||||||
value={issue?.priority || undefined}
|
value={issue?.priority || undefined}
|
||||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -274,7 +266,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
||||||
}
|
}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -297,7 +289,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -309,7 +301,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
relationKey="blocking"
|
relationKey="blocking"
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -319,7 +311,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
relationKey="blocked_by"
|
relationKey="blocked_by"
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -329,7 +321,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
relationKey="duplicate"
|
relationKey="duplicate"
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -339,7 +331,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
relationKey="relates_to"
|
relationKey="relates_to"
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -358,7 +350,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
className="border-none bg-custom-background-80"
|
className="border-none bg-custom-background-80"
|
||||||
maxDate={maxDate ?? undefined}
|
maxDate={maxDate ?? undefined}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -379,7 +371,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
className="border-none bg-custom-background-80"
|
className="border-none bg-custom-background-80"
|
||||||
minDate={minDate ?? undefined}
|
minDate={minDate ?? undefined}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -401,7 +393,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -419,7 +411,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -429,7 +421,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||||
<div className="flex flex-wrap items-start py-2">
|
<div className={`flex flex-wrap items-start py-2 ${!is_editable ? "opacity-60" : ""}`}>
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||||
<p>Label</p>
|
<p>Label</p>
|
||||||
@ -439,7 +431,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -450,8 +442,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
is_editable={is_editable}
|
disabled={!is_editable}
|
||||||
is_archived={is_archived}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,17 +5,17 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
export type TIssueSubscription = {
|
export type TIssueSubscription = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
disabled?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, issueId, currentUserId, disabled } = props;
|
const { workspaceSlug, projectId, issueId, currentUserId } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
@ -23,16 +23,32 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
|||||||
createSubscription,
|
createSubscription,
|
||||||
removeSubscription,
|
removeSubscription,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
// state
|
// state
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
const subscription = getSubscriptionByIssueId(issueId);
|
const subscription = getSubscriptionByIssueId(issueId);
|
||||||
|
|
||||||
const handleSubscription = () => {
|
const handleSubscription = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (subscription?.subscribed) removeSubscription(workspaceSlug, projectId, issueId);
|
try {
|
||||||
else createSubscription(workspaceSlug, projectId, issueId);
|
if (subscription?.subscribed) await removeSubscription(workspaceSlug, projectId, issueId);
|
||||||
|
else await createSubscription(workspaceSlug, projectId, issueId);
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`,
|
||||||
|
message: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error",
|
||||||
|
message: "Something went wrong. Please try again later.",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (issue?.created_by === currentUserId || issue?.assignee_ids.includes(currentUserId)) return <></>;
|
if (issue?.created_by === currentUserId || issue?.assignee_ids.includes(currentUserId)) return <></>;
|
||||||
@ -45,7 +61,6 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
|||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
className="hover:!bg-custom-primary-100/20"
|
className="hover:!bg-custom-primary-100/20"
|
||||||
onClick={handleSubscription}
|
onClick={handleSubscription}
|
||||||
disabled={disabled}
|
|
||||||
>
|
>
|
||||||
{loading ? "Loading..." : subscription?.subscribed ? "Unsubscribe" : "Subscribe"}
|
{loading ? "Loading..." : subscription?.subscribed ? "Unsubscribe" : "Subscribe"}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -258,8 +258,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
issueId={issue?.id}
|
issueId={issue?.id}
|
||||||
is_editable={uneditable}
|
|
||||||
is_archived={isAllowed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,32 +71,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [peekIssue, fetchIssue]);
|
}, [peekIssue, fetchIssue]);
|
||||||
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
|
|
||||||
|
|
||||||
const issue = getIssueById(peekIssue.issueId) || undefined;
|
|
||||||
|
|
||||||
const redirectToIssueDetail = () => {
|
|
||||||
router.push({
|
|
||||||
pathname: `/${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${
|
|
||||||
isArchived ? "archived-issues" : "issues"
|
|
||||||
}/${peekIssue.issueId}`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
copyUrlToClipboard(
|
|
||||||
`${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${isArchived ? "archived-issues" : "issues"}/${
|
|
||||||
peekIssue.issueId
|
|
||||||
}`
|
|
||||||
).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link Copied!",
|
|
||||||
message: "Issue link copied to clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const issueOperations: TIssuePeekOperations = useMemo(
|
const issueOperations: TIssuePeekOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -168,6 +142,34 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
[addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule, setToastAlert]
|
[addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule, setToastAlert]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
|
||||||
|
|
||||||
|
const issue = getIssueById(peekIssue.issueId) || undefined;
|
||||||
|
|
||||||
|
const redirectToIssueDetail = () => {
|
||||||
|
router.push({
|
||||||
|
pathname: `/${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${
|
||||||
|
isArchived ? "archived-issues" : "issues"
|
||||||
|
}/${peekIssue.issueId}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
copyUrlToClipboard(
|
||||||
|
`${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${isArchived ? "archived-issues" : "issues"}/${
|
||||||
|
peekIssue.issueId
|
||||||
|
}`
|
||||||
|
).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link Copied!",
|
||||||
|
message: "Issue link copied to clipboard.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const issueUpdate = async (_data: Partial<TIssue>) => {
|
const issueUpdate = async (_data: Partial<TIssue>) => {
|
||||||
if (!issue) return;
|
if (!issue) return;
|
||||||
await updateIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, _data);
|
await updateIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, _data);
|
||||||
|
Loading…
Reference in New Issue
Block a user