mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: speeding up reactions and votes 🚀
This commit is contained in:
parent
f71a62f142
commit
1732945ec6
@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
// constants
|
||||
import { issueGroupFilter } from "constants/data";
|
||||
|
||||
@ -8,8 +6,8 @@ export const IssueBlockState = ({ state }: any) => {
|
||||
|
||||
if (stateGroup === null) return <></>;
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-1 w-full rounded shadow-sm border-[0.5px] border-custom-border-300 duration-300 focus:outline-none px-2.5 py-1 text-xs cursor-pointer hover:bg-custom-background-80">
|
||||
<div className="flex items-center cursor-pointer w-full gap-1.5 text-custom-text-200">
|
||||
<div className="flex items-center justify-between gap-1 w-full rounded shadow-sm border-[0.5px] border-custom-border-300 duration-300 focus:outline-none px-2.5 py-1 text-xs">
|
||||
<div className="flex items-center w-full gap-1.5 text-custom-text-200">
|
||||
<stateGroup.icon />
|
||||
<div className="text-xs">{state?.name}</div>
|
||||
</div>
|
||||
|
@ -1,25 +1,32 @@
|
||||
"use client";
|
||||
|
||||
// next theme
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const NavbarTheme = observer(() => {
|
||||
const [appTheme, setAppTheme] = useState("light");
|
||||
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
const handleTheme = () => {
|
||||
setTheme(theme === "light" ? "dark" : "light");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!theme) return;
|
||||
|
||||
setAppTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTheme}
|
||||
className="relative w-7 h-7 grid place-items-center bg-custom-background-100 hover:bg-custom-background-80 text-custom-text-100 rounded"
|
||||
>
|
||||
<span className="material-symbols-rounded text-sm">{theme === "light" ? "dark_mode" : "light_mode"}</span>
|
||||
<span className="material-symbols-rounded text-sm">{appTheme === "light" ? "dark_mode" : "light_mode"}</span>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
@ -70,7 +70,7 @@ export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
{issueDetailStore.peekMode === "side" && (
|
||||
<button type="button" onClick={handleClose} autoFocus={false}>
|
||||
<button type="button" onClick={handleClose}>
|
||||
<East
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
@ -134,7 +134,7 @@ export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
{(issueDetailStore.peekMode === "side" || issueDetailStore.peekMode === "modal") && (
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
||||
<button type="button" onClick={handleCopyLink} className="-rotate-45 focus:outline-none" tabIndex={1}>
|
||||
<Icon iconName="link" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -47,7 +47,7 @@ export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="bg-custom-background-80 px-2 py-2.5 flex items-center justify-between gap-2 border border-custom-border-300 rounded">
|
||||
<div className="bg-custom-background-80 px-2 py-2.5 flex items-center justify-between gap-2 border border-custom-border-300 rounded mt-4">
|
||||
<p className="flex gap-2 text-sm text-custom-text-200 break-words overflow-hidden">
|
||||
<Icon iconName="lock" className="!text-sm" />
|
||||
Sign in to add your comment
|
||||
|
@ -24,9 +24,7 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
||||
if (!workspace_slug || !project_slug || !issueId) return;
|
||||
const userReaction = reactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||
if (userReaction) return;
|
||||
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, {
|
||||
reaction: reactionHex,
|
||||
});
|
||||
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||
};
|
||||
|
||||
const handleReactionClick = (reactionHex: string) => {
|
||||
@ -57,6 +55,7 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
||||
if (reactions.length > 0)
|
||||
return (
|
||||
<Tooltip
|
||||
key={reaction}
|
||||
tooltipContent={
|
||||
<div>
|
||||
{reactions
|
||||
@ -74,7 +73,6 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
||||
handleReactionClick(reaction);
|
||||
});
|
||||
}}
|
||||
key={reaction}
|
||||
className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
|
||||
reactions?.some((r) => r.actor_detail.id === user?.id && r.reaction === reaction)
|
||||
? "bg-custom-primary-100/10"
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { makeObservable, observable, action, runInAction } from "mobx";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// store
|
||||
import { RootStore } from "./root";
|
||||
// services
|
||||
import IssueService from "services/issue.service";
|
||||
import { IIssue } from "types/issue";
|
||||
import { IIssue, IVote } from "types/issue";
|
||||
|
||||
export type IPeekMode = "side" | "modal" | "full";
|
||||
|
||||
@ -32,8 +33,8 @@ export interface IIssueDetailStore {
|
||||
) => Promise<any>;
|
||||
deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void;
|
||||
// issue reactions
|
||||
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, data: any) => void;
|
||||
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionId: string) => void;
|
||||
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
||||
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
||||
// issue votes
|
||||
addIssueVote: (workspaceId: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => Promise<void>;
|
||||
removeIssueVote: (workspaceId: string, projectId: string, issueId: string) => Promise<void>;
|
||||
@ -174,84 +175,144 @@ class IssueDetailStore implements IssueDetailStore {
|
||||
}
|
||||
};
|
||||
|
||||
addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, data: any) => {
|
||||
addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
|
||||
try {
|
||||
const issueVoteResponse = await this.issueService.createIssueReaction(workspaceSlug, projectId, issueId, data);
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
reactions: [
|
||||
...this.details[issueId].reactions,
|
||||
{
|
||||
id: uuidv4(),
|
||||
issue: issueId,
|
||||
reaction: reactionHex,
|
||||
actor_detail: this.rootStore.user.currentActor,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (issueVoteResponse) {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
reactions: [...this.details[issueId].reactions, issueVoteResponse],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
await this.issueService.createIssueReaction(workspaceSlug, projectId, issueId, {
|
||||
reaction: reactionHex,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Failed to add issue vote");
|
||||
const issueReactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId);
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
reactions: issueReactions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionId: string) => {
|
||||
removeIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
|
||||
try {
|
||||
await this.issueService.deleteIssueReaction(workspaceSlug, projectId, issueId, reactionId);
|
||||
const reactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId);
|
||||
const newReactions = this.details[issueId].reactions.filter(
|
||||
(_r) => !(_r.reaction === reactionHex && _r.actor_detail.id === this.rootStore.user.currentUser?.id)
|
||||
);
|
||||
|
||||
if (reactions) {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
reactions: reactions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
reactions: newReactions,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueService.deleteIssueReaction(workspaceSlug, projectId, issueId, reactionHex);
|
||||
} catch (error) {
|
||||
console.log("Failed to remove issue reaction");
|
||||
const reactions = await this.issueService.getIssueReactions(workspaceSlug, projectId, issueId);
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
reactions: reactions,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addIssueVote = async (workspaceSlug: string, projectId: string, issueId: string, data: { vote: 1 | -1 }) => {
|
||||
try {
|
||||
const issueVoteResponse = await this.issueService.createIssueVote(workspaceSlug, projectId, issueId, data);
|
||||
const issueDetails = await this.issueService.getIssueById(workspaceSlug, projectId, issueId);
|
||||
const newVote: IVote = {
|
||||
actor: this.rootStore.user.currentUser?.id ?? "",
|
||||
actor_detail: this.rootStore.user.currentActor,
|
||||
issue: issueId,
|
||||
project: projectId,
|
||||
workspace: workspaceSlug,
|
||||
vote: data.vote,
|
||||
};
|
||||
|
||||
if (issueVoteResponse) {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...issueDetails,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
const filteredVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.currentUser?.id);
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
votes: [...filteredVotes, newVote],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueService.createIssueVote(workspaceSlug, projectId, issueId, data);
|
||||
} catch (error) {
|
||||
console.log("Failed to add issue vote");
|
||||
const issueVotes = await this.issueService.getIssueVotes(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
votes: issueVotes,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
removeIssueVote = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await this.issueService.deleteIssueVote(workspaceSlug, projectId, issueId);
|
||||
const issueDetails = await this.issueService.getIssueById(workspaceSlug, projectId, issueId);
|
||||
const newVotes = this.details[issueId].votes.filter((v) => v.actor !== this.rootStore.user.currentUser?.id);
|
||||
|
||||
if (issueDetails) {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...issueDetails,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
votes: newVotes,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await this.issueService.deleteIssueVote(workspaceSlug, projectId, issueId);
|
||||
} catch (error) {
|
||||
console.log("Failed to remove issue vote");
|
||||
const issueVotes = await this.issueService.getIssueVotes(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
this.details = {
|
||||
...this.details,
|
||||
[issueId]: {
|
||||
...this.details[issueId],
|
||||
votes: issueVotes,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -2,14 +2,18 @@
|
||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||
// service
|
||||
import UserService from "services/user.service";
|
||||
import { ActorDetail } from "types/issue";
|
||||
// types
|
||||
import { IUser } from "types/user";
|
||||
|
||||
export interface IUserStore {
|
||||
currentUser: any | null;
|
||||
fetchCurrentUser: () => void;
|
||||
currentActor: () => any;
|
||||
}
|
||||
|
||||
class UserStore implements IUserStore {
|
||||
currentUser: any | null = null;
|
||||
currentUser: IUser | null = null;
|
||||
// root store
|
||||
rootStore;
|
||||
// service
|
||||
@ -22,6 +26,7 @@ class UserStore implements IUserStore {
|
||||
// actions
|
||||
setCurrentUser: action,
|
||||
// computed
|
||||
currentActor: computed,
|
||||
});
|
||||
this.rootStore = _rootStore;
|
||||
this.userService = new UserService();
|
||||
@ -33,6 +38,17 @@ class UserStore implements IUserStore {
|
||||
});
|
||||
};
|
||||
|
||||
get currentActor(): any {
|
||||
return {
|
||||
avatar: this.currentUser?.avatar,
|
||||
display_name: this.currentUser?.display_name,
|
||||
first_name: this.currentUser?.first_name,
|
||||
id: this.currentUser?.id,
|
||||
is_bot: false,
|
||||
last_name: this.currentUser?.last_name,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param callback
|
||||
|
@ -42,14 +42,7 @@ export interface IIssue {
|
||||
state: string;
|
||||
state_detail: any;
|
||||
target_date: any;
|
||||
votes: {
|
||||
issue: string;
|
||||
vote: -1 | 1;
|
||||
workspace: string;
|
||||
project: string;
|
||||
actor: string;
|
||||
actor_detail: ActorDetail;
|
||||
}[];
|
||||
votes: IVote[];
|
||||
}
|
||||
|
||||
export interface IIssueState {
|
||||
@ -65,6 +58,15 @@ export interface IIssueLabel {
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface IVote {
|
||||
issue: string;
|
||||
vote: -1 | 1;
|
||||
workspace: string;
|
||||
project: string;
|
||||
actor: string;
|
||||
actor_detail: ActorDetail;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: string;
|
||||
actor_detail: ActorDetail;
|
||||
@ -95,12 +97,12 @@ export interface IIssueReaction {
|
||||
}
|
||||
|
||||
export interface ActorDetail {
|
||||
avatar: string;
|
||||
display_name: string;
|
||||
first_name: string;
|
||||
id: string;
|
||||
is_bot: boolean;
|
||||
last_name: string;
|
||||
avatar?: string;
|
||||
display_name?: string;
|
||||
first_name?: string;
|
||||
id?: string;
|
||||
is_bot?: boolean;
|
||||
last_name?: string;
|
||||
}
|
||||
|
||||
export interface IssueDetail {
|
||||
|
@ -1,3 +1,23 @@
|
||||
export interface IUser {
|
||||
avatar: string;
|
||||
cover_image: string | null;
|
||||
created_at: Date;
|
||||
created_location: string;
|
||||
date_joined: Date;
|
||||
email: string;
|
||||
display_name: string;
|
||||
first_name: string;
|
||||
id: string;
|
||||
is_email_verified: boolean;
|
||||
is_onboarded: boolean;
|
||||
is_tour_completed: boolean;
|
||||
last_location: string;
|
||||
last_login: Date;
|
||||
last_name: string;
|
||||
mobile_number: string;
|
||||
role: string;
|
||||
token: string;
|
||||
updated_at: Date;
|
||||
username: string;
|
||||
user_timezone: string;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user