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:
guru_sainath 2024-01-11 18:26:58 +05:30 committed by GitHub
parent 3c9926d383
commit 57d5ff7646
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 431 additions and 189 deletions

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
import { FC } from "react";
type TLabelExistingSelect = {};
export const LabelExistingSelect: FC<TLabelExistingSelect> = (props) => {
const {} = props;
return <></>;
};

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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