Merge pull request #1750 from makeplane/develop

promote: develop to stage-release
This commit is contained in:
Nikhil 2023-08-01 19:35:54 +05:30 committed by GitHub
commit 9f69fe6060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 695 additions and 467 deletions

View File

@ -291,7 +291,8 @@ class IssueCreateSerializer(BaseSerializer):
class IssueActivitySerializer(BaseSerializer): class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor") actor_detail = UserLiteSerializer(read_only=True, source="actor")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
class Meta: class Meta:
model = IssueActivity model = IssueActivity

View File

@ -477,7 +477,7 @@ class IssueActivityEndpoint(BaseAPIView):
~Q(field="comment"), ~Q(field="comment"),
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
) )
.select_related("actor", "workspace") .select_related("actor", "workspace", "issue", "project")
).order_by("created_at") ).order_by("created_at")
issue_comments = ( issue_comments = (
IssueComment.objects.filter(issue_id=issue_id) IssueComment.objects.filter(issue_id=issue_id)

View File

@ -140,7 +140,7 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request): def get(self, request):
try: try:
queryset = IssueActivity.objects.filter(actor=request.user).select_related( queryset = IssueActivity.objects.filter(actor=request.user).select_related(
"actor", "workspace" "actor", "workspace", "issue", "project"
) )
return self.paginate( return self.paginate(

View File

@ -267,7 +267,7 @@ class ProjectViewSet(BaseViewSet):
status=status.HTTP_410_GONE, status=status.HTTP_410_GONE,
) )
except Exception as e: except Exception as e:
pr(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,

View File

@ -1190,7 +1190,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
workspace__slug=slug, workspace__slug=slug,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
actor=user_id, actor=user_id,
).select_related("actor", "workspace") ).select_related("actor", "workspace", "issue", "project")
if projects: if projects:
queryset = queryset.filter(project__in=projects) queryset = queryset.filter(project__in=projects)

View File

@ -68,4 +68,14 @@ class Migration(migrations.Migration):
'unique_together': {('comment', 'actor', 'reaction')}, 'unique_together': {('comment', 'actor', 'reaction')},
}, },
), ),
migrations.AlterField(
model_name='project',
name='identifier',
field=models.CharField(max_length=12, verbose_name='Project Identifier'),
),
migrations.AlterField(
model_name='projectidentifier',
name='name',
field=models.CharField(max_length=12),
),
] ]

View File

@ -1,5 +1,7 @@
import { useRouter } from "next/router";
// icons // icons
import { Icon } from "components/ui"; import { Icon, Tooltip } from "components/ui";
import { Squares2X2Icon } from "@heroicons/react/24/outline"; import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon } from "components/icons"; import { BlockedIcon, BlockerIcon } from "components/icons";
// helpers // helpers
@ -8,26 +10,65 @@ import { capitalizeFirstLetter } from "helpers/string.helper";
// types // types
import { IIssueActivity } from "types"; import { IIssueActivity } from "types";
export const activityDetails: { const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<Tooltip
tooltipContent={
activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"
}
>
<a
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.issue_detail
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
: "Issue"}
<Icon iconName="launch" className="!text-xs" />
</a>
</Tooltip>
);
};
const activityDetails: {
[key: string]: { [key: string]: {
message: (activity: IIssueActivity) => React.ReactNode; message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
icon: React.ReactNode; icon: React.ReactNode;
}; };
} = { } = {
assignees: { assignees: {
message: (activity) => { message: (activity, showIssue) => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
<> <>
added a new assignee{" "} added a new assignee{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
.
</> </>
); );
else else
return ( return (
<> <>
removed the assignee{" "} removed the assignee{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>. <span className="font-medium text-custom-text-100">{activity.old_value}</span>
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</> </>
); );
}, },
@ -41,7 +82,7 @@ export const activityDetails: {
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
}, },
attachment: { attachment: {
message: (activity) => { message: (activity, showIssue) => {
if (activity.verb === "created") if (activity.verb === "created")
return ( return (
<> <>
@ -55,9 +96,27 @@ export const activityDetails: {
attachment attachment
<Icon iconName="launch" className="!text-xs" /> <Icon iconName="launch" className="!text-xs" />
</a> </a>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
</>
);
else
return (
<>
removed an attachment
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</> </>
); );
else return "removed an attachment.";
}, },
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
}, },
@ -126,17 +185,47 @@ export const activityDetails: {
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
}, },
description: { description: {
message: (activity) => "updated the description.", message: (activity, showIssue) => (
<>
updated the description
{showIssue && (
<>
{" "}
of <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
}, },
estimate_point: { estimate_point: {
message: (activity) => { message: (activity, showIssue) => {
if (!activity.new_value) return "removed the estimate point."; if (!activity.new_value)
return (
<>
removed the estimate point
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
else else
return ( return (
<> <>
set the estimate point to{" "} set the estimate point to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</> </>
); );
}, },
@ -150,7 +239,7 @@ export const activityDetails: {
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
}, },
labels: { labels: {
message: (activity) => { message: (activity, showIssue) => {
if (activity.old_value === "") if (activity.old_value === "")
return ( return (
<> <>
@ -165,6 +254,12 @@ export const activityDetails: {
/> />
<span className="font-medium text-custom-text-100">{activity.new_value}</span> <span className="font-medium text-custom-text-100">{activity.new_value}</span>
</span> </span>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
</> </>
); );
else else
@ -181,13 +276,19 @@ export const activityDetails: {
/> />
<span className="font-medium text-custom-text-100">{activity.old_value}</span> <span className="font-medium text-custom-text-100">{activity.old_value}</span>
</span> </span>
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
</> </>
); );
}, },
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
}, },
link: { link: {
message: (activity) => { message: (activity, showIssue) => {
if (activity.verb === "created") if (activity.verb === "created")
return ( return (
<> <>
@ -200,8 +301,14 @@ export const activityDetails: {
> >
link link
<Icon iconName="launch" className="!text-xs" /> <Icon iconName="launch" className="!text-xs" />
</a>{" "} </a>
to the issue. {showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
.
</> </>
); );
else else
@ -216,8 +323,14 @@ export const activityDetails: {
> >
link link
<Icon iconName="launch" className="!text-xs" /> <Icon iconName="launch" className="!text-xs" />
</a>{" "} </a>
from the issue. {showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</> </>
); );
}, },
@ -250,52 +363,102 @@ export const activityDetails: {
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
}, },
name: { name: {
message: (activity) => `set the name to ${activity.new_value}.`, message: (activity, showIssue) => (
<>
set the name to {activity.new_value}
{showIssue && (
<>
{" "}
of <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
}, },
parent: { parent: {
message: (activity) => { message: (activity, showIssue) => {
if (!activity.new_value) if (!activity.new_value)
return ( return (
<> <>
removed the parent{" "} removed the parent{" "}
<span className="font-medium text-custom-text-100">{activity.old_value}</span>. <span className="font-medium text-custom-text-100">{activity.old_value}</span>
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</> </>
); );
else else
return ( return (
<> <>
set the parent to{" "} set the parent to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</> </>
); );
}, },
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
}, },
priority: { priority: {
message: (activity) => ( message: (activity, showIssue) => (
<> <>
set the priority to{" "} set the priority to{" "}
<span className="font-medium text-custom-text-100"> <span className="font-medium text-custom-text-100">
{activity.new_value ? capitalizeFirstLetter(activity.new_value) : "None"} {activity.new_value ? capitalizeFirstLetter(activity.new_value) : "None"}
</span> </span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
. .
</> </>
), ),
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
}, },
state: { state: {
message: (activity) => ( message: (activity, showIssue) => (
<> <>
set the state to{" "} set the state to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>. <span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</> </>
), ),
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />, icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
}, },
target_date: { target_date: {
message: (activity) => { message: (activity, showIssue) => {
if (!activity.new_value) return "removed the due date."; if (!activity.new_value)
return (
<>
removed the due date
{showIssue && (
<>
{" "}
from <IssueLink activity={activity} />
</>
)}
.
</>
);
else else
return ( return (
<> <>
@ -303,6 +466,12 @@ export const activityDetails: {
<span className="font-medium text-custom-text-100"> <span className="font-medium text-custom-text-100">
{renderShortDateWithYearFormat(activity.new_value)} {renderShortDateWithYearFormat(activity.new_value)}
</span> </span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
. .
</> </>
); );
@ -310,3 +479,19 @@ export const activityDetails: {
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />, icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
}, },
}; };
export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
);
export const ActivityMessage = ({
activity,
showIssue = false,
}: {
activity: IIssueActivity;
showIssue?: boolean;
}) => (
<>
{activityDetails[activity.field as keyof typeof activityDetails]?.message(activity, showIssue)}
</>
);

View File

@ -1,5 +1,7 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
// icons // icons
@ -99,7 +101,11 @@ const activityDetails: {
}, },
}; };
export const Feeds: React.FC<any> = ({ activities }) => ( export const Feeds: React.FC<any> = ({ activities }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<div> <div>
<ul role="list" className="-mb-4"> <ul role="list" className="-mb-4">
{activities.map((activity: any, activityIdx: number) => { {activities.map((activity: any, activityIdx: number) => {
@ -152,11 +158,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
activity.field !== "link" && activity.field !== "link" &&
activity.field !== "estimate" activity.field !== "estimate"
) { ) {
const { workspace_detail, project, issue } = activity; const { project, issue } = activity;
value = ( value = (
<span className="text-custom-text-200"> <span className="text-custom-text-200">
created{" "} created{" "}
<Link href={`/${workspace_detail.slug}/projects/${project}/issues/${issue}`}> <Link href={`/${workspaceSlug}/projects/${project}/issues/${issue}`}>
<a className="inline-flex items-center hover:underline"> <a className="inline-flex items-center hover:underline">
this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" /> this issue. <ArrowTopRightOnSquareIcon className="ml-1 h-3.5 w-3.5" />
</a> </a>
@ -193,7 +199,8 @@ export const Feeds: React.FC<any> = ({ activities }) => (
value = "link"; value = "link";
} else if (activity.field === "estimate_point") { } else if (activity.field === "estimate_point") {
value = activity.new_value value = activity.new_value
? activity.new_value + ` Point${parseInt(activity.new_value ?? "", 10) > 1 ? "s" : ""}` ? activity.new_value +
` Point${parseInt(activity.new_value ?? "", 10) > 1 ? "s" : ""}`
: "None"; : "None";
} }
@ -235,7 +242,9 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div> <div>
<div className="text-xs"> <div className="text-xs">
{activity.actor_detail.first_name} {activity.actor_detail.first_name}
{activity.actor_detail.is_bot ? "Bot" : " " + activity.actor_detail.last_name} {activity.actor_detail.is_bot
? "Bot"
: " " + activity.actor_detail.last_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(activity.created_at)} Commented {timeAgo(activity.created_at)}
@ -276,7 +285,8 @@ export const Feeds: React.FC<any> = ({ activities }) => (
<div className="mt-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"> <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">
{activity.field ? ( {activity.field ? (
activityDetails[activity.field as keyof typeof activityDetails]?.icon activityDetails[activity.field as keyof typeof activityDetails]
?.icon
) : activity.actor_detail.avatar && ) : activity.actor_detail.avatar &&
activity.actor_detail.avatar !== "" ? ( activity.actor_detail.avatar !== "" ? (
<img <img
@ -328,4 +338,5 @@ export const Feeds: React.FC<any> = ({ activities }) => (
})} })}
</ul> </ul>
</div> </div>
); );
};

View File

@ -3,6 +3,7 @@ export * from "./modals";
export * from "./sidebar"; export * from "./sidebar";
export * from "./theme"; export * from "./theme";
export * from "./views"; export * from "./views";
export * from "./activity";
export * from "./feeds"; export * from "./feeds";
export * from "./reaction-selector"; export * from "./reaction-selector";
export * from "./image-picker-popover"; export * from "./image-picker-popover";

View File

@ -8,12 +8,12 @@ import useSWR from "swr";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
// components // components
import { ActivityIcon, ActivityMessage } from "components/core";
import { CommentCard } from "components/issues/comment"; import { CommentCard } from "components/issues/comment";
// ui // ui
import { Icon, Loader } from "components/ui"; import { Icon, Loader } from "components/ui";
// helpers // helpers
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
import { activityDetails } from "helpers/activity.helper";
// types // types
import { ICurrentUserResponse, IIssueComment } from "types"; import { ICurrentUserResponse, IIssueComment } from "types";
// fetch-keys // fetch-keys
@ -91,11 +91,11 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
<ul role="list" className="-mb-4"> <ul role="list" className="-mb-4">
{issueActivities.map((activityItem, index) => { {issueActivities.map((activityItem, index) => {
// determines what type of action is performed // determines what type of action is performed
const message = activityItem.field const message = activityItem.field ? (
? activityDetails[activityItem.field as keyof typeof activityDetails]?.message( <ActivityMessage activity={activityItem} />
activityItem ) : (
) "created the issue."
: "created the issue."; );
if ("field" in activityItem && activityItem.field !== "updated_by") { if ("field" in activityItem && activityItem.field !== "updated_by") {
return ( return (
@ -116,8 +116,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
activityItem.new_value === "restore" ? ( activityItem.new_value === "restore" ? (
<Icon iconName="history" className="text-sm text-custom-text-200" /> <Icon iconName="history" className="text-sm text-custom-text-200" />
) : ( ) : (
activityDetails[activityItem.field as keyof typeof activityDetails] <ActivityIcon activity={activityItem} />
?.icon
) )
) : activityItem.actor_detail.avatar && ) : activityItem.actor_detail.avatar &&
activityItem.actor_detail.avatar !== "" ? ( activityItem.actor_detail.avatar !== "" ? (

View File

@ -140,11 +140,7 @@ export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentD
ref={showEditorRef} ref={showEditorRef}
/> />
<CommentReaction <CommentReaction projectId={comment.project} commentId={comment.id} />
workspaceSlug={comment?.workspace_detail?.slug}
projectId={comment.project}
commentId={comment.id}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,7 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useCommentReaction from "hooks/use-comment-reaction"; import useCommentReaction from "hooks/use-comment-reaction";
@ -9,13 +11,15 @@ import { ReactionSelector } from "components/core";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
type Props = { type Props = {
workspaceSlug?: string | string[];
projectId?: string | string[]; projectId?: string | string[];
commentId: string; commentId: string;
}; };
export const CommentReaction: React.FC<Props> = (props) => { export const CommentReaction: React.FC<Props> = (props) => {
const { workspaceSlug, projectId, commentId } = props; const { projectId, commentId } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUser(); const { user } = useUser();

View File

@ -18,6 +18,7 @@ import useToast from "hooks/use-toast";
import useInboxView from "hooks/use-inbox-view"; import useInboxView from "hooks/use-inbox-view";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
import useMyIssues from "hooks/my-issues/use-my-issues";
// components // components
import { IssueForm } from "components/issues"; import { IssueForm } from "components/issues";
// types // types
@ -85,6 +86,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const { user } = useUser(); const { user } = useUser();
const { projects } = useProjects(); const { projects } = useProjects();
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string }; if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
@ -243,6 +246,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (issueView === "calendar") mutate(calendarFetchKey); if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "gantt_chart") mutate(ganttFetchKey); if (issueView === "gantt_chart") mutate(ganttFetchKey);
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey); if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();
setToastAlert({ setToastAlert({
type: "success", type: "success",

View File

@ -146,13 +146,16 @@ export const MyIssuesViewOptions: React.FC = () => {
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4> <h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu <CustomMenu
label={ label={
groupBy === "project" groupBy === "project"
? "Project" ? "Project"
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)?.name ?? : GROUP_BY_OPTIONS.find((option) => option.key === groupBy)
"Select" ?.name ?? "Select"
} }
className="!w-full"
buttonClassName="w-full"
> >
{GROUP_BY_OPTIONS.map((option) => { {GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null; if (issueView === "kanban" && option.key === null) return null;
@ -170,16 +173,21 @@ export const MyIssuesViewOptions: React.FC = () => {
})} })}
</CustomMenu> </CustomMenu>
</div> </div>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4> <h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu <CustomMenu
label={ label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ?? ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select" "Select"
} }
className="!w-full"
buttonClassName="w-full"
> >
{ORDER_BY_OPTIONS.map((option) => { {ORDER_BY_OPTIONS.map((option) => {
if (groupBy === "priority" && option.key === "priority") return null; if (groupBy === "priority" && option.key === "priority")
return null;
if (option.key === "sort_order") return null; if (option.key === "sort_order") return null;
return ( return (
@ -195,15 +203,19 @@ export const MyIssuesViewOptions: React.FC = () => {
})} })}
</CustomMenu> </CustomMenu>
</div> </div>
</div>
</> </>
)} )}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4> <h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu <CustomMenu
label={ label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type) FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
?.name ?? "Select" ?.name ?? "Select"
} }
className="!w-full"
buttonClassName="w-full"
> >
{FILTER_ISSUE_OPTIONS.map((option) => ( {FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem <CustomMenu.MenuItem
@ -219,32 +231,23 @@ export const MyIssuesViewOptions: React.FC = () => {
))} ))}
</CustomMenu> </CustomMenu>
</div> </div>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && ( {issueView !== "calendar" && issueView !== "spreadsheet" && (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4> <h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} /> <ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
</div> </div>
{/* <div className="relative flex justify-end gap-x-3"> </div>
<button type="button" onClick={() => resetFilterToDefault()}>
Reset to default
</button>
<button
type="button"
className="font-medium text-custom-primary"
onClick={() => setNewFilterDefaultView()}
>
Set as default
</button>
</div> */}
</> </>
)} )}
</div> </div>
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4> <h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2 text-custom-text-200">
{Object.keys(properties).map((key) => { {Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null; if (key === "estimate" && !isEstimateActive) return null;

View File

@ -1,14 +1,14 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr"; import useSWR from "swr";
// services // services
import userService from "services/user.service"; import userService from "services/user.service";
// components
import { ActivityMessage } from "components/core";
// ui // ui
import { Icon, Loader } from "components/ui"; import { Icon, Loader } from "components/ui";
// helpers // helpers
import { activityDetails } from "helpers/activity.helper";
import { timeAgo } from "helpers/date-time.helper"; import { timeAgo } from "helpers/date-time.helper";
// fetch-keys // fetch-keys
import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys"; import { USER_PROFILE_ACTIVITY } from "constants/fetch-keys";
@ -55,12 +55,12 @@ export const ProfileActivity = () => {
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "} {activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
</span> </span>
{activity.field ? ( {activity.field ? (
activityDetails[activity.field]?.message(activity as any) <ActivityMessage activity={activity} showIssue />
) : ( ) : (
<span> <span>
created this{" "} created this{" "}
<a <a
href={`/${activity.workspace_detail.slug}/projects/${activity.project}/issues/${activity.issue}`} href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline" className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"

View File

@ -172,13 +172,17 @@ export const ProfileIssuesViewOptions: React.FC = () => {
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4> <h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu <CustomMenu
label={ label={
groupByProperty === "project" groupByProperty === "project"
? "Project" ? "Project"
: GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty) : GROUP_BY_OPTIONS.find(
?.name ?? "Select" (option) => option.key === groupByProperty
)?.name ?? "Select"
} }
className="!w-full"
buttonClassName="w-full"
> >
{GROUP_BY_OPTIONS.map((option) => { {GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null; if (issueView === "kanban" && option.key === null) return null;
@ -196,13 +200,17 @@ export const ProfileIssuesViewOptions: React.FC = () => {
})} })}
</CustomMenu> </CustomMenu>
</div> </div>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4> <h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu <CustomMenu
label={ label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ?? ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select" "Select"
} }
className="!w-full"
buttonClassName="w-full"
> >
{ORDER_BY_OPTIONS.map((option) => { {ORDER_BY_OPTIONS.map((option) => {
if (groupByProperty === "priority" && option.key === "priority") if (groupByProperty === "priority" && option.key === "priority")
@ -222,15 +230,19 @@ export const ProfileIssuesViewOptions: React.FC = () => {
})} })}
</CustomMenu> </CustomMenu>
</div> </div>
</div>
</> </>
)} )}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4> <h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu <CustomMenu
label={ label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type) FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
?.name ?? "Select" ?.name ?? "Select"
} }
className="!w-full"
buttonClassName="w-full"
> >
{FILTER_ISSUE_OPTIONS.map((option) => ( {FILTER_ISSUE_OPTIONS.map((option) => (
<CustomMenu.MenuItem <CustomMenu.MenuItem
@ -246,32 +258,23 @@ export const ProfileIssuesViewOptions: React.FC = () => {
))} ))}
</CustomMenu> </CustomMenu>
</div> </div>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && ( {issueView !== "calendar" && issueView !== "spreadsheet" && (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4> <h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} /> <ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
</div> </div>
{/* <div className="relative flex justify-end gap-x-3"> </div>
<button type="button" onClick={() => resetFilterToDefault()}>
Reset to default
</button>
<button
type="button"
className="font-medium text-custom-primary"
onClick={() => setNewFilterDefaultView()}
>
Set as default
</button>
</div> */}
</> </>
)} )}
</div> </div>
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4> <h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2 text-custom-text-200">
{Object.keys(properties).map((key) => { {Object.keys(properties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null; if (key === "estimate" && !isEstimateActive) return null;

View File

@ -8,6 +8,7 @@ import useSWR from "swr";
import { DropResult } from "react-beautiful-dnd"; import { DropResult } from "react-beautiful-dnd";
// services // services
import issuesService from "services/issues.service"; import issuesService from "services/issues.service";
import userService from "services/user.service";
// hooks // hooks
import useProfileIssues from "hooks/use-profile-issues"; import useProfileIssues from "hooks/use-profile-issues";
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
@ -19,7 +20,7 @@ import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IIssue, IIssueFilterOptions } from "types"; import { IIssue, IIssueFilterOptions } from "types";
// fetch-keys // fetch-keys
import { WORKSPACE_LABELS } from "constants/fetch-keys"; import { USER_PROFILE_PROJECT_SEGREGATION, WORKSPACE_LABELS } from "constants/fetch-keys";
export const ProfileIssuesView = () => { export const ProfileIssuesView = () => {
// create issue modal // create issue modal
@ -60,6 +61,16 @@ export const ProfileIssuesView = () => {
params, params,
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString()); } = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
const { data: profileData } = useSWR(
workspaceSlug && userId
? USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
: null,
workspaceSlug && userId
? () =>
userService.getUserProfileProjectsSegregation(workspaceSlug.toString(), userId.toString())
: null
);
const { data: labels } = useSWR( const { data: labels } = useSWR(
workspaceSlug && (filters?.labels ?? []).length > 0 workspaceSlug && (filters?.labels ?? []).length > 0
? WORKSPACE_LABELS(workspaceSlug.toString()) ? WORKSPACE_LABELS(workspaceSlug.toString())
@ -268,10 +279,10 @@ export const ProfileIssuesView = () => {
dragDisabled={groupByProperty !== "priority"} dragDisabled={groupByProperty !== "priority"}
emptyState={{ emptyState={{
title: router.pathname.includes("assigned") title: router.pathname.includes("assigned")
? `Issues assigned to ${user?.first_name} ${user?.last_name} will appear here` ? `Issues assigned to ${profileData?.user_data.first_name} ${profileData?.user_data.last_name} will appear here`
: router.pathname.includes("created") : router.pathname.includes("created")
? `Issues created by ${user?.first_name} ${user?.last_name} will appear here` ? `Issues created by ${profileData?.user_data.first_name} ${profileData?.user_data.last_name} will appear here`
: `Issues subscribed by ${user?.first_name} ${user?.last_name} will appear here`, : `Issues subscribed by ${profileData?.user_data.first_name} ${profileData?.user_data.last_name} will appear here`,
}} }}
handleOnDragEnd={handleOnDragEnd} handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction} handleIssueAction={handleIssueAction}

View File

@ -137,7 +137,7 @@ export const SingleSidebarProject: React.FC<Props> = ({
{({ open }) => ( {({ open }) => (
<> <>
<div <div
className={`group relative text-custom-sidebar-text-10 px-2 py-1 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${ className={`group relative text-custom-sidebar-text-10 px-2 py-1 ml-1.5 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${
snapshot?.isDragging ? "opacity-60" : "" snapshot?.isDragging ? "opacity-60" : ""
}`} }`}
> >

View File

@ -284,7 +284,7 @@ export const WorkspaceSidebarDropdown = () => {
onClick={handleSignOut} onClick={handleSignOut}
> >
<Icon iconName="logout" className="!text-base" /> <Icon iconName="logout" className="!text-base" />
Log out Sign out
</Menu.Item> </Menu.Item>
</div> </div>
</Menu.Items> </Menu.Items>

View File

@ -1,5 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// services // services
@ -12,6 +14,8 @@ import { IIssue } from "types";
import { USER_ISSUES } from "constants/fetch-keys"; import { USER_ISSUES } from "constants/fetch-keys";
const useMyIssues = (workspaceSlug: string | undefined) => { const useMyIssues = (workspaceSlug: string | undefined) => {
const router = useRouter();
const { filters, groupBy, orderBy } = useMyIssuesFilters(workspaceSlug); const { filters, groupBy, orderBy } = useMyIssuesFilters(workspaceSlug);
const params: any = { const params: any = {
@ -27,8 +31,12 @@ const useMyIssues = (workspaceSlug: string | undefined) => {
}; };
const { data: myIssues, mutate: mutateMyIssues } = useSWR( const { data: myIssues, mutate: mutateMyIssues } = useSWR(
workspaceSlug ? USER_ISSUES(workspaceSlug.toString(), params) : null, workspaceSlug && router.pathname.includes("my-issues")
workspaceSlug ? () => userService.userIssues(workspaceSlug.toString(), params) : null ? USER_ISSUES(workspaceSlug.toString(), params)
: null,
workspaceSlug && router.pathname.includes("my-issues")
? () => userService.userIssues(workspaceSlug.toString(), params)
: null
); );
const groupedIssues: const groupedIssues:

View File

@ -25,6 +25,25 @@
:root { :root {
color-scheme: light !important; color-scheme: light !important;
--color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-background-100: 255, 255, 255; /* primary bg */ --color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */ --color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* tertiary bg */ --color-background-80: 245, 245, 245; /* tertiary bg */

View File

@ -38,19 +38,6 @@ export interface IIssueModule {
workspace: string; workspace: string;
} }
export interface IIssueCycle {
created_at: Date;
created_by: string;
cycle: string;
cycle_detail: ICycle;
id: string;
issue: string;
project: string;
updated_at: Date;
updated_by: string;
workspace: string;
}
export interface IIssueParent { export interface IIssueParent {
description: any; description: any;
id: string; id: string;
@ -200,18 +187,26 @@ export interface IIssueActivity {
created_by: string; created_by: string;
field: string | null; field: string | null;
id: string; id: string;
issue: string; issue: string | null;
issue_comment: string | null; issue_comment: string | null;
issue_detail: {
description: any;
description_html: string;
id: string;
name: string;
priority: string | null;
sequence_id: string;
} | null;
new_identifier: string | null; new_identifier: string | null;
new_value: string | null; new_value: string | null;
old_identifier: string | null; old_identifier: string | null;
old_value: string | null; old_value: string | null;
project: string; project: string;
project_detail: IProjectLite;
updated_at: Date; updated_at: Date;
updated_by: string; updated_by: string;
verb: string; verb: string;
workspace: string; workspace: string;
workspace_detail: IWorkspaceLite;
} }
export interface IIssueComment extends IIssueActivity { export interface IIssueComment extends IIssueActivity {

View File

@ -1,5 +1,6 @@
import { import {
IIssue, IIssue,
IIssueActivity,
IIssueLite, IIssueLite,
IWorkspace, IWorkspace,
IWorkspaceLite, IWorkspaceLite,
@ -99,29 +100,6 @@ export interface IUserWorkspaceDashboard {
upcoming_issues: IIssueLite[]; upcoming_issues: IIssueLite[];
} }
export interface IUserDetailedActivity {
actor: string;
actor_detail: IUserLite;
attachments: any[];
comment: string;
created_at: string;
created_by: string | null;
field: string;
id: string;
issue: string;
issue_comment: string | null;
new_identifier: string | null;
new_value: string | null;
old_identifier: string | null;
old_value: string | null;
project: string;
updated_at: string;
updated_by: string | null;
verb: string;
workspace: string;
workspace_detail: IWorkspaceLite;
}
export interface IUserActivityResponse { export interface IUserActivityResponse {
count: number; count: number;
extra_stats: null; extra_stats: null;
@ -129,7 +107,7 @@ export interface IUserActivityResponse {
next_page_results: boolean; next_page_results: boolean;
prev_cursor: string; prev_cursor: string;
prev_page_results: boolean; prev_page_results: boolean;
results: IUserDetailedActivity[]; results: IIssueActivity[];
total_pages: number; total_pages: number;
} }