From 8d5ff1a62879770946c9af020f5a61ac2d2c3489 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 5 Sep 2023 16:12:17 +0530 Subject: [PATCH 01/11] fix: redirection after signing in on space --- space/components/accounts/sign-in.tsx | 7 ++++--- space/store/user.ts | 12 ++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/space/components/accounts/sign-in.tsx b/space/components/accounts/sign-in.tsx index 50d9c7da0..661675e07 100644 --- a/space/components/accounts/sign-in.tsx +++ b/space/components/accounts/sign-in.tsx @@ -19,7 +19,6 @@ export const SignInView = observer(() => { const { user: userStore } = useMobxStore(); const router = useRouter(); - const { next_path } = router.query; const { setToastAlert } = useToast(); @@ -34,13 +33,15 @@ export const SignInView = observer(() => { const onSignInSuccess = (response: any) => { const isOnboarded = response?.user?.onboarding_step?.profile_complete || false; + const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/"; + userStore.setCurrentUser(response?.user); if (!isOnboarded) { - router.push(`/onboarding?next_path=${next_path}`); + router.push(`/onboarding?next_path=${nextPath}`); return; } - router.push((next_path ?? "/").toString()); + router.push((nextPath ?? "/").toString()); }; const handleGoogleSignIn = async ({ clientId, credential }: any) => { diff --git a/space/store/user.ts b/space/store/user.ts index 8d34b0bdc..3a76c2111 100644 --- a/space/store/user.ts +++ b/space/store/user.ts @@ -62,17 +62,13 @@ class UserStore implements IUserStore { return; } + const currentPath = window.location.pathname + window.location.search; this.fetchCurrentUser() .then(() => { - if (!this.currentUser) { - const currentPath = window.location.pathname; - window.location.href = `/?next_path=${currentPath}`; - } else callback(); + if (!this.currentUser) window.location.href = `/?next_path=${currentPath}`; + else callback(); }) - .catch(() => { - const currentPath = window.location.pathname; - window.location.href = `/?next_path=${currentPath}`; - }); + .catch(() => (window.location.href = `/?next_path=${currentPath}`)); }; fetchCurrentUser = async () => { From 60c3d1a6e9d058eab721981b16e9df4554f07f8f Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Wed, 6 Sep 2023 11:59:57 +0530 Subject: [PATCH 02/11] feat: comment reactions --- .../{ => comment}/add-comment.tsx | 0 .../{ => comment}/comment-detail-card.tsx | 16 ++- .../comment/comment-reactions.tsx | 117 ++++++++++++++++++ .../issues/peek-overview/comment/index.ts | 3 + .../issues/peek-overview/header.tsx | 6 +- .../components/issues/peek-overview/index.ts | 3 +- .../peek-overview/issue-emoji-reactions.tsx | 20 ++- space/components/ui/reaction-selector.tsx | 11 +- space/services/issue.service.ts | 43 +++++-- space/store/issue_details.ts | 113 ++++++++++++++++- space/types/issue.ts | 37 +++--- 11 files changed, 324 insertions(+), 45 deletions(-) rename space/components/issues/peek-overview/{ => comment}/add-comment.tsx (100%) rename space/components/issues/peek-overview/{ => comment}/comment-detail-card.tsx (96%) create mode 100644 space/components/issues/peek-overview/comment/comment-reactions.tsx create mode 100644 space/components/issues/peek-overview/comment/index.ts diff --git a/space/components/issues/peek-overview/add-comment.tsx b/space/components/issues/peek-overview/comment/add-comment.tsx similarity index 100% rename from space/components/issues/peek-overview/add-comment.tsx rename to space/components/issues/peek-overview/comment/add-comment.tsx diff --git a/space/components/issues/peek-overview/comment-detail-card.tsx b/space/components/issues/peek-overview/comment/comment-detail-card.tsx similarity index 96% rename from space/components/issues/peek-overview/comment-detail-card.tsx rename to space/components/issues/peek-overview/comment/comment-detail-card.tsx index a43942f4c..7c785d988 100644 --- a/space/components/issues/peek-overview/comment-detail-card.tsx +++ b/space/components/issues/peek-overview/comment/comment-detail-card.tsx @@ -1,17 +1,22 @@ -import React, { useEffect, useState, useRef } from "react"; -import { useForm, Controller } from "react-hook-form"; +import React, { useState } from "react"; + +// mobx import { observer } from "mobx-react-lite"; +// react-hook-form +import { useForm, Controller } from "react-hook-form"; +// headless ui import { Menu, Transition } from "@headlessui/react"; // lib import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { TipTapEditor } from "components/tiptap"; +import { CommentReactions } from "components/issues/peek-overview"; // icons import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline"; // helpers import { timeAgo } from "helpers/date-time.helper"; // types import { Comment } from "types/issue"; -// components -import { TipTapEditor } from "components/tiptap"; type Props = { workspaceSlug: string; @@ -76,7 +81,7 @@ export const CommentCard: React.FC = observer((props) => { {comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}

- <>Commented {timeAgo(comment.created_at)} + <>commented {timeAgo(comment.created_at)}

@@ -125,6 +130,7 @@ export const CommentCard: React.FC = observer((props) => { editable={false} customClassName="text-xs border border-custom-border-200 bg-custom-background-100" /> +
diff --git a/space/components/issues/peek-overview/comment/comment-reactions.tsx b/space/components/issues/peek-overview/comment/comment-reactions.tsx new file mode 100644 index 000000000..7ec5c73b0 --- /dev/null +++ b/space/components/issues/peek-overview/comment/comment-reactions.tsx @@ -0,0 +1,117 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +// mobx +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { ReactionSelector } from "components/ui"; +// helpers +import { groupReactions, renderEmoji } from "helpers/emoji.helper"; + +type Props = { + commentId: string; + projectId: string; +}; + +export const CommentReactions: React.FC = observer((props) => { + const { commentId, projectId } = props; + + const router = useRouter(); + const { workspace_slug } = router.query; + + const { issueDetails: issueDetailsStore, user: userStore } = useMobxStore(); + + const peekId = issueDetailsStore.peekId; + const user = userStore.currentUser; + + const commentReactions = peekId + ? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions + : []; + const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {}; + + const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id); + + const handleAddReaction = (reactionHex: string) => { + if (!workspace_slug || !projectId || !peekId) return; + + issueDetailsStore.addCommentReaction( + workspace_slug.toString(), + projectId.toString(), + peekId, + commentId, + reactionHex + ); + }; + + const handleRemoveReaction = (reactionHex: string) => { + if (!workspace_slug || !projectId || !peekId) return; + + issueDetailsStore.removeCommentReaction( + workspace_slug.toString(), + projectId.toString(), + peekId, + commentId, + reactionHex + ); + }; + + const handleReactionClick = (reactionHex: string) => { + const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex); + + if (userReaction) handleRemoveReaction(reactionHex); + else handleAddReaction(reactionHex); + }; + + return ( +
+ { + userStore.requiredLogin(() => { + handleReactionClick(value); + }); + }} + position="top" + selected={userReactions?.map((r) => r.reaction)} + size="md" + /> + + {Object.keys(groupedReactions || {}).map( + (reaction) => + groupedReactions?.[reaction]?.length && + groupedReactions[reaction].length > 0 && ( + + ) + )} +
+ ); +}); diff --git a/space/components/issues/peek-overview/comment/index.ts b/space/components/issues/peek-overview/comment/index.ts new file mode 100644 index 000000000..d217c0ddf --- /dev/null +++ b/space/components/issues/peek-overview/comment/index.ts @@ -0,0 +1,3 @@ +export * from "./add-comment"; +export * from "./comment-detail-card"; +export * from "./comment-reactions"; diff --git a/space/components/issues/peek-overview/header.tsx b/space/components/issues/peek-overview/header.tsx index 2aa43ff47..7a0b43b98 100644 --- a/space/components/issues/peek-overview/header.tsx +++ b/space/components/issues/peek-overview/header.tsx @@ -1,5 +1,7 @@ import React from "react"; +// mobx +import { observer } from "mobx-react-lite"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // hooks @@ -41,7 +43,7 @@ const peekModes: { }, ]; -export const PeekOverviewHeader: React.FC = (props) => { +export const PeekOverviewHeader: React.FC = observer((props) => { const { handleClose, issueDetails } = props; const { issueDetails: issueDetailStore }: RootStore = useMobxStore(); @@ -137,4 +139,4 @@ export const PeekOverviewHeader: React.FC = (props) => { ); -}; +}); diff --git a/space/components/issues/peek-overview/index.ts b/space/components/issues/peek-overview/index.ts index 44ab04b88..f42253e5e 100644 --- a/space/components/issues/peek-overview/index.ts +++ b/space/components/issues/peek-overview/index.ts @@ -1,3 +1,4 @@ +export * from "./comment"; export * from "./full-screen-peek-view"; export * from "./header"; export * from "./issue-activity"; @@ -8,5 +9,3 @@ export * from "./side-peek-view"; export * from "./issue-reaction"; export * from "./issue-vote-reactions"; export * from "./issue-emoji-reactions"; -export * from "./comment-detail-card"; -export * from "./add-comment"; diff --git a/space/components/issues/peek-overview/issue-emoji-reactions.tsx b/space/components/issues/peek-overview/issue-emoji-reactions.tsx index 3d2bfadac..b0c5b0361 100644 --- a/space/components/issues/peek-overview/issue-emoji-reactions.tsx +++ b/space/components/issues/peek-overview/issue-emoji-reactions.tsx @@ -20,18 +20,27 @@ export const IssueEmojiReactions: React.FC = observer(() => { const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : []; const groupedReactions = groupReactions(reactions, "reaction"); - const handleReactionSelectClick = (reactionHex: string) => { + const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id); + + const handleAddReaction = (reactionHex: string) => { if (!workspace_slug || !project_slug || !issueId) return; - const userReaction = reactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex); - if (userReaction) return; + issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex); }; - const handleReactionClick = (reactionHex: string) => { + const handleRemoveReaction = (reactionHex: string) => { if (!workspace_slug || !project_slug || !issueId) return; + issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex); }; + const handleReactionClick = (reactionHex: string) => { + const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex); + + if (userReaction) handleRemoveReaction(reactionHex); + else handleAddReaction(reactionHex); + }; + useEffect(() => { if (user) return; userStore.fetchCurrentUser(); @@ -42,9 +51,10 @@ export const IssueEmojiReactions: React.FC = observer(() => { { userStore.requiredLogin(() => { - handleReactionSelectClick(value); + handleReactionClick(value); }); }} + selected={userReactions?.map((r) => r.reaction)} size="md" />
diff --git a/space/components/ui/reaction-selector.tsx b/space/components/ui/reaction-selector.tsx index 70994460c..302ff7387 100644 --- a/space/components/ui/reaction-selector.tsx +++ b/space/components/ui/reaction-selector.tsx @@ -12,13 +12,14 @@ import { Icon } from "components/ui"; const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"]; interface Props { - size?: "sm" | "md" | "lg"; - position?: "top" | "bottom"; onSelect: (emoji: string) => void; + position?: "top" | "bottom"; + selected?: string[]; + size?: "sm" | "md" | "lg"; } export const ReactionSelector: React.FC = (props) => { - const { onSelect, position, size } = props; + const { onSelect, position, selected = [], size } = props; return ( @@ -61,7 +62,9 @@ export const ReactionSelector: React.FC = (props) => { onSelect(emoji); closePopover(); }} - className="flex select-none items-center justify-between rounded-md text-sm p-1 hover:bg-custom-sidebar-background-90" + className={`grid place-items-center select-none rounded-md text-sm p-1 ${ + selected.includes(emoji) ? "bg-custom-primary-100/10" : "hover:bg-custom-sidebar-background-80" + }`} > {renderEmoji(emoji)} diff --git a/space/services/issue.service.ts b/space/services/issue.service.ts index 03a94654b..835778fb2 100644 --- a/space/services/issue.service.ts +++ b/space/services/issue.service.ts @@ -93,16 +93,6 @@ class IssueService extends APIService { }); } - async getCommentsReactions(workspaceSlug: string, projectId: string, commentId: string): Promise { - return this.get( - `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/` - ) - .then((response) => response?.data) - .catch((error) => { - throw error?.response; - }); - } - async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise { return this.post( `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`, @@ -140,6 +130,39 @@ class IssueService extends APIService { throw error?.response; }); } + + async createCommentReaction( + workspaceSlug: string, + projectId: string, + commentId: string, + data: { + reaction: string; + } + ): Promise { + return this.post( + `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async deleteCommentReaction( + workspaceSlug: string, + projectId: string, + commentId: string, + reactionHex: string + ): Promise { + return this.delete( + `/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } export default IssueService; diff --git a/space/store/issue_details.ts b/space/store/issue_details.ts index 10679cf17..26b6148c3 100644 --- a/space/store/issue_details.ts +++ b/space/store/issue_details.ts @@ -32,6 +32,20 @@ export interface IIssueDetailStore { data: any ) => Promise; deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void; + addCommentReaction: ( + workspaceId: string, + projectId: string, + issueId: string, + commentId: string, + reactionHex: string + ) => void; + removeCommentReaction: ( + workspaceId: string, + projectId: string, + issueId: string, + commentId: string, + reactionHex: string + ) => void; // issue reactions addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void; removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void; @@ -61,8 +75,17 @@ class IssueDetailStore implements IssueDetailStore { details: observable.ref, // actions setPeekId: action, - fetchIssueDetails: action, setPeekMode: action, + fetchIssueDetails: action, + addIssueComment: action, + updateIssueComment: action, + deleteIssueComment: action, + addCommentReaction: action, + removeCommentReaction: action, + addIssueReaction: action, + removeIssueReaction: action, + addIssueVote: action, + removeIssueVote: action, }); this.issueService = new IssueService(); this.rootStore = _rootStore; @@ -175,6 +198,94 @@ class IssueDetailStore implements IssueDetailStore { } }; + addCommentReaction = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + reactionHex: string + ) => { + const newReaction = { + id: uuidv4(), + comment: commentId, + reaction: reactionHex, + actor_detail: this.rootStore.user.currentActor, + }; + const newComments = this.details[issueId].comments.map((comment) => ({ + ...comment, + comment_reactions: + comment.id === commentId ? [...comment.comment_reactions, newReaction] : comment.comment_reactions, + })); + + try { + runInAction(() => { + this.details = { + ...this.details, + [issueId]: { + ...this.details[issueId], + comments: [...newComments], + }, + }; + }); + + await this.issueService.createCommentReaction(workspaceSlug, projectId, commentId, { + reaction: reactionHex, + }); + } catch (error) { + const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.details = { + ...this.details, + [issueId]: { + ...this.details[issueId], + comments: issueComments, + }, + }; + }); + } + }; + + removeCommentReaction = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + commentId: string, + reactionHex: string + ) => { + try { + const comment = this.details[issueId].comments.find((c) => c.id === commentId); + const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? []; + + runInAction(() => { + this.details = { + ...this.details, + [issueId]: { + ...this.details[issueId], + comments: this.details[issueId].comments.map((c) => ({ + ...c, + comment_reactions: c.id === commentId ? newCommentReactions : c.comment_reactions, + })), + }, + }; + }); + + await this.issueService.deleteCommentReaction(workspaceSlug, projectId, commentId, reactionHex); + } catch (error) { + const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.details = { + ...this.details, + [issueId]: { + ...this.details[issueId], + comments: issueComments, + }, + }; + }); + } + }; + addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => { try { runInAction(() => { diff --git a/space/types/issue.ts b/space/types/issue.ts index 754da6ae3..206327fcd 100644 --- a/space/types/issue.ts +++ b/space/types/issue.ts @@ -68,25 +68,30 @@ export interface IVote { } export interface Comment { - id: string; actor_detail: ActorDetail; - issue_detail: IssueDetail; - project_detail: ProjectDetail; - workspace_detail: WorkspaceDetail; - comment_reactions: any[]; - is_member: boolean; - created_at: Date; - updated_at: Date; - comment_stripped: string; - comment_html: string; - attachments: any[]; access: string; - created_by: string; - updated_by: string; - project: string; - workspace: string; - issue: string; actor: string; + attachments: any[]; + comment_html: string; + comment_reactions: { + actor_detail: ActorDetail; + comment: string; + id: string; + reaction: string; + }[]; + comment_stripped: string; + created_at: Date; + created_by: string; + id: string; + is_member: boolean; + issue: string; + issue_detail: IssueDetail; + project: string; + project_detail: ProjectDetail; + updated_at: Date; + updated_by: string; + workspace: string; + workspace_detail: WorkspaceDetail; } export interface IIssueReaction { From ac4127c93dacf1823f8d741c5227b5995418f51b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Wed, 6 Sep 2023 12:04:18 +0530 Subject: [PATCH 03/11] chore: add tooltip for user info --- .../comment/comment-reactions.tsx | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/space/components/issues/peek-overview/comment/comment-reactions.tsx b/space/components/issues/peek-overview/comment/comment-reactions.tsx index 7ec5c73b0..4045d3edf 100644 --- a/space/components/issues/peek-overview/comment/comment-reactions.tsx +++ b/space/components/issues/peek-overview/comment/comment-reactions.tsx @@ -6,7 +6,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { useMobxStore } from "lib/mobx/store-provider"; // ui -import { ReactionSelector } from "components/ui"; +import { ReactionSelector, Tooltip } from "components/ui"; // helpers import { groupReactions, renderEmoji } from "helpers/emoji.helper"; @@ -77,41 +77,55 @@ export const CommentReactions: React.FC = observer((props) => { size="md" /> - {Object.keys(groupedReactions || {}).map( - (reaction) => - groupedReactions?.[reaction]?.length && - groupedReactions[reaction].length > 0 && ( - - ) - )} + {renderEmoji(reaction)} + r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction + ) + ? "text-custom-primary-100" + : "" + } + > + {groupedReactions?.[reaction].length}{" "} + + + + ); + })}
); }); From 6d133328188adc8c4a2828bb294bc0ded76e10be Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Wed, 6 Sep 2023 12:17:47 +0530 Subject: [PATCH 04/11] style: add shadow to reaction selector --- space/components/ui/reaction-selector.tsx | 2 +- web/components/core/reaction-selector.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/space/components/ui/reaction-selector.tsx b/space/components/ui/reaction-selector.tsx index 302ff7387..a7b67afa6 100644 --- a/space/components/ui/reaction-selector.tsx +++ b/space/components/ui/reaction-selector.tsx @@ -52,7 +52,7 @@ export const ReactionSelector: React.FC = (props) => { position === "top" ? "-top-12" : "-bottom-12" }`} > -
+
{reactionEmojis.map((emoji) => (
diff --git a/web/components/issues/parent-issues-list-modal.tsx b/web/components/issues/parent-issues-list-modal.tsx index 62091511a..a29fa6521 100644 --- a/web/components/issues/parent-issues-list-modal.tsx +++ b/web/components/issues/parent-issues-list-modal.tsx @@ -136,7 +136,7 @@ export const ParentIssuesListModal: React.FC = ({ onClick={() => setIsWorkspaceLevel((prevData) => !prevData)} className="flex-shrink-0" > - workspace level + Workspace Level
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index 882bdb41e..a178e28fa 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -187,7 +187,7 @@ const GeneralSettings: NextPage = () => { />
-
+

Icon & Name

diff --git a/web/pages/[workspaceSlug]/settings/index.tsx b/web/pages/[workspaceSlug]/settings/index.tsx index 407f9b402..bd7ea1bd9 100644 --- a/web/pages/[workspaceSlug]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/settings/index.tsx @@ -90,9 +90,8 @@ const WorkspaceSettings: NextPage = () => { await workspaceService .updateWorkspace(activeWorkspace.slug, payload, user) .then((res) => { - mutate( - USER_WORKSPACES, - (prevData) => prevData?.map((workspace) => (workspace.id === res.id ? res : workspace)) + mutate(USER_WORKSPACES, (prevData) => + prevData?.map((workspace) => (workspace.id === res.id ? res : workspace)) ); mutate(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => { if (!prevData) return prevData; @@ -125,9 +124,8 @@ const WorkspaceSettings: NextPage = () => { title: "Success!", message: "Workspace picture removed successfully.", }); - mutate( - USER_WORKSPACES, - (prevData) => prevData?.map((workspace) => (workspace.id === res.id ? res : workspace)) + mutate(USER_WORKSPACES, (prevData) => + prevData?.map((workspace) => (workspace.id === res.id ? res : workspace)) ); mutate(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => { if (!prevData) return prevData; @@ -183,7 +181,7 @@ const WorkspaceSettings: NextPage = () => {
{activeWorkspace ? ( -
+

Logo

From 1655d0cb1c678020cdfc4982c31430647ee10cf0 Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Wed, 6 Sep 2023 17:08:19 +0530 Subject: [PATCH 10/11] feat: view, create, update and delete comment (#2106) * feat: update, delete link refactor: using old fetch-key * feat: issue activity with ability to view & add comment feat: click on view more to view more options in the issue detail * fix: upload image not working on mobile --- web/components/web-view/add-comment.tsx | 129 ++++++++++ .../web-view/create-update-link-form.tsx | 36 ++- web/components/web-view/index.ts | 4 + web/components/web-view/issue-activity.tsx | 233 ++++++++++++++++++ web/components/web-view/issue-attachments.tsx | 26 +- web/components/web-view/issue-link-list.tsx | 87 ++++++- .../web-view/issue-properties-detail.tsx | 93 ++++++- web/components/web-view/select-assignee.tsx | 95 +++++++ web/components/web-view/select-estimate.tsx | 83 +++++++ web/components/web-view/select-priority.tsx | 14 +- web/components/web-view/select-state.tsx | 2 +- web/components/web-view/web-view-modal.tsx | 8 +- web/constants/fetch-keys.ts | 2 - .../projects/[projectId]/issues/[issueId].tsx | 17 +- 14 files changed, 781 insertions(+), 48 deletions(-) create mode 100644 web/components/web-view/add-comment.tsx create mode 100644 web/components/web-view/issue-activity.tsx create mode 100644 web/components/web-view/select-assignee.tsx create mode 100644 web/components/web-view/select-estimate.tsx diff --git a/web/components/web-view/add-comment.tsx b/web/components/web-view/add-comment.tsx new file mode 100644 index 000000000..b5bff0cb5 --- /dev/null +++ b/web/components/web-view/add-comment.tsx @@ -0,0 +1,129 @@ +import React from "react"; + +// next +import { useRouter } from "next/router"; + +// react-hook-form +import { useForm, Controller } from "react-hook-form"; + +// hooks +import useProjectDetails from "hooks/use-project-details"; + +// components +import { TipTapEditor } from "components/tiptap"; + +// icons +import { Send } from "lucide-react"; + +// ui +import { Icon, SecondaryButton, Tooltip, PrimaryButton } from "components/ui"; + +// types +import type { IIssueComment } from "types"; + +const defaultValues: Partial = { + access: "INTERNAL", + comment_html: "", +}; + +type Props = { + disabled?: boolean; + onSubmit: (data: IIssueComment) => Promise; +}; + +const commentAccess = [ + { + icon: "lock", + key: "INTERNAL", + label: "Private", + }, + { + icon: "public", + key: "EXTERNAL", + label: "Public", + }, +]; + +export const AddComment: React.FC = ({ disabled = false, onSubmit }) => { + const editorRef = React.useRef(null); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { projectDetails } = useProjectDetails(); + + const showAccessSpecifier = projectDetails?.is_deployed; + + const { + control, + formState: { isSubmitting }, + handleSubmit, + reset, + } = useForm({ defaultValues }); + + const handleAddComment = async (formData: IIssueComment) => { + if (!formData.comment_html || isSubmitting) return; + + await onSubmit(formData).then(() => { + reset(defaultValues); + editorRef.current?.clearEditor(); + }); + }; + + return ( + +
+ {showAccessSpecifier && ( +
+ ( +
+ {commentAccess.map((access) => ( + + + + ))} +
+ )} + /> +
+ )} + ( +

" : value} + customClassName="p-3 min-h-[100px] shadow-sm" + debouncedUpdatesEnabled={false} + onChange={(comment_json: Object, comment_html: string) => onChange(comment_html)} + /> + )} + /> +
+ +
+ + + +
+ + ); +}; diff --git a/web/components/web-view/create-update-link-form.tsx b/web/components/web-view/create-update-link-form.tsx index 3e1d1368c..fa1a33939 100644 --- a/web/components/web-view/create-update-link-form.tsx +++ b/web/components/web-view/create-update-link-form.tsx @@ -1,5 +1,5 @@ // react -import React from "react"; +import React, { useEffect } from "react"; // next import { useRouter } from "next/router"; @@ -14,7 +14,7 @@ import { useForm } from "react-hook-form"; import issuesService from "services/issues.service"; // fetch keys -import { M_ISSUE_DETAILS } from "constants/fetch-keys"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; // hooks import useToast from "hooks/use-toast"; @@ -26,13 +26,14 @@ import { PrimaryButton, Input } from "components/ui"; import type { linkDetails, IIssueLink } from "types"; type Props = { - links?: linkDetails[]; + isOpen: boolean; data?: linkDetails; + links?: linkDetails[]; onSuccess: () => void; }; export const CreateUpdateLinkForm: React.FC = (props) => { - const { data, links, onSuccess } = props; + const { isOpen, data, links, onSuccess } = props; const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -42,6 +43,7 @@ export const CreateUpdateLinkForm: React.FC = (props) => { const { register, handleSubmit, + reset, formState: { errors, isSubmitting }, } = useForm({ defaultValues: { @@ -50,6 +52,22 @@ export const CreateUpdateLinkForm: React.FC = (props) => { }, }); + useEffect(() => { + if (!data) return; + reset({ + title: data.title, + url: data.url, + }); + }, [data, reset]); + + useEffect(() => { + if (!isOpen) + reset({ + title: "", + url: "", + }); + }, [isOpen, reset]); + const onSubmit = async (formData: IIssueLink) => { if (!workspaceSlug || !projectId || !issueId) return; @@ -65,9 +83,7 @@ export const CreateUpdateLinkForm: React.FC = (props) => { ) .then(() => { onSuccess(); - mutate( - M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString()) - ); + mutate(ISSUE_DETAILS(issueId.toString())); }) .catch((err) => { if (err?.status === 400) @@ -95,7 +111,7 @@ export const CreateUpdateLinkForm: React.FC = (props) => { ); mutate( - M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString()), + ISSUE_DETAILS(issueId.toString()), (prevData) => ({ ...prevData, issue_link: updatedLinks }), false ); @@ -110,9 +126,7 @@ export const CreateUpdateLinkForm: React.FC = (props) => { ) .then(() => { onSuccess(); - mutate( - M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString()) - ); + mutate(ISSUE_DETAILS(issueId.toString())); }); } }; diff --git a/web/components/web-view/index.ts b/web/components/web-view/index.ts index 817f5f2f1..2b87ad820 100644 --- a/web/components/web-view/index.ts +++ b/web/components/web-view/index.ts @@ -8,3 +8,7 @@ export * from "./issue-attachments"; export * from "./issue-properties-detail"; export * from "./issue-link-list"; export * from "./create-update-link-form"; +export * from "./issue-activity"; +export * from "./select-assignee"; +export * from "./select-estimate"; +export * from "./add-comment"; diff --git a/web/components/web-view/issue-activity.tsx b/web/components/web-view/issue-activity.tsx new file mode 100644 index 000000000..39f6036c2 --- /dev/null +++ b/web/components/web-view/issue-activity.tsx @@ -0,0 +1,233 @@ +// react +import React from "react"; + +// next +import Link from "next/link"; +import { useRouter } from "next/router"; + +// swr +import useSWR, { mutate } from "swr"; + +// fetch key +import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; + +// services +import issuesService from "services/issues.service"; + +// hooks +import useUser from "hooks/use-user"; +import useToast from "hooks/use-toast"; + +// components +import { Label, AddComment } from "components/web-view"; +import { CommentCard } from "components/issues/comment"; +import { ActivityIcon, ActivityMessage } from "components/core"; + +// helpers +import { timeAgo } from "helpers/date-time.helper"; + +// ui +import { Icon } from "components/ui"; + +// types +import type { IIssue, IIssueComment } from "types"; + +type Props = { + allowed: boolean; + issueDetails: IIssue; +}; + +export const IssueActivity: React.FC = (props) => { + const { issueDetails, allowed } = props; + + const router = useRouter(); + const { workspaceSlug, projectId, issueId } = router.query; + + const { user } = useUser(); + const { setToastAlert } = useToast(); + + const { data: issueActivities, mutate: mutateIssueActivity } = useSWR( + workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId.toString()) : null, + workspaceSlug && projectId && issueId + ? () => + issuesService.getIssueActivities( + workspaceSlug.toString(), + projectId.toString(), + issueId.toString() + ) + : null + ); + + const handleCommentUpdate = async (comment: any) => { + if (!workspaceSlug || !projectId || !issueId) return; + + await issuesService + .patchIssueComment( + workspaceSlug as string, + projectId as string, + issueId as string, + comment.id, + comment, + user + ) + .then(() => mutateIssueActivity()); + }; + + const handleCommentDelete = async (commentId: string) => { + if (!workspaceSlug || !projectId || !issueId) return; + + mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false); + + await issuesService + .deleteIssueComment( + workspaceSlug as string, + projectId as string, + issueId as string, + commentId, + user + ) + .then(() => mutateIssueActivity()); + }; + + const handleAddComment = async (formData: IIssueComment) => { + if (!workspaceSlug || !issueDetails) return; + + await issuesService + .createIssueComment( + workspaceSlug.toString(), + issueDetails.project, + issueDetails.id, + formData, + user + ) + .then(() => { + mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id)); + }) + .catch(() => + setToastAlert({ + type: "error", + title: "Error!", + message: "Comment could not be posted. Please try again.", + }) + ); + }; + + return ( +
+ +
+
    + {issueActivities?.map((activityItem, index) => { + // determines what type of action is performed + const message = activityItem.field ? ( + + ) : ( + "created the issue." + ); + + if ("field" in activityItem && activityItem.field !== "updated_by") { + return ( +
  • +
    + {issueActivities.length > 1 && index !== issueActivities.length - 1 ? ( +
    +
  • + ); + } else if ("comment_json" in activityItem) + return ( +
    + +
    + ); + })} +
  • +
    + +
    +
  • +
+
+
+ ); +}; diff --git a/web/components/web-view/issue-attachments.tsx b/web/components/web-view/issue-attachments.tsx index ba6523e9b..838ee8e44 100644 --- a/web/components/web-view/issue-attachments.tsx +++ b/web/components/web-view/issue-attachments.tsx @@ -21,10 +21,11 @@ import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys import useToast from "hooks/use-toast"; // icons -import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import { ChevronRightIcon, XMarkIcon } from "@heroicons/react/24/outline"; // components import { Label, WebViewModal } from "components/web-view"; +import { DeleteAttachmentModal } from "components/issues"; // types import type { IIssueAttachment } from "types"; @@ -42,6 +43,9 @@ export const IssueAttachments: React.FC = (props) => { const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [deleteAttachment, setDeleteAttachment] = useState(null); + const [attachmentDeleteModal, setAttachmentDeleteModal] = useState(false); + const { setToastAlert } = useToast(); const onDrop = useCallback( @@ -92,7 +96,7 @@ export const IssueAttachments: React.FC = (props) => { [issueId, projectId, setToastAlert, workspaceSlug] ); - const { getRootProps } = useDropzone({ + const { getRootProps, getInputProps } = useDropzone({ onDrop, maxSize: 5 * 1024 * 1024, disabled: !allowed || isLoading, @@ -112,6 +116,12 @@ export const IssueAttachments: React.FC = (props) => { return (
+ + setIsOpen(false)} modalTitle="Insert file">
= (props) => { !allowed || isLoading ? "cursor-not-allowed" : "cursor-pointer" }`} > + {isLoading ? (

Uploading...

) : ( @@ -144,6 +155,17 @@ export const IssueAttachments: React.FC = (props) => { {attachment.attributes.name} + {allowed && ( + + )}
))} + +
+ )}
))} ; submitChanges: (data: Partial) => Promise; }; export const IssuePropertiesDetail: React.FC = (props) => { const { control, submitChanges } = props; + const [isViewAllOpen, setIsViewAllOpen] = useState(false); + + const { isEstimateActive } = useEstimateOption(); + return (
-
+
@@ -44,10 +60,10 @@ export const IssuePropertiesDetail: React.FC = (props) => {
-
+
- + Priority
@@ -64,6 +80,67 @@ export const IssuePropertiesDetail: React.FC = (props) => {
+
+
+
+ + Assignee +
+
+ ( + submitChanges({ assignees_list: [val] })} + /> + )} + /> +
+
+
+ {isViewAllOpen && ( + <> + {isEstimateActive && ( +
+
+
+ + Estimate +
+
+ ( + submitChanges({ estimate_point: val })} + /> + )} + /> +
+
+
+ )} + + )} +
+ setIsViewAllOpen((prev) => !prev)} + className="w-full flex justify-center items-center gap-1 !py-2" + > + + {isViewAllOpen ? "View less" : "View all"} + + + +
); }; diff --git a/web/components/web-view/select-assignee.tsx b/web/components/web-view/select-assignee.tsx new file mode 100644 index 000000000..13ebd377f --- /dev/null +++ b/web/components/web-view/select-assignee.tsx @@ -0,0 +1,95 @@ +// react +import React, { useState } from "react"; + +// next +import { useRouter } from "next/router"; + +// swr +import useSWR from "swr"; + +// icons +import { ChevronDownIcon } from "@heroicons/react/24/outline"; + +// services +import projectService from "services/project.service"; + +// fetch key +import { PROJECT_MEMBERS } from "constants/fetch-keys"; + +// components +import { Avatar } from "components/ui/avatar"; +import { WebViewModal } from "./web-view-modal"; + +type Props = { + value: string[]; + onChange: (value: any) => void; + disabled?: boolean; +}; + +export const AssigneeSelect: React.FC = (props) => { + const { value, onChange, disabled = false } = props; + + const [isOpen, setIsOpen] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: members } = useSWR( + workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, + workspaceSlug && projectId + ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + : null + ); + + const selectedAssignees = members?.filter((member) => value?.includes(member.member.id)); + + return ( + <> + { + setIsOpen(false); + }} + > + ({ + label: member.member.display_name, + value: member.member.id, + checked: value?.includes(member.member.id), + icon: , + onClick: () => { + setIsOpen(false); + if (disabled) return; + onChange(member.member.id); + }, + })) || [] + } + /> + + + + + ); +}; diff --git a/web/components/web-view/select-estimate.tsx b/web/components/web-view/select-estimate.tsx new file mode 100644 index 000000000..751375d7b --- /dev/null +++ b/web/components/web-view/select-estimate.tsx @@ -0,0 +1,83 @@ +// react +import React, { useState } from "react"; + +// icons +import { ChevronDownIcon, PlayIcon } from "lucide-react"; + +// hooks +import useEstimateOption from "hooks/use-estimate-option"; + +// components +import { WebViewModal } from "./web-view-modal"; + +type Props = { + value: any; + onChange: (value: any) => void; + disabled?: boolean; +}; + +export const EstimateSelect: React.FC = (props) => { + const { value, onChange, disabled = false } = props; + + const [isOpen, setIsOpen] = useState(false); + + const { estimatePoints } = useEstimateOption(); + + return ( + <> + { + setIsOpen(false); + }} + > + { + setIsOpen(false); + if (disabled) return; + onChange(null); + }, + icon: , + }, + ...estimatePoints?.map((point) => ({ + label: point.value, + value: point.key, + checked: point.key === value, + icon: , + onClick: () => { + setIsOpen(false); + if (disabled) return; + onChange(point.key); + }, + })), + ]} + /> + + + + + ); +}; diff --git a/web/components/web-view/select-priority.tsx b/web/components/web-view/select-priority.tsx index 11f7ab9f1..adb12714a 100644 --- a/web/components/web-view/select-priority.tsx +++ b/web/components/web-view/select-priority.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; // icons -import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { ChevronDownIcon } from "lucide-react"; // constants import { PRIORITIES } from "constants/project"; @@ -35,11 +35,16 @@ export const PrioritySelect: React.FC = (props) => { }} > ({ label: priority ? capitalizeFirstLetter(priority) : "None", value: priority, + checked: priority === value, + onClick: () => { + setIsOpen(false); + if (disabled) return; + onChange(priority); + }, icon: ( = (props) => { {getPriorityIcon(priority, "text-sm")} ), - onClick: () => { - setIsOpen(false); - if (disabled) return; - onChange(priority); - }, })) || [] } /> diff --git a/web/components/web-view/select-state.tsx b/web/components/web-view/select-state.tsx index bceabfd2c..c28d30f19 100644 --- a/web/components/web-view/select-state.tsx +++ b/web/components/web-view/select-state.tsx @@ -57,11 +57,11 @@ export const StateSelect: React.FC = (props) => { }} > ({ label: state.name, value: state.id, + checked: state.id === selectedState?.id, icon: getStateGroupIcon(state.group, "16", "16", state.color), onClick: () => { setIsOpen(false); diff --git a/web/components/web-view/web-view-modal.tsx b/web/components/web-view/web-view-modal.tsx index 980ddc79a..93f9ab46d 100644 --- a/web/components/web-view/web-view-modal.tsx +++ b/web/components/web-view/web-view-modal.tsx @@ -74,24 +74,24 @@ export const WebViewModal = (props: Props) => { }; type OptionsProps = { - selectedOption: string | null; options: Array<{ label: string; value: string | null; + checked: boolean; icon?: any; onClick: () => void; }>; }; -const Options: React.FC = ({ options, selectedOption }) => ( +const Options: React.FC = ({ options }) => (
{options.map((option) => (
diff --git a/web/constants/fetch-keys.ts b/web/constants/fetch-keys.ts index 172c74683..14d34a96a 100644 --- a/web/constants/fetch-keys.ts +++ b/web/constants/fetch-keys.ts @@ -229,8 +229,6 @@ export const INBOX_ISSUE_DETAILS = (inboxId: string, issueId: string) => // Issues export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`; -export const M_ISSUE_DETAILS = (workspaceSlug: string, projectId: string, issueId: string) => - `M_ISSUE_DETAILS_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId}`; export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`; export const ISSUE_ATTACHMENTS = (issueId: string) => `ISSUE_ATTACHMENTS_${issueId.toUpperCase()}`; export const ARCHIVED_ISSUE_DETAILS = (issueId: string) => diff --git a/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 7054c86a5..637c953e7 100644 --- a/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/m/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -14,7 +14,7 @@ import { useForm } from "react-hook-form"; import issuesService from "services/issues.service"; // fetch key -import { M_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; +import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; // hooks import useUser from "hooks/use-user"; @@ -33,6 +33,7 @@ import { IssueAttachments, IssuePropertiesDetail, IssueLinks, + IssueActivity, } from "components/web-view"; // types @@ -66,9 +67,7 @@ const MobileWebViewIssueDetail = () => { mutate: mutateIssueDetails, error, } = useSWR( - workspaceSlug && projectId && issueId - ? M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString()) - : null, + workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null, workspaceSlug && projectId && issueId ? () => issuesService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString()) @@ -83,6 +82,10 @@ const MobileWebViewIssueDetail = () => { description: issueDetails.description, description_html: issueDetails.description_html, state: issueDetails.state, + assignees_list: + issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), + labels_list: issueDetails.labels_list ?? issueDetails.labels, + labels: issueDetails.labels_list ?? issueDetails.labels, }); }, [issueDetails, reset]); @@ -91,7 +94,7 @@ const MobileWebViewIssueDetail = () => { if (!workspaceSlug || !projectId || !issueId) return; mutate( - M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString()), + ISSUE_DETAILS(issueId.toString()), (prevData) => { if (!prevData) return prevData; @@ -161,7 +164,9 @@ const MobileWebViewIssueDetail = () => { - + + +
); From 85f797058dd47b227bf0a46bf2652b041458793d Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 6 Sep 2023 19:02:59 +0530 Subject: [PATCH 11/11] fix: edit issue comment mutation (#2109) --- space/store/issue_details.ts | 45 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/space/store/issue_details.ts b/space/store/issue_details.ts index 26b6148c3..346206e94 100644 --- a/space/store/issue_details.ts +++ b/space/store/issue_details.ts @@ -154,29 +154,32 @@ class IssueDetailStore implements IssueDetailStore { data: any ) => { try { - const issueCommentUpdateResponse = await this.issueService.updateIssueComment( - workspaceSlug, - projectId, - issueId, - commentId, - data - ); + runInAction(() => { + this.details = { + ...this.details, + [issueId]: { + ...this.details[issueId], + comments: this.details[issueId].comments.map((c) => ({ + ...c, + ...(c.id === commentId ? data : {}), + })), + }, + }; + }); - if (issueCommentUpdateResponse) { - const remainingComments = this.details[issueId].comments.filter((com) => com.id != commentId); - runInAction(() => { - this.details = { - ...this.details, - [issueId]: { - ...this.details[issueId], - comments: [...remainingComments, issueCommentUpdateResponse], - }, - }; - }); - } - return issueCommentUpdateResponse; + await this.issueService.updateIssueComment(workspaceSlug, projectId, issueId, commentId, data); } catch (error) { - console.log("Failed to add issue comment"); + const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId); + + runInAction(() => { + this.details = { + ...this.details, + [issueId]: { + ...this.details[issueId], + comments: issueComments, + }, + }; + }); } };