[WEB-1680] dev: issue detail activity revamp and issue detail page improvement (#5075)

* chore: issue link activity message updated

* chore: activity filter type constant added

* dev: issue activity revamp and code refactor

* chore: issue detail widget oreder updated in peek overview

* chore: issue detail page padding improvement

* fix: relation widget toast alert

* fix: relation widget toast alert

* fix: peek overview attachment delete modal

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: issue detail sidebar parent field
This commit is contained in:
Anmol Singh Bhatia
2024-07-08 19:33:19 +05:30
committed by GitHub
parent fd61079c8b
commit 53e5d4b40c
14 changed files with 208 additions and 132 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { FC, useState } from "react";
import { FC } from "react";
import { observer } from "mobx-react";
import { Trash } from "lucide-react";
// ui
@@ -33,9 +33,9 @@ export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer(
const { getUserDetails } = useMember();
const {
attachment: { getAttachmentById },
isDeleteAttachmentModalOpen,
toggleDeleteAttachmentModal,
} = useIssueDetail();
// state
const [isDeleteIssueAttachmentModalOpen, setIsDeleteIssueAttachmentModalOpen] = useState(false);
// derived values
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
@@ -46,10 +46,10 @@ export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer(
return (
<>
{isDeleteIssueAttachmentModalOpen && (
{isDeleteAttachmentModalOpen && (
<IssueAttachmentDeleteModal
isOpen={isDeleteIssueAttachmentModalOpen}
onClose={() => setIsDeleteIssueAttachmentModalOpen(false)}
isOpen={!!isDeleteAttachmentModalOpen}
onClose={() => toggleDeleteAttachmentModal(null)}
handleAttachmentOperations={handleAttachmentOperations}
data={attachment}
/>
@@ -95,7 +95,7 @@ export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer(
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsDeleteIssueAttachmentModalOpen(true);
toggleDeleteAttachmentModal(attachmentId);
}}
>
<div className="flex items-center gap-2">

View File

@@ -160,7 +160,6 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
onSubmit={async () =>
await issueOperations.remove(workspaceSlug, projectId, issueCrudState?.delete?.issue?.id as string)
}
isSubIssue
/>
)}

View File

@@ -71,22 +71,12 @@ export const useRelationOperations = (): TRelationIssueOperations => {
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await removeIssue(workspaceSlug, projectId, issueId);
setToast({
title: "Success!",
type: TOAST_TYPE.SUCCESS,
message: "Issue deleted successfully",
});
captureIssueEvent({
eventName: ISSUE_DELETED,
payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" },
path: pathname,
});
} catch (error) {
setToast({
title: "Error!",
type: TOAST_TYPE.ERROR,
message: "Issue delete failed",
});
captureIssueEvent({
eventName: ISSUE_DELETED,
payload: { id: issueId, state: "FAILED", element: "Issue detail page" },

View File

@@ -1,9 +1,10 @@
import { FC } from "react";
import { observer } from "mobx-react";
// hooks
import { EActivityFilterType } from "@/constants/issue";
import { useIssueDetail } from "@/hooks/store";
// components
import { IssueActivityList } from "./activity/activity-list";
import { IssueActivityItem } from "./activity/activity-list";
import { IssueCommentCard } from "./comments/comment-card";
// types
import { TActivityOperations } from "./root";
@@ -12,13 +13,15 @@ type TIssueActivityCommentRoot = {
workspaceSlug: string;
projectId: string;
issueId: string;
selectedFilters: EActivityFilterType[];
activityOperations: TActivityOperations;
showAccessSpecifier?: boolean;
disabled?: boolean;
};
export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer((props) => {
const { workspaceSlug, issueId, activityOperations, showAccessSpecifier, projectId, disabled } = props;
const { workspaceSlug, issueId, selectedFilters, activityOperations, showAccessSpecifier, projectId, disabled } =
props;
// hooks
const {
activity: { getActivityCommentByIssueId },
@@ -28,9 +31,19 @@ export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer(
const activityComments = getActivityCommentByIssueId(issueId);
if (!activityComments || (activityComments && activityComments.length <= 0)) return <></>;
const isCommentFilterSelected = selectedFilters.includes(EActivityFilterType.COMMENT);
const isActivityFilterSelected = selectedFilters.includes(EActivityFilterType.ACTIVITY);
const filteredActivityComments = activityComments.filter(
(activityComment) =>
(activityComment.activity_type === "COMMENT" && isCommentFilterSelected) ||
(activityComment.activity_type === "ACTIVITY" && isActivityFilterSelected)
);
return (
<div>
{activityComments.map((activityComment, index) =>
{filteredActivityComments.map((activityComment, index) =>
activityComment.activity_type === "COMMENT" ? (
<IssueCommentCard
projectId={projectId}
@@ -38,14 +51,14 @@ export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer(
workspaceSlug={workspaceSlug}
commentId={activityComment.id}
activityOperations={activityOperations}
ends={index === 0 ? "top" : index === activityComments.length - 1 ? "bottom" : undefined}
ends={index === 0 ? "top" : index === filteredActivityComments.length - 1 ? "bottom" : undefined}
showAccessSpecifier={showAccessSpecifier}
disabled={disabled}
/>
) : activityComment.activity_type === "ACTIVITY" ? (
<IssueActivityList
<IssueActivityItem
activityId={activityComment.id}
ends={index === 0 ? "top" : index === activityComments.length - 1 ? "bottom" : undefined}
ends={index === 0 ? "top" : index === filteredActivityComments.length - 1 ? "bottom" : undefined}
/>
) : (
<></>

View File

@@ -0,0 +1,83 @@
import React, { FC, Fragment } from "react";
import { observer } from "mobx-react";
import { Check, ListFilter } from "lucide-react";
import { Popover, Transition } from "@headlessui/react";
// ui
import { Button } from "@plane/ui";
// constants
import { ACTIVITY_FILTER_TYPE_OPTIONS, EActivityFilterType } from "@/constants/issue";
// helper
import { cn } from "@/helpers/common.helper";
type Props = {
selectedFilters: EActivityFilterType[];
toggleFilter: (filter: EActivityFilterType) => void;
};
export const ActivityFilter: FC<Props> = observer((props) => {
const { selectedFilters, toggleFilter } = props;
return (
<Popover as="div" className="relative">
{({ open }) => (
<>
<Popover.Button as={React.Fragment}>
<Button
variant="neutral-primary"
size="sm"
prependIcon={<ListFilter className="h-3 w-3" />}
className="relative"
>
<span className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>Filters</span>
</Button>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute mt-2 right-0 z-10 min-w-40">
<div className="p-2 rounded-md border border-custom-border-200 bg-custom-background-100">
{ACTIVITY_FILTER_TYPE_OPTIONS.map((filter) => {
const isSelected = selectedFilters.includes(filter.value);
return (
<div
key={filter.value}
className="flex items-center gap-2 text-sm cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
onClick={() => toggleFilter(filter.value)}
>
<div
className={cn(
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all bg-custom-background-90",
{
"bg-custom-primary text-white": isSelected,
"bg-custom-background-80 text-custom-text-400": isSelected && selectedFilters.length === 1,
"bg-custom-background-90": !isSelected,
}
)}
>
{isSelected && <Check className="h-2.5 w-2.5" />}
</div>
<div
className={cn(
"whitespace-nowrap",
isSelected ? "text-custom-text-100" : "text-custom-text-200"
)}
>
{filter.label}
</div>
</div>
);
})}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
});

View File

@@ -27,7 +27,7 @@ export const IssueLinkActivity: FC<TIssueLinkActivity> = observer((props) => {
<>
{activity.verb === "created" ? (
<>
<span>added this </span>
<span>added </span>
<a
href={`${activity.new_value}`}
target="_blank"

View File

@@ -24,12 +24,12 @@ import {
IssueInboxActivity,
} from "./actions";
type TIssueActivityList = {
type TIssueActivityItem = {
activityId: string;
ends: "top" | "bottom" | undefined;
};
export const IssueActivityList: FC<TIssueActivityList> = observer((props) => {
export const IssueActivityItem: FC<TIssueActivityItem> = observer((props) => {
const { activityId, ends } = props;
// hooks
const {

View File

@@ -4,6 +4,7 @@ export * from "./activity-comment-root";
// activity
export * from "./activity/activity-list";
export * from "./activity-filter";
// issue comment
export * from "./comments";

View File

@@ -1,14 +1,15 @@
"use client";
import { FC, useMemo, useState } from "react";
import { FC, Fragment, useMemo, useState } from "react";
import { observer } from "mobx-react";
import { History, LucideIcon, MessageCircle } from "lucide-react";
// types
import { TIssueComment } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { IssueActivityCommentRoot, IssueCommentRoot, IssueCommentCreate } from "@/components/issues";
import { ActivityFilter, IssueActivityCommentRoot, IssueCommentCreate } from "@/components/issues";
// constants
import { EActivityFilterType } from "@/constants/issue";
// hooks
import { useIssueDetail, useProject } from "@/hooks/store";
@@ -19,21 +20,6 @@ type TIssueActivity = {
disabled?: boolean;
};
type TActivityTabs = "all" | "comments";
const activityTabs: { key: TActivityTabs; title: string; icon: LucideIcon }[] = [
{
key: "comments",
title: "Comments",
icon: MessageCircle,
},
{
key: "all",
title: "All activity",
icon: History,
},
];
export type TActivityOperations = {
createComment: (data: Partial<TIssueComment>) => Promise<void>;
updateComment: (commentId: string, data: Partial<TIssueComment>) => Promise<void>;
@@ -46,7 +32,18 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
const { createComment, updateComment, removeComment } = useIssueDetail();
const { getProjectById } = useProject();
// state
const [activityTab, setActivityTab] = useState<TActivityTabs>("comments");
const [selectedFilters, setSelectedFilters] = useState([EActivityFilterType.COMMENT, EActivityFilterType.ACTIVITY]);
// toggle filter
const toggleFilter = (filter: EActivityFilterType) => {
setSelectedFilters((prevFilters) => {
if (prevFilters.includes(filter)) {
if (prevFilters.length === 1) return prevFilters; // Ensure at least one filter is applied
return prevFilters.filter((f) => f !== filter);
} else {
return [...prevFilters, filter];
}
});
};
const activityOperations: TActivityOperations = useMemo(
() => ({
@@ -109,74 +106,36 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
if (!project) return <></>;
return (
<div className="space-y-3 pt-3">
<div className="space-y-4 pt-3">
{/* header */}
<div className="text-lg text-custom-text-100">Activity</div>
<div className="flex items-center justify-between">
<div className="text-lg text-custom-text-100">Activity</div>
<ActivityFilter selectedFilters={selectedFilters} toggleFilter={toggleFilter} />
</div>
{/* rendering activity */}
<div className="space-y-3">
<div className="relative flex items-center gap-1">
{activityTabs.map((tab) => (
<div
key={tab.key}
className={`relative flex items-center px-2 py-1.5 gap-1 cursor-pointer transition-all rounded
${
tab.key === activityTab
? `text-custom-text-100 bg-custom-background-80`
: `text-custom-text-200 hover:bg-custom-background-80`
}`}
onClick={() => setActivityTab(tab.key)}
>
<div className="flex-shrink-0 w-4 h-4 flex justify-center items-center">
<tab.icon className="w-3 h-3" />
</div>
<div className="text-sm">{tab.title}</div>
</div>
))}
</div>
<div className="min-h-[200px]">
{activityTab === "all" ? (
<div className="space-y-3">
<IssueActivityCommentRoot
<div className="space-y-3">
<IssueActivityCommentRoot
projectId={projectId}
workspaceSlug={workspaceSlug}
issueId={issueId}
selectedFilters={selectedFilters}
activityOperations={activityOperations}
showAccessSpecifier={!!project.anchor}
disabled={disabled}
/>
{!disabled && (
<IssueCommentCreate
issueId={issueId}
projectId={projectId}
workspaceSlug={workspaceSlug}
issueId={issueId}
activityOperations={activityOperations}
showAccessSpecifier={!!project.anchor}
disabled={disabled}
/>
{!disabled && (
<IssueCommentCreate
issueId={issueId}
projectId={projectId}
workspaceSlug={workspaceSlug}
activityOperations={activityOperations}
showAccessSpecifier={!!project.anchor}
/>
)}
</div>
) : (
<div className="space-y-3">
<IssueCommentRoot
projectId={projectId}
workspaceSlug={workspaceSlug}
issueId={issueId}
activityOperations={activityOperations}
showAccessSpecifier={!!project.anchor}
disabled={disabled}
/>
{!disabled && (
<IssueCommentCreate
issueId={issueId}
projectId={projectId}
workspaceSlug={workspaceSlug}
activityOperations={activityOperations}
showAccessSpecifier={!!project.anchor}
/>
)}
</div>
)}
)}
</div>
</div>
</div>
</div>

View File

@@ -58,7 +58,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
return (
<>
<div className="rounded-lg space-y-4 pl-3">
<div className="rounded-lg space-y-4">
{issue.parent_id && (
<IssueParentDetail
workspaceSlug={workspaceSlug}
@@ -117,18 +117,14 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
)}
</div>
<div className="pl-3">
<IssueDetailWidgets
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={!isEditable}
/>
</div>
<IssueDetailWidgets
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={!isEditable}
/>
<div className="pl-3">
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={isArchived} />
</div>
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={isArchived} />
</>
);
});

View File

@@ -348,7 +348,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
/>
) : (
<div className="flex h-full w-full overflow-hidden">
<div className="max-w-2/3 h-full w-full space-y-8 overflow-y-auto px-6 py-5">
<div className="max-w-2/3 h-full w-full space-y-8 overflow-y-auto px-9 py-5">
<IssueMainContent
workspaceSlug={workspaceSlug}
swrIssueDetails={swrIssueDetails}

View File

@@ -2,7 +2,7 @@
import React from "react";
import { observer } from "mobx-react";
import { CalendarCheck2, CalendarClock, Signal, Tag, Triangle, UserCircle2, Users } from "lucide-react";
import { CalendarCheck2, CalendarClock, LayoutPanelTop, Signal, Tag, Triangle, UserCircle2, Users } from "lucide-react";
// ui
import { ContrastIcon, DiceIcon, DoubleCircleIcon } from "@plane/ui";
// components
@@ -14,7 +14,7 @@ import {
StateDropdown,
} from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { IssueCycleSelect, IssueLabel, IssueModuleSelect } from "@/components/issues";
import { IssueCycleSelect, IssueLabel, IssueModuleSelect, IssueParentSelect } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
@@ -250,6 +250,21 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
</div>
)}
<div className="flex h-8 items-center gap-2">
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
<LayoutPanelTop className="h-4 w-4 flex-shrink-0" />
<span>Parent</span>
</div>
<IssueParentSelect
className="h-full w-3/5 flex-grow"
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issueOperations={issueOperations}
disabled={!isEditable}
/>
</div>
<div className="flex min-h-8 gap-2">
<div className="flex w-2/5 flex-shrink-0 gap-1 pt-2 text-sm text-custom-text-300">
<Tag className="h-4 w-4 flex-shrink-0" />

View File

@@ -184,13 +184,6 @@ export const IssueView: FC<IIssueView> = observer((props) => {
setIsSubmitting={(value) => setIsSubmitting(value)}
/>
<IssueDetailWidgets
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled}
/>
<PeekOverviewProperties
workspaceSlug={workspaceSlug}
projectId={projectId}
@@ -199,6 +192,15 @@ export const IssueView: FC<IIssueView> = observer((props) => {
disabled={disabled || is_archived}
/>
<div className="py-2">
<IssueDetailWidgets
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled}
/>
</div>
<IssueActivity
workspaceSlug={workspaceSlug}
projectId={projectId}
@@ -221,12 +223,14 @@ export const IssueView: FC<IIssueView> = observer((props) => {
setIsSubmitting={(value) => setIsSubmitting(value)}
/>
<IssueDetailWidgets
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled}
/>
<div className="py-2">
<IssueDetailWidgets
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
disabled={disabled}
/>
</div>
<IssueActivity
workspaceSlug={workspaceSlug}

View File

@@ -485,3 +485,19 @@ export enum EServerGroupByToFilterOptions {
"project_id" = "project",
"created_by" = "created_by",
}
export enum EActivityFilterType {
COMMENT = "COMMENT",
ACTIVITY = "ACTIVITY",
}
export const ACTIVITY_FILTER_TYPE_OPTIONS = [
{
value: EActivityFilterType.COMMENT,
label: "Comments",
},
{
value: EActivityFilterType.ACTIVITY,
label: "Updates",
},
];