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";
|
import { FC } from "react";
|
||||||
// packages
|
// packages
|
||||||
import { RichTextEditor } from "@plane/rich-text-editor";
|
import { RichTextEditor } from "@plane/rich-text-editor";
|
||||||
|
// components
|
||||||
|
import { IssueReaction } from "./reactions";
|
||||||
// hooks
|
// hooks
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
// types
|
// types
|
||||||
@ -13,24 +15,29 @@ const fileService = new FileService();
|
|||||||
interface IPeekOverviewIssueDetails {
|
interface IPeekOverviewIssueDetails {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
issueReactions: any;
|
||||||
|
user: any;
|
||||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||||
|
issueReactionCreate: (reaction: string) => void;
|
||||||
|
issueReactionRemove: (reaction: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
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) => {
|
const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => {
|
||||||
issueUpdate({ ...issue, description_html: _data });
|
issueUpdate({ ...issue, description_html: _data });
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
<div className="font-medium text-sm text-custom-text-200">
|
<div className="font-medium text-sm text-custom-text-200">
|
||||||
{issue?.project_detail?.identifier}-{issue?.sequence_id}
|
{issue?.project_detail?.identifier}-{issue?.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="font-medium text-xl">{issue?.name}</div>
|
<div className="font-medium text-xl">{issue?.name}</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
@ -40,6 +47,14 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
debouncedIssueDescription(description_html);
|
debouncedIssueDescription(description_html);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<IssueReaction
|
||||||
|
issueReactions={issueReactions}
|
||||||
|
user={user}
|
||||||
|
issueReactionCreate={issueReactionCreate}
|
||||||
|
issueReactionRemove={issueReactionRemove}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</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 (
|
return (
|
||||||
<IssueView
|
<IssueView
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
@ -43,6 +51,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
members={members}
|
members={members}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
issueUpdate={issueUpdate}
|
issueUpdate={issueUpdate}
|
||||||
|
issueReactionCreate={issueReactionCreate}
|
||||||
|
issueReactionRemove={issueReactionRemove}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</IssueView>
|
</IssueView>
|
||||||
|
@ -2,6 +2,7 @@ import { FC, ReactNode, useEffect, useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Maximize2, ArrowRight, Link, Trash, PanelRightOpen, Square, SquareCode } from "lucide-react";
|
import { Maximize2, ArrowRight, Link, Trash, PanelRightOpen, Square, SquareCode } from "lucide-react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
import { PeekOverviewIssueDetails } from "./issue-detail";
|
import { PeekOverviewIssueDetails } from "./issue-detail";
|
||||||
import { PeekOverviewProperties } from "./properties";
|
import { PeekOverviewProperties } from "./properties";
|
||||||
@ -16,6 +17,8 @@ interface IIssueView {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||||
|
issueReactionCreate: (reaction: string) => void;
|
||||||
|
issueReactionRemove: (reaction: string) => void;
|
||||||
states: any;
|
states: any;
|
||||||
members: any;
|
members: any;
|
||||||
priorities: any;
|
priorities: any;
|
||||||
@ -43,12 +46,23 @@ const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const IssueView: FC<IIssueView> = observer((props) => {
|
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 router = useRouter();
|
||||||
const { peekIssueId } = router.query as { peekIssueId: string };
|
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 [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||||
const handlePeekMode = (_peek: TPeekModes) => {
|
const handlePeekMode = (_peek: TPeekModes) => {
|
||||||
@ -81,12 +95,20 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useSWR(
|
||||||
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId)
|
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
|
||||||
issueDetailStore.fetchIssueDetails(workspaceSlug, projectId, issueId);
|
? `ISSUE_PEEK_OVERVIEW_${workspaceSlug}_${projectId}_${peekIssueId}`
|
||||||
}, [workspaceSlug, projectId, issueId, peekIssueId, issueDetailStore]);
|
: null,
|
||||||
|
async () => {
|
||||||
|
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) {
|
||||||
|
await issueDetailStore.fetchPeekIssueDetails(workspaceSlug, projectId, issueId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const issue = issueDetailStore.getIssue;
|
const issue = issueDetailStore.getIssue;
|
||||||
|
const issueReactions = issueDetailStore.getIssueReactions;
|
||||||
|
const user = userStore?.currentUser;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full !text-base">
|
<div className="w-full !text-base">
|
||||||
@ -96,14 +118,14 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
|
|
||||||
{issueId === peekIssueId && (
|
{issueId === peekIssueId && (
|
||||||
<div
|
<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 === "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 === "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` : ``}
|
${peekMode === "full-screen" ? `top-0 right-0 bottom-0 left-0 m-4` : ``}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* header */}
|
{/* 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
|
<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"
|
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}
|
onClick={removeRoutePeekId}
|
||||||
@ -156,10 +178,16 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
issue && (
|
issue && (
|
||||||
<>
|
<>
|
||||||
{["side-peek", "modal"].includes(peekMode) ? (
|
{["side-peek", "modal"].includes(peekMode) ? (
|
||||||
<div className="space-y-8 p-3 py-5">
|
<div className="space-y-8 p-4 py-5">
|
||||||
<PeekOverviewIssueDetails workspaceSlug={workspaceSlug} issue={issue} issueUpdate={issueUpdate} />
|
<PeekOverviewIssueDetails
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
{/* reactions */}
|
issue={issue}
|
||||||
|
issueUpdate={issueUpdate}
|
||||||
|
issueReactions={issueReactions}
|
||||||
|
user={user}
|
||||||
|
issueReactionCreate={issueReactionCreate}
|
||||||
|
issueReactionRemove={issueReactionRemove}
|
||||||
|
/>
|
||||||
|
|
||||||
<PeekOverviewProperties
|
<PeekOverviewProperties
|
||||||
issue={issue}
|
issue={issue}
|
||||||
@ -169,22 +197,24 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* activity */}
|
{/* <div className="border border-red-500">Activity</div> */}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex">
|
<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
|
<PeekOverviewIssueDetails
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
|
issueReactions={issueReactions}
|
||||||
issueUpdate={issueUpdate}
|
issueUpdate={issueUpdate}
|
||||||
|
user={user}
|
||||||
|
issueReactionCreate={issueReactionCreate}
|
||||||
|
issueReactionRemove={issueReactionRemove}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* reactions */}
|
{/* <div className="border border-red-500">Activity</div> */}
|
||||||
|
|
||||||
{/* 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
|
<PeekOverviewProperties
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueUpdate={issueUpdate}
|
issueUpdate={issueUpdate}
|
||||||
|
@ -393,3 +393,25 @@ export const getValueFromObject = (object: Object, key: string): string | number
|
|||||||
for (const _key of keys) value = value[_key];
|
for (const _key of keys) value = value[_key];
|
||||||
return value;
|
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";
|
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "services/issue";
|
import { IssueService, IssueReactionService } from "services/issue";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "../root";
|
import { RootStore } from "../root";
|
||||||
import { IUser, IIssue } from "types";
|
import { IUser, IIssue } from "types";
|
||||||
|
// constants
|
||||||
export type IPeekMode = "side" | "modal" | "full";
|
import { groupReactionEmojis } from "constants/issue";
|
||||||
|
|
||||||
export interface IIssueDetailStore {
|
export interface IIssueDetailStore {
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
error: any | null;
|
error: any | null;
|
||||||
|
|
||||||
peekId: string | null;
|
peekId: string | null;
|
||||||
peekMode: IPeekMode | null;
|
|
||||||
|
|
||||||
issues: {
|
issues: {
|
||||||
[issueId: string]: IIssue;
|
[issueId: string]: IIssue;
|
||||||
};
|
};
|
||||||
|
issue_reactions: {
|
||||||
|
[issueId: string]: any;
|
||||||
|
};
|
||||||
|
issue_comments: {
|
||||||
|
[issueId: string]: any;
|
||||||
|
};
|
||||||
|
issue_comment_reactions: {
|
||||||
|
[issueId: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
setPeekId: (issueId: string | null) => void;
|
setPeekId: (issueId: string | null) => void;
|
||||||
setPeekMode: (issueId: IPeekMode | null) => void;
|
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
getIssue: IIssue | null;
|
getIssue: IIssue | null;
|
||||||
|
getIssueReactions: any | null;
|
||||||
|
getIssueComments: any | null;
|
||||||
|
getIssueCommentReactions: any | null;
|
||||||
|
|
||||||
// fetch issue details
|
// fetch issue details
|
||||||
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => void;
|
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
||||||
// creating issue
|
// creating issue
|
||||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => Promise<IIssue>;
|
createIssue: (workspaceSlug: string, projectId: string, data: Partial<IIssue>, user: IUser) => Promise<IIssue>;
|
||||||
// updating issue
|
// updating issue
|
||||||
@ -38,6 +47,44 @@ export interface IIssueDetailStore {
|
|||||||
) => void;
|
) => void;
|
||||||
// deleting issue
|
// deleting issue
|
||||||
deleteIssue: (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => void;
|
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 {
|
export class IssueDetailStore implements IIssueDetailStore {
|
||||||
@ -45,16 +92,24 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
error: any | null = null;
|
error: any | null = null;
|
||||||
|
|
||||||
peekId: string | null = null;
|
peekId: string | null = null;
|
||||||
peekMode: IPeekMode | null = null;
|
|
||||||
|
|
||||||
issues: {
|
issues: {
|
||||||
[issueId: string]: IIssue;
|
[issueId: string]: IIssue;
|
||||||
} = {};
|
} = {};
|
||||||
|
issue_reactions: {
|
||||||
|
[issueId: string]: any;
|
||||||
|
} = {};
|
||||||
|
issue_comments: {
|
||||||
|
[issueId: string]: any;
|
||||||
|
} = {};
|
||||||
|
issue_comment_reactions: {
|
||||||
|
[issueId: string]: any;
|
||||||
|
} = {};
|
||||||
|
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
// service
|
// service
|
||||||
issueService;
|
issueService;
|
||||||
|
issueReactionService;
|
||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
@ -63,23 +118,42 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
error: observable.ref,
|
error: observable.ref,
|
||||||
|
|
||||||
peekId: observable.ref,
|
peekId: observable.ref,
|
||||||
peekMode: observable.ref,
|
|
||||||
|
|
||||||
issues: observable.ref,
|
issues: observable.ref,
|
||||||
|
issue_reactions: observable.ref,
|
||||||
|
issue_comments: observable.ref,
|
||||||
|
issue_comment_reactions: observable.ref,
|
||||||
|
|
||||||
getIssue: computed,
|
getIssue: computed,
|
||||||
|
getIssueReactions: computed,
|
||||||
|
getIssueComments: computed,
|
||||||
|
getIssueCommentReactions: computed,
|
||||||
|
|
||||||
setPeekId: action,
|
setPeekId: action,
|
||||||
setPeekMode: action,
|
|
||||||
|
|
||||||
fetchIssueDetails: action,
|
fetchIssueDetails: action,
|
||||||
createIssue: action,
|
createIssue: action,
|
||||||
updateIssue: action,
|
updateIssue: action,
|
||||||
deleteIssue: 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.rootStore = _rootStore;
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
|
this.issueReactionService = new IssueReactionService();
|
||||||
}
|
}
|
||||||
|
|
||||||
get getIssue() {
|
get getIssue() {
|
||||||
@ -88,9 +162,25 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
return _issue || null;
|
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) => {
|
fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
@ -108,13 +198,15 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
[issueId]: issueDetailsResponse,
|
[issueId]: issueDetailsResponse,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return issueDetailsResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = false;
|
this.loader = false;
|
||||||
this.error = error;
|
this.error = error;
|
||||||
});
|
});
|
||||||
|
|
||||||
return error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -218,4 +310,187 @@ export class IssueDetailStore implements IIssueDetailStore {
|
|||||||
return error;
|
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