forked from github/plane
chore: inbox issue restructure the components and store (#3456)
* chore: inbox-issues store and type updates * chore: issue inbox payload change for GET and POST * chore: issue inbox payload change for PATCH * chore: inbox-issue new hooks and store updates * chore: update inbox issue template. * chore: UI root * chore: sidebar issues render * chore: inbox issue details page layout. * chore: inbox issue filters * chore: inbox issue status card. * chore: add loader. * chore: active inbox issue styles. * chore: inbox filters * chore: inbox applied filters UI * chore: inbox issue approval header * chore: inbox issue approval header operations * chore: issue reaction and activity fetch in issue_inbox store * chore: posthog enabled --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
parent
911211cf3d
commit
b66f07845a
@ -111,6 +111,7 @@ from .inbox import (
|
||||
InboxSerializer,
|
||||
InboxIssueSerializer,
|
||||
IssueStateInboxSerializer,
|
||||
InboxIssueLiteSerializer,
|
||||
)
|
||||
|
||||
from .analytic import AnalyticViewSerializer
|
||||
|
@ -60,6 +60,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
CycleIssueSerializer,
|
||||
IssueFlatSerializer,
|
||||
IssueRelationSerializer,
|
||||
InboxIssueLiteSerializer
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
@ -80,9 +81,10 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueSerializer,
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_inbox" : InboxIssueLiteSerializer,
|
||||
}
|
||||
|
||||
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation"] else False)
|
||||
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False)
|
||||
|
||||
return self.fields
|
||||
|
||||
@ -103,6 +105,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
LabelSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueRelationSerializer,
|
||||
InboxIssueLiteSerializer
|
||||
)
|
||||
|
||||
# Expansion mapper
|
||||
@ -122,7 +125,8 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
"labels": LabelSerializer,
|
||||
"issue_cycle": CycleIssueSerializer,
|
||||
"parent": IssueSerializer,
|
||||
"issue_relation": IssueRelationSerializer
|
||||
"issue_relation": IssueRelationSerializer,
|
||||
"issue_inbox" : InboxIssueLiteSerializer,
|
||||
}
|
||||
# Check if field in expansion then expand the field
|
||||
if expand in expansion:
|
||||
|
@ -88,39 +88,24 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
Q(snoozed_till__gte=timezone.now())
|
||||
| Q(snoozed_till__isnull=True),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
inbox_id=self.kwargs.get("inbox_id"),
|
||||
)
|
||||
.select_related("issue", "workspace", "project")
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id, inbox_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
return (
|
||||
Issue.objects.filter(
|
||||
issue_inbox__inbox_id=inbox_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
|
||||
)
|
||||
.filter(**filters)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
.prefetch_related("labels", "assignees")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_inbox",
|
||||
queryset=InboxIssue.objects.only(
|
||||
"status", "duplicate_to", "snoozed_till", "source"
|
||||
),
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@ -135,16 +120,20 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_inbox",
|
||||
queryset=InboxIssue.objects.only(
|
||||
"status", "duplicate_to", "snoozed_till", "source"
|
||||
),
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(
|
||||
parent=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
||||
).distinct()
|
||||
|
||||
def list(self, request, slug, project_id, inbox_id):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||
issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data
|
||||
return Response(
|
||||
issues_data,
|
||||
status=status.HTTP_200_OK,
|
||||
@ -211,7 +200,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
|
||||
serializer = IssueStateInboxSerializer(issue)
|
||||
issue = (self.get_queryset().filter(pk=issue.id).first())
|
||||
serializer = IssueSerializer(issue ,expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
|
||||
@ -331,22 +321,20 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
||||
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||
serializer = IssueSerializer(issue, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(
|
||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
InboxIssueSerializer(inbox_issue).data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||
serializer = IssueSerializer(issue ,expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = IssueStateInboxSerializer(issue)
|
||||
issue = self.get_queryset().filter(pk=issue_id).first()
|
||||
serializer = IssueSerializer(issue, expand=self.expand,)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
||||
|
@ -68,7 +68,7 @@ from plane.bgtasks.project_invitation_task import project_invitation
|
||||
|
||||
|
||||
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
serializer_class = ProjectSerializer
|
||||
serializer_class = ProjectListSerializer
|
||||
model = Project
|
||||
webhook_event = "project"
|
||||
|
||||
@ -76,11 +76,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
ProjectBasePermission,
|
||||
]
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action in ["update", "partial_update"]:
|
||||
return ProjectSerializer
|
||||
return ProjectDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
|
65
packages/types/src/inbox/inbox-issue.d.ts
vendored
Normal file
65
packages/types/src/inbox/inbox-issue.d.ts
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
import { TIssue } from "../issues/base";
|
||||
|
||||
export enum EInboxStatus {
|
||||
PENDING = -2,
|
||||
REJECT = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
|
||||
export type TInboxStatus =
|
||||
| EInboxStatus.PENDING
|
||||
| EInboxStatus.REJECT
|
||||
| EInboxStatus.SNOOZED
|
||||
| EInboxStatus.ACCEPTED
|
||||
| EInboxStatus.DUPLICATE;
|
||||
|
||||
export type TInboxIssueDetail = {
|
||||
id?: string;
|
||||
source: "in-app";
|
||||
status: TInboxStatus;
|
||||
duplicate_to: string | undefined;
|
||||
snoozed_till: Date | undefined;
|
||||
};
|
||||
|
||||
export type TInboxIssueDetailMap = Record<
|
||||
string,
|
||||
Record<string, TInboxIssueDetail>
|
||||
>; // inbox_id -> issue_id -> TInboxIssueDetail
|
||||
|
||||
export type TInboxIssueDetailIdMap = Record<string, string[]>; // inbox_id -> issue_id[]
|
||||
|
||||
export type TInboxIssueExtendedDetail = TIssue & {
|
||||
issue_inbox: TInboxIssueDetail[];
|
||||
};
|
||||
|
||||
// property type checks
|
||||
export type TInboxPendingStatus = {
|
||||
status: EInboxStatus.PENDING;
|
||||
};
|
||||
|
||||
export type TInboxRejectStatus = {
|
||||
status: EInboxStatus.REJECT;
|
||||
};
|
||||
|
||||
export type TInboxSnoozedStatus = {
|
||||
status: EInboxStatus.SNOOZED;
|
||||
snoozed_till: Date;
|
||||
};
|
||||
|
||||
export type TInboxAcceptedStatus = {
|
||||
status: EInboxStatus.ACCEPTED;
|
||||
};
|
||||
|
||||
export type TInboxDuplicateStatus = {
|
||||
status: EInboxStatus.DUPLICATE;
|
||||
duplicate_to: string; // issue_id
|
||||
};
|
||||
|
||||
export type TInboxDetailedStatus =
|
||||
| TInboxPendingStatus
|
||||
| TInboxRejectStatus
|
||||
| TInboxSnoozedStatus
|
||||
| TInboxAcceptedStatus
|
||||
| TInboxDuplicateStatus;
|
27
packages/types/src/inbox/inbox.d.ts
vendored
Normal file
27
packages/types/src/inbox/inbox.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
export type TInboxIssueFilterOptions = {
|
||||
priority: string[];
|
||||
inbox_status: number[];
|
||||
};
|
||||
|
||||
export type TInboxIssueQueryParams = "priority" | "inbox_status";
|
||||
|
||||
export type TInboxIssueFilters = { filters: TInboxIssueFilterOptions };
|
||||
|
||||
export type TInbox = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
workspace: string;
|
||||
project: string;
|
||||
is_default: boolean;
|
||||
view_props: TInboxIssueFilters;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
pending_issue_count: number;
|
||||
};
|
||||
|
||||
export type TInboxDetailMap = Record<string, TInbox>; // inbox_id -> TInbox
|
||||
|
||||
export type TInboxDetailIdMap = Record<string, string[]>; // project_id -> inbox_id[]
|
2
packages/types/src/inbox/root.d.ts
vendored
Normal file
2
packages/types/src/inbox/root.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./inbox";
|
||||
export * from "./inbox-issue";
|
4
packages/types/src/index.d.ts
vendored
4
packages/types/src/index.d.ts
vendored
@ -13,7 +13,11 @@ export * from "./pages";
|
||||
export * from "./ai";
|
||||
export * from "./estimate";
|
||||
export * from "./importer";
|
||||
|
||||
// FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable
|
||||
export * from "./inbox";
|
||||
export * from "./inbox/root";
|
||||
|
||||
export * from "./analytics";
|
||||
export * from "./calendar";
|
||||
export * from "./notifications";
|
||||
|
@ -51,12 +51,14 @@ export const ProjectInboxHeader: FC = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<CreateInboxIssueModal isOpen={createIssueModal} onClose={() => setCreateIssueModal(false)} />
|
||||
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => setCreateIssueModal(true)}>
|
||||
Add Issue
|
||||
</Button>
|
||||
</div>
|
||||
{currentProjectDetails?.inbox_view && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CreateInboxIssueModal isOpen={createIssueModal} onClose={() => setCreateIssueModal(false)} />
|
||||
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => setCreateIssueModal(true)}>
|
||||
Add Issue
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -32,7 +32,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const { inboxesList, isInboxEnabled, getInboxId } = useInbox();
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
eventTracker: { setTrackElement },
|
||||
@ -43,6 +42,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const { getInboxesByProjectId, getInboxById } = useInbox();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
@ -89,7 +89,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const inboxDetails = projectId ? inboxesList?.[projectId]?.[0] : undefined;
|
||||
const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined;
|
||||
const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined;
|
||||
|
||||
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||
const canUserCreateIssue =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
@ -190,14 +192,15 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
{projectId && isInboxEnabled && inboxDetails && (
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${getInboxId(projectId)}`}>
|
||||
|
||||
{currentProjectDetails?.inbox_view && inboxDetails && (
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxDetails?.id}`}>
|
||||
<span>
|
||||
<Button variant="neutral-primary" size="sm" className="relative">
|
||||
Inbox
|
||||
{inboxDetails.pending_issue_count > 0 && (
|
||||
{inboxDetails?.pending_issue_count > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1.5 h-4 w-4 rounded-full border border-custom-sidebar-border-200 bg-custom-sidebar-background-80 text-custom-text-100">
|
||||
{inboxDetails.pending_issue_count}
|
||||
{inboxDetails?.pending_issue_count}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
@ -1,247 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import DatePicker from "react-datepicker";
|
||||
import { Popover } from "@headlessui/react";
|
||||
// hooks
|
||||
import { useUser, useInboxIssues } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
AcceptIssueModal,
|
||||
DeclineIssueModal,
|
||||
DeleteInboxIssueModal,
|
||||
FiltersDropdown,
|
||||
SelectDuplicateInboxIssueModal,
|
||||
} from "components/inbox";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { CheckCircle2, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react";
|
||||
// types
|
||||
import type { TInboxStatus } from "@plane/types";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
|
||||
export const InboxActionsHeader = observer(() => {
|
||||
// states
|
||||
const [date, setDate] = useState(new Date());
|
||||
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
|
||||
const [acceptIssueModal, setAcceptIssueModal] = useState(false);
|
||||
const [declineIssueModal, setDeclineIssueModal] = useState(false);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
||||
// store hooks
|
||||
const { updateIssueStatus, getIssueById } = useInboxIssues();
|
||||
const {
|
||||
currentUser,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
// derived values
|
||||
const issue = getIssueById(inboxId as string, inboxIssueId as string);
|
||||
|
||||
const markInboxStatus = async (data: TInboxStatus) => {
|
||||
if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId || !issue) return;
|
||||
|
||||
await updateIssueStatus(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
inboxId.toString(),
|
||||
issue.issue_inbox[0].id!,
|
||||
data
|
||||
).catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong while updating inbox status. Please try again.",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// const currentIssueIndex = issuesList?.findIndex((issue) => issue.issue_inbox[0].id === inboxIssueId) ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!issue?.issue_inbox[0].snoozed_till) return;
|
||||
|
||||
setDate(new Date(issue.issue_inbox[0].snoozed_till));
|
||||
}, [issue]);
|
||||
|
||||
const issueStatus = issue?.issue_inbox[0].status;
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today);
|
||||
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
{issue && (
|
||||
<>
|
||||
<SelectDuplicateInboxIssueModal
|
||||
isOpen={selectDuplicateIssue}
|
||||
onClose={() => setSelectDuplicateIssue(false)}
|
||||
value={issue?.issue_inbox[0].duplicate_to}
|
||||
onSubmit={(dupIssueId) => {
|
||||
markInboxStatus({
|
||||
status: 2,
|
||||
duplicate_to: dupIssueId,
|
||||
}).finally(() => setSelectDuplicateIssue(false));
|
||||
}}
|
||||
/>
|
||||
<AcceptIssueModal
|
||||
data={issue}
|
||||
isOpen={acceptIssueModal}
|
||||
onClose={() => setAcceptIssueModal(false)}
|
||||
onSubmit={async () => {
|
||||
await markInboxStatus({
|
||||
status: 1,
|
||||
}).finally(() => setAcceptIssueModal(false));
|
||||
}}
|
||||
/>
|
||||
<DeclineIssueModal
|
||||
data={issue}
|
||||
isOpen={declineIssueModal}
|
||||
onClose={() => setDeclineIssueModal(false)}
|
||||
onSubmit={async () => {
|
||||
await markInboxStatus({
|
||||
status: -1,
|
||||
}).finally(() => setDeclineIssueModal(false));
|
||||
}}
|
||||
/>
|
||||
<DeleteInboxIssueModal data={issue} isOpen={deleteIssueModal} onClose={() => setDeleteIssueModal(false)} />
|
||||
</>
|
||||
)}
|
||||
<div className="grid grid-cols-4 divide-x divide-custom-border-200 border-b border-custom-border-200">
|
||||
<div className="col-span-1 flex justify-between p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Inbox className="text-custom-text-200" size={16} strokeWidth={2} />
|
||||
<h3 className="font-medium">Inbox</h3>
|
||||
</div>
|
||||
<FiltersDropdown />
|
||||
</div>
|
||||
{inboxIssueId && (
|
||||
<div className="col-span-3 flex items-center justify-between gap-4 px-4">
|
||||
{/* <div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<ChevronUp size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<ChevronDown size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<div className="text-sm">
|
||||
{currentIssueIndex + 1}/{issuesList?.length ?? 0}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{isAllowed && (issueStatus === 0 || issueStatus === -2) && (
|
||||
<div className="flex-shrink-0">
|
||||
<Popover className="relative">
|
||||
<Popover.Button as="button" type="button">
|
||||
<Button variant="neutral-primary" prependIcon={<Clock size={14} strokeWidth={2} />} size="sm">
|
||||
Snooze
|
||||
</Button>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 w-80 rounded-md bg-custom-background-100 p-2 shadow-lg">
|
||||
{({ close }) => (
|
||||
<div className="flex h-full w-full flex-col gap-y-1">
|
||||
<DatePicker
|
||||
selected={date ? new Date(date) : null}
|
||||
onChange={(val) => {
|
||||
if (!val) return;
|
||||
setDate(val);
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={tomorrow}
|
||||
inline
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
close();
|
||||
markInboxStatus({
|
||||
status: 0,
|
||||
snoozed_till: new Date(date),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Snooze
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
{isAllowed && issueStatus === -2 && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<FileStack size={14} strokeWidth={2} />}
|
||||
onClick={() => setSelectDuplicateIssue(true)}
|
||||
>
|
||||
Mark as duplicate
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isAllowed && (issueStatus === 0 || issueStatus === -2) && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<CheckCircle2 className="text-green-500" size={14} strokeWidth={2} />}
|
||||
onClick={() => setAcceptIssueModal(true)}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isAllowed && issueStatus === -2 && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<XCircle className="text-red-500" size={14} strokeWidth={2} />}
|
||||
onClick={() => setDeclineIssueModal(true)}
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{(isAllowed || currentUser?.id === issue?.created_by) && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<Trash2 className="text-red-500" size={14} strokeWidth={2} />}
|
||||
onClick={() => setDeleteIssueModal(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
329
web/components/inbox/inbox-issue-actions.tsx
Normal file
329
web/components/inbox/inbox-issue-actions.tsx
Normal file
@ -0,0 +1,329 @@
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import DatePicker from "react-datepicker";
|
||||
import { Popover } from "@headlessui/react";
|
||||
// hooks
|
||||
import { useApplication, useUser, useInboxIssues, useIssueDetail, useWorkspace } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
AcceptIssueModal,
|
||||
DeclineIssueModal,
|
||||
DeleteInboxIssueModal,
|
||||
SelectDuplicateInboxIssueModal,
|
||||
} from "components/inbox";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react";
|
||||
// types
|
||||
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
|
||||
type TInboxIssueActionsHeader = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
inboxId: string;
|
||||
inboxIssueId: string | undefined;
|
||||
};
|
||||
|
||||
type TInboxIssueOperations = {
|
||||
updateInboxIssueStatus: (data: TInboxStatus) => Promise<void>;
|
||||
removeInboxIssue: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// hooks
|
||||
const {
|
||||
eventTracker: { postHogEventTracker },
|
||||
} = useApplication();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const {
|
||||
issues: { getInboxIssuesByInboxId, getInboxIssueByIssueId, updateInboxIssueStatus, removeInboxIssue },
|
||||
} = useInboxIssues();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const {
|
||||
currentUser,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// states
|
||||
const [date, setDate] = useState(new Date());
|
||||
const [selectDuplicateIssue, setSelectDuplicateIssue] = useState(false);
|
||||
const [acceptIssueModal, setAcceptIssueModal] = useState(false);
|
||||
const [declineIssueModal, setDeclineIssueModal] = useState(false);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
// derived values
|
||||
const inboxIssues = getInboxIssuesByInboxId(inboxId);
|
||||
const issueStatus = (inboxIssueId && inboxId && getInboxIssueByIssueId(inboxId, inboxIssueId)) || undefined;
|
||||
const issue = (inboxIssueId && getIssueById(inboxIssueId)) || undefined;
|
||||
|
||||
const currentIssueIndex = inboxIssues?.findIndex((issue) => issue === inboxIssueId) ?? 0;
|
||||
|
||||
const inboxIssueOperations: TInboxIssueOperations = useMemo(
|
||||
() => ({
|
||||
updateInboxIssueStatus: async (data: TInboxDetailedStatus) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId) throw new Error("Missing required parameters");
|
||||
await updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data);
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong while updating inbox status. Please try again.",
|
||||
});
|
||||
}
|
||||
},
|
||||
removeInboxIssue: async () => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !inboxId || !inboxIssueId || !currentWorkspace)
|
||||
throw new Error("Missing required parameters");
|
||||
await removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||
postHogEventTracker(
|
||||
"ISSUE_DELETED",
|
||||
{
|
||||
state: "SUCCESS",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
groupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong while deleting inbox issue. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_DELETED",
|
||||
{
|
||||
state: "FAILED",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
groupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[
|
||||
currentWorkspace,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
inboxId,
|
||||
inboxIssueId,
|
||||
updateInboxIssueStatus,
|
||||
removeInboxIssue,
|
||||
setToastAlert,
|
||||
postHogEventTracker,
|
||||
router,
|
||||
]
|
||||
);
|
||||
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const today = new Date();
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
useEffect(() => {
|
||||
if (!issueStatus || !issueStatus.snoozed_till) return;
|
||||
setDate(new Date(issueStatus.snoozed_till));
|
||||
}, [issueStatus]);
|
||||
|
||||
if (!issueStatus || !issue || !inboxIssues) return <></>;
|
||||
return (
|
||||
<>
|
||||
{issue && (
|
||||
<>
|
||||
<SelectDuplicateInboxIssueModal
|
||||
isOpen={selectDuplicateIssue}
|
||||
onClose={() => setSelectDuplicateIssue(false)}
|
||||
value={issueStatus.duplicate_to}
|
||||
onSubmit={(dupIssueId) => {
|
||||
inboxIssueOperations
|
||||
.updateInboxIssueStatus({
|
||||
status: 2,
|
||||
duplicate_to: dupIssueId,
|
||||
})
|
||||
.finally(() => setSelectDuplicateIssue(false));
|
||||
}}
|
||||
/>
|
||||
|
||||
<AcceptIssueModal
|
||||
data={issue}
|
||||
isOpen={acceptIssueModal}
|
||||
onClose={() => setAcceptIssueModal(false)}
|
||||
onSubmit={async () => {
|
||||
await inboxIssueOperations
|
||||
.updateInboxIssueStatus({
|
||||
status: 1,
|
||||
})
|
||||
.finally(() => setAcceptIssueModal(false));
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeclineIssueModal
|
||||
data={issue}
|
||||
isOpen={declineIssueModal}
|
||||
onClose={() => setDeclineIssueModal(false)}
|
||||
onSubmit={async () => {
|
||||
await inboxIssueOperations
|
||||
.updateInboxIssueStatus({
|
||||
status: -1,
|
||||
})
|
||||
.finally(() => setDeclineIssueModal(false));
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteInboxIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
onClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={async () => {
|
||||
await inboxIssueOperations.removeInboxIssue().finally(() => setDeclineIssueModal(false));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{inboxIssueId && (
|
||||
<div className="px-4 w-full h-full relative flex items-center gap-2 justify-between">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<ChevronUp size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<ChevronDown size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<div className="text-sm">
|
||||
{currentIssueIndex + 1}/{inboxIssues?.length ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{isAllowed && (issueStatus.status === 0 || issueStatus.status === -2) && (
|
||||
<div className="flex-shrink-0">
|
||||
<Popover className="relative">
|
||||
<Popover.Button as="button" type="button">
|
||||
<Button variant="neutral-primary" prependIcon={<Clock size={14} strokeWidth={2} />} size="sm">
|
||||
Snooze
|
||||
</Button>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute right-0 z-10 mt-2 w-80 rounded-md bg-custom-background-100 p-2 shadow-lg">
|
||||
{({ close }) => (
|
||||
<div className="flex h-full w-full flex-col gap-y-1">
|
||||
<DatePicker
|
||||
selected={date ? new Date(date) : null}
|
||||
onChange={(val) => {
|
||||
if (!val) return;
|
||||
setDate(val);
|
||||
}}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
minDate={tomorrow}
|
||||
inline
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
close();
|
||||
inboxIssueOperations.updateInboxIssueStatus({
|
||||
status: 0,
|
||||
snoozed_till: new Date(date),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Snooze
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAllowed && issueStatus.status === -2 && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<FileStack size={14} strokeWidth={2} />}
|
||||
onClick={() => setSelectDuplicateIssue(true)}
|
||||
>
|
||||
Mark as duplicate
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAllowed && (issueStatus.status === 0 || issueStatus.status === -2) && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<CheckCircle2 className="text-green-500" size={14} strokeWidth={2} />}
|
||||
onClick={() => setAcceptIssueModal(true)}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAllowed && issueStatus.status === -2 && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<XCircle className="text-red-500" size={14} strokeWidth={2} />}
|
||||
onClick={() => setDeclineIssueModal(true)}
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(isAllowed || currentUser?.id === issue?.created_by) && (
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
prependIcon={<Trash2 className="text-red-500" size={14} strokeWidth={2} />}
|
||||
onClick={() => setDeleteIssueModal(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
55
web/components/inbox/inbox-issue-status.tsx
Normal file
55
web/components/inbox/inbox-issue-status.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// constants
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
inboxId: string;
|
||||
issueId: string;
|
||||
iconSize?: number;
|
||||
showDescription?: boolean;
|
||||
};
|
||||
|
||||
export const InboxIssueStatus: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId, inboxId, issueId, iconSize = 18, showDescription = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
issues: { getInboxIssueByIssueId },
|
||||
} = useInboxIssues();
|
||||
|
||||
const inboxIssueDetail = getInboxIssueByIssueId(inboxId, issueId);
|
||||
if (!inboxIssueDetail) return <></>;
|
||||
|
||||
const inboxIssueStatusDetail = INBOX_STATUS.find((s) => s.status === inboxIssueDetail.status);
|
||||
if (!inboxIssueStatusDetail) return <></>;
|
||||
|
||||
const isSnoozedDatePassed =
|
||||
inboxIssueDetail.status === 0 && new Date(inboxIssueDetail.snoozed_till ?? "") < new Date();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center ${inboxIssueStatusDetail.textColor(isSnoozedDatePassed)} ${
|
||||
showDescription
|
||||
? `p-3 gap-2 text-sm rounded-md border ${inboxIssueStatusDetail.bgColor(
|
||||
isSnoozedDatePassed
|
||||
)} ${inboxIssueStatusDetail.borderColor(isSnoozedDatePassed)} `
|
||||
: "w-full justify-end gap-1 text-xs"
|
||||
}`}
|
||||
>
|
||||
<inboxIssueStatusDetail.icon size={iconSize} strokeWidth={2} />
|
||||
{showDescription ? (
|
||||
inboxIssueStatusDetail.description(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
inboxIssueDetail.duplicate_to ?? "",
|
||||
new Date(inboxIssueDetail.snoozed_till ?? "")
|
||||
)
|
||||
) : (
|
||||
<span>{inboxIssueStatusDetail.title}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,8 +1,12 @@
|
||||
export * from "./modals";
|
||||
export * from "./actions-header";
|
||||
export * from "./filters-dropdown";
|
||||
export * from "./filters-list";
|
||||
export * from "./issue-activity";
|
||||
export * from "./issue-card";
|
||||
export * from "./issues-list-sidebar";
|
||||
export * from "./main-content";
|
||||
|
||||
export * from "./inbox-issue-actions";
|
||||
export * from "./inbox-issue-status";
|
||||
|
||||
export * from "./sidebar/root";
|
||||
|
||||
export * from "./sidebar/filter/filter-selection";
|
||||
export * from "./sidebar/filter/applied-filters";
|
||||
|
||||
export * from "./sidebar/inbox-list";
|
||||
export * from "./sidebar/inbox-list-item";
|
||||
|
@ -1,130 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useApplication, useUser, useWorkspace } from "hooks/store";
|
||||
// components
|
||||
import { AddComment, IssueActivitySection } from "components/issues";
|
||||
// services
|
||||
import { IssueService, IssueCommentService } from "services/issue";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { TIssue, IIssueActivity } from "@plane/types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||
|
||||
type Props = { issueDetails: TIssue };
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
const issueCommentService = new IssueCommentService();
|
||||
|
||||
export const InboxIssueActivity: React.FC<Props> = observer(({ issueDetails }) => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store hooks
|
||||
const {
|
||||
eventTracker: { postHogEventTracker },
|
||||
} = useApplication();
|
||||
const { currentUser } = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
||||
workspaceSlug && projectId && issueDetails ? PROJECT_ISSUES_ACTIVITY(issueDetails.id) : null,
|
||||
workspaceSlug && projectId && issueDetails
|
||||
? () => issueService.getIssueActivities(workspaceSlug.toString(), projectId.toString(), issueDetails.id)
|
||||
: null
|
||||
);
|
||||
|
||||
const handleCommentUpdate = async (commentId: string, data: Partial<any>) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetails.id || !currentUser) return;
|
||||
|
||||
await issueCommentService
|
||||
.patchIssueComment(workspaceSlug.toString(), projectId.toString(), issueDetails.id, commentId, data)
|
||||
.then((res) => {
|
||||
mutateIssueActivity();
|
||||
postHogEventTracker(
|
||||
"COMMENT_UPDATED",
|
||||
{
|
||||
...res,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
groupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCommentDelete = async (commentId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetails.id || !currentUser) return;
|
||||
|
||||
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||
|
||||
await issueCommentService
|
||||
.deleteIssueComment(workspaceSlug.toString(), projectId.toString(), issueDetails.id, commentId)
|
||||
.then(() => {
|
||||
mutateIssueActivity();
|
||||
postHogEventTracker(
|
||||
"COMMENT_DELETED",
|
||||
{
|
||||
state: "SUCCESS",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
groupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddComment = async (formData: IIssueActivity) => {
|
||||
if (!workspaceSlug || !issueDetails || !currentUser) return;
|
||||
|
||||
/* FIXME: Replace this with the new issue activity component --issue-detail-- */
|
||||
// await issueCommentService
|
||||
// .createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData)
|
||||
// .then((res) => {
|
||||
// mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||
// postHogEventTracker(
|
||||
// "COMMENT_ADDED",
|
||||
// {
|
||||
// ...res,
|
||||
// state: "SUCCESS",
|
||||
// },
|
||||
// {
|
||||
// isGrouping: true,
|
||||
// groupType: "Workspace_metrics",
|
||||
// groupId: currentWorkspace?.id!,
|
||||
// }
|
||||
// );
|
||||
// })
|
||||
// .catch(() =>
|
||||
// setToastAlert({
|
||||
// type: "error",
|
||||
// title: "Error!",
|
||||
// message: "Comment could not be posted. Please try again.",
|
||||
// })
|
||||
// );
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* FIXME: Replace this with the new issue activity component --issue-detail-- */}
|
||||
{/* <h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
<IssueActivitySection
|
||||
activity={issueActivity}
|
||||
handleCommentUpdate={handleCommentUpdate}
|
||||
handleCommentDelete={handleCommentDelete}
|
||||
/>
|
||||
<AddComment onSubmit={handleAddComment} /> */}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,96 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { AlertTriangle, CalendarDays, CheckCircle2, Clock, Copy, XCircle } from "lucide-react";
|
||||
// ui
|
||||
import { Tooltip, PriorityIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useInboxIssues, useProject } from "hooks/store";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
// constants
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
|
||||
type Props = {
|
||||
active: boolean;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const InboxIssueCard: React.FC<Props> = (props) => {
|
||||
const { active } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
||||
// store hooks
|
||||
const { getIssueById } = useInboxIssues();
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const issue = getIssueById(inboxId as string, props.issueId);
|
||||
const issueStatus = issue?.issue_inbox[0].status;
|
||||
|
||||
if (!issue) return null;
|
||||
|
||||
return (
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${issue.issue_inbox[0].id}`}>
|
||||
<div
|
||||
id={issue.id}
|
||||
className={`relative min-h-[5rem] cursor-pointer select-none space-y-3 border-b border-custom-border-200 px-4 py-2 hover:bg-custom-primary/5 ${
|
||||
active ? "bg-custom-primary/5" : " "
|
||||
} ${issue.issue_inbox[0].status !== -2 ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
|
||||
</p>
|
||||
<h5 className="truncate text-sm">{issue.name}</h5>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}>
|
||||
<PriorityIcon priority={issue.priority ?? null} className="h-3.5 w-3.5" />
|
||||
</Tooltip>
|
||||
<Tooltip tooltipHeading="Created on" tooltipContent={`${renderFormattedDate(issue.created_at ?? "")}`}>
|
||||
<div className="flex items-center gap-1 rounded border border-custom-border-200 px-2 py-[0.19rem] text-xs text-custom-text-200 shadow-sm">
|
||||
<CalendarDays size={12} strokeWidth={1.5} />
|
||||
<span>{renderFormattedDate(issue.created_at ?? "")}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={`flex w-full items-center justify-end gap-1 text-xs ${
|
||||
issueStatus === 0 && new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "text-red-500"
|
||||
: INBOX_STATUS.find((s) => s.value === issueStatus)?.textColor
|
||||
}`}
|
||||
>
|
||||
{issueStatus === -2 ? (
|
||||
<>
|
||||
<AlertTriangle size={14} strokeWidth={2} />
|
||||
<span>Pending</span>
|
||||
</>
|
||||
) : issueStatus === -1 ? (
|
||||
<>
|
||||
<XCircle size={14} strokeWidth={2} />
|
||||
<span>Declined</span>
|
||||
</>
|
||||
) : issueStatus === 0 ? (
|
||||
<>
|
||||
<Clock size={14} strokeWidth={2} />
|
||||
<span>
|
||||
{new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date() ? "Snoozed date passed" : "Snoozed"}
|
||||
</span>
|
||||
</>
|
||||
) : issueStatus === 1 ? (
|
||||
<>
|
||||
<CheckCircle2 size={14} strokeWidth={2} />
|
||||
<span>Accepted</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={14} strokeWidth={2} />
|
||||
<span>Duplicate</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// components
|
||||
import { InboxIssueCard, InboxFiltersList } from "components/inbox";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
|
||||
export const InboxIssuesListSidebar = observer(() => {
|
||||
const router = useRouter();
|
||||
const { inboxIssueId } = router.query;
|
||||
|
||||
const { currentInboxIssueIds: currentInboxIssues } = useInboxIssues();
|
||||
|
||||
const issuesList = currentInboxIssues;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<InboxFiltersList />
|
||||
{issuesList ? (
|
||||
issuesList.length > 0 ? (
|
||||
<div className="h-full divide-y divide-custom-border-200 overflow-auto">
|
||||
{issuesList.map((id) => (
|
||||
<InboxIssueCard key={id} active={id === inboxIssueId} issueId={id} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full place-items-center p-4 text-center text-sm text-custom-text-200">
|
||||
{/* TODO: add filtersLength logic here */}
|
||||
{/* {filtersLength > 0 && "No issues found for the selected filters. Try changing the filters."} */}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-4 p-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,292 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { AlertTriangle, CheckCircle2, Clock, Copy, ExternalLink, Inbox, XCircle } from "lucide-react";
|
||||
// hooks
|
||||
import { useProjectState, useUser, useInboxIssues } from "hooks/store";
|
||||
// components
|
||||
import {
|
||||
IssueDescriptionForm,
|
||||
// FIXME: have to replace this once the issue details page is ready --issue-detail--
|
||||
// IssueDetailsSidebar,
|
||||
// IssueReaction,
|
||||
IssueUpdateStatus,
|
||||
} from "components/issues";
|
||||
import { InboxIssueActivity } from "components/inbox";
|
||||
// ui
|
||||
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IInboxIssue, TIssue } from "@plane/types";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
|
||||
const defaultValues: Partial<IInboxIssue> = {
|
||||
name: "",
|
||||
description_html: "",
|
||||
assignee_ids: [],
|
||||
priority: "low",
|
||||
target_date: new Date().toString(),
|
||||
label_ids: [],
|
||||
};
|
||||
|
||||
export const InboxMainContent: React.FC = observer(() => {
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
||||
// store hooks
|
||||
const { currentInboxIssueIds: currentInboxIssues, fetchIssueDetails, getIssueById, updateIssue } = useInboxIssues();
|
||||
const {
|
||||
currentUser,
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { projectStates } = useProjectState();
|
||||
// form info
|
||||
const { reset, control, watch } = useForm<TIssue>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && inboxId && inboxIssueId ? `INBOX_ISSUE_DETAILS_${inboxIssueId.toString()}` : null,
|
||||
workspaceSlug && projectId && inboxId && inboxIssueId
|
||||
? () =>
|
||||
fetchIssueDetails(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), inboxIssueId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const issuesList = currentInboxIssues;
|
||||
const issueDetails = inboxIssueId ? getIssueById(inboxId as string, inboxIssueId.toString()) : undefined;
|
||||
const currentIssueState = projectStates?.find((s) => s.id === issueDetails?.state_id);
|
||||
|
||||
const submitChanges = useCallback(
|
||||
async (formData: Partial<IInboxIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !inboxIssueId || !inboxId || !issueDetails) return;
|
||||
|
||||
await updateIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
inboxId.toString(),
|
||||
issueDetails.issue_inbox[0].id,
|
||||
formData
|
||||
);
|
||||
},
|
||||
[workspaceSlug, inboxIssueId, projectId, inboxId, issueDetails, updateIssue]
|
||||
);
|
||||
|
||||
// const onKeyDown = useCallback(
|
||||
// (e: KeyboardEvent) => {
|
||||
// if (!issuesList || !inboxIssueId) return;
|
||||
|
||||
// const currentIssueIndex = issuesList.findIndex((issue) => issue.issue_inbox[0].id === inboxIssueId);
|
||||
|
||||
// switch (e.key) {
|
||||
// case "ArrowUp":
|
||||
// Router.push({
|
||||
// pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
// query: {
|
||||
// inboxIssueId:
|
||||
// currentIssueIndex === 0
|
||||
// ? issuesList[issuesList.length - 1].issue_inbox[0].id
|
||||
// : issuesList[currentIssueIndex - 1].issue_inbox[0].id,
|
||||
// },
|
||||
// });
|
||||
// break;
|
||||
// case "ArrowDown":
|
||||
// Router.push({
|
||||
// pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
// query: {
|
||||
// inboxIssueId:
|
||||
// currentIssueIndex === issuesList.length - 1
|
||||
// ? issuesList[0].issue_inbox[0].id
|
||||
// : issuesList[currentIssueIndex + 1].issue_inbox[0].id,
|
||||
// },
|
||||
// });
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// [workspaceSlug, projectId, inboxIssueId, inboxId, issuesList]
|
||||
// );
|
||||
|
||||
// useEffect(() => {
|
||||
// document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
// return () => {
|
||||
// document.removeEventListener("keydown", onKeyDown);
|
||||
// };
|
||||
// }, [onKeyDown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!issueDetails || !inboxIssueId) return;
|
||||
|
||||
reset({
|
||||
...issueDetails,
|
||||
assignee_ids: issueDetails.assignee_ids ?? issueDetails.assignee_ids,
|
||||
label_ids: issueDetails.label_ids ?? issueDetails.label_ids,
|
||||
});
|
||||
}, [issueDetails, reset, inboxIssueId]);
|
||||
|
||||
const issueStatus = issueDetails?.issue_inbox[0].status;
|
||||
|
||||
if (!inboxIssueId)
|
||||
return (
|
||||
<div className="grid h-full place-items-center p-4 text-custom-text-200">
|
||||
<div className="grid h-full place-items-center">
|
||||
<div className="my-5 flex flex-col items-center gap-4">
|
||||
<Inbox size={60} strokeWidth={1.5} />
|
||||
{issuesList && issuesList.length > 0 ? (
|
||||
<span className="text-custom-text-200">
|
||||
{issuesList?.length} issues found. Select an issue from the sidebar to view its details.
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-custom-text-200">No issues found</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueDetails ? (
|
||||
<div className="flex h-full divide-x overflow-auto">
|
||||
<div className="h-full basis-2/3 space-y-3 overflow-auto p-5">
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md border p-3 text-sm ${
|
||||
issueStatus === -2
|
||||
? "border-yellow-500 bg-yellow-500/10 text-yellow-500"
|
||||
: issueStatus === -1
|
||||
? "border-red-500 bg-red-500/10 text-red-500"
|
||||
: issueStatus === 0
|
||||
? new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date()
|
||||
? "border-red-500 bg-red-500/10 text-red-500"
|
||||
: "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: issueStatus === 1
|
||||
? "border-green-500 bg-green-500/10 text-green-500"
|
||||
: issueStatus === 2
|
||||
? "border-gray-500 bg-gray-500/10 text-custom-text-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{issueStatus === -2 ? (
|
||||
<>
|
||||
<AlertTriangle size={18} strokeWidth={2} />
|
||||
<p>This issue is still pending.</p>
|
||||
</>
|
||||
) : issueStatus === -1 ? (
|
||||
<>
|
||||
<XCircle size={18} strokeWidth={2} />
|
||||
<p>This issue has been declined.</p>
|
||||
</>
|
||||
) : issueStatus === 0 ? (
|
||||
<>
|
||||
<Clock size={18} strokeWidth={2} />
|
||||
{new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() ? (
|
||||
<p>
|
||||
This issue was snoozed till {renderFormattedDate(issueDetails.issue_inbox[0].snoozed_till ?? "")}.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
This issue has been snoozed till{" "}
|
||||
{renderFormattedDate(issueDetails.issue_inbox[0].snoozed_till ?? "")}.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : issueStatus === 1 ? (
|
||||
<>
|
||||
<CheckCircle2 size={18} strokeWidth={2} />
|
||||
<p>This issue has been accepted.</p>
|
||||
</>
|
||||
) : issueStatus === 2 ? (
|
||||
<>
|
||||
<Copy size={18} strokeWidth={2} />
|
||||
<p className="flex items-center gap-1">
|
||||
This issue has been marked as a duplicate of
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueDetails.issue_inbox[0].duplicate_to}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-2 underline"
|
||||
>
|
||||
this issue <ExternalLink size={12} strokeWidth={2} />
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-2.5 flex items-center">
|
||||
{currentIssueState && (
|
||||
<StateGroupIcon
|
||||
className="mr-3 h-4 w-4"
|
||||
stateGroup={currentIssueState.group}
|
||||
color={currentIssueState.color}
|
||||
/>
|
||||
)}
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issueDetails} />
|
||||
</div>
|
||||
|
||||
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||
{/* <div>
|
||||
<IssueDescriptionForm
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
isSubmitting={isSubmitting}
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
issue={{
|
||||
name: issueDetails.name,
|
||||
description_html: issueDetails.description_html,
|
||||
id: issueDetails.id,
|
||||
}}
|
||||
handleFormSubmit={submitChanges}
|
||||
isAllowed={isAllowed || currentUser?.id === issueDetails.created_by}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||
{/* {workspaceSlug && projectId && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
issueId={issueDetails.id}
|
||||
/>
|
||||
)} */}
|
||||
<InboxIssueActivity issueDetails={issueDetails} />
|
||||
</div>
|
||||
|
||||
<div className="basis-1/3 space-y-5 border-custom-border-200 py-5">
|
||||
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||
{/* <IssueDetailsSidebar
|
||||
control={control}
|
||||
issueDetail={issueDetails}
|
||||
submitChanges={submitChanges}
|
||||
watch={watch}
|
||||
fieldsToShow={["assignee", "priority", "estimate", "dueDate", "label", "state"]}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="flex h-full gap-5 p-5">
|
||||
<div className="basis-2/3 space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="40%" />
|
||||
</div>
|
||||
<div className="basis-1/3 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -1,16 +1,15 @@
|
||||
import React, { useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
// icons
|
||||
import { CheckCircle } from "lucide-react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { IInboxIssue } from "@plane/types";
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { useProject } from "hooks/store";
|
||||
|
||||
type Props = {
|
||||
data: IInboxIssue;
|
||||
data: TIssue;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
@ -28,7 +27,6 @@ export const AcceptIssueModal: React.FC<Props> = ({ isOpen, onClose, data, onSub
|
||||
|
||||
const handleAccept = () => {
|
||||
setIsAccepting(true);
|
||||
|
||||
onSubmit().finally(() => setIsAccepting(false));
|
||||
};
|
||||
|
||||
|
@ -58,7 +58,9 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||
|
||||
// store hooks
|
||||
const { createIssue } = useInboxIssues();
|
||||
const {
|
||||
issues: { createInboxIssue },
|
||||
} = useInboxIssues();
|
||||
const {
|
||||
config: { envConfig },
|
||||
eventTracker: { postHogEventTracker },
|
||||
@ -85,10 +87,10 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
||||
const handleFormSubmit = async (formData: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
||||
|
||||
await createIssue(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), formData)
|
||||
await createInboxIssue(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), formData)
|
||||
.then((res) => {
|
||||
if (!createMore) {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.id}`);
|
||||
handleClose();
|
||||
} else reset(defaultValues);
|
||||
postHogEventTracker(
|
||||
|
@ -1,16 +1,15 @@
|
||||
import React, { useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
// icons
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { IInboxIssue } from "@plane/types";
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { useProject } from "hooks/store";
|
||||
|
||||
type Props = {
|
||||
data: IInboxIssue;
|
||||
data: TIssue;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
@ -28,7 +27,6 @@ export const DeclineIssueModal: React.FC<Props> = ({ isOpen, onClose, data, onSu
|
||||
|
||||
const handleDecline = () => {
|
||||
setIsDeclining(true);
|
||||
|
||||
onSubmit().finally(() => setIsDeclining(false));
|
||||
};
|
||||
|
||||
|
@ -1,39 +1,27 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import { useApplication, useProject, useWorkspace } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useProject } from "hooks/store";
|
||||
// icons
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { IInboxIssue } from "@plane/types";
|
||||
import { useInboxIssues } from "hooks/store/use-inbox-issues";
|
||||
import type { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
data: IInboxIssue;
|
||||
data: TIssue;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClose, data }) => {
|
||||
export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClose, onSubmit, data }) => {
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
||||
// store hooks
|
||||
const { deleteIssue } = useInboxIssues();
|
||||
const {
|
||||
eventTracker: { postHogEventTracker },
|
||||
} = useApplication();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeleting(false);
|
||||
@ -41,59 +29,13 @@ export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClos
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
|
||||
deleteIssue(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), data.issue_inbox[0].id)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue deleted successfully.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_DELETED",
|
||||
{
|
||||
state: "SUCCESS",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
groupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
// remove inboxIssueId from the url
|
||||
router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
});
|
||||
|
||||
handleClose();
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be deleted. Please try again.",
|
||||
});
|
||||
postHogEventTracker(
|
||||
"ISSUE_DELETED",
|
||||
{
|
||||
state: "FAILED",
|
||||
},
|
||||
{
|
||||
isGrouping: true,
|
||||
groupType: "Workspace_metrics",
|
||||
groupId: currentWorkspace?.id!,
|
||||
}
|
||||
);
|
||||
})
|
||||
.finally(() => setIsDeleting(false));
|
||||
onSubmit().finally(() => setIsDeleting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -137,7 +79,7 @@ export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClos
|
||||
</p>
|
||||
</span>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDelete} loading={isDeleting}>
|
||||
|
@ -1,66 +1,73 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useInboxFilters } from "hooks/store";
|
||||
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IInboxFilterOptions, TIssuePriorities } from "@plane/types";
|
||||
import { TInboxIssueFilterOptions, TIssuePriorities } from "@plane/types";
|
||||
// constants
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
|
||||
export const InboxFiltersList = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
||||
type TInboxIssueAppliedFilter = { workspaceSlug: string; projectId: string; inboxId: string };
|
||||
|
||||
const { inboxFilters, updateInboxFilters } = useInboxFilters();
|
||||
export const IssueStatusLabel = ({ status }: { status: number }) => {
|
||||
const issueStatusDetail = INBOX_STATUS.find((s) => s.status === status);
|
||||
|
||||
const filters = inboxId ? inboxFilters[inboxId.toString()]?.filters : undefined;
|
||||
if (!issueStatusDetail) return <></>;
|
||||
|
||||
const handleUpdateFilter = (filter: Partial<IInboxFilterOptions>) => {
|
||||
return (
|
||||
<div className="relative flex items-center gap-1">
|
||||
<div className={issueStatusDetail.textColor(false)}>
|
||||
<issueStatusDetail.icon size={12} />
|
||||
</div>
|
||||
<div>{issueStatusDetail.title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InboxIssueAppliedFilter: FC<TInboxIssueAppliedFilter> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId } = props;
|
||||
// hooks
|
||||
const {
|
||||
filters: { inboxFilters, updateInboxFilters },
|
||||
} = useInboxIssues();
|
||||
|
||||
const filters = inboxFilters?.filters;
|
||||
|
||||
const handleUpdateFilter = (filter: Partial<TInboxIssueFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
||||
|
||||
updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), filter);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
||||
|
||||
const newFilters: IInboxFilterOptions = {};
|
||||
Object.keys(filters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IInboxFilterOptions] = null;
|
||||
});
|
||||
|
||||
const newFilters: TInboxIssueFilterOptions = { priority: [], inbox_status: [] };
|
||||
updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), newFilters);
|
||||
};
|
||||
|
||||
let filtersLength = 0;
|
||||
Object.keys(filters ?? {}).forEach((key) => {
|
||||
const filterKey = key as keyof IInboxFilterOptions;
|
||||
|
||||
const filterKey = key as keyof TInboxIssueFilterOptions;
|
||||
if (filters?.[filterKey] && Array.isArray(filters[filterKey])) filtersLength += (filters[filterKey] ?? []).length;
|
||||
});
|
||||
|
||||
if (!filters || filtersLength <= 0) return null;
|
||||
|
||||
if (!filters || filtersLength <= 0) return <></>;
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2 p-3 text-[0.65rem]">
|
||||
<div className="relative flex flex-wrap items-center gap-2 p-3 text-[0.65rem] border-b border-custom-border-100">
|
||||
{Object.keys(filters).map((key) => {
|
||||
const filterKey = key as keyof IInboxFilterOptions;
|
||||
const filterKey = key as keyof TInboxIssueFilterOptions;
|
||||
|
||||
if (filters[filterKey])
|
||||
if (filters[filterKey].length > 0)
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
||||
>
|
||||
<span className="capitalize text-custom-text-200">{replaceUnderscoreIfSnakeCase(key)}:</span>
|
||||
{filters[filterKey] === null || (filters[filterKey]?.length ?? 0) <= 0 ? (
|
||||
{filters[filterKey]?.length < 0 ? (
|
||||
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
|
||||
) : (
|
||||
<div className="space-x-2">
|
||||
@ -81,9 +88,12 @@ export const InboxFiltersList = observer(() => {
|
||||
: "bg-custom-background-90 text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
<PriorityIcon priority={priority as TIssuePriorities} />
|
||||
</span>
|
||||
<div className="relative flex items-center gap-1">
|
||||
<div>
|
||||
<PriorityIcon priority={priority as TIssuePriorities} size={14} />
|
||||
</div>
|
||||
<div>{priority}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer"
|
||||
@ -101,7 +111,7 @@ export const InboxFiltersList = observer(() => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleUpdateFilter({
|
||||
priority: null,
|
||||
priority: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
@ -115,7 +125,7 @@ export const InboxFiltersList = observer(() => {
|
||||
key={status}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-2 py-0.5 capitalize text-custom-text-200"
|
||||
>
|
||||
<span>{INBOX_STATUS.find((s) => s.value === status)?.label}</span>
|
||||
<IssueStatusLabel status={status} />
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer"
|
||||
@ -133,7 +143,7 @@ export const InboxFiltersList = observer(() => {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleUpdateFilter({
|
||||
inbox_status: null,
|
||||
inbox_status: [],
|
||||
})
|
||||
}
|
||||
>
|
@ -1,30 +1,31 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useInboxFilters } from "hooks/store";
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// ui
|
||||
import { MultiLevelDropdown } from "components/ui";
|
||||
// icons
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
// types
|
||||
import { IInboxFilterOptions } from "@plane/types";
|
||||
import { TInboxIssueFilterOptions } from "@plane/types";
|
||||
// constants
|
||||
import { INBOX_STATUS } from "constants/inbox";
|
||||
import { ISSUE_PRIORITIES } from "constants/issue";
|
||||
|
||||
export const FiltersDropdown: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
||||
type TInboxIssueFilterSelection = { workspaceSlug: string; projectId: string; inboxId: string };
|
||||
|
||||
const { inboxFilters, updateInboxFilters } = useInboxFilters();
|
||||
export const InboxIssueFilterSelection: FC<TInboxIssueFilterSelection> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId } = props;
|
||||
// hooks
|
||||
const {
|
||||
filters: { inboxFilters, updateInboxFilters },
|
||||
} = useInboxIssues();
|
||||
|
||||
const filters = inboxId ? inboxFilters[inboxId.toString()]?.filters : undefined;
|
||||
const filters = inboxFilters?.filters;
|
||||
|
||||
let filtersLength = 0;
|
||||
Object.keys(filters ?? {}).forEach((key) => {
|
||||
const filterKey = key as keyof IInboxFilterOptions;
|
||||
|
||||
const filterKey = key as keyof TInboxIssueFilterOptions;
|
||||
if (filters?.[filterKey] && Array.isArray(filters[filterKey])) filtersLength += (filters[filterKey] ?? []).length;
|
||||
});
|
||||
|
||||
@ -35,7 +36,7 @@ export const FiltersDropdown: React.FC = observer(() => {
|
||||
onSelect={(option) => {
|
||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
||||
|
||||
const key = option.key as keyof IInboxFilterOptions;
|
||||
const key = option.key as keyof TInboxIssueFilterOptions;
|
||||
const currentValue: any[] = filters?.[key] ?? [];
|
||||
|
||||
const valueExists = currentValue.includes(option.value);
|
||||
@ -74,20 +75,28 @@ export const FiltersDropdown: React.FC = observer(() => {
|
||||
{
|
||||
id: "inbox_status",
|
||||
label: "Status",
|
||||
value: INBOX_STATUS.map((status) => status.value),
|
||||
value: INBOX_STATUS.map((status) => status.status),
|
||||
hasChildren: true,
|
||||
children: INBOX_STATUS.map((status) => ({
|
||||
id: status.key,
|
||||
label: status.label,
|
||||
id: status.status.toString(),
|
||||
label: (
|
||||
<div className="relative inline-flex gap-2 items-center">
|
||||
<div className={status.textColor(false)}>
|
||||
<status.icon size={12} />
|
||||
</div>
|
||||
<div>{status.title}</div>
|
||||
</div>
|
||||
),
|
||||
value: {
|
||||
key: "inbox_status",
|
||||
value: status.value,
|
||||
value: status.status,
|
||||
},
|
||||
selected: filters?.inbox_status?.includes(status.value),
|
||||
selected: filters?.inbox_status?.includes(status.status),
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{filtersLength > 0 && (
|
||||
<div className="absolute -right-2 -top-2 z-10 grid h-4 w-4 place-items-center rounded-full border border-custom-border-200 bg-custom-background-80 text-[0.65rem] text-custom-text-100">
|
||||
<span>{filtersLength}</span>
|
100
web/components/inbox/sidebar/inbox-list-item.tsx
Normal file
100
web/components/inbox/sidebar/inbox-list-item.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { CalendarDays } from "lucide-react";
|
||||
// hooks
|
||||
import { useInboxIssues, useIssueDetail, useProject } from "hooks/store";
|
||||
// ui
|
||||
import { Tooltip, PriorityIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
// components
|
||||
import { InboxIssueStatus } from "components/inbox/inbox-issue-status";
|
||||
|
||||
type TInboxIssueListItem = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
inboxId: string;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const InboxIssueListItem: FC<TInboxIssueListItem> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId, issueId } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { inboxIssueId } = router.query;
|
||||
// hooks
|
||||
const { getProjectById } = useProject();
|
||||
const {
|
||||
issues: { getInboxIssueByIssueId },
|
||||
} = useInboxIssues();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const inboxIssueDetail = getInboxIssueByIssueId(inboxId, issueId);
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue || !inboxIssueDetail) return <></>;
|
||||
|
||||
useEffect(() => {
|
||||
if (issueId === inboxIssueId) {
|
||||
setTimeout(() => {
|
||||
const issueItemCard = document.getElementById(`inbox-issue-list-item-${issueId}`);
|
||||
if (issueItemCard)
|
||||
issueItemCard.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}, [issueId, inboxIssueId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
id={`inbox-issue-list-item-${issue.id}`}
|
||||
key={`${inboxId}_${issueId}`}
|
||||
href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${issueId}`}
|
||||
>
|
||||
<div
|
||||
className={`relative min-h-[5rem]select-none space-y-3 border-b border-custom-border-200 px-4 py-2 hover:bg-custom-primary/5 cursor-pointer ${
|
||||
inboxIssueId === issueId ? "bg-custom-primary/5" : " "
|
||||
} ${inboxIssueDetail.status !== -2 ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
|
||||
</p>
|
||||
<h5 className="truncate text-sm">{issue.name}</h5>
|
||||
</div>
|
||||
<div>
|
||||
<InboxIssueStatus
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
issueId={issueId}
|
||||
iconSize={14}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={`${issue.priority ?? "None"}`}>
|
||||
<PriorityIcon priority={issue.priority ?? null} className="h-3.5 w-3.5" />
|
||||
</Tooltip>
|
||||
<Tooltip tooltipHeading="Created on" tooltipContent={`${renderFormattedDate(issue.created_at ?? "")}`}>
|
||||
<div className="flex items-center gap-1 rounded border border-custom-border-200 px-2 py-[0.19rem] text-xs text-custom-text-200 shadow-sm">
|
||||
<CalendarDays size={12} strokeWidth={1.5} />
|
||||
<span>{renderFormattedDate(issue.created_at ?? "")}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
});
|
27
web/components/inbox/sidebar/inbox-list.tsx
Normal file
27
web/components/inbox/sidebar/inbox-list.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// components
|
||||
import { InboxIssueListItem } from "../";
|
||||
|
||||
type TInboxIssueList = { workspaceSlug: string; projectId: string; inboxId: string };
|
||||
|
||||
export const InboxIssueList: FC<TInboxIssueList> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId } = props;
|
||||
// hooks
|
||||
const {
|
||||
issues: { getInboxIssuesByInboxId },
|
||||
} = useInboxIssues();
|
||||
|
||||
const inboxIssueIds = getInboxIssuesByInboxId(inboxId);
|
||||
|
||||
if (!inboxIssueIds) return <></>;
|
||||
return (
|
||||
<div className="overflow-y-auto w-full h-full">
|
||||
{inboxIssueIds.map((issueId) => (
|
||||
<InboxIssueListItem workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} issueId={issueId} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
38
web/components/inbox/sidebar/root.tsx
Normal file
38
web/components/inbox/sidebar/root.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { FC } from "react";
|
||||
import { Inbox } from "lucide-react";
|
||||
// components
|
||||
import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../";
|
||||
|
||||
type TInboxSidebarRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
inboxId: string;
|
||||
};
|
||||
|
||||
export const InboxSidebarRoot: FC<TInboxSidebarRoot> = (props) => {
|
||||
const { workspaceSlug, projectId, inboxId } = props;
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col w-full h-full">
|
||||
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-100">
|
||||
<div className="relative flex items-center gap-1">
|
||||
<div className="relative w-6 h-6 flex justify-center items-center rounded bg-custom-background-80">
|
||||
<Inbox className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="font-medium">Inbox</div>
|
||||
</div>
|
||||
<div className="z-[999999]">
|
||||
<InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-auto">
|
||||
<InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -35,8 +35,8 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
|
||||
const handleIssueCycleChange = async (cycleId: string | null) => {
|
||||
if (!issue || issue.cycle_id === cycleId) return;
|
||||
setIsUpdating(true);
|
||||
if (cycleId) await issueOperations.addIssueToCycle(workspaceSlug, projectId, cycleId, [issueId]);
|
||||
else await issueOperations.removeIssueFromCycle(workspaceSlug, projectId, issue.cycle_id ?? "", issueId);
|
||||
if (cycleId) await issueOperations.addIssueToCycle?.(workspaceSlug, projectId, cycleId, [issueId]);
|
||||
else await issueOperations.removeIssueFromCycle?.(workspaceSlug, projectId, issue.cycle_id ?? "", issueId);
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
||||
|
3
web/components/issues/issue-detail/inbox/index.ts
Normal file
3
web/components/issues/issue-detail/inbox/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./root"
|
||||
export * from "./main-content"
|
||||
export * from "./sidebar"
|
84
web/components/issues/issue-detail/inbox/main-content.tsx
Normal file
84
web/components/issues/issue-detail/inbox/main-content.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||
// components
|
||||
import { IssueDescriptionForm, IssueUpdateStatus, TIssueOperations } from "components/issues";
|
||||
import { IssueReaction } from "../reactions";
|
||||
import { IssueActivity } from "../issue-activity";
|
||||
import { InboxIssueStatus } from "../../../inbox/inbox-issue-status";
|
||||
// ui
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
inboxId: string;
|
||||
issueId: string;
|
||||
issueOperations: TIssueOperations;
|
||||
is_editable: boolean;
|
||||
};
|
||||
|
||||
export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId, issueId, issueOperations, is_editable } = props;
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
// hooks
|
||||
const { currentUser } = useUser();
|
||||
const { projectStates } = useProjectState();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
|
||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg space-y-4">
|
||||
<InboxIssueStatus
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
issueId={issueId}
|
||||
showDescription={true}
|
||||
/>
|
||||
|
||||
<div className="mb-2.5 flex items-center">
|
||||
{currentIssueState && (
|
||||
<StateGroupIcon
|
||||
className="mr-3 h-4 w-4"
|
||||
stateGroup={currentIssueState.group}
|
||||
color={currentIssueState.color}
|
||||
/>
|
||||
)}
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
||||
</div>
|
||||
|
||||
<IssueDescriptionForm
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
isSubmitting={isSubmitting}
|
||||
issue={issue}
|
||||
issueOperations={issueOperations}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
<IssueReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
|
||||
</>
|
||||
);
|
||||
});
|
150
web/components/issues/issue-detail/inbox/root.tsx
Normal file
150
web/components/issues/issue-detail/inbox/root.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import { FC, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { InboxIssueMainContent } from "./main-content";
|
||||
import { InboxIssueDetailsSidebar } from "./sidebar";
|
||||
// hooks
|
||||
import { useInboxIssues, useIssueDetail, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { TIssueOperations } from "../root";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
|
||||
export type TInboxIssueDetailRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
inboxId: string;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
|
||||
const { workspaceSlug, projectId, inboxId, issueId } = props;
|
||||
// hooks
|
||||
const {
|
||||
issues: { fetchInboxIssueById, updateInboxIssue, removeInboxIssue },
|
||||
} = useInboxIssues();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { setToastAlert } = useToast();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const issueOperations: TIssueOperations = useMemo(
|
||||
() => ({
|
||||
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await fetchInboxIssueById(workspaceSlug, projectId, inboxId, issueId);
|
||||
} catch (error) {
|
||||
console.error("Error fetching the parent issue");
|
||||
}
|
||||
},
|
||||
update: async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: Partial<TIssue>,
|
||||
showToast: boolean = true
|
||||
) => {
|
||||
try {
|
||||
await updateInboxIssue(workspaceSlug, projectId, inboxId, issueId, data);
|
||||
if (showToast) {
|
||||
setToastAlert({
|
||||
title: "Issue updated successfully",
|
||||
type: "success",
|
||||
message: "Issue updated successfully",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Issue update failed",
|
||||
type: "error",
|
||||
message: "Issue update failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await removeInboxIssue(workspaceSlug, projectId, inboxId, issueId);
|
||||
setToastAlert({
|
||||
title: "Issue deleted successfully",
|
||||
type: "success",
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Issue delete failed",
|
||||
type: "error",
|
||||
message: "Issue delete failed",
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[inboxId, fetchInboxIssueById, updateInboxIssue, removeInboxIssue, setToastAlert]
|
||||
);
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && inboxId && issueId
|
||||
? `INBOX_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${inboxId}_${issueId}`
|
||||
: null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && inboxId && issueId) {
|
||||
await issueOperations.fetch(workspaceSlug, projectId, issueId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// checking if issue is editable, based on user role
|
||||
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
// issue details
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{issue ? (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
||||
<InboxIssueMainContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5">
|
||||
<InboxIssueDetailsSidebar
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="flex h-full gap-5 p-5">
|
||||
<div className="basis-2/3 space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="40%" />
|
||||
</div>
|
||||
<div className="basis-1/3 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
149
web/components/issues/issue-detail/inbox/sidebar.tsx
Normal file
149
web/components/issues/issue-detail/inbox/sidebar.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import React from "react";
|
||||
// import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { CalendarDays, Signal, Tag } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject } from "hooks/store";
|
||||
// components
|
||||
import { IssueLabel, TIssueOperations } from "components/issues";
|
||||
import { PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
// icons
|
||||
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
issueOperations: TIssueOperations;
|
||||
is_editable: boolean;
|
||||
};
|
||||
|
||||
export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issueOperations, is_editable } = props;
|
||||
// router
|
||||
// FIXME: Check if we need this. Previously it was used to render Project Identifier conditionally.
|
||||
// const router = useRouter();
|
||||
// const { inboxIssueId } = router.query;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
|
||||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
|
||||
const projectDetails = issue ? getProjectById(issue.project_id) : null;
|
||||
|
||||
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-5 pb-3">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StateGroupIcon className="h-4 w-4" stateGroup="backlog" color="#ff7700" />
|
||||
<h4 className="text-lg font-medium text-custom-text-300">
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full overflow-y-auto px-5">
|
||||
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="py-1">
|
||||
{/* State */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<StateDropdown
|
||||
value={issue?.state_id ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="background-with-text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Assignee */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<ProjectMemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||
disabled={!is_editable}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
placeholder="Assignees"
|
||||
multiple
|
||||
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "background-with-text"}
|
||||
buttonClassName={issue?.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Priority */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="background-with-text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="py-1">
|
||||
{/* Due Date */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Due date</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={issue.target_date || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { target_date: val })}
|
||||
className="border-none bg-custom-background-80"
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Labels */}
|
||||
<div className={`flex flex-wrap items-start py-2 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Label</p>
|
||||
</div>
|
||||
<div className="space-y-1 sm:w-1/2">
|
||||
<IssueLabel
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -35,8 +35,8 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
|
||||
const handleIssueModuleChange = async (moduleId: string | null) => {
|
||||
if (!issue || issue.module_id === moduleId) return;
|
||||
setIsUpdating(true);
|
||||
if (moduleId) await issueOperations.addIssueToModule(workspaceSlug, projectId, moduleId, [issueId]);
|
||||
else await issueOperations.removeIssueFromModule(workspaceSlug, projectId, issue.module_id ?? "", issueId);
|
||||
if (moduleId) await issueOperations.addIssueToModule?.(workspaceSlug, projectId, moduleId, [issueId]);
|
||||
else await issueOperations.removeIssueFromModule?.(workspaceSlug, projectId, issue.module_id ?? "", issueId);
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
||||
|
@ -27,10 +27,15 @@ export type TIssueOperations = {
|
||||
showToast?: boolean
|
||||
) => Promise<void>;
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addIssueToModule?: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromModule?: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
moduleId: string,
|
||||
issueId: string
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
export type TIssueDetailRoot = {
|
||||
|
@ -1,44 +0,0 @@
|
||||
export const INBOX_STATUS = [
|
||||
{
|
||||
key: "pending",
|
||||
label: "Pending",
|
||||
value: -2,
|
||||
textColor: "text-yellow-500",
|
||||
bgColor: "bg-yellow-500/10",
|
||||
borderColor: "border-yellow-500",
|
||||
},
|
||||
{
|
||||
key: "declined",
|
||||
label: "Declined",
|
||||
value: -1,
|
||||
textColor: "text-red-500",
|
||||
bgColor: "bg-red-500/10",
|
||||
borderColor: "border-red-500",
|
||||
},
|
||||
{
|
||||
key: "snoozed",
|
||||
label: "Snoozed",
|
||||
value: 0,
|
||||
textColor: "text-custom-text-200",
|
||||
bgColor: "bg-gray-500/10",
|
||||
borderColor: "border-gray-500",
|
||||
},
|
||||
{
|
||||
key: "accepted",
|
||||
label: "Accepted",
|
||||
value: 1,
|
||||
textColor: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
borderColor: "border-green-500",
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
label: "Duplicate",
|
||||
value: 2,
|
||||
textColor: "text-custom-text-200",
|
||||
bgColor: "bg-gray-500/10",
|
||||
borderColor: "border-gray-500",
|
||||
},
|
||||
];
|
||||
|
||||
export const INBOX_ISSUE_SOURCE = "in-app";
|
86
web/constants/inbox.tsx
Normal file
86
web/constants/inbox.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
// icons
|
||||
import { AlertTriangle, CheckCircle2, Clock, Copy, ExternalLink, LucideIcon, XCircle } from "lucide-react";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
|
||||
export const INBOX_STATUS: {
|
||||
key: string;
|
||||
status: number;
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: (workspaceSlug: string, projectId: string, issueId: string, snoozedTillDate: Date) => JSX.Element;
|
||||
textColor: (snoozeDatePassed: boolean) => string;
|
||||
bgColor: (snoozeDatePassed: boolean) => string;
|
||||
borderColor: (snoozeDatePassed: boolean) => string;
|
||||
}[] = [
|
||||
{
|
||||
key: "pending",
|
||||
status: -2,
|
||||
icon: AlertTriangle,
|
||||
title: "Pending",
|
||||
description: () => <p>This issue is still pending.</p>,
|
||||
textColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "text-yellow-500"),
|
||||
bgColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "bg-yellow-500/10"),
|
||||
borderColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "border-yellow-500"),
|
||||
},
|
||||
{
|
||||
key: "declined",
|
||||
status: -1,
|
||||
icon: XCircle,
|
||||
title: "Declined",
|
||||
description: () => <p>This issue has been declined.</p>,
|
||||
textColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "text-red-500"),
|
||||
bgColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "bg-red-500/10"),
|
||||
borderColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "border-red-500"),
|
||||
},
|
||||
{
|
||||
key: "snoozed",
|
||||
status: 0,
|
||||
icon: Clock,
|
||||
title: "Snoozed",
|
||||
description: (workspaceSlug: string, projectId: string, issueId: string, snoozedTillDate: Date = new Date()) =>
|
||||
snoozedTillDate < new Date() ? (
|
||||
<p>This issue was snoozed till {renderFormattedDate(snoozedTillDate)}.</p>
|
||||
) : (
|
||||
<p>This issue has been snoozed till {renderFormattedDate(snoozedTillDate)}.</p>
|
||||
),
|
||||
textColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "text-red-500" : "text-custom-text-200"),
|
||||
bgColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "bg-red-500/10" : "bg-gray-500/10"),
|
||||
borderColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "border-red-500" : "border-gray-500"),
|
||||
},
|
||||
{
|
||||
key: "accepted",
|
||||
status: 1,
|
||||
icon: CheckCircle2,
|
||||
title: "Accepted",
|
||||
description: () => <p>This issue has been accepted.</p>,
|
||||
textColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "text-green-500"),
|
||||
bgColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "bg-green-500/10"),
|
||||
borderColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "border-green-500"),
|
||||
},
|
||||
{
|
||||
key: "duplicate",
|
||||
status: 2,
|
||||
icon: Copy,
|
||||
title: "Duplicate",
|
||||
description: (workspaceSlug: string, projectId: string, issueId: string) => (
|
||||
<p className="flex items-center gap-1">
|
||||
This issue has been marked as a duplicate of
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-2 underline"
|
||||
>
|
||||
this issue <ExternalLink size={12} strokeWidth={2} />
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
),
|
||||
textColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "text-custom-text-200"),
|
||||
bgColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "bg-gray-500/10"),
|
||||
borderColor: (snoozeDatePassed: boolean = false) => (snoozeDatePassed ? "" : "border-gray-500"),
|
||||
},
|
||||
];
|
||||
|
||||
export const INBOX_ISSUE_SOURCE = "in-app";
|
@ -4,9 +4,6 @@ export * from "./use-cycle";
|
||||
export * from "./use-dashboard";
|
||||
export * from "./use-estimate";
|
||||
export * from "./use-global-view";
|
||||
export * from "./use-inbox";
|
||||
export * from "./use-inbox-filters";
|
||||
export * from "./use-inbox-issues";
|
||||
export * from "./use-label";
|
||||
export * from "./use-member";
|
||||
export * from "./use-mention";
|
||||
@ -22,3 +19,5 @@ export * from "./use-workspace";
|
||||
export * from "./use-issues";
|
||||
export * from "./use-kanban-view";
|
||||
export * from "./use-issue-detail";
|
||||
export * from "./use-inbox";
|
||||
export * from "./use-inbox-issues";
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "contexts/store-context";
|
||||
// types
|
||||
import { IInboxFiltersStore } from "store/inbox/inbox_filter.store";
|
||||
|
||||
export const useInboxFilters = (): IInboxFiltersStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useInboxFilters must be used within StoreProvider");
|
||||
return context.inboxRoot.inboxFilters;
|
||||
};
|
@ -2,10 +2,14 @@ import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "contexts/store-context";
|
||||
// types
|
||||
import { IInboxIssuesStore } from "store/inbox/inbox_issue.store";
|
||||
import { IInboxIssue } from "store/inbox/inbox_issue.store";
|
||||
import { IInboxFilter } from "store/inbox/inbox_filter.store";
|
||||
|
||||
export const useInboxIssues = (): IInboxIssuesStore => {
|
||||
export const useInboxIssues = (): {
|
||||
issues: IInboxIssue;
|
||||
filters: IInboxFilter;
|
||||
} => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useInboxIssues must be used within StoreProvider");
|
||||
return context.inboxRoot.inboxIssues;
|
||||
return { issues: context.inbox.inboxIssue, filters: context.inbox.inboxFilter };
|
||||
};
|
||||
|
@ -2,10 +2,10 @@ import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "contexts/store-context";
|
||||
// types
|
||||
import { IInboxStore } from "store/inbox/inbox.store";
|
||||
import { IInbox } from "store/inbox/inbox.store";
|
||||
|
||||
export const useInbox = (): IInboxStore => {
|
||||
export const useInbox = (): IInbox => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useInbox must be used within StoreProvider");
|
||||
return context.inboxRoot.inbox;
|
||||
return context.inbox.inbox;
|
||||
};
|
||||
|
@ -30,14 +30,14 @@ interface IProjectAuthWrapper {
|
||||
export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
const { children } = props;
|
||||
// store
|
||||
const { fetchInboxesList, isInboxEnabled } = useInbox();
|
||||
const { fetchInboxes } = useInbox();
|
||||
const {
|
||||
commandPalette: { toggleCreateProjectModal },
|
||||
} = useApplication();
|
||||
const {
|
||||
membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
|
||||
} = useUser();
|
||||
const { getProjectById, fetchProjectDetails } = useProject();
|
||||
const { getProjectById, fetchProjectDetails, currentProjectDetails } = useProject();
|
||||
const { fetchAllCycles } = useCycle();
|
||||
const { fetchModules } = useModule();
|
||||
const { fetchViews } = useProjectView();
|
||||
@ -96,11 +96,13 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchViews(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
// fetching project inboxes if inbox is enabled
|
||||
// fetching project inboxes if inbox is enabled in project settings
|
||||
useSWR(
|
||||
workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId && isInboxEnabled
|
||||
? () => fetchInboxesList(workspaceSlug.toString(), projectId.toString())
|
||||
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails.inbox_view
|
||||
? `PROJECT_INBOXES_${workspaceSlug}_${projectId}`
|
||||
: null,
|
||||
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails.inbox_view
|
||||
? () => fetchInboxes(workspaceSlug.toString(), projectId.toString())
|
||||
: null,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
|
@ -1,41 +1,109 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react";
|
||||
import { Inbox } from "lucide-react";
|
||||
// hooks
|
||||
import { useInboxFilters } from "hooks/store/";
|
||||
import { useProject, useInboxIssues } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { InboxActionsHeader, InboxMainContent, InboxIssuesListSidebar } from "components/inbox";
|
||||
import { ProjectInboxHeader } from "components/headers";
|
||||
import { InboxSidebarRoot, InboxIssueActionsHeader } from "components/inbox";
|
||||
import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const ProjectInboxPage: NextPageWithLayout = () => {
|
||||
const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
||||
|
||||
const { fetchInboxFilters } = useInboxFilters();
|
||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
||||
// store hooks
|
||||
const {
|
||||
issues: { getInboxIssuesByInboxId },
|
||||
} = useInboxIssues();
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
filters: { fetchInboxFilters },
|
||||
issues: { loader, fetchInboxIssues },
|
||||
} = useInboxIssues();
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && inboxId ? `INBOX_FILTERS_${inboxId.toString()}` : null,
|
||||
workspaceSlug && projectId && inboxId
|
||||
? () => fetchInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString())
|
||||
: null
|
||||
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
|
||||
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
|
||||
: null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && inboxId && currentProjectDetails && currentProjectDetails?.inbox_view) {
|
||||
await fetchInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString());
|
||||
await fetchInboxIssues(workspaceSlug.toString(), projectId.toString(), inboxId.toString());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// inbox issues list
|
||||
const inboxIssuesList = inboxId ? getInboxIssuesByInboxId(inboxId?.toString()) : undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId || !inboxId) return <></>;
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<InboxActionsHeader />
|
||||
<div className="grid flex-1 grid-cols-4 divide-x divide-custom-border-200 overflow-hidden">
|
||||
<InboxIssuesListSidebar />
|
||||
<div className="col-span-3 h-full overflow-auto">
|
||||
<InboxMainContent />
|
||||
<>
|
||||
{loader === "fetch" ? (
|
||||
<div className="relative flex w-full h-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex h-full overflow-hidden">
|
||||
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-100">
|
||||
{workspaceSlug && projectId && inboxId && (
|
||||
<InboxSidebarRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{workspaceSlug && projectId && inboxId && inboxIssueId ? (
|
||||
<div className="w-full h-full overflow-hidden relative flex flex-col">
|
||||
<div className="flex-shrink-0 min-h-[50px] border-b border-custom-border-100">
|
||||
<InboxIssueActionsHeader
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<InboxIssueDetailRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
issueId={inboxIssueId.toString()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full place-items-center p-4 text-custom-text-200">
|
||||
<div className="grid h-full place-items-center">
|
||||
<div className="my-5 flex flex-col items-center gap-4">
|
||||
<Inbox size={60} strokeWidth={1.5} />
|
||||
{inboxIssuesList && inboxIssuesList.length > 0 ? (
|
||||
<span className="text-custom-text-200">
|
||||
{inboxIssuesList?.length} issues found. Select an issue from the sidebar to view its details.
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-custom-text-200">No issues found</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ProjectInboxPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
@ -0,0 +1,49 @@
|
||||
import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useInbox, useProject } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { ProjectInboxHeader } from "components/headers";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { fetchInboxes } = useInbox();
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
|
||||
? `INBOX_${workspaceSlug.toString()}_${projectId.toString()}`
|
||||
: null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view) {
|
||||
const inboxes = await fetchInboxes(workspaceSlug.toString(), projectId.toString());
|
||||
if (inboxes && inboxes.length > 0)
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxes[0].id}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{currentProjectDetails?.inbox_view ? <div>Loading...</div> : <div>You don{"'"}t have access to inbox</div>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ProjectInboxPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AppLayout header={<ProjectInboxHeader />} withProjectWrapper>
|
||||
{page}
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectInboxPage;
|
112
web/services/inbox/inbox-issue.service.ts
Normal file
112
web/services/inbox/inbox-issue.service.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { TInboxIssueFilterOptions, TInboxIssueExtendedDetail, TIssue, TInboxDetailedStatus } from "@plane/types";
|
||||
|
||||
export class InboxIssueService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchInboxIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
params?: TInboxIssueFilterOptions | {}
|
||||
): Promise<TInboxIssueExtendedDetail[]> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/?expand=issue_inbox`,
|
||||
{
|
||||
params,
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchInboxIssueById(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string
|
||||
): Promise<TInboxIssueExtendedDetail> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
data: {
|
||||
source: string;
|
||||
issue: Partial<TIssue>;
|
||||
}
|
||||
): Promise<TInboxIssueExtendedDetail> {
|
||||
return this.post(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/?expand=issue_inbox`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: { issue: Partial<TIssue> }
|
||||
): Promise<TInboxIssueExtendedDetail> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async removeInboxIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string
|
||||
): Promise<void> {
|
||||
return this.delete(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInboxIssueStatus(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
inboxIssueId: string,
|
||||
data: TInboxDetailedStatus
|
||||
): Promise<TInboxIssueExtendedDetail> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/inbox-issues/${inboxIssueId}/?expand=issue_inbox`,
|
||||
data
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
35
web/services/inbox/inbox.service.ts
Normal file
35
web/services/inbox/inbox.service.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { TInbox } from "@plane/types";
|
||||
|
||||
export class InboxService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchInboxes(workspaceSlug: string, projectId: string): Promise<TInbox[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchInboxById(workspaceSlug: string, projectId: string, inboxId: string): Promise<TInbox> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateInbox(workspaceSlug: string, projectId: string, inboxId: string, data: Partial<TInbox>): Promise<TInbox> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/inboxes/${inboxId}/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
2
web/services/inbox/index.ts
Normal file
2
web/services/inbox/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./inbox.service";
|
||||
export * from "./inbox-issue.service";
|
@ -1,37 +1,32 @@
|
||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { set } from "lodash";
|
||||
import set from "lodash/set";
|
||||
import update from "lodash/update";
|
||||
import concat from "lodash/concat";
|
||||
import uniq from "lodash/uniq";
|
||||
// services
|
||||
import { InboxService } from "services/inbox.service";
|
||||
import { InboxService } from "services/inbox/inbox.service";
|
||||
// types
|
||||
import { RootStore } from "store/root.store";
|
||||
import { IInbox } from "@plane/types";
|
||||
import { TInboxDetailMap, TInboxDetailIdMap, TInbox } from "@plane/types";
|
||||
|
||||
export interface IInboxStore {
|
||||
export interface IInbox {
|
||||
// observables
|
||||
inboxesList: {
|
||||
[projectId: string]: IInbox[];
|
||||
};
|
||||
inboxDetails: {
|
||||
[inboxId: string]: IInbox;
|
||||
};
|
||||
// computed
|
||||
isInboxEnabled: boolean;
|
||||
// computed actions
|
||||
getInboxId: (projectId: string) => string | null;
|
||||
inboxes: TInboxDetailIdMap;
|
||||
inboxMap: TInboxDetailMap;
|
||||
// helper methods
|
||||
getInboxesByProjectId: (projectId: string) => string[] | undefined;
|
||||
getInboxById: (inboxId: string) => TInbox | undefined;
|
||||
// fetch actions
|
||||
fetchInboxesList: (workspaceSlug: string, projectId: string) => Promise<IInbox[]>;
|
||||
fetchInboxDetails: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInbox>;
|
||||
fetchInboxes: (workspaceSlug: string, projectId: string) => Promise<TInbox[]>;
|
||||
fetchInboxById: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<TInbox>;
|
||||
updateInbox: (workspaceSlug: string, projectId: string, inboxId: string, data: Partial<TInbox>) => Promise<TInbox>;
|
||||
}
|
||||
|
||||
export class InboxStore implements IInboxStore {
|
||||
export class Inbox implements IInbox {
|
||||
// observables
|
||||
inboxesList: {
|
||||
[projectId: string]: IInbox[];
|
||||
} = {};
|
||||
inboxDetails: {
|
||||
[inboxId: string]: IInbox;
|
||||
} = {};
|
||||
inboxes: TInboxDetailIdMap = {};
|
||||
inboxMap: TInboxDetailMap = {};
|
||||
// root store
|
||||
rootStore;
|
||||
// services
|
||||
@ -40,68 +35,80 @@ export class InboxStore implements IInboxStore {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
inboxesList: observable,
|
||||
inboxDetails: observable,
|
||||
// computed
|
||||
isInboxEnabled: computed,
|
||||
inboxMap: observable,
|
||||
inboxes: observable,
|
||||
// actions
|
||||
fetchInboxesList: action,
|
||||
fetchInboxes: action,
|
||||
fetchInboxById: action,
|
||||
updateInbox: action,
|
||||
});
|
||||
|
||||
// root store
|
||||
this.rootStore = _rootStore;
|
||||
// services
|
||||
this.inboxService = new InboxService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if inbox is enabled for current project
|
||||
*/
|
||||
get isInboxEnabled() {
|
||||
const projectId = this.rootStore.app.router.projectId;
|
||||
if (!projectId) return false;
|
||||
const projectDetails = this.rootStore.projectRoot.project.currentProjectDetails;
|
||||
if (!projectDetails) return false;
|
||||
return projectDetails.inbox_view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inbox Id belongs to a specific project
|
||||
*/
|
||||
getInboxId = computedFn((projectId: string) => {
|
||||
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId);
|
||||
if (!projectDetails || !projectDetails.inbox_view) return null;
|
||||
return this.inboxesList[projectId]?.[0]?.id ?? null;
|
||||
// helper methods
|
||||
getInboxesByProjectId = computedFn((projectId: string) => {
|
||||
if (!projectId) return undefined;
|
||||
return this.inboxes?.[projectId] ?? undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches the inboxes list belongs to a specific project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @returns Promise<IInbox[]>
|
||||
*/
|
||||
fetchInboxesList = async (workspaceSlug: string, projectId: string) => {
|
||||
return await this.inboxService.getInboxes(workspaceSlug, projectId).then((inboxes) => {
|
||||
getInboxById = computedFn((inboxId: string) => {
|
||||
if (!inboxId) return undefined;
|
||||
return this.inboxMap[inboxId] ?? undefined;
|
||||
});
|
||||
|
||||
// actions
|
||||
fetchInboxes = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
const response = await this.inboxService.fetchInboxes(workspaceSlug, projectId);
|
||||
|
||||
const _inboxIds = response.map((inbox) => inbox.id);
|
||||
runInAction(() => {
|
||||
set(this.inboxesList, projectId, inboxes);
|
||||
response.forEach((inbox) => {
|
||||
set(this.inboxMap, inbox.id, inbox);
|
||||
});
|
||||
set(this.inboxes, projectId, _inboxIds);
|
||||
});
|
||||
return inboxes;
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the inbox details belongs to a specific inbox
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @returns Promise<IInbox>
|
||||
*/
|
||||
fetchInboxDetails = async (workspaceSlug: string, projectId: string, inboxId: string) => {
|
||||
return await this.inboxService.getInboxById(workspaceSlug, projectId, inboxId).then((inboxDetailsResponse) => {
|
||||
fetchInboxById = async (workspaceSlug: string, projectId: string, inboxId: string) => {
|
||||
try {
|
||||
const response = await this.inboxService.fetchInboxById(workspaceSlug, projectId, inboxId);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.inboxDetails, inboxId, inboxDetailsResponse);
|
||||
set(this.inboxMap, inboxId, response);
|
||||
update(this.inboxes, projectId, (inboxIds: string[] = []) => {
|
||||
if (inboxIds.includes(inboxId)) return inboxIds;
|
||||
return uniq(concat(inboxIds, inboxId));
|
||||
});
|
||||
});
|
||||
return inboxDetailsResponse;
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateInbox = async (workspaceSlug: string, projectId: string, inboxId: string, data: Partial<TInbox>) => {
|
||||
try {
|
||||
const response = await this.inboxService.updateInbox(workspaceSlug, projectId, inboxId, data);
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(response).forEach((key) => {
|
||||
set(this.inboxMap, [inboxId, key], response[key as keyof TInbox]);
|
||||
});
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,34 +1,31 @@
|
||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
import { set } from "lodash";
|
||||
import set from "lodash/set";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
// services
|
||||
import { InboxService } from "services/inbox.service";
|
||||
// types
|
||||
import { RootStore } from "store/root.store";
|
||||
import { IInbox, IInboxFilterOptions, IInboxQueryParams } from "@plane/types";
|
||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { TInboxIssueFilterOptions, TInboxIssueFilters, TInboxIssueQueryParams, TInbox } from "@plane/types";
|
||||
|
||||
export interface IInboxFiltersStore {
|
||||
export interface IInboxFilter {
|
||||
// observables
|
||||
inboxFilters: Record<string, { filters: IInboxFilterOptions }>;
|
||||
filters: Record<string, TInboxIssueFilters>; // inbox_id -> TInboxIssueFilters
|
||||
// computed
|
||||
appliedFilters: IInboxQueryParams | null;
|
||||
// fetch action
|
||||
fetchInboxFilters: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInbox>;
|
||||
// update action
|
||||
inboxFilters: TInboxIssueFilters | undefined;
|
||||
inboxAppliedFilters: Partial<Record<TInboxIssueQueryParams, string>> | undefined;
|
||||
// actions
|
||||
fetchInboxFilters: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<TInbox>;
|
||||
updateInboxFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
filters: Partial<IInboxFilterOptions>
|
||||
) => Promise<void>;
|
||||
filters: Partial<TInboxIssueFilterOptions>
|
||||
) => Promise<TInbox>;
|
||||
}
|
||||
|
||||
export class InboxFiltersStore implements IInboxFiltersStore {
|
||||
export class InboxFilter implements IInboxFilter {
|
||||
// observables
|
||||
inboxFilters: {
|
||||
[inboxId: string]: { filters: IInboxFilterOptions };
|
||||
} = {};
|
||||
filters: Record<string, TInboxIssueFilters> = {};
|
||||
// root store
|
||||
rootStore;
|
||||
// services
|
||||
@ -37,12 +34,12 @@ export class InboxFiltersStore implements IInboxFiltersStore {
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
inboxFilters: observable,
|
||||
filters: observable,
|
||||
// computed
|
||||
appliedFilters: computed,
|
||||
// fetch action
|
||||
inboxFilters: computed,
|
||||
inboxAppliedFilters: computed,
|
||||
// actions
|
||||
fetchInboxFilters: action,
|
||||
// update action
|
||||
updateInboxFilters: action,
|
||||
});
|
||||
// root store
|
||||
@ -51,69 +48,81 @@ export class InboxFiltersStore implements IInboxFiltersStore {
|
||||
this.inboxService = new InboxService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns applied filters to specific inbox
|
||||
*/
|
||||
get appliedFilters(): IInboxQueryParams | null {
|
||||
get inboxFilters() {
|
||||
const inboxId = this.rootStore.app.router.inboxId;
|
||||
if (!inboxId) return null;
|
||||
const filtersList = this.inboxFilters[inboxId]?.filters;
|
||||
if (!filtersList) return null;
|
||||
const filteredRouteParams: IInboxQueryParams = {
|
||||
priority: filtersList.priority ? filtersList.priority.join(",") : null,
|
||||
inbox_status: filtersList.inbox_status ? filtersList.inbox_status.join(",") : null,
|
||||
if (!inboxId) return undefined;
|
||||
|
||||
const displayFilters = this.filters[inboxId] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
const _filters: TInboxIssueFilters = {
|
||||
filters: {
|
||||
priority: isEmpty(displayFilters?.filters?.priority) ? [] : displayFilters?.filters?.priority,
|
||||
inbox_status: isEmpty(displayFilters?.filters?.inbox_status) ? [] : displayFilters?.filters?.inbox_status,
|
||||
},
|
||||
};
|
||||
return filteredRouteParams;
|
||||
return _filters;
|
||||
}
|
||||
|
||||
get inboxAppliedFilters() {
|
||||
const userFilters = this.inboxFilters;
|
||||
if (!userFilters) return undefined;
|
||||
|
||||
const filteredParams = {
|
||||
priority: userFilters?.filters?.priority?.join(",") || undefined,
|
||||
inbox_status: userFilters?.filters?.inbox_status?.join(",") || undefined,
|
||||
};
|
||||
return filteredParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches filters of a specific inbox and adds it to the store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @returns Promise<IInbox[]>
|
||||
*
|
||||
*/
|
||||
fetchInboxFilters = async (workspaceSlug: string, projectId: string, inboxId: string) => {
|
||||
return await this.inboxService.getInboxById(workspaceSlug, projectId, inboxId).then((issuesResponse) => {
|
||||
try {
|
||||
const response = await this.rootStore.inbox.inbox.fetchInboxById(workspaceSlug, projectId, inboxId);
|
||||
|
||||
const filters: TInboxIssueFilterOptions = {
|
||||
priority: response?.view_props?.filters?.priority || [],
|
||||
inbox_status: response?.view_props?.filters?.inbox_status || [],
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
set(this.inboxFilters, [inboxId], issuesResponse.view_props);
|
||||
set(this.filters, [inboxId], { filters: filters });
|
||||
});
|
||||
return issuesResponse;
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates filters of a specific inbox and updates it in the store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @param filters
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
updateInboxFilters = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
filters: Partial<IInboxFilterOptions>
|
||||
filters: Partial<TInboxIssueFilterOptions>
|
||||
) => {
|
||||
const newViewProps = {
|
||||
...this.inboxFilters[inboxId],
|
||||
filters: {
|
||||
...this.inboxFilters[inboxId]?.filters,
|
||||
...filters,
|
||||
},
|
||||
};
|
||||
const userRole = this.rootStore.user.membership?.currentProjectRole || EUserProjectRoles.GUEST;
|
||||
if (userRole > EUserWorkspaceRoles.VIEWER)
|
||||
await this.inboxService
|
||||
.patchInbox(workspaceSlug, projectId, inboxId, { view_props: newViewProps })
|
||||
.then((response) => {
|
||||
runInAction(() => {
|
||||
set(this.inboxFilters, [inboxId], newViewProps);
|
||||
});
|
||||
return response;
|
||||
try {
|
||||
runInAction(() => {
|
||||
Object.keys(filters).forEach((_key) => {
|
||||
const _filterKey = _key as keyof TInboxIssueFilterOptions;
|
||||
set(this.filters, [inboxId, "filters", _key], filters[_filterKey]);
|
||||
});
|
||||
});
|
||||
|
||||
const inboxFilters = this.inboxFilters;
|
||||
let _filters: TInboxIssueFilterOptions = {
|
||||
priority: inboxFilters?.filters?.priority || [],
|
||||
inbox_status: inboxFilters?.filters?.inbox_status || [],
|
||||
};
|
||||
_filters = { ..._filters, ...filters };
|
||||
|
||||
const response = await this.rootStore.inbox.inbox.updateInbox(workspaceSlug, projectId, inboxId, {
|
||||
view_props: { filters: _filters },
|
||||
});
|
||||
|
||||
this.rootStore.inbox.inboxIssue.fetchInboxIssues(workspaceSlug, projectId, inboxId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,244 +1,282 @@
|
||||
import { observable, action, makeObservable, runInAction, autorun, computed } from "mobx";
|
||||
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { set } from "lodash";
|
||||
import set from "lodash/set";
|
||||
import update from "lodash/update";
|
||||
import concat from "lodash/concat";
|
||||
import uniq from "lodash/uniq";
|
||||
import pull from "lodash/pull";
|
||||
// services
|
||||
import { InboxService } from "services/inbox.service";
|
||||
import { InboxIssueService } from "services/inbox/inbox-issue.service";
|
||||
// types
|
||||
import { RootStore } from "store/root.store";
|
||||
import { IInboxIssue, TIssue, TInboxStatus } from "@plane/types";
|
||||
import type {
|
||||
TInboxIssueDetailIdMap,
|
||||
TInboxIssueDetailMap,
|
||||
TInboxIssueDetail,
|
||||
TInboxIssueExtendedDetail,
|
||||
TInboxDetailedStatus,
|
||||
TIssue,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
||||
|
||||
export interface IInboxIssuesStore {
|
||||
type TInboxIssueLoader = "fetch" | undefined;
|
||||
|
||||
export interface IInboxIssue {
|
||||
// observables
|
||||
issueMap: Record<string, Record<string, IInboxIssue>>; // {inboxId: {issueId: IInboxIssue}}
|
||||
// computed
|
||||
currentInboxIssueIds: string[] | null;
|
||||
// computed actions
|
||||
getIssueById: (inboxId: string, issueId: string) => IInboxIssue | null;
|
||||
// fetch actions
|
||||
fetchIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInboxIssue[]>;
|
||||
fetchIssueDetails: (
|
||||
loader: TInboxIssueLoader;
|
||||
inboxIssues: TInboxIssueDetailIdMap;
|
||||
inboxIssueMap: TInboxIssueDetailMap;
|
||||
// helper methods
|
||||
getInboxIssuesByInboxId: (inboxId: string) => string[] | undefined;
|
||||
getInboxIssueByIssueId: (inboxId: string, issueId: string) => TInboxIssueDetail | undefined;
|
||||
// actions
|
||||
fetchInboxIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<TInboxIssueExtendedDetail[]>;
|
||||
fetchInboxIssueById: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
issueId: string
|
||||
) => Promise<IInboxIssue>;
|
||||
// CRUD actions
|
||||
createIssue: (
|
||||
inboxIssueId: string
|
||||
) => Promise<TInboxIssueExtendedDetail[]>;
|
||||
createInboxIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
data: Partial<TIssue>
|
||||
) => Promise<IInboxIssue>;
|
||||
updateIssue: (
|
||||
data: Partial<TInboxIssueExtendedDetail>
|
||||
) => Promise<TInboxIssueExtendedDetail>;
|
||||
updateInboxIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
issueId: string,
|
||||
data: Partial<IInboxIssue>
|
||||
) => Promise<void>;
|
||||
updateIssueStatus: (
|
||||
inboxIssueId: string,
|
||||
data: Partial<TInboxIssueExtendedDetail>
|
||||
) => Promise<TInboxIssueExtendedDetail>;
|
||||
removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
||||
updateInboxIssueStatus: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
issueId: string,
|
||||
data: TInboxStatus
|
||||
) => Promise<void>;
|
||||
deleteIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
||||
inboxIssueId: string,
|
||||
data: TInboxDetailedStatus
|
||||
) => Promise<TInboxIssueExtendedDetail>;
|
||||
}
|
||||
|
||||
export class InboxIssuesStore implements IInboxIssuesStore {
|
||||
export class InboxIssue implements IInboxIssue {
|
||||
// observables
|
||||
issueMap: { [inboxId: string]: Record<string, IInboxIssue> } = {};
|
||||
loader: TInboxIssueLoader = "fetch";
|
||||
inboxIssues: TInboxIssueDetailIdMap = {};
|
||||
inboxIssueMap: TInboxIssueDetailMap = {};
|
||||
// root store
|
||||
rootStore;
|
||||
// services
|
||||
inboxService;
|
||||
inboxIssueService;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
issueMap: observable,
|
||||
// computed
|
||||
currentInboxIssueIds: computed,
|
||||
// fetch actions
|
||||
fetchIssues: action,
|
||||
fetchIssueDetails: action,
|
||||
// CRUD actions
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
updateIssueStatus: action,
|
||||
deleteIssue: action,
|
||||
loader: observable.ref,
|
||||
inboxIssues: observable,
|
||||
inboxIssueMap: observable,
|
||||
// actions
|
||||
fetchInboxIssues: action,
|
||||
fetchInboxIssueById: action,
|
||||
createInboxIssue: action,
|
||||
updateInboxIssue: action,
|
||||
removeInboxIssue: action,
|
||||
updateInboxIssueStatus: action,
|
||||
});
|
||||
|
||||
// root store
|
||||
this.rootStore = _rootStore;
|
||||
// services
|
||||
this.inboxService = new InboxService();
|
||||
autorun(() => {
|
||||
const routerStore = this.rootStore.app.router;
|
||||
const workspaceSlug = routerStore?.workspaceSlug;
|
||||
const projectId = routerStore?.projectId;
|
||||
const inboxId = routerStore?.inboxId;
|
||||
if (workspaceSlug && projectId && inboxId && this.rootStore.inboxRoot.inboxFilters.inboxFilters[inboxId])
|
||||
this.fetchIssues(workspaceSlug, projectId, inboxId);
|
||||
});
|
||||
this.inboxIssueService = new InboxIssueService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the issue IDs belong to a specific inbox issues list
|
||||
*/
|
||||
get currentInboxIssueIds() {
|
||||
const inboxId = this.rootStore.app.router.inboxId;
|
||||
if (!inboxId) return null;
|
||||
return Object.keys(this.issueMap?.[inboxId] ?? {}) ?? null;
|
||||
}
|
||||
// helper methods
|
||||
getInboxIssuesByInboxId = computedFn((inboxId: string) => {
|
||||
if (!inboxId) return undefined;
|
||||
return this.inboxIssues?.[inboxId] ?? undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the issue details belongs to a specific inbox issue
|
||||
*/
|
||||
getIssueById = computedFn(
|
||||
(inboxId: string, issueId: string): IInboxIssue | null => this.issueMap?.[inboxId]?.[issueId] ?? null
|
||||
);
|
||||
getInboxIssueByIssueId = computedFn((inboxId: string, issueId: string) => {
|
||||
if (!inboxId) return undefined;
|
||||
return this.inboxIssueMap?.[inboxId]?.[issueId] ?? undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches issues of a specific inbox and adds it to the store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @returns Promise<IInbox[]>
|
||||
*/
|
||||
fetchIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => {
|
||||
const queryParams = this.rootStore.inboxRoot.inboxFilters.appliedFilters ?? undefined;
|
||||
return await this.inboxService
|
||||
.getInboxIssues(workspaceSlug, projectId, inboxId, queryParams)
|
||||
.then((issuesResponse) => {
|
||||
runInAction(() => {
|
||||
issuesResponse.forEach((issue) => {
|
||||
set(this.issueMap, [inboxId, issue.issue_inbox?.[0].id], issue);
|
||||
});
|
||||
});
|
||||
return issuesResponse;
|
||||
});
|
||||
};
|
||||
// actions
|
||||
fetchInboxIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => {
|
||||
try {
|
||||
this.loader = "fetch";
|
||||
const queryParams = this.rootStore.inbox.inboxFilter.inboxAppliedFilters ?? {};
|
||||
|
||||
/**
|
||||
* Fetches issue details of a specific inbox issue and updates it to the store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @param issueId
|
||||
* returns Promise<IInboxIssue>
|
||||
*/
|
||||
fetchIssueDetails = async (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => {
|
||||
return await this.inboxService
|
||||
.getInboxIssueById(workspaceSlug, projectId, inboxId, issueId)
|
||||
.then((issueResponse) => {
|
||||
runInAction(() => {
|
||||
set(this.issueMap, [inboxId, issueId], issueResponse);
|
||||
});
|
||||
return issueResponse;
|
||||
});
|
||||
};
|
||||
const response = await this.inboxIssueService.fetchInboxIssues(workspaceSlug, projectId, inboxId, queryParams);
|
||||
|
||||
/**
|
||||
* Creates a new issue for a specific inbox and add it to the store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @param data
|
||||
* @returns Promise<IInboxIssue>
|
||||
*/
|
||||
createIssue = async (workspaceSlug: string, projectId: string, inboxId: string, data: Partial<TIssue>) => {
|
||||
const payload = {
|
||||
issue: {
|
||||
name: data.name,
|
||||
// description: data.description,
|
||||
description_html: data.description_html,
|
||||
priority: data.priority,
|
||||
},
|
||||
source: INBOX_ISSUE_SOURCE,
|
||||
};
|
||||
return await this.inboxService.createInboxIssue(workspaceSlug, projectId, inboxId, payload).then((response) => {
|
||||
runInAction(() => {
|
||||
set(this.issueMap, [inboxId, response.issue_inbox?.[0].id], response);
|
||||
response.forEach((_inboxIssue) => {
|
||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = _inboxIssue;
|
||||
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
||||
set(this.inboxIssueMap, [inboxId, _inboxIssue.id], inboxIssue);
|
||||
});
|
||||
});
|
||||
|
||||
const _inboxIssueIds = response.map((inboxIssue) => inboxIssue.id);
|
||||
runInAction(() => {
|
||||
set(this.inboxIssues, inboxId, _inboxIssueIds);
|
||||
this.loader = undefined;
|
||||
});
|
||||
|
||||
return response;
|
||||
});
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an issue for a specific inbox and update it in the store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @param issueId
|
||||
* @param data
|
||||
* @returns Promise<IInboxIssue>
|
||||
*/
|
||||
updateIssue = async (
|
||||
fetchInboxIssueById = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
||||
try {
|
||||
const response = await this.inboxIssueService.fetchInboxIssueById(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
inboxId,
|
||||
inboxIssueId
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
||||
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
|
||||
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
|
||||
return uniq(concat(inboxIssueIds, response.id));
|
||||
});
|
||||
});
|
||||
|
||||
// fetching issue activity
|
||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||
// fetching issue reaction
|
||||
await this.rootStore.issue.issueDetail.fetchReactions(workspaceSlug, projectId, inboxIssueId);
|
||||
return response as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
createInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, data: Partial<TIssue>) => {
|
||||
try {
|
||||
const response = await this.inboxIssueService.createInboxIssue(workspaceSlug, projectId, inboxId, {
|
||||
source: "in-app",
|
||||
issue: data,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
||||
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
|
||||
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
|
||||
return uniq(concat(inboxIssueIds, response.id));
|
||||
});
|
||||
});
|
||||
|
||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, response.id);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateInboxIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
issueId: string,
|
||||
data: Partial<IInboxIssue>
|
||||
inboxIssueId: string,
|
||||
data: Partial<TIssue>
|
||||
) => {
|
||||
const issueDetails = this.rootStore.inboxRoot.inboxIssues.getIssueById(inboxId, issueId);
|
||||
return await this.inboxService
|
||||
.patchInboxIssue(workspaceSlug, projectId, inboxId, issueId, { issue: data })
|
||||
.then((issueResponse) => {
|
||||
runInAction(() => {
|
||||
set(this.issueMap, [inboxId, issueId], {
|
||||
...issueDetails,
|
||||
...issueResponse,
|
||||
});
|
||||
});
|
||||
return issueResponse;
|
||||
try {
|
||||
const response = await this.inboxIssueService.updateInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId, {
|
||||
issue: data,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
||||
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
|
||||
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
|
||||
return uniq(concat(inboxIssueIds, response.id));
|
||||
});
|
||||
});
|
||||
|
||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||
return response as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an issue status for a specific inbox issue and update it in the store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @param issueId
|
||||
* @param data
|
||||
* @returns Promise<IInboxIssue>
|
||||
*/
|
||||
updateIssueStatus = async (
|
||||
removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
||||
try {
|
||||
const response = await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.inboxIssues[inboxId], inboxIssueId);
|
||||
delete this.inboxIssueMap[inboxId][inboxIssueId];
|
||||
});
|
||||
|
||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||
return response as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateInboxIssueStatus = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
issueId: string,
|
||||
data: TInboxStatus
|
||||
inboxIssueId: string,
|
||||
data: TInboxDetailedStatus
|
||||
) => {
|
||||
const issueDetails = this.rootStore.inboxRoot.inboxIssues.getIssueById(inboxId, issueId);
|
||||
await this.inboxService.markInboxStatus(workspaceSlug, projectId, inboxId, issueId, data).then((response) => {
|
||||
try {
|
||||
const response = await this.inboxIssueService.updateInboxIssueStatus(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
inboxId,
|
||||
inboxIssueId,
|
||||
data
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.issueMap, [inboxId, issueId, "issue_inbox", 0], {
|
||||
...issueDetails?.issue_inbox?.[0],
|
||||
...response?.issue_inbox?.[0],
|
||||
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
||||
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
||||
const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0];
|
||||
set(this.inboxIssueMap, [inboxId, response.id], inboxIssue);
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => {
|
||||
if (inboxIssueIds.includes(response.id)) return inboxIssueIds;
|
||||
return uniq(concat(inboxIssueIds, response.id));
|
||||
});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an issue for a specific inbox and removes it from the store
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param inboxId
|
||||
* @param issueId
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
deleteIssue = async (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => {
|
||||
await this.inboxService.deleteInboxIssue(workspaceSlug, projectId, inboxId, issueId).then((_) => {
|
||||
runInAction(() => {
|
||||
delete this.issueMap?.[inboxId]?.[issueId];
|
||||
});
|
||||
});
|
||||
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||
return response as any;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
// types
|
||||
import { RootStore } from "store/root.store";
|
||||
import { IInboxIssuesStore, InboxIssuesStore } from "./inbox_issue.store";
|
||||
import { IInboxFiltersStore, InboxFiltersStore } from "./inbox_filter.store";
|
||||
import { IInboxStore, InboxStore } from "./inbox.store";
|
||||
|
||||
export interface IInboxRootStore {
|
||||
inbox: IInboxStore;
|
||||
inboxFilters: IInboxFiltersStore;
|
||||
inboxIssues: IInboxIssuesStore;
|
||||
}
|
||||
|
||||
export class InboxRootStore implements IInboxRootStore {
|
||||
inbox: IInboxStore;
|
||||
inboxFilters: IInboxFiltersStore;
|
||||
inboxIssues: IInboxIssuesStore;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeAutoObservable(this, {});
|
||||
this.inbox = new InboxStore(_rootStore);
|
||||
this.inboxFilters = new InboxFiltersStore(_rootStore);
|
||||
this.inboxIssues = new InboxIssuesStore(_rootStore);
|
||||
}
|
||||
}
|
26
web/store/inbox/root.store.ts
Normal file
26
web/store/inbox/root.store.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// types
|
||||
import { RootStore } from "store/root.store";
|
||||
import { IInbox, Inbox } from "./inbox.store";
|
||||
import { IInboxIssue, InboxIssue } from "./inbox_issue.store";
|
||||
import { IInboxFilter, InboxFilter } from "./inbox_filter.store";
|
||||
|
||||
export interface IInboxRootStore {
|
||||
rootStore: RootStore;
|
||||
inbox: IInbox;
|
||||
inboxIssue: IInboxIssue;
|
||||
inboxFilter: IInboxFilter;
|
||||
}
|
||||
|
||||
export class InboxRootStore implements IInboxRootStore {
|
||||
rootStore: RootStore;
|
||||
inbox: IInbox;
|
||||
inboxIssue: IInboxIssue;
|
||||
inboxFilter: IInboxFilter;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
this.rootStore = _rootStore;
|
||||
this.inbox = new Inbox(_rootStore);
|
||||
this.inboxIssue = new InboxIssue(_rootStore);
|
||||
this.inboxFilter = new InboxFilter(_rootStore);
|
||||
}
|
||||
}
|
@ -8,9 +8,9 @@ import { IModuleStore, ModulesStore } from "./module.store";
|
||||
import { IUserRootStore, UserRootStore } from "./user";
|
||||
import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace";
|
||||
import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
|
||||
import { IInboxRootStore, InboxRootStore } from "./inbox/root.store";
|
||||
import { IStateStore, StateStore } from "./state.store";
|
||||
import { IMemberRootStore, MemberRootStore } from "./member";
|
||||
import { IInboxRootStore, InboxRootStore } from "./inbox";
|
||||
import { IEstimateStore, EstimateStore } from "./estimate.store";
|
||||
import { GlobalViewStore, IGlobalViewStore } from "./global-view.store";
|
||||
import { IMentionStore, MentionStore } from "./mention.store";
|
||||
@ -26,12 +26,12 @@ export class RootStore {
|
||||
workspaceRoot: IWorkspaceRootStore;
|
||||
projectRoot: IProjectRootStore;
|
||||
memberRoot: IMemberRootStore;
|
||||
inboxRoot: IInboxRootStore;
|
||||
cycle: ICycleStore;
|
||||
module: IModuleStore;
|
||||
projectView: IProjectViewStore;
|
||||
globalView: IGlobalViewStore;
|
||||
issue: IIssueRootStore;
|
||||
inbox: IInboxRootStore;
|
||||
state: IStateStore;
|
||||
label: ILabelStore;
|
||||
estimate: IEstimateStore;
|
||||
@ -45,13 +45,13 @@ export class RootStore {
|
||||
this.workspaceRoot = new WorkspaceRootStore(this);
|
||||
this.projectRoot = new ProjectRootStore(this);
|
||||
this.memberRoot = new MemberRootStore(this);
|
||||
this.inboxRoot = new InboxRootStore(this);
|
||||
// independent stores
|
||||
this.cycle = new CycleStore(this);
|
||||
this.module = new ModulesStore(this);
|
||||
this.projectView = new ProjectViewStore(this);
|
||||
this.globalView = new GlobalViewStore(this);
|
||||
this.issue = new IssueRootStore(this);
|
||||
this.inbox = new InboxRootStore(this);
|
||||
this.state = new StateStore(this);
|
||||
this.label = new LabelStore(this);
|
||||
this.estimate = new EstimateStore(this);
|
||||
|
Loading…
Reference in New Issue
Block a user