From 57d5ff7646d030f512482eac490fac720c9d48b6 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 11 Jan 2024 18:26:58 +0530 Subject: [PATCH] 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 --- .../issues/attachment/attachment-detail.tsx | 28 +-- .../issues/attachment/attachments-list.tsx | 20 ++- .../delete-attachment-confirmation-modal.tsx | 7 +- web/components/issues/attachment/root.tsx | 13 +- web/components/issues/description-form.tsx | 65 +++---- .../issue-detail/label/create-label.tsx | 8 +- .../issues/issue-detail/label/index.ts | 2 + .../issue-detail/label/label-list-item.tsx | 15 +- .../issues/issue-detail/label/label-list.tsx | 4 +- .../issues/issue-detail/label/root.tsx | 27 ++- .../issue-detail/label/select-existing.tsx | 9 - .../label/select/label-select.tsx | 159 ++++++++++++++++++ .../issues/issue-detail/label/select/root.tsx | 24 +++ .../issues/issue-detail/links/links.tsx | 9 +- .../issues/issue-detail/links/root.tsx | 15 +- .../issues/issue-detail/main-content.tsx | 14 +- .../issues/issue-detail/parent-select.tsx | 14 +- .../issues/issue-detail/relation-select.tsx | 34 ++-- web/components/issues/issue-detail/root.tsx | 25 ++- .../issues/issue-detail/sidebar.tsx | 45 ++--- .../issues/issue-detail/subscription.tsx | 27 ++- .../issues/peek-overview/properties.tsx | 2 - web/components/issues/peek-overview/root.tsx | 54 +++--- 23 files changed, 431 insertions(+), 189 deletions(-) delete mode 100644 web/components/issues/issue-detail/label/select-existing.tsx create mode 100644 web/components/issues/issue-detail/label/select/label-select.tsx create mode 100644 web/components/issues/issue-detail/label/select/root.tsx diff --git a/web/components/issues/attachment/attachment-detail.tsx b/web/components/issues/attachment/attachment-detail.tsx index 58ded14e139..bd07f6a4437 100644 --- a/web/components/issues/attachment/attachment-detail.tsx +++ b/web/components/issues/attachment/attachment-detail.tsx @@ -13,16 +13,20 @@ import { getFileIcon } from "components/icons"; import { truncateText } from "helpers/string.helper"; import { renderFormattedDate } from "helpers/date-time.helper"; import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper"; -// type -import { TIssueAttachmentsList } from "./attachments-list"; +// types +import { TAttachmentOperations } from "./root"; -export type TIssueAttachmentsDetail = TIssueAttachmentsList & { +type TAttachmentOperationsRemoveModal = Exclude; + +type TIssueAttachmentsDetail = { attachmentId: string; + handleAttachmentOperations: TAttachmentOperationsRemoveModal; + disabled?: boolean; }; export const IssueAttachmentsDetail: FC = (props) => { // props - const { attachmentId, handleAttachmentOperations } = props; + const { attachmentId, handleAttachmentOperations, disabled } = props; // store hooks const { getUserDetails } = useMember(); const { @@ -75,13 +79,15 @@ export const IssueAttachmentsDetail: FC = (props) => { - + {!disabled && ( + + )} ); diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx index 6644d7e8c9f..2129a4f61b6 100644 --- a/web/components/issues/attachment/attachments-list.tsx +++ b/web/components/issues/attachment/attachments-list.tsx @@ -7,25 +7,35 @@ import { IssueAttachmentsDetail } from "./attachment-detail"; // types import { TAttachmentOperations } from "./root"; -export type TAttachmentOperationsRemoveModal = Exclude; +type TAttachmentOperationsRemoveModal = Exclude; -export type TIssueAttachmentsList = { +type TIssueAttachmentsList = { + issueId: string; handleAttachmentOperations: TAttachmentOperationsRemoveModal; + disabled?: boolean; }; export const IssueAttachmentsList: FC = observer((props) => { - const { handleAttachmentOperations } = props; + const { issueId, handleAttachmentOperations, disabled } = props; // store hooks const { - attachment: { issueAttachments }, + attachment: { getAttachmentsByIssueId }, } = useIssueDetail(); + const issueAttachments = getAttachmentsByIssueId(issueId); + + if (!issueAttachments) return <>; + return ( <> {issueAttachments && issueAttachments.length > 0 && issueAttachments.map((attachmentId) => ( - + ))} ); diff --git a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx index 6c26bf85080..e01d2828e48 100644 --- a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx +++ b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx @@ -8,12 +8,15 @@ import { Button } from "@plane/ui"; import { getFileName } from "helpers/attachment.helper"; // types import type { TIssueAttachment } from "@plane/types"; -import { TIssueAttachmentsList } from "./attachments-list"; +import { TAttachmentOperations } from "./root"; -type Props = TIssueAttachmentsList & { +export type TAttachmentOperationsRemoveModal = Exclude; + +type Props = { isOpen: boolean; setIsOpen: Dispatch>; data: TIssueAttachment; + handleAttachmentOperations: TAttachmentOperationsRemoveModal; }; export const IssueAttachmentDeleteModal: FC = (props) => { diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx index 209058f9f10..79a6dc8408e 100644 --- a/web/components/issues/attachment/root.tsx +++ b/web/components/issues/attachment/root.tsx @@ -10,8 +10,7 @@ export type TIssueAttachmentRoot = { workspaceSlug: string; projectId: string; issueId: string; - is_archived: boolean; - is_editable: boolean; + disabled?: boolean; }; export type TAttachmentOperations = { @@ -21,7 +20,7 @@ export type TAttachmentOperations = { export const IssueAttachmentRoot: FC = (props) => { // props - const { workspaceSlug, projectId, issueId, is_archived, is_editable } = props; + const { workspaceSlug, projectId, issueId, disabled = false } = props; // hooks const { createAttachment, removeAttachment } = useIssueDetail(); const { setToastAlert } = useToast(); @@ -72,10 +71,14 @@ export const IssueAttachmentRoot: FC = (props) => {
+ -
); diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index cd678735d47..8dc3d01d347 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -5,7 +5,7 @@ import useReloadConfirmations from "hooks/use-reload-confirmation"; import debounce from "lodash/debounce"; // components import { TextArea } from "@plane/ui"; -import { RichTextEditor } from "@plane/rich-text-editor"; +import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; // types import { TIssue } from "@plane/types"; import { TIssueOperations } from "./issue-detail"; @@ -29,7 +29,7 @@ export interface IssueDetailsProps { project_id?: string; }; issueOperations: TIssueOperations; - isAllowed: boolean; + disabled: boolean; isSubmitting: "submitting" | "submitted" | "saved"; setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } @@ -37,7 +37,7 @@ export interface IssueDetailsProps { const fileService = new FileService(); export const IssueDescriptionForm: FC = (props) => { - const { workspaceSlug, projectId, issueId, issue, issueOperations, isAllowed, isSubmitting, setIsSubmitting } = props; + const { workspaceSlug, projectId, issueId, issue, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; // states const [characterLimit, setCharacterLimit] = useState(false); @@ -119,7 +119,7 @@ export const IssueDescriptionForm: FC = (props) => { return (
- {isAllowed ? ( + {!disabled ? ( = (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" hasError={Boolean(errors?.name)} role="textbox" - disabled={!isAllowed} /> )} /> ) : (

{issue.name}

)} - {characterLimit && isAllowed && ( + {characterLimit && !disabled && (
255 ? "text-red-500" : ""}`}> {watch("name").length} @@ -162,29 +161,37 @@ export const IssueDescriptionForm: FC = (props) => { ( - { - setShowAlert(true); - setIsSubmitting("submitting"); - onChange(description_html); - debouncedFormSave(); - }} - mentionSuggestions={mentionSuggestions} - mentionHighlights={mentionHighlights} - /> - )} + render={({ field: { onChange } }) => + !disabled ? ( + { + setShowAlert(true); + setIsSubmitting("submitting"); + onChange(description_html); + debouncedFormSave(); + }} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} + /> + ) : ( + + ) + } />
diff --git a/web/components/issues/issue-detail/label/create-label.tsx b/web/components/issues/issue-detail/label/create-label.tsx index 94af347d6b2..7babaee00ab 100644 --- a/web/components/issues/issue-detail/label/create-label.tsx +++ b/web/components/issues/issue-detail/label/create-label.tsx @@ -74,15 +74,11 @@ export const LabelCreate: FC = (props) => { return ( <>
- {isCreateToggle ? ( - - ) : ( - - )} + {isCreateToggle ? : }
{isCreateToggle ? "Cancel" : "New"}
diff --git a/web/components/issues/issue-detail/label/index.ts b/web/components/issues/issue-detail/label/index.ts index 005620ddd5a..83f1e73bc63 100644 --- a/web/components/issues/issue-detail/label/index.ts +++ b/web/components/issues/issue-detail/label/index.ts @@ -3,3 +3,5 @@ export * from "./root"; export * from "./label-list"; export * from "./label-list-item"; export * from "./create-label"; +export * from "./select/root"; +export * from "./select/label-select"; diff --git a/web/components/issues/issue-detail/label/label-list-item.tsx b/web/components/issues/issue-detail/label/label-list-item.tsx index 3368e9a56c7..926d287aa57 100644 --- a/web/components/issues/issue-detail/label/label-list-item.tsx +++ b/web/components/issues/issue-detail/label/label-list-item.tsx @@ -10,10 +10,11 @@ type TLabelListItem = { issueId: string; labelId: string; labelOperations: TLabelOperations; + disabled: boolean; }; export const LabelListItem: FC = (props) => { - const { workspaceSlug, projectId, issueId, labelId, labelOperations } = props; + const { workspaceSlug, projectId, issueId, labelId, labelOperations, disabled } = props; // hooks const { issue: { getIssueById }, @@ -34,7 +35,9 @@ export const LabelListItem: FC = (props) => { return (
= (props) => { }} />
{label.name}
-
- -
+ {!disabled && ( +
+ +
+ )}
); }; diff --git a/web/components/issues/issue-detail/label/label-list.tsx b/web/components/issues/issue-detail/label/label-list.tsx index b29e9b9202b..fd714e002b6 100644 --- a/web/components/issues/issue-detail/label/label-list.tsx +++ b/web/components/issues/issue-detail/label/label-list.tsx @@ -11,10 +11,11 @@ type TLabelList = { projectId: string; issueId: string; labelOperations: TLabelOperations; + disabled: boolean; }; export const LabelList: FC = (props) => { - const { workspaceSlug, projectId, issueId, labelOperations } = props; + const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props; // hooks const { issue: { getIssueById }, @@ -33,6 +34,7 @@ export const LabelList: FC = (props) => { issueId={issueId} labelId={labelId} labelOperations={labelOperations} + disabled={disabled} /> ))} diff --git a/web/components/issues/issue-detail/label/root.tsx b/web/components/issues/issue-detail/label/root.tsx index f0ffdd19d4e..93e303f6115 100644 --- a/web/components/issues/issue-detail/label/root.tsx +++ b/web/components/issues/issue-detail/label/root.tsx @@ -1,8 +1,7 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; // components -import { LabelList, LabelCreate } from "./"; - +import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./"; // hooks import { useIssueDetail, useLabel } from "hooks/store"; // types @@ -77,16 +76,26 @@ export const IssueLabel: FC = observer((props) => { projectId={projectId} issueId={issueId} labelOperations={labelOperations} + disabled={disabled} /> - {/*
select existing labels
*/} + {!disabled && ( + + )} - + {!disabled && ( + + )}
); }); diff --git a/web/components/issues/issue-detail/label/select-existing.tsx b/web/components/issues/issue-detail/label/select-existing.tsx deleted file mode 100644 index f4c287e86af..00000000000 --- a/web/components/issues/issue-detail/label/select-existing.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { FC } from "react"; - -type TLabelExistingSelect = {}; - -export const LabelExistingSelect: FC = (props) => { - const {} = props; - - return <>; -}; diff --git a/web/components/issues/issue-detail/label/select/label-select.tsx b/web/components/issues/issue-detail/label/select/label-select.tsx new file mode 100644 index 00000000000..c553ef33352 --- /dev/null +++ b/web/components/issues/issue-detail/label/select/label-select.tsx @@ -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 = observer((props) => { + const { workspaceSlug, projectId, issueId, onSelect } = props; + // store hooks + const { + issue: { getIssueById }, + } = useIssueDetail(); + const { + project: { fetchProjectLabels, projectLabels }, + } = useLabel(); + // states + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + const [isLoading, setIsLoading] = useState(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: ( +
+ +
{label.name}
+
+ ), + })); + + 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 = ( +
+
+ +
+
Select Label
+
+ ); + + if (!issue) return <>; + + return ( + <> + onSelect(value)} + multiple + > + + + + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {isLoading ? ( +

Loading...

+ ) : filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `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 && ( +
+ +
+ )} + + )} +
+ )) + ) : ( + +

No matching results

+
+ )} +
+
+
+
+ + ); +}); diff --git a/web/components/issues/issue-detail/label/select/root.tsx b/web/components/issues/issue-detail/label/select/root.tsx new file mode 100644 index 00000000000..c31e1bc612c --- /dev/null +++ b/web/components/issues/issue-detail/label/select/root.tsx @@ -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 = (props) => { + const { workspaceSlug, projectId, issueId, labelOperations } = props; + + const handleLabel = async (_labelIds: string[]) => { + await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: _labelIds }); + }; + + return ( + + ); +}; diff --git a/web/components/issues/issue-detail/links/links.tsx b/web/components/issues/issue-detail/links/links.tsx index dbcb411ce5d..368bddb9170 100644 --- a/web/components/issues/issue-detail/links/links.tsx +++ b/web/components/issues/issue-detail/links/links.tsx @@ -9,20 +9,25 @@ import { TLinkOperations } from "./root"; export type TLinkOperationsModal = Exclude; export type TIssueLinkList = { + issueId: string; linkOperations: TLinkOperationsModal; }; export const IssueLinkList: FC = observer((props) => { // props - const { linkOperations } = props; + const { issueId, linkOperations } = props; // hooks const { - link: { issueLinks }, + link: { getLinksByIssueId }, } = useIssueDetail(); const { membership: { currentProjectRole }, } = useUser(); + const issueLinks = getLinksByIssueId(issueId); + + if (!issueLinks) return <>; + return (
{issueLinks && diff --git a/web/components/issues/issue-detail/links/root.tsx b/web/components/issues/issue-detail/links/root.tsx index 5a0fb2bdff6..1c226b7a7ea 100644 --- a/web/components/issues/issue-detail/links/root.tsx +++ b/web/components/issues/issue-detail/links/root.tsx @@ -19,13 +19,12 @@ export type TIssueLinkRoot = { workspaceSlug: string; projectId: string; issueId: string; - is_editable: boolean; - is_archived: boolean; + disabled?: boolean; }; export const IssueLinkRoot: FC = (props) => { // props - const { workspaceSlug, projectId, issueId, is_editable, is_archived } = props; + const { workspaceSlug, projectId, issueId, disabled = false } = props; // hooks const { toggleIssueLinkModal: toggleIssueLinkModalStore, createLink, updateLink, removeLink } = useIssueDetail(); // state @@ -108,17 +107,17 @@ export const IssueLinkRoot: FC = (props) => { linkOperations={handleLinkOperations} /> -
+

Links

- {is_editable && ( + {!disabled && ( @@ -126,7 +125,7 @@ export const IssueLinkRoot: FC = (props) => {
- +
diff --git a/web/components/issues/issue-detail/main-content.tsx b/web/components/issues/issue-detail/main-content.tsx index 116a0a006c3..6e7ac42897f 100644 --- a/web/components/issues/issue-detail/main-content.tsx +++ b/web/components/issues/issue-detail/main-content.tsx @@ -11,8 +11,6 @@ import { SubIssuesRoot } from "../sub-issues"; import { StateGroupIcon } from "@plane/ui"; // types import { TIssueOperations } from "./root"; -// constants -import { EUserProjectRoles } from "constants/project"; type Props = { workspaceSlug: string; @@ -28,10 +26,7 @@ export const IssueMainContent: React.FC = observer((props) => { // states const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); // hooks - const { - currentUser, - membership: { currentProjectRole }, - } = useUser(); + const { currentUser } = useUser(); const { getProjectById } = useProject(); const { projectStates } = useProjectState(); const { @@ -44,8 +39,6 @@ export const IssueMainContent: React.FC = observer((props) => { const projectDetails = projectId ? getProjectById(projectId) : null; const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); - const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - return ( <>
@@ -78,7 +71,7 @@ export const IssueMainContent: React.FC = observer((props) => { isSubmitting={isSubmitting} issue={issue} issueOperations={issueOperations} - isAllowed={isAllowed || !is_editable} + disabled={!is_editable} /> {currentUser && ( @@ -107,8 +100,7 @@ export const IssueMainContent: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} - is_archived={is_archived} - is_editable={is_editable} + disabled={!is_editable} /> {/*
diff --git a/web/components/issues/issue-detail/parent-select.tsx b/web/components/issues/issue-detail/parent-select.tsx index ad1bb6dda4e..2a7fb3d8388 100644 --- a/web/components/issues/issue-detail/parent-select.tsx +++ b/web/components/issues/issue-detail/parent-select.tsx @@ -30,16 +30,20 @@ export const IssueParentSelect: React.FC = observer( 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 = parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined; const handleParentIssue = async (_issueId: string | null = null) => { 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); setUpdating(false); - }); + } catch (error) { + console.error("something went wrong while fetching the issue"); + } }; if (!issue) return <>; @@ -61,14 +65,14 @@ export const IssueParentSelect: React.FC = observer( disabled={disabled} >
toggleParentIssueModal(true)}> - {parentIssue ? ( + {issue?.parent_id && parentIssue ? ( `${parentIssueProjectDetails?.identifier}-${parentIssue.sequence_id}` ) : ( Select issue )}
- {parentIssue && ( + {issue?.parent_id && parentIssue && !disabled && (
handleParentIssue(null)}>
diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 801c04ebdee..30a81f2dd0e 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -126,22 +126,24 @@ export const IssueRelationSelect: React.FC = observer((pro {issueRelationObject[relationKey].icon(10)} {`${projectDetails?.identifier}-${currentIssue?.sequence_id}`} - + {!disabled && ( + + )}
); }) diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index 876f5536966..b52857e0a35 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -8,12 +8,15 @@ import { EmptyState } from "components/common"; // images import emptyIssue from "public/empty-state/issue.svg"; // hooks -import { useIssueDetail } from "hooks/store"; +import { useIssueDetail, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // types import { TIssue } from "@plane/types"; +// constants +import { EUserProjectRoles } from "constants/project"; export type TIssueOperations = { + fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; @@ -27,16 +30,16 @@ export type TIssueDetailRoot = { projectId: string; issueId: string; is_archived?: boolean; - is_editable?: boolean; }; export const IssueDetailRoot: FC = (props) => { - const { workspaceSlug, projectId, issueId, is_archived = false, is_editable = true } = props; + const { workspaceSlug, projectId, issueId, is_archived = false } = props; // router const router = useRouter(); // hooks const { issue: { getIssueById }, + fetchIssue, updateIssue, removeIssue, addIssueToCycle, @@ -45,9 +48,19 @@ export const IssueDetailRoot: FC = (props) => { removeIssueFromModule, } = useIssueDetail(); const { setToastAlert } = useToast(); + const { + membership: { currentProjectRole }, + } = useUser(); 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) => { try { await updateIssue(workspaceSlug, projectId, issueId, data); @@ -146,6 +159,7 @@ export const IssueDetailRoot: FC = (props) => { }, }), [ + fetchIssue, updateIssue, removeIssue, addIssueToCycle, @@ -156,7 +170,10 @@ export const IssueDetailRoot: FC = (props) => { ] ); + // Issue details const issue = getIssueById(issueId); + // Check if issue is editable, based on user role + const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return ( <> @@ -189,7 +206,7 @@ export const IssueDetailRoot: FC = (props) => { issueId={issueId} issueOperations={issueOperations} is_archived={is_archived} - is_editable={true} + is_editable={is_editable} />
diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index ce4071f066d..a80f8873005 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -25,8 +25,6 @@ import { ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon import { copyTextToClipboard } from "helpers/string.helper"; // types import type { TIssueOperations } from "./root"; -// fetch-keys -import { EUserProjectRoles } from "constants/project"; type Props = { workspaceSlug: string; @@ -72,10 +70,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const { inboxIssueId } = router.query; // store hooks const { getProjectById } = useProject(); - const { - currentUser, - membership: { currentProjectRole }, - } = useUser(); + const { currentUser } = useUser(); const { projectStates } = useProjectState(); const { areEstimatesEnabledForCurrentProject } = useEstimate(); const { setToastAlert } = useToast(); @@ -124,8 +119,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const maxDate = issue.target_date ? new Date(issue.target_date) : null; maxDate?.setDate(maxDate.getDate()); - const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - const currentIssueState = projectStates?.find((s) => s.id === issue.state_id); return ( @@ -166,7 +159,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} issueId={issueId} currentUserId={currentUser?.id} - disabled={!isAllowed || !is_editable} /> )} @@ -193,7 +185,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => {
-
+
{showFirstSection && (
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( @@ -208,7 +200,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { value={issue?.state_id ?? undefined} onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })} projectId={projectId?.toString() ?? ""} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} buttonVariant="background-with-text" />
@@ -228,7 +220,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val }) } - disabled={!isAllowed || !is_editable} + disabled={!is_editable} projectId={projectId?.toString() ?? ""} placeholder="Assignees" multiple @@ -252,7 +244,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} buttonVariant="background-with-text" />
@@ -274,7 +266,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val }) } projectId={projectId} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} buttonVariant="background-with-text" />
@@ -297,7 +289,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} issueId={issueId} issueOperations={issueOperations} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} />
@@ -309,7 +301,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} issueId={issueId} relationKey="blocking" - disabled={!isAllowed || !is_editable} + disabled={!is_editable} /> )} @@ -319,7 +311,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} issueId={issueId} relationKey="blocked_by" - disabled={!isAllowed || !is_editable} + disabled={!is_editable} /> )} @@ -329,7 +321,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} issueId={issueId} relationKey="duplicate" - disabled={!isAllowed || !is_editable} + disabled={!is_editable} /> )} @@ -339,7 +331,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} issueId={issueId} relationKey="relates_to" - disabled={!isAllowed || !is_editable} + disabled={!is_editable} /> )} @@ -358,7 +350,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { } className="border-none bg-custom-background-80" maxDate={maxDate ?? undefined} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} /> @@ -379,7 +371,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { } className="border-none bg-custom-background-80" minDate={minDate ?? undefined} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} /> @@ -401,7 +393,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} issueId={issueId} issueOperations={issueOperations} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} /> @@ -419,7 +411,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { projectId={projectId} issueId={issueId} issueOperations={issueOperations} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} /> @@ -429,7 +421,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( -
+

Label

@@ -439,7 +431,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} - disabled={!isAllowed || !is_editable} + disabled={!is_editable} />
@@ -450,8 +442,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} - is_editable={is_editable} - is_archived={is_archived} + disabled={!is_editable} /> )}
diff --git a/web/components/issues/issue-detail/subscription.tsx b/web/components/issues/issue-detail/subscription.tsx index 8f76eca25e0..7093f862744 100644 --- a/web/components/issues/issue-detail/subscription.tsx +++ b/web/components/issues/issue-detail/subscription.tsx @@ -5,17 +5,17 @@ import { observer } from "mobx-react-lite"; import { Button } from "@plane/ui"; // hooks import { useIssueDetail } from "hooks/store"; +import useToast from "hooks/use-toast"; export type TIssueSubscription = { workspaceSlug: string; projectId: string; issueId: string; currentUserId: string; - disabled?: boolean; }; export const IssueSubscription: FC = observer((props) => { - const { workspaceSlug, projectId, issueId, currentUserId, disabled } = props; + const { workspaceSlug, projectId, issueId, currentUserId } = props; // hooks const { issue: { getIssueById }, @@ -23,16 +23,32 @@ export const IssueSubscription: FC = observer((props) => { createSubscription, removeSubscription, } = useIssueDetail(); + const { setToastAlert } = useToast(); // state const [loading, setLoading] = useState(false); const issue = getIssueById(issueId); const subscription = getSubscriptionByIssueId(issueId); - const handleSubscription = () => { + const handleSubscription = async () => { setLoading(true); - if (subscription?.subscribed) removeSubscription(workspaceSlug, projectId, issueId); - else createSubscription(workspaceSlug, projectId, issueId); + try { + 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 <>; @@ -45,7 +61,6 @@ export const IssueSubscription: FC = observer((props) => { variant="outline-primary" className="hover:!bg-custom-primary-100/20" onClick={handleSubscription} - disabled={disabled} > {loading ? "Loading..." : subscription?.subscribed ? "Unsubscribe" : "Subscribe"} diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 7f21f01b771..e6c6d88f30d 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -258,8 +258,6 @@ export const PeekOverviewProperties: FC = observer((pro workspaceSlug={workspaceSlug?.toString() ?? ""} projectId={projectId?.toString() ?? ""} issueId={issue?.id} - is_editable={uneditable} - is_archived={isAllowed} /> diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 8253600fd8d..ab177ed8c9a 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -71,32 +71,6 @@ export const IssuePeekOverview: FC = observer((props) => { }); } }, [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) => { - 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( () => ({ @@ -168,6 +142,34 @@ export const IssuePeekOverview: FC = observer((props) => { [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) => { + 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) => { if (!issue) return; await updateIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, _data);