mirror of
https://github.com/makeplane/plane.git
synced 2025-12-25 08:09:33 +01:00
Space app Kanban block reactions (#5272)
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
//plane
|
||||
import { cn } from "@plane/editor";
|
||||
// components
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
|
||||
// hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
};
|
||||
export const BlockReactions = observer((props: Props) => {
|
||||
const { issueId } = props;
|
||||
const { anchor } = useParams();
|
||||
const { canVote, canReact } = usePublish(anchor.toString());
|
||||
|
||||
// if the user cannot vote or react then return empty
|
||||
if (!canVote && !canReact) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap border-t-[1px] outline-transparent w-full border-t-custom-border-200 bg-custom-background-90 rounded-b"
|
||||
)}
|
||||
>
|
||||
<div className="py-2 px-3 flex flex-wrap items-center gap-2">
|
||||
{canVote && (
|
||||
<div
|
||||
className={cn(`flex items-center gap-2 pr-1`, {
|
||||
"after:h-6 after:ml-1 after:w-[1px] after:bg-custom-border-200": canReact,
|
||||
})}
|
||||
>
|
||||
<IssueVotes anchor={anchor.toString()} issueIdFromProps={issueId} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
{canReact && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<IssueEmojiReactions anchor={anchor.toString()} issueIdFromProps={issueId} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -14,10 +14,11 @@ import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish } from "@/hooks/store";
|
||||
|
||||
//
|
||||
import { IIssue } from "@/types/issue";
|
||||
import { IssueProperties } from "../properties/all-properties";
|
||||
import { getIssueBlockId } from "../utils";
|
||||
import { BlockReactions } from "./block-reactions";
|
||||
|
||||
interface IssueBlockProps {
|
||||
issueId: string;
|
||||
@@ -83,17 +84,22 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div className={cn("group/kanban-block relative p-1.5")}>
|
||||
<Link
|
||||
id={getIssueBlockId(issueId, groupId, subGroupId)}
|
||||
<div
|
||||
className={cn(
|
||||
"block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
"relative block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id) }
|
||||
)}
|
||||
href={`?${queryParam}`}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<KanbanIssueDetailsBlock issue={issue} displayProperties={displayProperties} />
|
||||
</Link>
|
||||
<Link
|
||||
id={getIssueBlockId(issueId, groupId, subGroupId)}
|
||||
className="w-full"
|
||||
href={`?${queryParam}`}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<KanbanIssueDetailsBlock issue={issue} displayProperties={displayProperties} />
|
||||
</Link>
|
||||
<BlockReactions issueId={issueId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,5 +7,3 @@ export * from "./issue-properties";
|
||||
export * from "./layout";
|
||||
export * from "./side-peek-view";
|
||||
export * from "./issue-reaction";
|
||||
export * from "./issue-vote-reactions";
|
||||
export * from "./issue-emoji-reactions";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/peek-overview";
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
|
||||
// hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
2
space/core/components/issues/reactions/index.ts
Normal file
2
space/core/components/issues/reactions/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./issue-emoji-reactions";
|
||||
export * from "./issue-vote-reactions";
|
||||
@@ -13,10 +13,12 @@ import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
|
||||
type IssueEmojiReactionsProps = {
|
||||
anchor: string;
|
||||
issueIdFromProps?: string;
|
||||
size?: "md" | "sm";
|
||||
};
|
||||
|
||||
export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
const { anchor, issueIdFromProps, size = "md" } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
@@ -31,7 +33,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||
const issueDetailsStore = useIssueDetails();
|
||||
const { data: user } = useUser();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
const issueId = issueIdFromProps ?? issueDetailsStore.peekId;
|
||||
const reactions = issueDetailsStore.details[issueId ?? ""]?.reaction_items ?? [];
|
||||
const groupedReactions = groupReactions(reactions, "reaction");
|
||||
|
||||
@@ -55,6 +57,7 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||
|
||||
// derived values
|
||||
const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels });
|
||||
const reactionDimensions = size === "sm" ? "h-6 px-2 py-1" : "h-full px-2 py-1";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,54 +67,52 @@ export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer(
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
selected={userReactions?.map((r) => r.reaction)}
|
||||
size="md"
|
||||
size={size}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
const REACTIONS_LIMIT = 1000;
|
||||
{Object.keys(groupedReactions || {}).map((reaction) => {
|
||||
const reactions = groupedReactions?.[reaction] ?? [];
|
||||
const REACTIONS_LIMIT = 1000;
|
||||
|
||||
if (reactions.length > 0)
|
||||
return (
|
||||
<Tooltip
|
||||
key={reaction}
|
||||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
?.map((r) => r?.actor_details?.display_name)
|
||||
?.splice(0, REACTIONS_LIMIT)
|
||||
?.join(", ")}
|
||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||
</div>
|
||||
}
|
||||
if (reactions.length > 0)
|
||||
return (
|
||||
<Tooltip
|
||||
key={reaction}
|
||||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
?.map((r) => r?.actor_details?.display_name)
|
||||
?.splice(0, REACTIONS_LIMIT)
|
||||
?.join(", ")}
|
||||
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (user) handleReactionClick(reaction);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex items-center gap-1 rounded-md text-sm text-custom-text-100 ${
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
} ${reactionDimensions}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (user) handleReactionClick(reaction);
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
reactions.some((r) => r?.actor_details?.id === user?.id && r.reaction === reaction)
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -13,10 +13,12 @@ import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type TIssueVotes = {
|
||||
anchor: string;
|
||||
issueIdFromProps?: string;
|
||||
size?: "md" | "sm";
|
||||
};
|
||||
|
||||
export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
const { anchor } = props;
|
||||
const { anchor, issueIdFromProps, size = "md" } = props;
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// router
|
||||
@@ -35,7 +37,7 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
|
||||
const isInIframe = useIsInIframe();
|
||||
|
||||
const issueId = issueDetailsStore.peekId;
|
||||
const issueId = issueIdFromProps ?? issueDetailsStore.peekId;
|
||||
|
||||
const votes = issueDetailsStore.details[issueId ?? ""]?.vote_items ?? [];
|
||||
|
||||
@@ -66,6 +68,7 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
|
||||
// derived values
|
||||
const { queryParam } = queryParamGenerator({ peekId, board, state, priority, labels });
|
||||
const votingDimensions = size === "sm" ? "px-1 h-6 min-w-9" : "px-2 h-7";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -96,7 +99,8 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border px-2 h-7 focus:outline-none",
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border focus:outline-none bg-custom-background-100",
|
||||
votingDimensions,
|
||||
{
|
||||
"border-custom-primary-200 text-custom-primary-200": isUpVotedByUser,
|
||||
"border-custom-border-300": !isUpVotedByUser,
|
||||
@@ -136,7 +140,8 @@ export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
|
||||
else router.push(`/?next_path=${pathName}?${queryParam}`);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-1 h-7 overflow-hidden rounded border px-2 focus:outline-none",
|
||||
"flex items-center justify-center gap-x-1 overflow-hidden rounded border focus:outline-none bg-custom-background-100",
|
||||
votingDimensions,
|
||||
{
|
||||
"border-red-600 text-red-600": isDownVotedByUser,
|
||||
"border-custom-border-300": !isDownVotedByUser,
|
||||
Reference in New Issue
Block a user