mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: impleted store for issue reaction, comments, and comment reactions. implemented ui and compoennts for issue reactions in issue peek overview (#2498)
This commit is contained in:
parent
2dd46be287
commit
9bddd2eb67
@ -1,6 +1,8 @@
|
||||
import { FC } from "react";
|
||||
// packages
|
||||
import { RichTextEditor } from "@plane/rich-text-editor";
|
||||
// components
|
||||
import { IssueReaction } from "./reactions";
|
||||
// hooks
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
// types
|
||||
@ -13,33 +15,46 @@ const fileService = new FileService();
|
||||
interface IPeekOverviewIssueDetails {
|
||||
workspaceSlug: string;
|
||||
issue: IIssue;
|
||||
issueReactions: any;
|
||||
user: any;
|
||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||
issueReactionCreate: (reaction: string) => void;
|
||||
issueReactionRemove: (reaction: string) => void;
|
||||
}
|
||||
|
||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
||||
const { workspaceSlug, issue, issueUpdate } = props;
|
||||
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
|
||||
|
||||
const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => {
|
||||
issueUpdate({ ...issue, description_html: _data });
|
||||
}, 1500);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<div className="font-medium text-sm text-custom-text-200">
|
||||
{issue?.project_detail?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
|
||||
<div className="font-medium text-xl">{issue?.name}</div>
|
||||
|
||||
<RichTextEditor
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
value={issue?.description_html}
|
||||
debouncedUpdatesEnabled={false}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
debouncedIssueDescription(description_html);
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<RichTextEditor
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
value={issue?.description_html}
|
||||
debouncedUpdatesEnabled={false}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
debouncedIssueDescription(description_html);
|
||||
}}
|
||||
/>
|
||||
|
||||
<IssueReaction
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,3 @@
|
||||
export * from "./root";
|
||||
export * from "./selector";
|
||||
export * from "./preview";
|
@ -0,0 +1,48 @@
|
||||
import { FC } from "react";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
interface IIssueReactionPreview {
|
||||
issueReactions: any;
|
||||
user: any;
|
||||
handleReaction: (reaction: string) => void;
|
||||
}
|
||||
|
||||
export const IssueReactionPreview: FC<IIssueReactionPreview> = (props) => {
|
||||
const { issueReactions, user, handleReaction } = props;
|
||||
|
||||
const isUserReacted = (reactions: any) => {
|
||||
const userReaction = reactions?.find((reaction: any) => reaction.actor === user?.id);
|
||||
if (userReaction) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{Object.keys(issueReactions || {}).map(
|
||||
(reaction) =>
|
||||
issueReactions[reaction]?.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReaction(reaction)}
|
||||
key={reaction}
|
||||
className={`flex items-center gap-1.5 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
|
||||
isUserReacted(issueReactions[reaction])
|
||||
? `bg-custom-primary-100/20 hover:bg-custom-primary-100/30`
|
||||
: `bg-custom-background-90 hover:bg-custom-background-100/30`
|
||||
}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={`${
|
||||
isUserReacted(issueReactions[reaction]) ? `text-custom-primary-100 hover:text-custom-primary-200` : ``
|
||||
}`}
|
||||
>
|
||||
{issueReactions[reaction].length}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
29
web/components/issues/issue-peek-overview/reactions/root.tsx
Normal file
29
web/components/issues/issue-peek-overview/reactions/root.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { IssueReactionPreview, IssueReactionSelector } from "./";
|
||||
|
||||
interface IIssueReaction {
|
||||
issueReactions: any;
|
||||
user: any;
|
||||
issueReactionCreate: (reaction: string) => void;
|
||||
issueReactionRemove: (reaction: string) => void;
|
||||
}
|
||||
|
||||
export const IssueReaction: FC<IIssueReaction> = (props) => {
|
||||
const { issueReactions, user, issueReactionCreate, issueReactionRemove } = props;
|
||||
|
||||
const handleReaction = (reaction: string) => {
|
||||
const isReactionAvailable =
|
||||
issueReactions[reaction].find((_reaction: any) => _reaction.actor === user?.id) ?? false;
|
||||
|
||||
if (isReactionAvailable) issueReactionRemove(reaction);
|
||||
else issueReactionCreate(reaction);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
<IssueReactionSelector onSelect={handleReaction} position="bottom" />
|
||||
<IssueReactionPreview issueReactions={issueReactions} user={user} handleReaction={handleReaction} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,73 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// icons
|
||||
import { SmilePlus } from "lucide-react";
|
||||
// constants
|
||||
import { issueReactionEmojis } from "constants/issue";
|
||||
|
||||
interface IIssueReactionSelector {
|
||||
size?: "sm" | "md" | "lg";
|
||||
position?: "top" | "bottom";
|
||||
onSelect: (reaction: any) => void;
|
||||
}
|
||||
|
||||
export const IssueReactionSelector: FC<IIssueReactionSelector> = (props) => {
|
||||
const { size = "md", position = "top", onSelect } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover className="relative">
|
||||
{({ open, close: closePopover }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`${
|
||||
open ? "" : "bg-custom-background-90"
|
||||
} group inline-flex items-center rounded-md bg-custom-background-90 focus:outline-none transition-all hover:bg-custom-background-100`}
|
||||
>
|
||||
<span
|
||||
className={`flex justify-center items-center rounded-md px-2 ${
|
||||
size === "sm" ? "w-6 h-6" : size === "md" ? "w-7 h-7" : "w-8 h-8"
|
||||
}`}
|
||||
>
|
||||
<SmilePlus className="text-custom-text-100 h-3.5 w-3.5" />
|
||||
</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={`bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded p-1 overflow-hidden absolute -left-2 z-10 ${
|
||||
position === "top" ? "-top-10" : "-bottom-10"
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-x-1">
|
||||
{issueReactionEmojis.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (onSelect) onSelect(emoji);
|
||||
closePopover();
|
||||
}}
|
||||
className="select-none rounded text-sm p-1 hover:bg-custom-sidebar-background-80 transition-all w-6 h-6 flex justify-center items-center"
|
||||
>
|
||||
<div className="w-4 h-4">{renderEmoji(emoji)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
@ -34,6 +34,14 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const issueReactionCreate = (data: string) => {
|
||||
issueDetailStore.createIssueReaction(workspaceSlug, projectId, issueId, data);
|
||||
};
|
||||
|
||||
const issueReactionRemove = (data: string) => {
|
||||
issueDetailStore.removeIssueReaction(workspaceSlug, projectId, issueId, data);
|
||||
};
|
||||
|
||||
return (
|
||||
<IssueView
|
||||
workspaceSlug={workspaceSlug}
|
||||
@ -43,6 +51,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
issueUpdate={issueUpdate}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
>
|
||||
{children}
|
||||
</IssueView>
|
||||
|
@ -2,6 +2,7 @@ import { FC, ReactNode, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Maximize2, ArrowRight, Link, Trash, PanelRightOpen, Square, SquareCode } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { PeekOverviewIssueDetails } from "./issue-detail";
|
||||
import { PeekOverviewProperties } from "./properties";
|
||||
@ -16,6 +17,8 @@ interface IIssueView {
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||
issueReactionCreate: (reaction: string) => void;
|
||||
issueReactionRemove: (reaction: string) => void;
|
||||
states: any;
|
||||
members: any;
|
||||
priorities: any;
|
||||
@ -43,12 +46,23 @@ const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [
|
||||
];
|
||||
|
||||
export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issueUpdate, states, members, priorities, children } = props;
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
issueUpdate,
|
||||
issueReactionCreate,
|
||||
issueReactionRemove,
|
||||
states,
|
||||
members,
|
||||
priorities,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { peekIssueId } = router.query as { peekIssueId: string };
|
||||
|
||||
const { issueDetail: issueDetailStore }: RootStore = useMobxStore();
|
||||
const { user: userStore, issueDetail: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||
const handlePeekMode = (_peek: TPeekModes) => {
|
||||
@ -81,12 +95,20 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId)
|
||||
issueDetailStore.fetchIssueDetails(workspaceSlug, projectId, issueId);
|
||||
}, [workspaceSlug, projectId, issueId, peekIssueId, issueDetailStore]);
|
||||
useSWR(
|
||||
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
|
||||
? `ISSUE_PEEK_OVERVIEW_${workspaceSlug}_${projectId}_${peekIssueId}`
|
||||
: null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) {
|
||||
await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, issueId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const issue = issueDetailStore.getIssue;
|
||||
const issueReactions = issueDetailStore.getIssueReactions;
|
||||
const user = userStore?.currentUser;
|
||||
|
||||
return (
|
||||
<div className="w-full !text-base">
|
||||
@ -96,14 +118,14 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
|
||||
{issueId === peekIssueId && (
|
||||
<div
|
||||
className={`fixed z-50 overflow-hidden bg-custom-background-80 flex flex-col transition-all duration-200 border border-custom-border-200 rounded shadow-custom-shadow-2xl
|
||||
className={`fixed z-50 overflow-hidden bg-custom-background-80 flex flex-col transition-all duration-300 border border-custom-border-200 rounded shadow-custom-shadow-2xl
|
||||
${peekMode === "side-peek" ? `w-full md:w-[50%] top-0 right-0 bottom-0` : ``}
|
||||
${peekMode === "modal" ? `top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] w-5/6 h-5/6` : ``}
|
||||
${peekMode === "full-screen" ? `top-0 right-0 bottom-0 left-0 m-4` : ``}
|
||||
`}
|
||||
>
|
||||
{/* header */}
|
||||
<div className="flex-shrink-0 w-full p-3 py-2.5 relative flex items-center gap-2 border-b border-custom-border-200">
|
||||
<div className="flex-shrink-0 w-full p-4 py-3 relative flex items-center gap-2 border-b border-custom-border-200">
|
||||
<div
|
||||
className="flex-shrink-0 overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100"
|
||||
onClick={removeRoutePeekId}
|
||||
@ -156,10 +178,16 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
issue && (
|
||||
<>
|
||||
{["side-peek", "modal"].includes(peekMode) ? (
|
||||
<div className="space-y-8 p-3 py-5">
|
||||
<PeekOverviewIssueDetails workspaceSlug={workspaceSlug} issue={issue} issueUpdate={issueUpdate} />
|
||||
|
||||
{/* reactions */}
|
||||
<div className="space-y-8 p-4 py-5">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
/>
|
||||
|
||||
<PeekOverviewProperties
|
||||
issue={issue}
|
||||
@ -169,22 +197,24 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
priorities={priorities}
|
||||
/>
|
||||
|
||||
{/* activity */}
|
||||
{/* <div className="border border-red-500">Activity</div> */}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex">
|
||||
<div className="w-full h-full space-y-8 p-3 py-5">
|
||||
<div className="w-full h-full space-y-8 p-4 py-5">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
issue={issue}
|
||||
issueReactions={issueReactions}
|
||||
issueUpdate={issueUpdate}
|
||||
user={user}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
/>
|
||||
|
||||
{/* reactions */}
|
||||
|
||||
{/* activity */}
|
||||
{/* <div className="border border-red-500">Activity</div> */}
|
||||
</div>
|
||||
<div className="flex-shrink-0 !w-[400px] h-full border-l border-custom-border-200 p-3 py-5">
|
||||
<div className="flex-shrink-0 !w-[400px] h-full border-l border-custom-border-200 p-4 py-5">
|
||||
<PeekOverviewProperties
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
|
@ -393,3 +393,25 @@ export const getValueFromObject = (object: Object, key: string): string | number
|
||||
for (const _key of keys) value = value[_key];
|
||||
return value;
|
||||
};
|
||||
|
||||
// issue reactions
|
||||
export const issueReactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
|
||||
|
||||
export const groupReactionEmojis = (reactions: any) => {
|
||||
let _groupedEmojis: any = {};
|
||||
|
||||
issueReactionEmojis.map((_r) => {
|
||||
_groupedEmojis = { ..._groupedEmojis, [_r]: [] };
|
||||
});
|
||||
|
||||
if (reactions && reactions.length > 0) {
|
||||
reactions.map((_reaction: any) => {
|
||||
_groupedEmojis = {
|
||||
..._groupedEmojis,
|
||||
[_reaction.reaction]: [..._groupedEmojis[_reaction.reaction], _reaction],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return _groupedEmojis;
|
||||
};
|
||||
|
@ -1,31 +1,40 @@
|
||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
import { IssueService, IssueReactionService } from "services/issue";
|
||||
// types
|
||||
import { RootStore } from "../root";
|
||||
import { IUser, IIssue } from "types";
|
||||
|
||||
export type IPeekMode = "side" | "modal" | "full";
|
||||
// constants
|
||||
import { groupReactionEmojis } from "constants/issue";
|
||||
|
||||
export interface IIssueDetailStore {
|
||||
loader: boolean;
|
||||
error: any | null;
|
||||
|
||||
peekId: string | null;
|
||||
peekMode: IPeekMode | null;
|
||||
|
||||
issues: {
|
||||
[issueId: string]: IIssue;
|
||||
};
|
||||
issue_reactions: {
|
||||
[issueId: string]: any;
|
||||
};
|
||||
issue_comments: {
|
||||
[issueId: string]: any;
|
||||
};
|
||||
issue_comment_reactions: {
|
||||
[issueId: string]: any;
|
||||
};
|
||||
|
||||
setPeekId: (issueId: string | null) => void;
|
||||
setPeekMode: (issueId: IPeekMode | null) => void;
|
||||
|
||||
// computed
|
||||
getIssue: IIssue | null;
|
||||
getIssueReactions: any | null;
|
||||
getIssueComments: any | null;
|
||||
getIssueCommentReactions: any | null;
|
||||
|
||||
// fetch issue details
|
||||
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => void;
|
||||
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
||||
// creating issue
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => Promise<IIssue>;
|
||||
// updating issue
|
||||
@ -38,6 +47,44 @@ export interface IIssueDetailStore {
|
||||
) => void;
|
||||
// deleting issue
|
||||
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => void;
|
||||
|
||||
fetchPeekIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
||||
|
||||
fetchIssueReactions: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
createIssueReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise<void>;
|
||||
removeIssueReaction: (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => Promise<void>;
|
||||
|
||||
fetchIssueComments: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
createIssueComment: (workspaceSlug: string, projectId: string, issueId: string, data: any) => Promise<void>;
|
||||
updateIssueComment: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
data: any
|
||||
) => Promise<void>;
|
||||
removeIssueComment: (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => Promise<void>;
|
||||
|
||||
fetchIssueCommentReactions: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string
|
||||
) => Promise<void>;
|
||||
addIssueCommentReaction: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
reaction: string
|
||||
) => Promise<void>;
|
||||
removeIssueCommentReaction: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
reaction: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class IssueDetailStore implements IIssueDetailStore {
|
||||
@ -45,16 +92,24 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
error: any | null = null;
|
||||
|
||||
peekId: string | null = null;
|
||||
peekMode: IPeekMode | null = null;
|
||||
|
||||
issues: {
|
||||
[issueId: string]: IIssue;
|
||||
} = {};
|
||||
issue_reactions: {
|
||||
[issueId: string]: any;
|
||||
} = {};
|
||||
issue_comments: {
|
||||
[issueId: string]: any;
|
||||
} = {};
|
||||
issue_comment_reactions: {
|
||||
[issueId: string]: any;
|
||||
} = {};
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
// service
|
||||
issueService;
|
||||
issueReactionService;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
@ -63,23 +118,42 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
error: observable.ref,
|
||||
|
||||
peekId: observable.ref,
|
||||
peekMode: observable.ref,
|
||||
|
||||
issues: observable.ref,
|
||||
issue_reactions: observable.ref,
|
||||
issue_comments: observable.ref,
|
||||
issue_comment_reactions: observable.ref,
|
||||
|
||||
getIssue: computed,
|
||||
getIssueReactions: computed,
|
||||
getIssueComments: computed,
|
||||
getIssueCommentReactions: computed,
|
||||
|
||||
setPeekId: action,
|
||||
setPeekMode: action,
|
||||
|
||||
fetchIssueDetails: action,
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
deleteIssue: action,
|
||||
|
||||
fetchPeekIssueDetails: action,
|
||||
|
||||
fetchIssueReactions: action,
|
||||
createIssueReaction: action,
|
||||
removeIssueReaction: action,
|
||||
|
||||
fetchIssueComments: action,
|
||||
createIssueComment: action,
|
||||
updateIssueComment: action,
|
||||
removeIssueComment: action,
|
||||
|
||||
fetchIssueCommentReactions: action,
|
||||
addIssueCommentReaction: action,
|
||||
removeIssueCommentReaction: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
this.issueService = new IssueService();
|
||||
this.issueReactionService = new IssueReactionService();
|
||||
}
|
||||
|
||||
get getIssue() {
|
||||
@ -88,9 +162,25 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
return _issue || null;
|
||||
}
|
||||
|
||||
setPeekId = (issueId: string | null) => (this.peekId = issueId);
|
||||
get getIssueReactions() {
|
||||
if (!this.peekId) return null;
|
||||
const _reactions = this.issue_reactions[this.peekId];
|
||||
return _reactions || null;
|
||||
}
|
||||
|
||||
setPeekMode = (mode: IPeekMode | null) => (this.peekMode = mode);
|
||||
get getIssueComments() {
|
||||
if (!this.peekId) return null;
|
||||
const _comments = this.issue_comments[this.peekId];
|
||||
return _comments || null;
|
||||
}
|
||||
|
||||
get getIssueCommentReactions() {
|
||||
if (!this.peekId) return null;
|
||||
const _reactions = this.issue_comment_reactions[this.peekId];
|
||||
return _reactions || null;
|
||||
}
|
||||
|
||||
setPeekId = (issueId: string | null) => (this.peekId = issueId);
|
||||
|
||||
fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
@ -108,13 +198,15 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
[issueId]: issueDetailsResponse,
|
||||
};
|
||||
});
|
||||
|
||||
return issueDetailsResponse;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
return error;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@ -218,4 +310,187 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
fetchPeekIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
this.loader = true;
|
||||
this.error = null;
|
||||
|
||||
this.peekId = issueId;
|
||||
|
||||
const issueDetailsResponse = await this.issueService.retrieve(workspaceSlug, projectId, issueId);
|
||||
await this.fetchIssueReactions(workspaceSlug, projectId, issueId);
|
||||
await this.fetchIssueComments(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
this.issues = {
|
||||
...this.issues,
|
||||
[issueId]: issueDetailsResponse,
|
||||
};
|
||||
});
|
||||
|
||||
return issueDetailsResponse;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// reactions
|
||||
fetchIssueReactions = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const _reactions = await this.issueReactionService.listIssueReactions(workspaceSlug, projectId, issueId);
|
||||
|
||||
const _issue_reactions = {
|
||||
...this.issue_reactions,
|
||||
[issueId]: groupReactionEmojis(_reactions),
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
this.issue_reactions = _issue_reactions;
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("error creating the issue reaction", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
createIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => {
|
||||
let _currentReactions = this.getIssueReactions;
|
||||
|
||||
try {
|
||||
const _reaction = await this.issueReactionService.createIssueReaction(workspaceSlug, projectId, issueId, {
|
||||
reaction,
|
||||
});
|
||||
|
||||
_currentReactions = {
|
||||
..._currentReactions,
|
||||
[reaction]: [..._currentReactions[reaction], { ..._reaction }],
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
this.issue_reactions = {
|
||||
...this.issue_reactions,
|
||||
[issueId]: _currentReactions,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.issue_reactions = {
|
||||
...this.issue_reactions,
|
||||
[issueId]: _currentReactions,
|
||||
};
|
||||
});
|
||||
console.warn("error creating the issue reaction", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reaction: string) => {
|
||||
let _currentReactions = this.getIssueReactions;
|
||||
|
||||
try {
|
||||
const user = this.rootStore.user.currentUser;
|
||||
|
||||
if (user) {
|
||||
_currentReactions = {
|
||||
..._currentReactions,
|
||||
[reaction]: [..._currentReactions[reaction].filter((r: any) => r.actor !== user.id)],
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
this.issue_reactions = {
|
||||
...this.issue_reactions,
|
||||
[issueId]: _currentReactions,
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueReactionService.deleteIssueReaction(workspaceSlug, projectId, issueId, reaction);
|
||||
}
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.issue_reactions = {
|
||||
...this.issue_reactions,
|
||||
[issueId]: _currentReactions,
|
||||
};
|
||||
});
|
||||
console.warn("error removing the issue reaction", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// comments
|
||||
fetchIssueComments = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn("error creating the issue comment", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
createIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, data: any) => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn("error creating the issue comment", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
updateIssueComment = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
data: any
|
||||
) => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn("error updating the issue comment", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
removeIssueComment = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn("error removing the issue comment", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// comment reaction
|
||||
fetchIssueCommentReactions = async (workspaceSlug: string, projectId: string, issueId: string, commentId: string) => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn("error removing the issue comment", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
addIssueCommentReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
reaction: string
|
||||
) => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn("error removing the issue comment", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
removeIssueCommentReaction = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
commentId: string,
|
||||
reaction: string
|
||||
) => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.warn("error removing the issue comment", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user