forked from github/plane
Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting
This commit is contained in:
commit
7c5936e463
@ -2100,6 +2100,12 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
|||||||
queryset=IssueReaction.objects.select_related("actor"),
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"votes",
|
||||||
|
queryset=IssueVote.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
.filter(**filters)
|
.filter(**filters)
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||||
.annotate(module_id=F("issue_module__module_id"))
|
.annotate(module_id=F("issue_module__module_id"))
|
||||||
|
@ -116,7 +116,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
issue_count = (
|
issue_count = (
|
||||||
Issue.objects.filter(workspace=OuterRef("id"))
|
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -203,7 +203,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
issue_count = (
|
issue_count = (
|
||||||
Issue.objects.filter(workspace=OuterRef("id"))
|
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -1075,7 +1075,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
|||||||
priority_order = ["urgent", "high", "medium", "low", None]
|
priority_order = ["urgent", "high", "medium", "low", None]
|
||||||
|
|
||||||
priority_distribution = (
|
priority_distribution = (
|
||||||
Issue.objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
assignees__in=[user_id],
|
assignees__in=[user_id],
|
||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
|
@ -32,7 +32,7 @@ def archive_old_issues():
|
|||||||
archive_in = project.archive_in
|
archive_in = project.archive_in
|
||||||
|
|
||||||
# Get all the issues whose updated_at in less that the archive_in month
|
# Get all the issues whose updated_at in less that the archive_in month
|
||||||
issues = Issue.objects.filter(
|
issues = Issue.issue_objects.filter(
|
||||||
Q(
|
Q(
|
||||||
project=project_id,
|
project=project_id,
|
||||||
archived_at__isnull=True,
|
archived_at__isnull=True,
|
||||||
@ -100,7 +100,7 @@ def close_old_issues():
|
|||||||
close_in = project.close_in
|
close_in = project.close_in
|
||||||
|
|
||||||
# Get all the issues whose updated_at in less that the close_in month
|
# Get all the issues whose updated_at in less that the close_in month
|
||||||
issues = Issue.objects.filter(
|
issues = Issue.issue_objects.filter(
|
||||||
Q(
|
Q(
|
||||||
project=project_id,
|
project=project_id,
|
||||||
archived_at__isnull=True,
|
archived_at__isnull=True,
|
||||||
|
@ -96,7 +96,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
|||||||
chart_data = {str(date): 0 for date in date_range}
|
chart_data = {str(date): 0 for date in date_range}
|
||||||
|
|
||||||
completed_issues_distribution = (
|
completed_issues_distribution = (
|
||||||
Issue.objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
issue_cycle__cycle_id=cycle_id,
|
issue_cycle__cycle_id=cycle_id,
|
||||||
@ -118,7 +118,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
|||||||
chart_data = {str(date): 0 for date in date_range}
|
chart_data = {str(date): 0 for date in date_range}
|
||||||
|
|
||||||
completed_issues_distribution = (
|
completed_issues_distribution = (
|
||||||
Issue.objects.filter(
|
Issue.issue_objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
issue_module__module_id=module_id,
|
issue_module__module_id=module_id,
|
||||||
|
@ -19,7 +19,6 @@ export const SignInView = observer(() => {
|
|||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { next_path } = router.query;
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -34,13 +33,15 @@ export const SignInView = observer(() => {
|
|||||||
const onSignInSuccess = (response: any) => {
|
const onSignInSuccess = (response: any) => {
|
||||||
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
||||||
|
|
||||||
|
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/";
|
||||||
|
|
||||||
userStore.setCurrentUser(response?.user);
|
userStore.setCurrentUser(response?.user);
|
||||||
|
|
||||||
if (!isOnboarded) {
|
if (!isOnboarded) {
|
||||||
router.push(`/onboarding?next_path=${next_path}`);
|
router.push(`/onboarding?next_path=${nextPath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push((next_path ?? "/").toString());
|
router.push((nextPath ?? "/").toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useState } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
|
||||||
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// react-hook-form
|
||||||
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
// headless ui
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
// lib
|
// lib
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { TipTapEditor } from "components/tiptap";
|
||||||
|
import { CommentReactions } from "components/issues/peek-overview";
|
||||||
// icons
|
// icons
|
||||||
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
import { ChatBubbleLeftEllipsisIcon, CheckIcon, XMarkIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { timeAgo } from "helpers/date-time.helper";
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { Comment } from "types/issue";
|
import { Comment } from "types/issue";
|
||||||
// components
|
|
||||||
import { TipTapEditor } from "components/tiptap";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -76,7 +81,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-xs text-custom-text-200">
|
<p className="mt-0.5 text-xs text-custom-text-200">
|
||||||
<>Commented {timeAgo(comment.created_at)}</>
|
<>commented {timeAgo(comment.created_at)}</>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="issue-comments-section p-0">
|
<div className="issue-comments-section p-0">
|
||||||
@ -125,6 +130,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
editable={false}
|
editable={false}
|
||||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||||
/>
|
/>
|
||||||
|
<CommentReactions commentId={comment.id} projectId={comment.project} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -0,0 +1,131 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// ui
|
||||||
|
import { ReactionSelector, Tooltip } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { groupReactions, renderEmoji } from "helpers/emoji.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
commentId: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CommentReactions: React.FC<Props> = observer((props) => {
|
||||||
|
const { commentId, projectId } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspace_slug } = router.query;
|
||||||
|
|
||||||
|
const { issueDetails: issueDetailsStore, user: userStore } = useMobxStore();
|
||||||
|
|
||||||
|
const peekId = issueDetailsStore.peekId;
|
||||||
|
const user = userStore.currentUser;
|
||||||
|
|
||||||
|
const commentReactions = peekId
|
||||||
|
? issueDetailsStore.details[peekId].comments.find((c) => c.id === commentId)?.comment_reactions
|
||||||
|
: [];
|
||||||
|
const groupedReactions = peekId ? groupReactions(commentReactions ?? [], "reaction") : {};
|
||||||
|
|
||||||
|
const userReactions = commentReactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||||
|
|
||||||
|
const handleAddReaction = (reactionHex: string) => {
|
||||||
|
if (!workspace_slug || !projectId || !peekId) return;
|
||||||
|
|
||||||
|
issueDetailsStore.addCommentReaction(
|
||||||
|
workspace_slug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
peekId,
|
||||||
|
commentId,
|
||||||
|
reactionHex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveReaction = (reactionHex: string) => {
|
||||||
|
if (!workspace_slug || !projectId || !peekId) return;
|
||||||
|
|
||||||
|
issueDetailsStore.removeCommentReaction(
|
||||||
|
workspace_slug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
peekId,
|
||||||
|
commentId,
|
||||||
|
reactionHex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReactionClick = (reactionHex: string) => {
|
||||||
|
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||||
|
|
||||||
|
if (userReaction) handleRemoveReaction(reactionHex);
|
||||||
|
else handleAddReaction(reactionHex);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1.5 items-center mt-2">
|
||||||
|
<ReactionSelector
|
||||||
|
onSelect={(value) => {
|
||||||
|
userStore.requiredLogin(() => {
|
||||||
|
handleReactionClick(value);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
position="top"
|
||||||
|
selected={userReactions?.map((r) => r.reaction)}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{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_detail.display_name)
|
||||||
|
.splice(0, REACTIONS_LIMIT)
|
||||||
|
.join(", ")}
|
||||||
|
{reactions.length > REACTIONS_LIMIT && " and " + (reactions.length - REACTIONS_LIMIT) + " more"}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
userStore.requiredLogin(() => {
|
||||||
|
handleReactionClick(reaction);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={`flex items-center gap-1 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
|
||||||
|
commentReactions?.some(
|
||||||
|
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
|
||||||
|
)
|
||||||
|
? "bg-custom-primary-100/10"
|
||||||
|
: "bg-custom-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{renderEmoji(reaction)}</span>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
commentReactions?.some(
|
||||||
|
(r) => r.actor_detail.id === userStore.currentUser?.id && r.reaction === reaction
|
||||||
|
)
|
||||||
|
? "text-custom-primary-100"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{groupedReactions?.[reaction].length}{" "}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
3
space/components/issues/peek-overview/comment/index.ts
Normal file
3
space/components/issues/peek-overview/comment/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./add-comment";
|
||||||
|
export * from "./comment-detail-card";
|
||||||
|
export * from "./comment-reactions";
|
@ -1,5 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -41,7 +43,7 @@ const peekModes: {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||||
const { handleClose, issueDetails } = props;
|
const { handleClose, issueDetails } = props;
|
||||||
|
|
||||||
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
const { issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||||
@ -137,4 +139,4 @@ export const PeekOverviewHeader: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./comment";
|
||||||
export * from "./full-screen-peek-view";
|
export * from "./full-screen-peek-view";
|
||||||
export * from "./header";
|
export * from "./header";
|
||||||
export * from "./issue-activity";
|
export * from "./issue-activity";
|
||||||
@ -8,5 +9,3 @@ export * from "./side-peek-view";
|
|||||||
export * from "./issue-reaction";
|
export * from "./issue-reaction";
|
||||||
export * from "./issue-vote-reactions";
|
export * from "./issue-vote-reactions";
|
||||||
export * from "./issue-emoji-reactions";
|
export * from "./issue-emoji-reactions";
|
||||||
export * from "./comment-detail-card";
|
|
||||||
export * from "./add-comment";
|
|
||||||
|
@ -20,18 +20,27 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
|||||||
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
|
const reactions = issueId ? issueDetailsStore.details[issueId]?.reactions || [] : [];
|
||||||
const groupedReactions = groupReactions(reactions, "reaction");
|
const groupedReactions = groupReactions(reactions, "reaction");
|
||||||
|
|
||||||
const handleReactionSelectClick = (reactionHex: string) => {
|
const userReactions = reactions?.filter((r) => r.actor_detail.id === user?.id);
|
||||||
|
|
||||||
|
const handleAddReaction = (reactionHex: string) => {
|
||||||
if (!workspace_slug || !project_slug || !issueId) return;
|
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, reactionHex);
|
issueDetailsStore.addIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReactionClick = (reactionHex: string) => {
|
const handleRemoveReaction = (reactionHex: string) => {
|
||||||
if (!workspace_slug || !project_slug || !issueId) return;
|
if (!workspace_slug || !project_slug || !issueId) return;
|
||||||
|
|
||||||
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
issueDetailsStore.removeIssueReaction(workspace_slug.toString(), project_slug.toString(), issueId, reactionHex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReactionClick = (reactionHex: string) => {
|
||||||
|
const userReaction = userReactions?.find((r) => r.actor_detail.id === user?.id && r.reaction === reactionHex);
|
||||||
|
|
||||||
|
if (userReaction) handleRemoveReaction(reactionHex);
|
||||||
|
else handleAddReaction(reactionHex);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) return;
|
if (user) return;
|
||||||
userStore.fetchCurrentUser();
|
userStore.fetchCurrentUser();
|
||||||
@ -42,9 +51,10 @@ export const IssueEmojiReactions: React.FC = observer(() => {
|
|||||||
<ReactionSelector
|
<ReactionSelector
|
||||||
onSelect={(value) => {
|
onSelect={(value) => {
|
||||||
userStore.requiredLogin(() => {
|
userStore.requiredLogin(() => {
|
||||||
handleReactionSelectClick(value);
|
handleReactionClick(value);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
selected={userReactions?.map((r) => r.reaction)}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
@ -12,13 +12,14 @@ import { Icon } from "components/ui";
|
|||||||
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
|
const reactionEmojis = ["128077", "128078", "128516", "128165", "128533", "129505", "9992", "128064"];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
size?: "sm" | "md" | "lg";
|
|
||||||
position?: "top" | "bottom";
|
|
||||||
onSelect: (emoji: string) => void;
|
onSelect: (emoji: string) => void;
|
||||||
|
position?: "top" | "bottom";
|
||||||
|
selected?: string[];
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReactionSelector: React.FC<Props> = (props) => {
|
export const ReactionSelector: React.FC<Props> = (props) => {
|
||||||
const { onSelect, position, size } = props;
|
const { onSelect, position, selected = [], size } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
@ -51,7 +52,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
|||||||
position === "top" ? "-top-12" : "-bottom-12"
|
position === "top" ? "-top-12" : "-bottom-12"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
|
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
{reactionEmojis.map((emoji) => (
|
{reactionEmojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
@ -61,7 +62,9 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
|||||||
onSelect(emoji);
|
onSelect(emoji);
|
||||||
closePopover();
|
closePopover();
|
||||||
}}
|
}}
|
||||||
className="flex select-none items-center justify-between rounded-md text-sm p-1 hover:bg-custom-sidebar-background-90"
|
className={`grid place-items-center select-none rounded-md text-sm p-1 ${
|
||||||
|
selected.includes(emoji) ? "bg-custom-primary-100/10" : "hover:bg-custom-sidebar-background-80"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{renderEmoji(emoji)}
|
{renderEmoji(emoji)}
|
||||||
</button>
|
</button>
|
||||||
|
@ -93,16 +93,6 @@ class IssueService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCommentsReactions(workspaceSlug: string, projectId: string, commentId: string): Promise<any> {
|
|
||||||
return this.get(
|
|
||||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`
|
|
||||||
)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
|
async createIssueComment(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<any> {
|
||||||
return this.post(
|
return this.post(
|
||||||
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
|
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/issues/${issueId}/comments/`,
|
||||||
@ -140,6 +130,39 @@ class IssueService extends APIService {
|
|||||||
throw error?.response;
|
throw error?.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createCommentReaction(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
commentId: string,
|
||||||
|
data: {
|
||||||
|
reaction: string;
|
||||||
|
}
|
||||||
|
): Promise<any> {
|
||||||
|
return this.post(
|
||||||
|
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCommentReaction(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
): Promise<any> {
|
||||||
|
return this.delete(
|
||||||
|
`/api/public/workspaces/${workspaceSlug}/project-boards/${projectId}/comments/${commentId}/reactions/${reactionHex}/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IssueService;
|
export default IssueService;
|
||||||
|
@ -32,6 +32,20 @@ export interface IIssueDetailStore {
|
|||||||
data: any
|
data: any
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void;
|
deleteIssueComment: (workspaceId: string, projectId: string, issueId: string, comment_id: string) => void;
|
||||||
|
addCommentReaction: (
|
||||||
|
workspaceId: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
) => void;
|
||||||
|
removeCommentReaction: (
|
||||||
|
workspaceId: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
) => void;
|
||||||
// issue reactions
|
// issue reactions
|
||||||
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
addIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
||||||
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
removeIssueReaction: (workspaceId: string, projectId: string, issueId: string, reactionHex: string) => void;
|
||||||
@ -61,8 +75,17 @@ class IssueDetailStore implements IssueDetailStore {
|
|||||||
details: observable.ref,
|
details: observable.ref,
|
||||||
// actions
|
// actions
|
||||||
setPeekId: action,
|
setPeekId: action,
|
||||||
fetchIssueDetails: action,
|
|
||||||
setPeekMode: action,
|
setPeekMode: action,
|
||||||
|
fetchIssueDetails: action,
|
||||||
|
addIssueComment: action,
|
||||||
|
updateIssueComment: action,
|
||||||
|
deleteIssueComment: action,
|
||||||
|
addCommentReaction: action,
|
||||||
|
removeCommentReaction: action,
|
||||||
|
addIssueReaction: action,
|
||||||
|
removeIssueReaction: action,
|
||||||
|
addIssueVote: action,
|
||||||
|
removeIssueVote: action,
|
||||||
});
|
});
|
||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
@ -131,29 +154,32 @@ class IssueDetailStore implements IssueDetailStore {
|
|||||||
data: any
|
data: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const issueCommentUpdateResponse = await this.issueService.updateIssueComment(
|
runInAction(() => {
|
||||||
workspaceSlug,
|
this.details = {
|
||||||
projectId,
|
...this.details,
|
||||||
issueId,
|
[issueId]: {
|
||||||
commentId,
|
...this.details[issueId],
|
||||||
data
|
comments: this.details[issueId].comments.map((c) => ({
|
||||||
);
|
...c,
|
||||||
|
...(c.id === commentId ? data : {}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (issueCommentUpdateResponse) {
|
await this.issueService.updateIssueComment(workspaceSlug, projectId, issueId, commentId, data);
|
||||||
const remainingComments = this.details[issueId].comments.filter((com) => com.id != commentId);
|
|
||||||
runInAction(() => {
|
|
||||||
this.details = {
|
|
||||||
...this.details,
|
|
||||||
[issueId]: {
|
|
||||||
...this.details[issueId],
|
|
||||||
comments: [...remainingComments, issueCommentUpdateResponse],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return issueCommentUpdateResponse;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Failed to add issue comment");
|
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: issueComments,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -175,6 +201,94 @@ class IssueDetailStore implements IssueDetailStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
addCommentReaction = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
) => {
|
||||||
|
const newReaction = {
|
||||||
|
id: uuidv4(),
|
||||||
|
comment: commentId,
|
||||||
|
reaction: reactionHex,
|
||||||
|
actor_detail: this.rootStore.user.currentActor,
|
||||||
|
};
|
||||||
|
const newComments = this.details[issueId].comments.map((comment) => ({
|
||||||
|
...comment,
|
||||||
|
comment_reactions:
|
||||||
|
comment.id === commentId ? [...comment.comment_reactions, newReaction] : comment.comment_reactions,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: [...newComments],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.issueService.createCommentReaction(workspaceSlug, projectId, commentId, {
|
||||||
|
reaction: reactionHex,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: issueComments,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
removeCommentReaction = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
commentId: string,
|
||||||
|
reactionHex: string
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const comment = this.details[issueId].comments.find((c) => c.id === commentId);
|
||||||
|
const newCommentReactions = comment?.comment_reactions.filter((r) => r.reaction !== reactionHex) ?? [];
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: this.details[issueId].comments.map((c) => ({
|
||||||
|
...c,
|
||||||
|
comment_reactions: c.id === commentId ? newCommentReactions : c.comment_reactions,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.issueService.deleteCommentReaction(workspaceSlug, projectId, commentId, reactionHex);
|
||||||
|
} catch (error) {
|
||||||
|
const issueComments = await this.issueService.getIssueComments(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.details = {
|
||||||
|
...this.details,
|
||||||
|
[issueId]: {
|
||||||
|
...this.details[issueId],
|
||||||
|
comments: issueComments,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
|
addIssueReaction = async (workspaceSlug: string, projectId: string, issueId: string, reactionHex: string) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
@ -62,17 +62,13 @@ class UserStore implements IUserStore {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentPath = window.location.pathname + window.location.search;
|
||||||
this.fetchCurrentUser()
|
this.fetchCurrentUser()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!this.currentUser) {
|
if (!this.currentUser) window.location.href = `/?next_path=${currentPath}`;
|
||||||
const currentPath = window.location.pathname;
|
else callback();
|
||||||
window.location.href = `/?next_path=${currentPath}`;
|
|
||||||
} else callback();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => (window.location.href = `/?next_path=${currentPath}`));
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
window.location.href = `/?next_path=${currentPath}`;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchCurrentUser = async () => {
|
fetchCurrentUser = async () => {
|
||||||
|
@ -68,25 +68,30 @@ export interface IVote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Comment {
|
export interface Comment {
|
||||||
id: string;
|
|
||||||
actor_detail: ActorDetail;
|
actor_detail: ActorDetail;
|
||||||
issue_detail: IssueDetail;
|
|
||||||
project_detail: ProjectDetail;
|
|
||||||
workspace_detail: WorkspaceDetail;
|
|
||||||
comment_reactions: any[];
|
|
||||||
is_member: boolean;
|
|
||||||
created_at: Date;
|
|
||||||
updated_at: Date;
|
|
||||||
comment_stripped: string;
|
|
||||||
comment_html: string;
|
|
||||||
attachments: any[];
|
|
||||||
access: string;
|
access: string;
|
||||||
created_by: string;
|
|
||||||
updated_by: string;
|
|
||||||
project: string;
|
|
||||||
workspace: string;
|
|
||||||
issue: string;
|
|
||||||
actor: string;
|
actor: string;
|
||||||
|
attachments: any[];
|
||||||
|
comment_html: string;
|
||||||
|
comment_reactions: {
|
||||||
|
actor_detail: ActorDetail;
|
||||||
|
comment: string;
|
||||||
|
id: string;
|
||||||
|
reaction: string;
|
||||||
|
}[];
|
||||||
|
comment_stripped: string;
|
||||||
|
created_at: Date;
|
||||||
|
created_by: string;
|
||||||
|
id: string;
|
||||||
|
is_member: boolean;
|
||||||
|
issue: string;
|
||||||
|
issue_detail: IssueDetail;
|
||||||
|
project: string;
|
||||||
|
project_detail: ProjectDetail;
|
||||||
|
updated_at: Date;
|
||||||
|
updated_by: string;
|
||||||
|
workspace: string;
|
||||||
|
workspace_detail: WorkspaceDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueReaction {
|
export interface IIssueReaction {
|
||||||
|
@ -212,7 +212,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
|
|||||||
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
>
|
>
|
||||||
workspace level
|
Workspace Level
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -61,7 +61,7 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
|||||||
position === "top" ? "-top-12" : "-bottom-12"
|
position === "top" ? "-top-12" : "-bottom-12"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 rounded-md p-1">
|
<div className="bg-custom-sidebar-background-100 border border-custom-border-200 shadow-custom-shadow-sm rounded-md p-1">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
{reactionEmojis.map((emoji) => (
|
{reactionEmojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
|
@ -63,7 +63,9 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
|
|||||||
const blockFormat = (blocks: ICycle[]) =>
|
const blockFormat = (blocks: ICycle[]) =>
|
||||||
blocks && blocks.length > 0
|
blocks && blocks.length > 0
|
||||||
? blocks
|
? blocks
|
||||||
.filter((b) => b.start_date && b.end_date)
|
.filter(
|
||||||
|
(b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)
|
||||||
|
)
|
||||||
.map((block) => ({
|
.map((block) => ({
|
||||||
data: block,
|
data: block,
|
||||||
id: block.id,
|
id: block.id,
|
||||||
|
@ -4,11 +4,13 @@ import { IGanttBlock } from "components/gantt-chart";
|
|||||||
|
|
||||||
export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] =>
|
export const renderIssueBlocksStructure = (blocks: IIssue[]): IGanttBlock[] =>
|
||||||
blocks && blocks.length > 0
|
blocks && blocks.length > 0
|
||||||
? blocks.map((block) => ({
|
? blocks
|
||||||
data: block,
|
.filter((b) => new Date(b?.start_date ?? "") <= new Date(b?.target_date ?? ""))
|
||||||
id: block.id,
|
.map((block) => ({
|
||||||
sort_order: block.sort_order,
|
data: block,
|
||||||
start_date: new Date(block.start_date ?? ""),
|
id: block.id,
|
||||||
target_date: new Date(block.target_date ?? ""),
|
sort_order: block.sort_order,
|
||||||
}))
|
start_date: new Date(block.start_date ?? ""),
|
||||||
|
target_date: new Date(block.target_date ?? ""),
|
||||||
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
@ -136,7 +136,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
|||||||
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
>
|
>
|
||||||
workspace level
|
Workspace Level
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -69,7 +69,10 @@ export const ModulesListGanttChartView: FC<Props> = ({ modules, mutateModules })
|
|||||||
const blockFormat = (blocks: IModule[]) =>
|
const blockFormat = (blocks: IModule[]) =>
|
||||||
blocks && blocks.length > 0
|
blocks && blocks.length > 0
|
||||||
? blocks
|
? blocks
|
||||||
.filter((b) => b.start_date && b.target_date)
|
.filter(
|
||||||
|
(b) =>
|
||||||
|
b.start_date && b.target_date && new Date(b.start_date) <= new Date(b.target_date)
|
||||||
|
)
|
||||||
.map((block) => ({
|
.map((block) => ({
|
||||||
data: block,
|
data: block,
|
||||||
id: block.id,
|
id: block.id,
|
||||||
|
129
web/components/web-view/add-comment.tsx
Normal file
129
web/components/web-view/add-comment.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// react-hook-form
|
||||||
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { TipTapEditor } from "components/tiptap";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { Send } from "lucide-react";
|
||||||
|
|
||||||
|
// ui
|
||||||
|
import { Icon, SecondaryButton, Tooltip, PrimaryButton } from "components/ui";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import type { IIssueComment } from "types";
|
||||||
|
|
||||||
|
const defaultValues: Partial<IIssueComment> = {
|
||||||
|
access: "INTERNAL",
|
||||||
|
comment_html: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
disabled?: boolean;
|
||||||
|
onSubmit: (data: IIssueComment) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const commentAccess = [
|
||||||
|
{
|
||||||
|
icon: "lock",
|
||||||
|
key: "INTERNAL",
|
||||||
|
label: "Private",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "public",
|
||||||
|
key: "EXTERNAL",
|
||||||
|
label: "Public",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit }) => {
|
||||||
|
const editorRef = React.useRef<any>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const showAccessSpecifier = projectDetails?.is_deployed;
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
} = useForm<IIssueComment>({ defaultValues });
|
||||||
|
|
||||||
|
const handleAddComment = async (formData: IIssueComment) => {
|
||||||
|
if (!formData.comment_html || isSubmitting) return;
|
||||||
|
|
||||||
|
await onSubmit(formData).then(() => {
|
||||||
|
reset(defaultValues);
|
||||||
|
editorRef.current?.clearEditor();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="w-full flex gap-x-2" onSubmit={handleSubmit(handleAddComment)}>
|
||||||
|
<div className="relative flex-grow">
|
||||||
|
{showAccessSpecifier && (
|
||||||
|
<div className="absolute bottom-2 left-3 z-[1]">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="access"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
|
||||||
|
{commentAccess.map((access) => (
|
||||||
|
<Tooltip key={access.key} tooltipContent={access.label}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(access.key)}
|
||||||
|
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${
|
||||||
|
value === access.key ? "bg-custom-background-80" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName={access.icon}
|
||||||
|
className={`w-4 h-4 -mt-1 ${
|
||||||
|
value === access.key ? "!text-custom-text-100" : "!text-custom-text-400"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
name="comment_html"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<TipTapEditor
|
||||||
|
workspaceSlug={workspaceSlug as string}
|
||||||
|
ref={editorRef}
|
||||||
|
value={!value || value === "" ? "<p></p>" : value}
|
||||||
|
customClassName="p-3 min-h-[100px] shadow-sm"
|
||||||
|
debouncedUpdatesEnabled={false}
|
||||||
|
onChange={(comment_json: Object, comment_html: string) => onChange(comment_html)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="inline">
|
||||||
|
<PrimaryButton type="submit" disabled={isSubmitting || disabled} className="mt-2">
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
// react
|
// react
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@ -14,7 +14,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
|
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { M_ISSUE_DETAILS } from "constants/fetch-keys";
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
@ -26,13 +26,14 @@ import { PrimaryButton, Input } from "components/ui";
|
|||||||
import type { linkDetails, IIssueLink } from "types";
|
import type { linkDetails, IIssueLink } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
links?: linkDetails[];
|
isOpen: boolean;
|
||||||
data?: linkDetails;
|
data?: linkDetails;
|
||||||
|
links?: linkDetails[];
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateUpdateLinkForm: React.FC<Props> = (props) => {
|
export const CreateUpdateLinkForm: React.FC<Props> = (props) => {
|
||||||
const { data, links, onSuccess } = props;
|
const { isOpen, data, links, onSuccess } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
@ -42,6 +43,7 @@ export const CreateUpdateLinkForm: React.FC<Props> = (props) => {
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -50,6 +52,22 @@ export const CreateUpdateLinkForm: React.FC<Props> = (props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) return;
|
||||||
|
reset({
|
||||||
|
title: data.title,
|
||||||
|
url: data.url,
|
||||||
|
});
|
||||||
|
}, [data, reset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen)
|
||||||
|
reset({
|
||||||
|
title: "",
|
||||||
|
url: "",
|
||||||
|
});
|
||||||
|
}, [isOpen, reset]);
|
||||||
|
|
||||||
const onSubmit = async (formData: IIssueLink) => {
|
const onSubmit = async (formData: IIssueLink) => {
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
@ -65,9 +83,7 @@ export const CreateUpdateLinkForm: React.FC<Props> = (props) => {
|
|||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onSuccess();
|
onSuccess();
|
||||||
mutate(
|
mutate(ISSUE_DETAILS(issueId.toString()));
|
||||||
M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err?.status === 400)
|
if (err?.status === 400)
|
||||||
@ -95,7 +111,7 @@ export const CreateUpdateLinkForm: React.FC<Props> = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mutate(
|
mutate(
|
||||||
M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString()),
|
ISSUE_DETAILS(issueId.toString()),
|
||||||
(prevData) => ({ ...prevData, issue_link: updatedLinks }),
|
(prevData) => ({ ...prevData, issue_link: updatedLinks }),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@ -110,9 +126,7 @@ export const CreateUpdateLinkForm: React.FC<Props> = (props) => {
|
|||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onSuccess();
|
onSuccess();
|
||||||
mutate(
|
mutate(ISSUE_DETAILS(issueId.toString()));
|
||||||
M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -8,3 +8,7 @@ export * from "./issue-attachments";
|
|||||||
export * from "./issue-properties-detail";
|
export * from "./issue-properties-detail";
|
||||||
export * from "./issue-link-list";
|
export * from "./issue-link-list";
|
||||||
export * from "./create-update-link-form";
|
export * from "./create-update-link-form";
|
||||||
|
export * from "./issue-activity";
|
||||||
|
export * from "./select-assignee";
|
||||||
|
export * from "./select-estimate";
|
||||||
|
export * from "./add-comment";
|
||||||
|
233
web/components/web-view/issue-activity.tsx
Normal file
233
web/components/web-view/issue-activity.tsx
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
// react
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// swr
|
||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
|
// fetch key
|
||||||
|
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Label, AddComment } from "components/web-view";
|
||||||
|
import { CommentCard } from "components/issues/comment";
|
||||||
|
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
|
|
||||||
|
// ui
|
||||||
|
import { Icon } from "components/ui";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import type { IIssue, IIssueComment } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
allowed: boolean;
|
||||||
|
issueDetails: IIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueActivity: React.FC<Props> = (props) => {
|
||||||
|
const { issueDetails, allowed } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { data: issueActivities, mutate: mutateIssueActivity } = useSWR(
|
||||||
|
workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId.toString()) : null,
|
||||||
|
workspaceSlug && projectId && issueId
|
||||||
|
? () =>
|
||||||
|
issuesService.getIssueActivities(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
issueId.toString()
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCommentUpdate = async (comment: any) => {
|
||||||
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.patchIssueComment(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
comment.id,
|
||||||
|
comment,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => mutateIssueActivity());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommentDelete = async (commentId: string) => {
|
||||||
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
|
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.deleteIssueComment(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
commentId,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => mutateIssueActivity());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddComment = async (formData: IIssueComment) => {
|
||||||
|
if (!workspaceSlug || !issueDetails) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createIssueComment(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
issueDetails.project,
|
||||||
|
issueDetails.id,
|
||||||
|
formData,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Comment could not be posted. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Label>Activity</Label>
|
||||||
|
<div className="mt-1 space-y-[6px] p-2 border rounded-[4px]">
|
||||||
|
<ul role="list" className="-mb-4">
|
||||||
|
{issueActivities?.map((activityItem, index) => {
|
||||||
|
// determines what type of action is performed
|
||||||
|
const message = activityItem.field ? (
|
||||||
|
<ActivityMessage activity={activityItem} />
|
||||||
|
) : (
|
||||||
|
"created the issue."
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||||
|
return (
|
||||||
|
<li key={activityItem.id}>
|
||||||
|
<div className="relative pb-1">
|
||||||
|
{issueActivities.length > 1 && index !== issueActivities.length - 1 ? (
|
||||||
|
<span
|
||||||
|
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className="relative flex items-start space-x-2">
|
||||||
|
<div>
|
||||||
|
<div className="relative px-1.5">
|
||||||
|
<div className="mt-1.5">
|
||||||
|
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||||
|
{activityItem.field ? (
|
||||||
|
activityItem.new_value === "restore" ? (
|
||||||
|
<Icon
|
||||||
|
iconName="history"
|
||||||
|
className="text-sm text-custom-text-200"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ActivityIcon activity={activityItem} />
|
||||||
|
)
|
||||||
|
) : activityItem.actor_detail.avatar &&
|
||||||
|
activityItem.actor_detail.avatar !== "" ? (
|
||||||
|
<img
|
||||||
|
src={activityItem.actor_detail.avatar}
|
||||||
|
alt={activityItem.actor_detail.display_name}
|
||||||
|
height={24}
|
||||||
|
width={24}
|
||||||
|
className="rounded-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||||
|
>
|
||||||
|
{activityItem.actor_detail.is_bot
|
||||||
|
? activityItem.actor_detail.first_name.charAt(0)
|
||||||
|
: activityItem.actor_detail.display_name.charAt(0)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1 py-3">
|
||||||
|
<div className="text-xs text-custom-text-200 break-words">
|
||||||
|
{activityItem.field === "archived_at" &&
|
||||||
|
activityItem.new_value !== "restore" ? (
|
||||||
|
<span className="text-gray font-medium">Plane</span>
|
||||||
|
) : activityItem.actor_detail.is_bot ? (
|
||||||
|
<span className="text-gray font-medium">
|
||||||
|
{activityItem.actor_detail.first_name} Bot
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
|
||||||
|
>
|
||||||
|
<a className="text-gray font-medium">
|
||||||
|
{activityItem.actor_detail.is_bot
|
||||||
|
? activityItem.actor_detail.first_name
|
||||||
|
: activityItem.actor_detail.display_name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}{" "}
|
||||||
|
{message}{" "}
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
{timeAgo(activityItem.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
} else if ("comment_json" in activityItem)
|
||||||
|
return (
|
||||||
|
<div key={activityItem.id} className="my-4">
|
||||||
|
<CommentCard
|
||||||
|
workspaceSlug={workspaceSlug as string}
|
||||||
|
comment={activityItem as any}
|
||||||
|
onSubmit={handleCommentUpdate}
|
||||||
|
handleCommentDeletion={handleCommentDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<li>
|
||||||
|
<div className="my-4">
|
||||||
|
<AddComment
|
||||||
|
onSubmit={handleAddComment}
|
||||||
|
disabled={
|
||||||
|
!allowed ||
|
||||||
|
!issueDetails ||
|
||||||
|
issueDetails.state === "closed" ||
|
||||||
|
issueDetails.state === "archived"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -21,10 +21,11 @@ import { ISSUE_ATTACHMENTS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { ChevronRightIcon } from "@heroicons/react/24/outline";
|
import { ChevronRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Label, WebViewModal } from "components/web-view";
|
import { Label, WebViewModal } from "components/web-view";
|
||||||
|
import { DeleteAttachmentModal } from "components/issues";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IIssueAttachment } from "types";
|
import type { IIssueAttachment } from "types";
|
||||||
@ -42,6 +43,9 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [deleteAttachment, setDeleteAttachment] = useState<IIssueAttachment | null>(null);
|
||||||
|
const [attachmentDeleteModal, setAttachmentDeleteModal] = useState<boolean>(false);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
@ -92,7 +96,7 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
|||||||
[issueId, projectId, setToastAlert, workspaceSlug]
|
[issueId, projectId, setToastAlert, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getRootProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
onDrop,
|
onDrop,
|
||||||
maxSize: 5 * 1024 * 1024,
|
maxSize: 5 * 1024 * 1024,
|
||||||
disabled: !allowed || isLoading,
|
disabled: !allowed || isLoading,
|
||||||
@ -112,6 +116,12 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<DeleteAttachmentModal
|
||||||
|
isOpen={allowed && attachmentDeleteModal}
|
||||||
|
setIsOpen={setAttachmentDeleteModal}
|
||||||
|
data={deleteAttachment}
|
||||||
|
/>
|
||||||
|
|
||||||
<WebViewModal isOpen={isOpen} onClose={() => setIsOpen(false)} modalTitle="Insert file">
|
<WebViewModal isOpen={isOpen} onClose={() => setIsOpen(false)} modalTitle="Insert file">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div
|
<div
|
||||||
@ -120,6 +130,7 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
|||||||
!allowed || isLoading ? "cursor-not-allowed" : "cursor-pointer"
|
!allowed || isLoading ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<p className="text-center">Uploading...</p>
|
<p className="text-center">Uploading...</p>
|
||||||
) : (
|
) : (
|
||||||
@ -144,6 +155,17 @@ export const IssueAttachments: React.FC<Props> = (props) => {
|
|||||||
{attachment.attributes.name}
|
{attachment.attributes.name}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
{allowed && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteAttachment(attachment);
|
||||||
|
setAttachmentDeleteModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="w-5 h-5 text-custom-text-100" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
|
@ -3,9 +3,16 @@ import React, { useState } from "react";
|
|||||||
|
|
||||||
// next
|
// next
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// swr
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { LinkIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { LinkIcon, PlusIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Label, WebViewModal, CreateUpdateLinkForm } from "components/web-view";
|
import { Label, WebViewModal, CreateUpdateLinkForm } from "components/web-view";
|
||||||
@ -13,26 +20,71 @@ import { Label, WebViewModal, CreateUpdateLinkForm } from "components/web-view";
|
|||||||
// ui
|
// ui
|
||||||
import { SecondaryButton } from "components/ui";
|
import { SecondaryButton } from "components/ui";
|
||||||
|
|
||||||
|
// fetch keys
|
||||||
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { linkDetails } from "types";
|
import type { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
allowed: boolean;
|
allowed: boolean;
|
||||||
links?: linkDetails[];
|
issueDetails: IIssue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueLinks: React.FC<Props> = (props) => {
|
export const IssueLinks: React.FC<Props> = (props) => {
|
||||||
const { links, allowed } = props;
|
const { issueDetails, allowed } = props;
|
||||||
|
|
||||||
|
const links = issueDetails?.issue_link;
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [selectedLink, setSelectedLink] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const handleDeleteLink = async (linkId: string) => {
|
||||||
|
if (!workspaceSlug || !projectId || !issueDetails) return;
|
||||||
|
|
||||||
|
const updatedLinks = issueDetails.issue_link.filter((l) => l.id !== linkId);
|
||||||
|
|
||||||
|
mutate<IIssue>(
|
||||||
|
ISSUE_DETAILS(issueDetails.id),
|
||||||
|
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.deleteIssueLink(workspaceSlug as string, projectId as string, issueDetails.id, linkId)
|
||||||
|
.then((res) => {
|
||||||
|
mutate(ISSUE_DETAILS(issueDetails.id));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<WebViewModal isOpen={isOpen} onClose={() => setIsOpen(false)} modalTitle="Add Link">
|
<WebViewModal
|
||||||
<CreateUpdateLinkForm links={links} onSuccess={() => setIsOpen(false)} />
|
isOpen={isOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectedLink(null);
|
||||||
|
}}
|
||||||
|
modalTitle={selectedLink ? "Update Link" : "Add Link"}
|
||||||
|
>
|
||||||
|
<CreateUpdateLinkForm
|
||||||
|
isOpen={isOpen}
|
||||||
|
links={links}
|
||||||
|
onSuccess={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectedLink(null);
|
||||||
|
}}
|
||||||
|
data={links?.find((link) => link.id === selectedLink)}
|
||||||
|
/>
|
||||||
</WebViewModal>
|
</WebViewModal>
|
||||||
|
|
||||||
<Label>Attachments</Label>
|
<Label>Links</Label>
|
||||||
<div className="mt-1 space-y-[6px]">
|
<div className="mt-1 space-y-[6px]">
|
||||||
{links?.map((link) => (
|
{links?.map((link) => (
|
||||||
<div
|
<div
|
||||||
@ -47,6 +99,27 @@ export const IssueLinks: React.FC<Props> = (props) => {
|
|||||||
<span>{link.title}</span>
|
<span>{link.title}</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
{allowed && (
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setSelectedLink(link.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PencilIcon className="w-5 h-5 text-custom-text-100" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
handleDeleteLink(link.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-5 h-5 text-red-500 hover:bg-red-500/20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
|
@ -1,30 +1,46 @@
|
|||||||
// react
|
// react
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
// react hook forms
|
// react hook forms
|
||||||
import { Controller } from "react-hook-form";
|
import { Control, Controller } from "react-hook-form";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { ChevronDownIcon, PlayIcon } from "lucide-react";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { Icon } from "components/ui";
|
import { Icon, SecondaryButton } from "components/ui";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Label, StateSelect, PrioritySelect } from "components/web-view";
|
import {
|
||||||
|
Label,
|
||||||
|
StateSelect,
|
||||||
|
PrioritySelect,
|
||||||
|
AssigneeSelect,
|
||||||
|
EstimateSelect,
|
||||||
|
} from "components/web-view";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: any;
|
control: Control<IIssue, any>;
|
||||||
submitChanges: (data: Partial<IIssue>) => Promise<void>;
|
submitChanges: (data: Partial<IIssue>) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
||||||
const { control, submitChanges } = props;
|
const { control, submitChanges } = props;
|
||||||
|
|
||||||
|
const [isViewAllOpen, setIsViewAllOpen] = useState(false);
|
||||||
|
|
||||||
|
const { isEstimateActive } = useEstimateOption();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Label>Details</Label>
|
<Label>Details</Label>
|
||||||
<div className="space-y-2 mb-[6px]">
|
<div className="mb-[6px]">
|
||||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Icon iconName="grid_view" />
|
<Icon iconName="grid_view" />
|
||||||
@ -44,10 +60,10 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="mb-[6px]">
|
||||||
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Icon iconName="grid_view" />
|
<Icon iconName="signal_cellular_alt" />
|
||||||
<span className="text-sm text-custom-text-200">Priority</span>
|
<span className="text-sm text-custom-text-200">Priority</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -64,6 +80,67 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-[6px]">
|
||||||
|
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Icon iconName="person" />
|
||||||
|
<span className="text-sm text-custom-text-200">Assignee</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="assignees_list"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<AssigneeSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => submitChanges({ assignees_list: [val] })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isViewAllOpen && (
|
||||||
|
<>
|
||||||
|
{isEstimateActive && (
|
||||||
|
<div className="mb-[6px]">
|
||||||
|
<div className="border border-custom-border-200 rounded-[4px] p-2 flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90" />
|
||||||
|
<span className="text-sm text-custom-text-200">Estimate</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="estimate_point"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<EstimateSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val) => submitChanges({ estimate_point: val })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="mb-[6px]">
|
||||||
|
<SecondaryButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsViewAllOpen((prev) => !prev)}
|
||||||
|
className="w-full flex justify-center items-center gap-1 !py-2"
|
||||||
|
>
|
||||||
|
<span className="text-base text-custom-primary-100">
|
||||||
|
{isViewAllOpen ? "View less" : "View all"}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
size={16}
|
||||||
|
className={`ml-1 text-custom-primary-100 ${isViewAllOpen ? "-rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</SecondaryButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
95
web/components/web-view/select-assignee.tsx
Normal file
95
web/components/web-view/select-assignee.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// swr
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
|
||||||
|
// fetch key
|
||||||
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Avatar } from "components/ui/avatar";
|
||||||
|
import { WebViewModal } from "./web-view-modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string[];
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AssigneeSelect: React.FC<Props> = (props) => {
|
||||||
|
const { value, onChange, disabled = false } = props;
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { data: members } = useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedAssignees = members?.filter((member) => value?.includes(member.member.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WebViewModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
modalTitle="Select state"
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WebViewModal.Options
|
||||||
|
options={
|
||||||
|
members?.map((member) => ({
|
||||||
|
label: member.member.display_name,
|
||||||
|
value: member.member.id,
|
||||||
|
checked: value?.includes(member.member.id),
|
||||||
|
icon: <Avatar user={member.member} />,
|
||||||
|
onClick: () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
if (disabled) return;
|
||||||
|
onChange(member.member.id);
|
||||||
|
},
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</WebViewModal>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className={
|
||||||
|
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value && value.length > 0 && Array.isArray(value) ? (
|
||||||
|
<div className="-my-0.5 flex items-center gap-2">
|
||||||
|
<Avatar user={selectedAssignees?.[0].member} />
|
||||||
|
<span className="text-custom-text-100 text-xs">
|
||||||
|
{selectedAssignees?.length} Assignees
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"No assignees"
|
||||||
|
)}
|
||||||
|
{/* {selectedAssignee?.member.display_name || "Select assignee"} */}
|
||||||
|
<ChevronDownIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
83
web/components/web-view/select-estimate.tsx
Normal file
83
web/components/web-view/select-estimate.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// react
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { ChevronDownIcon, PlayIcon } from "lucide-react";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { WebViewModal } from "./web-view-modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EstimateSelect: React.FC<Props> = (props) => {
|
||||||
|
const { value, onChange, disabled = false } = props;
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const { estimatePoints } = useEstimateOption();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WebViewModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
modalTitle="Select estimate"
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WebViewModal.Options
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "None",
|
||||||
|
value: null,
|
||||||
|
checked: value === null,
|
||||||
|
onClick: () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
if (disabled) return;
|
||||||
|
onChange(null);
|
||||||
|
},
|
||||||
|
icon: <PlayIcon className="h-4 w-4 -rotate-90" />,
|
||||||
|
},
|
||||||
|
...estimatePoints?.map((point) => ({
|
||||||
|
label: point.value,
|
||||||
|
value: point.key,
|
||||||
|
checked: point.key === value,
|
||||||
|
icon: <PlayIcon className="h-4 w-4 -rotate-90" />,
|
||||||
|
onClick: () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
if (disabled) return;
|
||||||
|
onChange(point.key);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</WebViewModal>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className={
|
||||||
|
"relative w-full px-2.5 py-0.5 text-base flex justify-between items-center gap-0.5 text-custom-text-100"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value ? (
|
||||||
|
<div className="flex items-center gap-x-1.5">
|
||||||
|
<PlayIcon className="h-4 w-4 -rotate-90" />
|
||||||
|
<span>{estimatePoints?.find((e) => e.key === value)?.value}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"No estimate"
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -2,7 +2,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
|
|
||||||
// constants
|
// constants
|
||||||
import { PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
@ -35,11 +35,16 @@ export const PrioritySelect: React.FC<Props> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WebViewModal.Options
|
<WebViewModal.Options
|
||||||
selectedOption={value}
|
|
||||||
options={
|
options={
|
||||||
PRIORITIES?.map((priority) => ({
|
PRIORITIES?.map((priority) => ({
|
||||||
label: priority ? capitalizeFirstLetter(priority) : "None",
|
label: priority ? capitalizeFirstLetter(priority) : "None",
|
||||||
value: priority,
|
value: priority,
|
||||||
|
checked: priority === value,
|
||||||
|
onClick: () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
if (disabled) return;
|
||||||
|
onChange(priority);
|
||||||
|
},
|
||||||
icon: (
|
icon: (
|
||||||
<span
|
<span
|
||||||
className={`text-left text-xs capitalize rounded ${
|
className={`text-left text-xs capitalize rounded ${
|
||||||
@ -57,11 +62,6 @@ export const PrioritySelect: React.FC<Props> = (props) => {
|
|||||||
{getPriorityIcon(priority, "text-sm")}
|
{getPriorityIcon(priority, "text-sm")}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
if (disabled) return;
|
|
||||||
onChange(priority);
|
|
||||||
},
|
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -57,11 +57,11 @@ export const StateSelect: React.FC<Props> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WebViewModal.Options
|
<WebViewModal.Options
|
||||||
selectedOption={selectedState?.id || null}
|
|
||||||
options={
|
options={
|
||||||
states?.map((state) => ({
|
states?.map((state) => ({
|
||||||
label: state.name,
|
label: state.name,
|
||||||
value: state.id,
|
value: state.id,
|
||||||
|
checked: state.id === selectedState?.id,
|
||||||
icon: getStateGroupIcon(state.group, "16", "16", state.color),
|
icon: getStateGroupIcon(state.group, "16", "16", state.color),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
@ -74,24 +74,24 @@ export const WebViewModal = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type OptionsProps = {
|
type OptionsProps = {
|
||||||
selectedOption: string | null;
|
|
||||||
options: Array<{
|
options: Array<{
|
||||||
label: string;
|
label: string;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
|
checked: boolean;
|
||||||
icon?: any;
|
icon?: any;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Options: React.FC<OptionsProps> = ({ options, selectedOption }) => (
|
const Options: React.FC<OptionsProps> = ({ options }) => (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<div key={option.value} className="flex items-center justify-between gap-2 py-2">
|
<div key={option.value} className="flex items-center justify-between gap-2 py-2">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={option.value === selectedOption}
|
checked={option.checked}
|
||||||
onClick={option.onClick}
|
onChange={option.onClick}
|
||||||
className="rounded-full border border-custom-border-200 bg-custom-background-100 w-4 h-4"
|
className="rounded-full border border-custom-border-200 bg-custom-background-100 w-4 h-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -229,8 +229,6 @@ export const INBOX_ISSUE_DETAILS = (inboxId: string, issueId: string) =>
|
|||||||
|
|
||||||
// Issues
|
// Issues
|
||||||
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
|
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
|
||||||
export const M_ISSUE_DETAILS = (workspaceSlug: string, projectId: string, issueId: string) =>
|
|
||||||
`M_ISSUE_DETAILS_${workspaceSlug.toUpperCase()}_${projectId.toUpperCase()}_${issueId}`;
|
|
||||||
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
|
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
|
||||||
export const ISSUE_ATTACHMENTS = (issueId: string) => `ISSUE_ATTACHMENTS_${issueId.toUpperCase()}`;
|
export const ISSUE_ATTACHMENTS = (issueId: string) => `ISSUE_ATTACHMENTS_${issueId.toUpperCase()}`;
|
||||||
export const ARCHIVED_ISSUE_DETAILS = (issueId: string) =>
|
export const ARCHIVED_ISSUE_DETAILS = (issueId: string) =>
|
||||||
|
@ -187,7 +187,7 @@ const GeneralSettings: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-8">
|
<form onSubmit={handleSubmit(onSubmit)} className="p-8">
|
||||||
<SettingsHeader />
|
<SettingsHeader />
|
||||||
<div className="space-y-8 sm:space-y-12 opacity-60">
|
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
|
<div className="grid grid-cols-12 items-start gap-4 sm:gap-16">
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="col-span-12 sm:col-span-6">
|
||||||
<h4 className="text-lg font-semibold">Icon & Name</h4>
|
<h4 className="text-lg font-semibold">Icon & Name</h4>
|
||||||
|
@ -90,9 +90,8 @@ const WorkspaceSettings: NextPage = () => {
|
|||||||
await workspaceService
|
await workspaceService
|
||||||
.updateWorkspace(activeWorkspace.slug, payload, user)
|
.updateWorkspace(activeWorkspace.slug, payload, user)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
mutate<IWorkspace[]>(
|
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) =>
|
||||||
USER_WORKSPACES,
|
prevData?.map((workspace) => (workspace.id === res.id ? res : workspace))
|
||||||
(prevData) => prevData?.map((workspace) => (workspace.id === res.id ? res : workspace))
|
|
||||||
);
|
);
|
||||||
mutate<IWorkspace>(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => {
|
mutate<IWorkspace>(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
@ -125,9 +124,8 @@ const WorkspaceSettings: NextPage = () => {
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Workspace picture removed successfully.",
|
message: "Workspace picture removed successfully.",
|
||||||
});
|
});
|
||||||
mutate<IWorkspace[]>(
|
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) =>
|
||||||
USER_WORKSPACES,
|
prevData?.map((workspace) => (workspace.id === res.id ? res : workspace))
|
||||||
(prevData) => prevData?.map((workspace) => (workspace.id === res.id ? res : workspace))
|
|
||||||
);
|
);
|
||||||
mutate<IWorkspace>(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => {
|
mutate<IWorkspace>(WORKSPACE_DETAILS(workspaceSlug as string), (prevData) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
@ -183,7 +181,7 @@ const WorkspaceSettings: NextPage = () => {
|
|||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<SettingsHeader />
|
<SettingsHeader />
|
||||||
{activeWorkspace ? (
|
{activeWorkspace ? (
|
||||||
<div className="space-y-8 sm:space-y-12 opacity-60">
|
<div className={`space-y-8 sm:space-y-12 ${isAdmin ? "" : "opacity-60"}`}>
|
||||||
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
<div className="grid grid-cols-12 gap-4 sm:gap-16">
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="col-span-12 sm:col-span-6">
|
||||||
<h4 className="text-lg font-semibold">Logo</h4>
|
<h4 className="text-lg font-semibold">Logo</h4>
|
||||||
|
@ -14,7 +14,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
|
|
||||||
// fetch key
|
// fetch key
|
||||||
import { M_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
@ -33,6 +33,7 @@ import {
|
|||||||
IssueAttachments,
|
IssueAttachments,
|
||||||
IssuePropertiesDetail,
|
IssuePropertiesDetail,
|
||||||
IssueLinks,
|
IssueLinks,
|
||||||
|
IssueActivity,
|
||||||
} from "components/web-view";
|
} from "components/web-view";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
@ -66,9 +67,7 @@ const MobileWebViewIssueDetail = () => {
|
|||||||
mutate: mutateIssueDetails,
|
mutate: mutateIssueDetails,
|
||||||
error,
|
error,
|
||||||
} = useSWR(
|
} = useSWR(
|
||||||
workspaceSlug && projectId && issueId
|
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null,
|
||||||
? M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId && issueId
|
workspaceSlug && projectId && issueId
|
||||||
? () =>
|
? () =>
|
||||||
issuesService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
issuesService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||||
@ -83,6 +82,10 @@ const MobileWebViewIssueDetail = () => {
|
|||||||
description: issueDetails.description,
|
description: issueDetails.description,
|
||||||
description_html: issueDetails.description_html,
|
description_html: issueDetails.description_html,
|
||||||
state: issueDetails.state,
|
state: issueDetails.state,
|
||||||
|
assignees_list:
|
||||||
|
issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
|
||||||
|
labels_list: issueDetails.labels_list ?? issueDetails.labels,
|
||||||
|
labels: issueDetails.labels_list ?? issueDetails.labels,
|
||||||
});
|
});
|
||||||
}, [issueDetails, reset]);
|
}, [issueDetails, reset]);
|
||||||
|
|
||||||
@ -91,7 +94,7 @@ const MobileWebViewIssueDetail = () => {
|
|||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
mutate<IIssue>(
|
mutate<IIssue>(
|
||||||
M_ISSUE_DETAILS(workspaceSlug.toString(), projectId.toString(), issueId.toString()),
|
ISSUE_DETAILS(issueId.toString()),
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
@ -161,7 +164,9 @@ const MobileWebViewIssueDetail = () => {
|
|||||||
|
|
||||||
<IssueAttachments allowed={isAllowed} />
|
<IssueAttachments allowed={isAllowed} />
|
||||||
|
|
||||||
<IssueLinks allowed={isAllowed} links={issueDetails?.issue_link} />
|
<IssueLinks allowed={isAllowed} issueDetails={issueDetails!} />
|
||||||
|
|
||||||
|
<IssueActivity allowed={isAllowed} issueDetails={issueDetails!} />
|
||||||
</div>
|
</div>
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user