Space app Kanban block reactions (#5272)

This commit is contained in:
rahulramesha
2024-07-30 19:32:24 +05:30
committed by GitHub
parent 1f8f6d1b26
commit fffa8648bb
7 changed files with 117 additions and 60 deletions

View File

@@ -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>
);
});

View File

@@ -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>
);
});

View File

@@ -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";

View File

@@ -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";

View File

@@ -0,0 +1,2 @@
export * from "./issue-emoji-reactions";
export * from "./issue-vote-reactions";

View File

@@ -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>
);
})}
</>
);
});

View File

@@ -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,