forked from github/plane
feat: issue & comment reaction (#1690)
* feat: developed reaction selector component * feat add reaction on issue & issue comment refactor: reaction selector component, made hooks to abstracted reaction logic & state by making custom hook * fix: emoji.helper.tsx function * refactor: reaction not working on inbox issue
This commit is contained in:
parent
5cfea3948f
commit
0cc4468091
@ -4,4 +4,5 @@ export * from "./sidebar";
|
||||
export * from "./theme";
|
||||
export * from "./views";
|
||||
export * from "./feeds";
|
||||
export * from "./reaction-selector";
|
||||
export * from "./image-picker-popover";
|
||||
|
85
apps/app/components/core/reaction-selector.tsx
Normal file
85
apps/app/components/core/reaction-selector.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { Fragment } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
|
||||
const reactionEmojis = [
|
||||
"128077",
|
||||
"128078",
|
||||
"128516",
|
||||
"128165",
|
||||
"128533",
|
||||
"129505",
|
||||
"9992",
|
||||
"128064",
|
||||
];
|
||||
|
||||
interface Props {
|
||||
size?: "sm" | "md" | "lg";
|
||||
position?: "top" | "bottom";
|
||||
value?: string | string[] | null;
|
||||
onSelect: (emoji: string) => void;
|
||||
}
|
||||
|
||||
export const ReactionSelector: React.FC<Props> = (props) => {
|
||||
const { value, onSelect, position, size } = props;
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open, close: closePopover }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`${
|
||||
open ? "" : "text-opacity-90"
|
||||
} group inline-flex items-center rounded-md bg-custom-background-80 focus:outline-none`}
|
||||
>
|
||||
<span
|
||||
className={`flex justify-center items-center rounded-md ${
|
||||
size === "sm" ? "w-6 h-6" : size === "md" ? "w-8 h-8" : "w-10 h-10"
|
||||
}`}
|
||||
>
|
||||
<Icon iconName="add_reaction" className="text-custom-text-100 scale-125" />
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel
|
||||
className={`absolute -left-2 z-10 ${position === "top" ? "-top-12" : "-bottom-12"}`}
|
||||
>
|
||||
<div className="bg-custom-background-0 border rounded-md px-2 py-1.5">
|
||||
<div className="flex gap-x-2">
|
||||
{reactionEmojis.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelect(emoji);
|
||||
closePopover();
|
||||
}}
|
||||
className="flex h-5 w-5 select-none items-center justify-between text-sm"
|
||||
>
|
||||
{renderEmoji(emoji)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
@ -19,6 +19,7 @@ import {
|
||||
IssueActivitySection,
|
||||
IssueDescriptionForm,
|
||||
IssueDetailsSidebar,
|
||||
IssueReaction,
|
||||
} from "components/issues";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
@ -303,6 +304,13 @@ export const InboxMainContent: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IssueReaction
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
issueId={issueDetails.id}
|
||||
/>
|
||||
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
<IssueActivitySection issueId={issueDetails.id} user={user} />
|
||||
|
@ -10,6 +10,7 @@ import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon } from "@heroicons/rea
|
||||
import useUser from "hooks/use-user";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
import { CommentReaction } from "components/issues";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
@ -138,6 +139,12 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
ref={showEditorRef}
|
||||
/>
|
||||
|
||||
<CommentReaction
|
||||
workspaceSlug={comment?.workspace_detail?.slug}
|
||||
projectId={comment.project}
|
||||
commentId={comment.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
80
apps/app/components/issues/comment/comment-reaction.tsx
Normal file
80
apps/app/components/issues/comment/comment-reaction.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useCommentReaction from "hooks/use-comment-reaction";
|
||||
// ui
|
||||
import { ReactionSelector } from "components/core";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug?: string | string[];
|
||||
projectId?: string | string[];
|
||||
commentId: string;
|
||||
};
|
||||
|
||||
export const CommentReaction: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId, commentId } = props;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const {
|
||||
commentReactions,
|
||||
groupedReactions,
|
||||
handleReactionCreate,
|
||||
handleReactionDelete,
|
||||
isLoading,
|
||||
} = useCommentReaction(workspaceSlug, projectId, commentId);
|
||||
|
||||
const handleReactionClick = (reaction: string) => {
|
||||
if (!workspaceSlug || !projectId || !commentId) return;
|
||||
|
||||
const isSelected = commentReactions?.some(
|
||||
(r) => r.actor === user?.id && r.reaction === reaction
|
||||
);
|
||||
|
||||
if (isSelected) {
|
||||
handleReactionDelete(reaction);
|
||||
} else {
|
||||
handleReactionCreate(reaction);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-1.5 items-center mt-2">
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={
|
||||
commentReactions
|
||||
?.filter((reaction) => reaction.actor === user?.id)
|
||||
.map((r) => r.reaction) || []
|
||||
}
|
||||
onSelect={handleReactionClick}
|
||||
/>
|
||||
|
||||
{Object.keys(groupedReactions || {}).map(
|
||||
(reaction) =>
|
||||
groupedReactions?.[reaction]?.length &&
|
||||
groupedReactions[reaction].length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleReactionClick(reaction);
|
||||
}}
|
||||
key={reaction}
|
||||
className={`flex items-center gap-1 text-custom-text-100 h-full px-2 py-1 rounded-md ${
|
||||
commentReactions?.some((r) => r.actor === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<span>{groupedReactions?.[reaction].length} </span>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,2 +1,3 @@
|
||||
export * from "./add-comment";
|
||||
export * from "./comment-card";
|
||||
export * from "./comment-reaction";
|
||||
|
@ -14,3 +14,4 @@ export * from "./parent-issues-list-modal";
|
||||
export * from "./sidebar";
|
||||
export * from "./sub-issues-list";
|
||||
export * from "./label";
|
||||
export * from "./issue-reaction";
|
||||
|
70
apps/app/components/issues/issue-reaction.tsx
Normal file
70
apps/app/components/issues/issue-reaction.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useIssueReaction from "hooks/use-issue-reaction";
|
||||
// components
|
||||
import { ReactionSelector } from "components/core";
|
||||
// string helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
// types
|
||||
type Props = {
|
||||
workspaceSlug?: string | string[];
|
||||
projectId?: string | string[];
|
||||
issueId?: string | string[];
|
||||
};
|
||||
|
||||
export const IssueReaction: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId, issueId } = props;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { reactions, groupedReactions, handleReactionCreate, handleReactionDelete } =
|
||||
useIssueReaction(workspaceSlug, projectId, issueId);
|
||||
|
||||
const handleReactionClick = (reaction: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
const isSelected = reactions?.some((r) => r.actor === user?.id && r.reaction === reaction);
|
||||
|
||||
if (isSelected) {
|
||||
handleReactionDelete(reaction);
|
||||
} else {
|
||||
handleReactionCreate(reaction);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-1.5 items-center mt-4">
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={
|
||||
reactions?.filter((reaction) => reaction.actor === user?.id).map((r) => r.reaction) || []
|
||||
}
|
||||
onSelect={handleReactionClick}
|
||||
/>
|
||||
|
||||
{Object.keys(groupedReactions || {}).map(
|
||||
(reaction) =>
|
||||
groupedReactions?.[reaction]?.length &&
|
||||
groupedReactions[reaction].length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleReactionClick(reaction);
|
||||
}}
|
||||
key={reaction}
|
||||
className={`flex items-center gap-1 text-custom-text-100 h-full px-2 py-1 rounded-md ${
|
||||
reactions?.some((r) => r.actor === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<span>{groupedReactions?.[reaction].length} </span>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -17,6 +17,7 @@ import {
|
||||
IssueAttachments,
|
||||
IssueDescriptionForm,
|
||||
SubIssuesList,
|
||||
IssueReaction,
|
||||
} from "components/issues";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
@ -117,6 +118,9 @@ export const IssueMainContent: React.FC<Props> = ({
|
||||
handleFormSubmit={submitChanges}
|
||||
isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable}
|
||||
/>
|
||||
|
||||
<IssueReaction workspaceSlug={workspaceSlug} issueId={issueId} projectId={projectId} />
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
<SubIssuesList parentIssue={issueDetails} user={user} disabled={uneditable} />
|
||||
</div>
|
||||
|
@ -301,3 +301,13 @@ export const getPaginatedNotificationKey = (
|
||||
cursor,
|
||||
})}`;
|
||||
};
|
||||
|
||||
export const ISSUE_REACTION_LIST = (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
`ISSUE_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId.toUpperCase()}`;
|
||||
|
||||
export const COMMENT_REACTION_LIST = (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commendId: string
|
||||
) =>
|
||||
`COMMENT_REACTION_LIST_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${commendId.toUpperCase()}`;
|
||||
|
@ -36,3 +36,18 @@ export const renderEmoji = (
|
||||
);
|
||||
else return isNaN(parseInt(emoji)) ? emoji : String.fromCodePoint(parseInt(emoji));
|
||||
};
|
||||
|
||||
export const groupReactions: (reactions: any[], key: string) => { [key: string]: any[] } = (
|
||||
reactions: any,
|
||||
key: string
|
||||
) => {
|
||||
const groupedReactions = reactions.reduce((acc: any, reaction: any) => {
|
||||
if (!acc[reaction[key]]) {
|
||||
acc[reaction[key]] = [];
|
||||
}
|
||||
acc[reaction[key]].push(reaction);
|
||||
return acc;
|
||||
}, {} as { [key: string]: any[] });
|
||||
|
||||
return groupedReactions;
|
||||
};
|
||||
|
95
apps/app/hooks/use-comment-reaction.tsx
Normal file
95
apps/app/hooks/use-comment-reaction.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
// fetch keys
|
||||
import { COMMENT_REACTION_LIST } from "constants/fetch-keys";
|
||||
|
||||
// services
|
||||
import reactionService from "services/reaction.service";
|
||||
|
||||
// helpers
|
||||
import { groupReactions } from "helpers/emoji.helper";
|
||||
|
||||
// hooks
|
||||
import useUser from "./use-user";
|
||||
|
||||
const useCommentReaction = (
|
||||
workspaceSlug?: string | string[] | null,
|
||||
projectId?: string | string[] | null,
|
||||
commendId?: string | string[] | null
|
||||
) => {
|
||||
const {
|
||||
data: commentReactions,
|
||||
mutate: mutateCommentReactions,
|
||||
error,
|
||||
} = useSWR(
|
||||
workspaceSlug && projectId && commendId
|
||||
? COMMENT_REACTION_LIST(workspaceSlug.toString(), projectId.toString(), commendId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && commendId
|
||||
? () =>
|
||||
reactionService.listIssueCommentReactions(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
commendId.toString()
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const user = useUser();
|
||||
|
||||
const groupedReactions = groupReactions(commentReactions || [], "reaction");
|
||||
|
||||
/**
|
||||
* @description Use this function to create user's reaction to an issue. This function will mutate the reactions state.
|
||||
* @param {string} reaction
|
||||
* @example handleReactionDelete("123") // 123 -> is emoji hexa-code
|
||||
*/
|
||||
|
||||
const handleReactionCreate = async (reaction: string) => {
|
||||
if (!workspaceSlug || !projectId || !commendId) return;
|
||||
|
||||
const data = await reactionService.createIssueCommentReaction(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
commendId.toString(),
|
||||
{ reaction }
|
||||
);
|
||||
|
||||
mutateCommentReactions((prev) => [...(prev || []), data]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Use this function to delete user's reaction from an issue. This function will mutate the reactions state.
|
||||
* @param {string} reaction
|
||||
* @example handleReactionDelete("123") // 123 -> is emoji hexa-code
|
||||
*/
|
||||
|
||||
const handleReactionDelete = async (reaction: string) => {
|
||||
if (!workspaceSlug || !projectId || !commendId) return;
|
||||
|
||||
mutateCommentReactions(
|
||||
(prevData) =>
|
||||
prevData?.filter((r) => r.actor !== user?.user?.id || r.reaction !== reaction) || []
|
||||
);
|
||||
|
||||
await reactionService.deleteIssueCommentReaction(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
commendId.toString(),
|
||||
reaction
|
||||
);
|
||||
|
||||
mutateCommentReactions();
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading: !commentReactions && !error,
|
||||
commentReactions,
|
||||
groupedReactions,
|
||||
handleReactionCreate,
|
||||
handleReactionDelete,
|
||||
mutateCommentReactions,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export default useCommentReaction;
|
96
apps/app/hooks/use-issue-reaction.tsx
Normal file
96
apps/app/hooks/use-issue-reaction.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
// fetch keys
|
||||
import { ISSUE_REACTION_LIST } from "constants/fetch-keys";
|
||||
|
||||
// helpers
|
||||
import { groupReactions } from "helpers/emoji.helper";
|
||||
|
||||
// services
|
||||
import reactionService from "services/reaction.service";
|
||||
|
||||
// hooks
|
||||
import useUser from "./use-user";
|
||||
|
||||
const useIssueReaction = (
|
||||
workspaceSlug?: string | string[] | null,
|
||||
projectId?: string | string[] | null,
|
||||
issueId?: string | string[] | null
|
||||
) => {
|
||||
const user = useUser();
|
||||
|
||||
const {
|
||||
data: reactions,
|
||||
mutate: mutateReaction,
|
||||
error,
|
||||
} = useSWR(
|
||||
workspaceSlug && projectId && issueId
|
||||
? ISSUE_REACTION_LIST(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () =>
|
||||
reactionService.listIssueReactions(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString()
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const groupedReactions = groupReactions(reactions || [], "reaction");
|
||||
|
||||
/**
|
||||
* @description Use this function to create user's reaction to an issue. This function will mutate the reactions state.
|
||||
* @param {string} reaction
|
||||
* @example handleReactionCreate("128077") // hexa-code of the emoji
|
||||
*/
|
||||
|
||||
const handleReactionCreate = async (reaction: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
const data = await reactionService.createIssueReaction(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString(),
|
||||
{ reaction }
|
||||
);
|
||||
|
||||
mutateReaction((prev) => [...(prev || []), data]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Use this function to delete user's reaction from an issue. This function will mutate the reactions state.
|
||||
* @param {string} reaction
|
||||
* @example handleReactionDelete("123") // 123 -> is emoji hexa-code
|
||||
*/
|
||||
|
||||
const handleReactionDelete = async (reaction: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueId) return;
|
||||
|
||||
mutateReaction(
|
||||
(prevData) =>
|
||||
prevData?.filter((r) => r.actor !== user?.user?.id || r.reaction !== reaction) || [],
|
||||
false
|
||||
);
|
||||
|
||||
await reactionService.deleteIssueReaction(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
issueId.toString(),
|
||||
reaction
|
||||
);
|
||||
|
||||
mutateReaction();
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading: !reactions && !error,
|
||||
reactions,
|
||||
groupedReactions,
|
||||
handleReactionCreate,
|
||||
handleReactionDelete,
|
||||
mutateReaction,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export default useIssueReaction;
|
145
apps/app/services/reaction.service.ts
Normal file
145
apps/app/services/reaction.service.ts
Normal file
@ -0,0 +1,145 @@
|
||||
// services
|
||||
import APIService from "services/api.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
|
||||
// types
|
||||
import type {
|
||||
ICurrentUserResponse,
|
||||
IssueReaction,
|
||||
IssueCommentReaction,
|
||||
IssueReactionForm,
|
||||
IssueCommentReactionForm,
|
||||
} from "types";
|
||||
|
||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||
|
||||
const trackEvent =
|
||||
process.env.NEXT_PUBLIC_TRACK_EVENTS === "true" || process.env.NEXT_PUBLIC_TRACK_EVENTS === "1";
|
||||
|
||||
class ReactionService extends APIService {
|
||||
constructor() {
|
||||
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||
}
|
||||
|
||||
async createIssueReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: IssueReactionForm,
|
||||
user?: ICurrentUserResponse
|
||||
): Promise<any> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/`,
|
||||
data
|
||||
)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_CREATE", user);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async listIssueReactions(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string
|
||||
): Promise<IssueReaction[]> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
reaction: string,
|
||||
user?: ICurrentUserResponse
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/reactions/${reaction}/`
|
||||
)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackReactionEvent(response?.data, "ISSUE_REACTION_DELETE", user);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssueCommentReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
data: IssueCommentReactionForm,
|
||||
user?: ICurrentUserResponse
|
||||
): Promise<any> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/`,
|
||||
data
|
||||
)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackReactionEvent(
|
||||
response?.data,
|
||||
"ISSUE_COMMENT_REACTION_CREATE",
|
||||
user
|
||||
);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async listIssueCommentReactions(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string
|
||||
): Promise<IssueCommentReaction[]> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssueCommentReaction(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
commentId: string,
|
||||
reaction: string,
|
||||
user?: ICurrentUserResponse
|
||||
): Promise<any> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/comments/${commentId}/reactions/${reaction}/`
|
||||
)
|
||||
.then((response) => {
|
||||
if (trackEvent)
|
||||
trackEventServices.trackReactionEvent(
|
||||
response?.data,
|
||||
"ISSUE_COMMENT_REACTION_DELETE",
|
||||
user
|
||||
);
|
||||
return response?.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const reactionService = new ReactionService();
|
||||
|
||||
export default reactionService;
|
@ -19,6 +19,8 @@ import type {
|
||||
IState,
|
||||
IView,
|
||||
IWorkspace,
|
||||
IssueCommentReaction,
|
||||
IssueReaction,
|
||||
} from "types";
|
||||
|
||||
type WorkspaceEventType =
|
||||
@ -110,6 +112,12 @@ type AnalyticsEventType =
|
||||
| "MODULE_CUSTOM_ANALYTICS"
|
||||
| "MODULE_ANALYTICS_EXPORT";
|
||||
|
||||
type ReactionEventType =
|
||||
| "ISSUE_REACTION_CREATE"
|
||||
| "ISSUE_COMMENT_REACTION_CREATE"
|
||||
| "ISSUE_REACTION_DELETE"
|
||||
| "ISSUE_COMMENT_REACTION_DELETE";
|
||||
|
||||
class TrackEventServices extends APIService {
|
||||
constructor() {
|
||||
super("/");
|
||||
@ -799,6 +807,32 @@ class TrackEventServices extends APIService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async trackReactionEvent(
|
||||
data: IssueReaction | IssueCommentReaction,
|
||||
eventName: ReactionEventType,
|
||||
user: ICurrentUserResponse | undefined
|
||||
): Promise<any> {
|
||||
let payload: any;
|
||||
if (eventName === "ISSUE_REACTION_DELETE" || eventName === "ISSUE_COMMENT_REACTION_DELETE")
|
||||
payload = data;
|
||||
else
|
||||
payload = {
|
||||
workspaceId: data?.workspace,
|
||||
projectId: data?.project,
|
||||
reaction: data?.reaction,
|
||||
};
|
||||
|
||||
return this.request({
|
||||
url: "/api/track-event",
|
||||
method: "POST",
|
||||
data: {
|
||||
eventName,
|
||||
extra: payload,
|
||||
user: user,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const trackEventServices = new TrackEventServices();
|
||||
|
1
apps/app/types/index.d.ts
vendored
1
apps/app/types/index.d.ts
vendored
@ -17,6 +17,7 @@ export * from "./analytics";
|
||||
export * from "./calendar";
|
||||
export * from "./notifications";
|
||||
export * from "./waitlist";
|
||||
export * from "./reaction";
|
||||
|
||||
export type NestedKeyOf<ObjectType extends object> = {
|
||||
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
|
||||
|
33
apps/app/types/reaction.d.ts
vendored
Normal file
33
apps/app/types/reaction.d.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
export interface IssueReaction {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
reaction: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
workspace: string;
|
||||
actor: string;
|
||||
issue: string;
|
||||
}
|
||||
|
||||
export interface IssueReactionForm {
|
||||
reaction: string;
|
||||
}
|
||||
|
||||
export interface IssueCommentReaction {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
reaction: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
project: string;
|
||||
workspace: string;
|
||||
actor: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export interface IssueCommentReactionForm {
|
||||
reaction: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user