mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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,
|
InboxSerializer,
|
||||||
InboxIssueSerializer,
|
InboxIssueSerializer,
|
||||||
IssueStateInboxSerializer,
|
IssueStateInboxSerializer,
|
||||||
|
InboxIssueLiteSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .analytic import AnalyticViewSerializer
|
from .analytic import AnalyticViewSerializer
|
||||||
|
@ -60,6 +60,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
IssueFlatSerializer,
|
IssueFlatSerializer,
|
||||||
IssueRelationSerializer,
|
IssueRelationSerializer,
|
||||||
|
InboxIssueLiteSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
# Expansion mapper
|
# Expansion mapper
|
||||||
@ -80,9 +81,10 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
"issue_cycle": CycleIssueSerializer,
|
"issue_cycle": CycleIssueSerializer,
|
||||||
"parent": IssueSerializer,
|
"parent": IssueSerializer,
|
||||||
"issue_relation": IssueRelationSerializer,
|
"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
|
return self.fields
|
||||||
|
|
||||||
@ -103,6 +105,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
LabelSerializer,
|
LabelSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
IssueRelationSerializer,
|
IssueRelationSerializer,
|
||||||
|
InboxIssueLiteSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
# Expansion mapper
|
# Expansion mapper
|
||||||
@ -122,7 +125,8 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
"labels": LabelSerializer,
|
"labels": LabelSerializer,
|
||||||
"issue_cycle": CycleIssueSerializer,
|
"issue_cycle": CycleIssueSerializer,
|
||||||
"parent": IssueSerializer,
|
"parent": IssueSerializer,
|
||||||
"issue_relation": IssueRelationSerializer
|
"issue_relation": IssueRelationSerializer,
|
||||||
|
"issue_inbox" : InboxIssueLiteSerializer,
|
||||||
}
|
}
|
||||||
# Check if field in expansion then expand the field
|
# Check if field in expansion then expand the field
|
||||||
if expand in expansion:
|
if expand in expansion:
|
||||||
|
@ -88,39 +88,24 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return (
|
||||||
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 = (
|
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
issue_inbox__inbox_id=inbox_id,
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=slug,
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=project_id,
|
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
|
||||||
)
|
)
|
||||||
.filter(**filters)
|
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related("assignees", "labels")
|
.prefetch_related("labels", "assignees")
|
||||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
.prefetch_related(
|
||||||
.annotate(
|
Prefetch(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
"issue_inbox",
|
||||||
parent=OuterRef("id")
|
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(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
@ -135,16 +120,20 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.prefetch_related(
|
.annotate(
|
||||||
Prefetch(
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
"issue_inbox",
|
parent=OuterRef("id")
|
||||||
queryset=InboxIssue.objects.only(
|
|
||||||
"status", "duplicate_to", "snoozed_till", "source"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
)
|
)
|
||||||
)
|
).distinct()
|
||||||
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
|
||||||
|
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(
|
return Response(
|
||||||
issues_data,
|
issues_data,
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
@ -211,7 +200,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
source=request.data.get("source", "in-app"),
|
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)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
|
def partial_update(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
@ -331,22 +321,20 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
if state is not None:
|
if state is not None:
|
||||||
issue.state = state
|
issue.state = state
|
||||||
issue.save()
|
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.data, status=status.HTTP_200_OK)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
issue = (self.get_queryset().filter(pk=issue_id).first())
|
||||||
InboxIssueSerializer(inbox_issue).data,
|
serializer = IssueSerializer(issue ,expand=self.expand)
|
||||||
status=status.HTTP_200_OK,
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
)
|
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
|
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
|
||||||
issue = Issue.objects.get(
|
issue = self.get_queryset().filter(pk=issue_id).first()
|
||||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
serializer = IssueSerializer(issue, expand=self.expand,)
|
||||||
)
|
|
||||||
serializer = IssueStateInboxSerializer(issue)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, inbox_id, issue_id):
|
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):
|
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectListSerializer
|
||||||
model = Project
|
model = Project
|
||||||
webhook_event = "project"
|
webhook_event = "project"
|
||||||
|
|
||||||
@ -76,11 +76,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_serializer_class(self, *args, **kwargs):
|
|
||||||
if self.action in ["update", "partial_update"]:
|
|
||||||
return ProjectSerializer
|
|
||||||
return ProjectDetailSerializer
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
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 "./ai";
|
||||||
export * from "./estimate";
|
export * from "./estimate";
|
||||||
export * from "./importer";
|
export * from "./importer";
|
||||||
|
|
||||||
|
// FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable
|
||||||
export * from "./inbox";
|
export * from "./inbox";
|
||||||
|
export * from "./inbox/root";
|
||||||
|
|
||||||
export * from "./analytics";
|
export * from "./analytics";
|
||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
export * from "./notifications";
|
export * from "./notifications";
|
||||||
|
@ -51,12 +51,14 @@ export const ProjectInboxHeader: FC = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
{currentProjectDetails?.inbox_view && (
|
||||||
<CreateInboxIssueModal isOpen={createIssueModal} onClose={() => setCreateIssueModal(false)} />
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => setCreateIssueModal(true)}>
|
<CreateInboxIssueModal isOpen={createIssueModal} onClose={() => setCreateIssueModal(false)} />
|
||||||
Add Issue
|
<Button variant="primary" prependIcon={<Plus />} size="sm" onClick={() => setCreateIssueModal(true)}>
|
||||||
</Button>
|
Add Issue
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -32,7 +32,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
const {
|
const {
|
||||||
issuesFilter: { issueFilters, updateFilters },
|
issuesFilter: { issueFilters, updateFilters },
|
||||||
} = useIssues(EIssuesStoreType.PROJECT);
|
} = useIssues(EIssuesStoreType.PROJECT);
|
||||||
const { inboxesList, isInboxEnabled, getInboxId } = useInbox();
|
|
||||||
const {
|
const {
|
||||||
commandPalette: { toggleCreateIssueModal },
|
commandPalette: { toggleCreateIssueModal },
|
||||||
eventTracker: { setTrackElement },
|
eventTracker: { setTrackElement },
|
||||||
@ -43,6 +42,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const { projectLabels } = useLabel();
|
const { projectLabels } = useLabel();
|
||||||
|
const { getInboxesByProjectId, getInboxById } = useInbox();
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
@ -89,7 +89,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
[workspaceSlug, projectId, updateFilters]
|
[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 deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL;
|
||||||
const canUserCreateIssue =
|
const canUserCreateIssue =
|
||||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||||
@ -190,14 +192,15 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</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>
|
<span>
|
||||||
<Button variant="neutral-primary" size="sm" className="relative">
|
<Button variant="neutral-primary" size="sm" className="relative">
|
||||||
Inbox
|
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">
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</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 "./modals";
|
||||||
export * from "./actions-header";
|
|
||||||
export * from "./filters-dropdown";
|
export * from "./inbox-issue-actions";
|
||||||
export * from "./filters-list";
|
export * from "./inbox-issue-status";
|
||||||
export * from "./issue-activity";
|
|
||||||
export * from "./issue-card";
|
export * from "./sidebar/root";
|
||||||
export * from "./issues-list-sidebar";
|
|
||||||
export * from "./main-content";
|
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 React, { useState } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { CheckCircle } from "lucide-react";
|
import { CheckCircle } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IInboxIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
import { useProject } from "hooks/store";
|
import { useProject } from "hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: IInboxIssue;
|
data: TIssue;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: () => Promise<void>;
|
onSubmit: () => Promise<void>;
|
||||||
@ -28,7 +27,6 @@ export const AcceptIssueModal: React.FC<Props> = ({ isOpen, onClose, data, onSub
|
|||||||
|
|
||||||
const handleAccept = () => {
|
const handleAccept = () => {
|
||||||
setIsAccepting(true);
|
setIsAccepting(true);
|
||||||
|
|
||||||
onSubmit().finally(() => setIsAccepting(false));
|
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;
|
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||||
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { createIssue } = useInboxIssues();
|
const {
|
||||||
|
issues: { createInboxIssue },
|
||||||
|
} = useInboxIssues();
|
||||||
const {
|
const {
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
eventTracker: { postHogEventTracker },
|
eventTracker: { postHogEventTracker },
|
||||||
@ -85,10 +87,10 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
const handleFormSubmit = async (formData: Partial<TIssue>) => {
|
const handleFormSubmit = async (formData: Partial<TIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
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) => {
|
.then((res) => {
|
||||||
if (!createMore) {
|
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();
|
handleClose();
|
||||||
} else reset(defaultValues);
|
} else reset(defaultValues);
|
||||||
postHogEventTracker(
|
postHogEventTracker(
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IInboxIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
import { useProject } from "hooks/store";
|
import { useProject } from "hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: IInboxIssue;
|
data: TIssue;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: () => Promise<void>;
|
onSubmit: () => Promise<void>;
|
||||||
@ -28,7 +27,6 @@ export const DeclineIssueModal: React.FC<Props> = ({ isOpen, onClose, data, onSu
|
|||||||
|
|
||||||
const handleDecline = () => {
|
const handleDecline = () => {
|
||||||
setIsDeclining(true);
|
setIsDeclining(true);
|
||||||
|
|
||||||
onSubmit().finally(() => setIsDeclining(false));
|
onSubmit().finally(() => setIsDeclining(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,39 +1,27 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useProject, useWorkspace } from "hooks/store";
|
import { useProject } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// icons
|
// icons
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IInboxIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
import { useInboxIssues } from "hooks/store/use-inbox-issues";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: IInboxIssue;
|
data: TIssue;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
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
|
// states
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
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 = () => {
|
const handleClose = () => {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
@ -41,59 +29,13 @@ export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClos
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
|
||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
|
onSubmit().finally(() => setIsDeleting(false));
|
||||||
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));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<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
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@ -137,7 +79,7 @@ export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClos
|
|||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleDelete} loading={isDeleting}>
|
<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";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useInboxFilters } from "hooks/store";
|
import { useInboxIssues } from "hooks/store";
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { PriorityIcon } from "@plane/ui";
|
import { PriorityIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IInboxFilterOptions, TIssuePriorities } from "@plane/types";
|
import { TInboxIssueFilterOptions, TIssuePriorities } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { INBOX_STATUS } from "constants/inbox";
|
import { INBOX_STATUS } from "constants/inbox";
|
||||||
|
|
||||||
export const InboxFiltersList = observer(() => {
|
type TInboxIssueAppliedFilter = { workspaceSlug: string; projectId: string; inboxId: string };
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
|
||||||
|
|
||||||
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;
|
if (!workspaceSlug || !projectId || !inboxId) return;
|
||||||
|
|
||||||
updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), filter);
|
updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), filter);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearAllFilters = () => {
|
const handleClearAllFilters = () => {
|
||||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
const newFilters: TInboxIssueFilterOptions = { priority: [], inbox_status: [] };
|
||||||
|
|
||||||
const newFilters: IInboxFilterOptions = {};
|
|
||||||
Object.keys(filters ?? {}).forEach((key) => {
|
|
||||||
newFilters[key as keyof IInboxFilterOptions] = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), newFilters);
|
updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), newFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
let filtersLength = 0;
|
let filtersLength = 0;
|
||||||
Object.keys(filters ?? {}).forEach((key) => {
|
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?.[filterKey] && Array.isArray(filters[filterKey])) filtersLength += (filters[filterKey] ?? []).length;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!filters || filtersLength <= 0) return null;
|
if (!filters || filtersLength <= 0) return <></>;
|
||||||
|
|
||||||
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) => {
|
{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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
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>
|
<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>
|
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
@ -81,9 +88,12 @@ export const InboxFiltersList = observer(() => {
|
|||||||
: "bg-custom-background-90 text-custom-text-200"
|
: "bg-custom-background-90 text-custom-text-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>
|
<div className="relative flex items-center gap-1">
|
||||||
<PriorityIcon priority={priority as TIssuePriorities} />
|
<div>
|
||||||
</span>
|
<PriorityIcon priority={priority as TIssuePriorities} size={14} />
|
||||||
|
</div>
|
||||||
|
<div>{priority}</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
@ -101,7 +111,7 @@ export const InboxFiltersList = observer(() => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleUpdateFilter({
|
handleUpdateFilter({
|
||||||
priority: null,
|
priority: [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -115,7 +125,7 @@ export const InboxFiltersList = observer(() => {
|
|||||||
key={status}
|
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"
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
@ -133,7 +143,7 @@ export const InboxFiltersList = observer(() => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleUpdateFilter({
|
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";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useInboxFilters } from "hooks/store";
|
import { useInboxIssues } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { MultiLevelDropdown } from "components/ui";
|
import { MultiLevelDropdown } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PriorityIcon } from "@plane/ui";
|
import { PriorityIcon } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IInboxFilterOptions } from "@plane/types";
|
import { TInboxIssueFilterOptions } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { INBOX_STATUS } from "constants/inbox";
|
import { INBOX_STATUS } from "constants/inbox";
|
||||||
import { ISSUE_PRIORITIES } from "constants/issue";
|
import { ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
export const FiltersDropdown: React.FC = observer(() => {
|
type TInboxIssueFilterSelection = { workspaceSlug: string; projectId: string; inboxId: string };
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
|
||||||
|
|
||||||
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;
|
let filtersLength = 0;
|
||||||
Object.keys(filters ?? {}).forEach((key) => {
|
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?.[filterKey] && Array.isArray(filters[filterKey])) filtersLength += (filters[filterKey] ?? []).length;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ export const FiltersDropdown: React.FC = observer(() => {
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
if (!workspaceSlug || !projectId || !inboxId) return;
|
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 currentValue: any[] = filters?.[key] ?? [];
|
||||||
|
|
||||||
const valueExists = currentValue.includes(option.value);
|
const valueExists = currentValue.includes(option.value);
|
||||||
@ -74,20 +75,28 @@ export const FiltersDropdown: React.FC = observer(() => {
|
|||||||
{
|
{
|
||||||
id: "inbox_status",
|
id: "inbox_status",
|
||||||
label: "Status",
|
label: "Status",
|
||||||
value: INBOX_STATUS.map((status) => status.value),
|
value: INBOX_STATUS.map((status) => status.status),
|
||||||
hasChildren: true,
|
hasChildren: true,
|
||||||
children: INBOX_STATUS.map((status) => ({
|
children: INBOX_STATUS.map((status) => ({
|
||||||
id: status.key,
|
id: status.status.toString(),
|
||||||
label: status.label,
|
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: {
|
value: {
|
||||||
key: "inbox_status",
|
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 && (
|
{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">
|
<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>
|
<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) => {
|
const handleIssueCycleChange = async (cycleId: string | null) => {
|
||||||
if (!issue || issue.cycle_id === cycleId) return;
|
if (!issue || issue.cycle_id === cycleId) return;
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
if (cycleId) await issueOperations.addIssueToCycle(workspaceSlug, projectId, cycleId, [issueId]);
|
if (cycleId) await issueOperations.addIssueToCycle?.(workspaceSlug, projectId, cycleId, [issueId]);
|
||||||
else await issueOperations.removeIssueFromCycle(workspaceSlug, projectId, issue.cycle_id ?? "", issueId);
|
else await issueOperations.removeIssueFromCycle?.(workspaceSlug, projectId, issue.cycle_id ?? "", issueId);
|
||||||
setIsUpdating(false);
|
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) => {
|
const handleIssueModuleChange = async (moduleId: string | null) => {
|
||||||
if (!issue || issue.module_id === moduleId) return;
|
if (!issue || issue.module_id === moduleId) return;
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
if (moduleId) await issueOperations.addIssueToModule(workspaceSlug, projectId, moduleId, [issueId]);
|
if (moduleId) await issueOperations.addIssueToModule?.(workspaceSlug, projectId, moduleId, [issueId]);
|
||||||
else await issueOperations.removeIssueFromModule(workspaceSlug, projectId, issue.module_id ?? "", issueId);
|
else await issueOperations.removeIssueFromModule?.(workspaceSlug, projectId, issue.module_id ?? "", issueId);
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -27,10 +27,15 @@ export type TIssueOperations = {
|
|||||||
showToast?: boolean
|
showToast?: boolean
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: 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>;
|
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: 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>;
|
removeIssueFromModule?: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string,
|
||||||
|
issueId: string
|
||||||
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueDetailRoot = {
|
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-dashboard";
|
||||||
export * from "./use-estimate";
|
export * from "./use-estimate";
|
||||||
export * from "./use-global-view";
|
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-label";
|
||||||
export * from "./use-member";
|
export * from "./use-member";
|
||||||
export * from "./use-mention";
|
export * from "./use-mention";
|
||||||
@ -22,3 +19,5 @@ export * from "./use-workspace";
|
|||||||
export * from "./use-issues";
|
export * from "./use-issues";
|
||||||
export * from "./use-kanban-view";
|
export * from "./use-kanban-view";
|
||||||
export * from "./use-issue-detail";
|
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
|
// mobx store
|
||||||
import { StoreContext } from "contexts/store-context";
|
import { StoreContext } from "contexts/store-context";
|
||||||
// types
|
// 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);
|
const context = useContext(StoreContext);
|
||||||
if (context === undefined) throw new Error("useInboxIssues must be used within StoreProvider");
|
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
|
// mobx store
|
||||||
import { StoreContext } from "contexts/store-context";
|
import { StoreContext } from "contexts/store-context";
|
||||||
// types
|
// 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);
|
const context = useContext(StoreContext);
|
||||||
if (context === undefined) throw new Error("useInbox must be used within StoreProvider");
|
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) => {
|
export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
// store
|
// store
|
||||||
const { fetchInboxesList, isInboxEnabled } = useInbox();
|
const { fetchInboxes } = useInbox();
|
||||||
const {
|
const {
|
||||||
commandPalette: { toggleCreateProjectModal },
|
commandPalette: { toggleCreateProjectModal },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
const {
|
const {
|
||||||
membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
|
membership: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { getProjectById, fetchProjectDetails } = useProject();
|
const { getProjectById, fetchProjectDetails, currentProjectDetails } = useProject();
|
||||||
const { fetchAllCycles } = useCycle();
|
const { fetchAllCycles } = useCycle();
|
||||||
const { fetchModules } = useModule();
|
const { fetchModules } = useModule();
|
||||||
const { fetchViews } = useProjectView();
|
const { fetchViews } = useProjectView();
|
||||||
@ -96,11 +96,13 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
|||||||
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null,
|
||||||
workspaceSlug && projectId ? () => fetchViews(workspaceSlug.toString(), projectId.toString()) : 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(
|
useSWR(
|
||||||
workspaceSlug && projectId && isInboxEnabled ? `PROJECT_INBOXES_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails.inbox_view
|
||||||
workspaceSlug && projectId && isInboxEnabled
|
? `PROJECT_INBOXES_${workspaceSlug}_${projectId}`
|
||||||
? () => fetchInboxesList(workspaceSlug.toString(), projectId.toString())
|
: null,
|
||||||
|
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails.inbox_view
|
||||||
|
? () => fetchInboxes(workspaceSlug.toString(), projectId.toString())
|
||||||
: null,
|
: null,
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
|
@ -1,41 +1,109 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Inbox } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInboxFilters } from "hooks/store/";
|
import { useProject, useInboxIssues } from "hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
|
// ui
|
||||||
|
import { Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { InboxActionsHeader, InboxMainContent, InboxIssuesListSidebar } from "components/inbox";
|
|
||||||
import { ProjectInboxHeader } from "components/headers";
|
import { ProjectInboxHeader } from "components/headers";
|
||||||
|
import { InboxSidebarRoot, InboxIssueActionsHeader } from "components/inbox";
|
||||||
|
import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox";
|
||||||
// types
|
// types
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
|
||||||
const ProjectInboxPage: NextPageWithLayout = () => {
|
const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
||||||
|
// store hooks
|
||||||
const { fetchInboxFilters } = useInboxFilters();
|
const {
|
||||||
|
issues: { getInboxIssuesByInboxId },
|
||||||
|
} = useInboxIssues();
|
||||||
|
const { currentProjectDetails } = useProject();
|
||||||
|
const {
|
||||||
|
filters: { fetchInboxFilters },
|
||||||
|
issues: { loader, fetchInboxIssues },
|
||||||
|
} = useInboxIssues();
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && inboxId ? `INBOX_FILTERS_${inboxId.toString()}` : null,
|
workspaceSlug && projectId && currentProjectDetails && currentProjectDetails?.inbox_view
|
||||||
workspaceSlug && projectId && inboxId
|
? `INBOX_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}`
|
||||||
? () => fetchInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString())
|
: null,
|
||||||
: 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 (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<>
|
||||||
<InboxActionsHeader />
|
{loader === "fetch" ? (
|
||||||
<div className="grid flex-1 grid-cols-4 divide-x divide-custom-border-200 overflow-hidden">
|
<div className="relative flex w-full h-full items-center justify-center">
|
||||||
<InboxIssuesListSidebar />
|
<Spinner />
|
||||||
<div className="col-span-3 h-full overflow-auto">
|
|
||||||
<InboxMainContent />
|
|
||||||
</div>
|
</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) {
|
ProjectInboxPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
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 { 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
|
// services
|
||||||
import { InboxService } from "services/inbox.service";
|
import { InboxService } from "services/inbox/inbox.service";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "store/root.store";
|
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
|
// observables
|
||||||
inboxesList: {
|
inboxes: TInboxDetailIdMap;
|
||||||
[projectId: string]: IInbox[];
|
inboxMap: TInboxDetailMap;
|
||||||
};
|
// helper methods
|
||||||
inboxDetails: {
|
getInboxesByProjectId: (projectId: string) => string[] | undefined;
|
||||||
[inboxId: string]: IInbox;
|
getInboxById: (inboxId: string) => TInbox | undefined;
|
||||||
};
|
|
||||||
// computed
|
|
||||||
isInboxEnabled: boolean;
|
|
||||||
// computed actions
|
|
||||||
getInboxId: (projectId: string) => string | null;
|
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchInboxesList: (workspaceSlug: string, projectId: string) => Promise<IInbox[]>;
|
fetchInboxes: (workspaceSlug: string, projectId: string) => Promise<TInbox[]>;
|
||||||
fetchInboxDetails: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInbox>;
|
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
|
// observables
|
||||||
inboxesList: {
|
inboxes: TInboxDetailIdMap = {};
|
||||||
[projectId: string]: IInbox[];
|
inboxMap: TInboxDetailMap = {};
|
||||||
} = {};
|
|
||||||
inboxDetails: {
|
|
||||||
[inboxId: string]: IInbox;
|
|
||||||
} = {};
|
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
// services
|
// services
|
||||||
@ -40,68 +35,80 @@ export class InboxStore implements IInboxStore {
|
|||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
inboxesList: observable,
|
inboxMap: observable,
|
||||||
inboxDetails: observable,
|
inboxes: observable,
|
||||||
// computed
|
|
||||||
isInboxEnabled: computed,
|
|
||||||
// actions
|
// actions
|
||||||
fetchInboxesList: action,
|
fetchInboxes: action,
|
||||||
|
fetchInboxById: action,
|
||||||
|
updateInbox: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
// root store
|
// root store
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
// services
|
// services
|
||||||
this.inboxService = new InboxService();
|
this.inboxService = new InboxService();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// helper methods
|
||||||
* Returns true if inbox is enabled for current project
|
getInboxesByProjectId = computedFn((projectId: string) => {
|
||||||
*/
|
if (!projectId) return undefined;
|
||||||
get isInboxEnabled() {
|
return this.inboxes?.[projectId] ?? undefined;
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
getInboxById = computedFn((inboxId: string) => {
|
||||||
* Fetches the inboxes list belongs to a specific project
|
if (!inboxId) return undefined;
|
||||||
* @param workspaceSlug
|
return this.inboxMap[inboxId] ?? undefined;
|
||||||
* @param projectId
|
});
|
||||||
* @returns Promise<IInbox[]>
|
|
||||||
*/
|
// actions
|
||||||
fetchInboxesList = async (workspaceSlug: string, projectId: string) => {
|
fetchInboxes = async (workspaceSlug: string, projectId: string) => {
|
||||||
return await this.inboxService.getInboxes(workspaceSlug, projectId).then((inboxes) => {
|
try {
|
||||||
|
const response = await this.inboxService.fetchInboxes(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
const _inboxIds = response.map((inbox) => inbox.id);
|
||||||
runInAction(() => {
|
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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
fetchInboxById = async (workspaceSlug: string, projectId: string, inboxId: string) => {
|
||||||
* Fetches the inbox details belongs to a specific inbox
|
try {
|
||||||
* @param workspaceSlug
|
const response = await this.inboxService.fetchInboxById(workspaceSlug, projectId, inboxId);
|
||||||
* @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) => {
|
|
||||||
runInAction(() => {
|
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 { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||||
import { set } from "lodash";
|
import set from "lodash/set";
|
||||||
|
import isEmpty from "lodash/isEmpty";
|
||||||
// services
|
// services
|
||||||
import { InboxService } from "services/inbox.service";
|
import { InboxService } from "services/inbox.service";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "store/root.store";
|
import { RootStore } from "store/root.store";
|
||||||
import { IInbox, IInboxFilterOptions, IInboxQueryParams } from "@plane/types";
|
import { TInboxIssueFilterOptions, TInboxIssueFilters, TInboxIssueQueryParams, TInbox } from "@plane/types";
|
||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
|
||||||
|
|
||||||
export interface IInboxFiltersStore {
|
export interface IInboxFilter {
|
||||||
// observables
|
// observables
|
||||||
inboxFilters: Record<string, { filters: IInboxFilterOptions }>;
|
filters: Record<string, TInboxIssueFilters>; // inbox_id -> TInboxIssueFilters
|
||||||
// computed
|
// computed
|
||||||
appliedFilters: IInboxQueryParams | null;
|
inboxFilters: TInboxIssueFilters | undefined;
|
||||||
// fetch action
|
inboxAppliedFilters: Partial<Record<TInboxIssueQueryParams, string>> | undefined;
|
||||||
fetchInboxFilters: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInbox>;
|
// actions
|
||||||
// update action
|
fetchInboxFilters: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<TInbox>;
|
||||||
updateInboxFilters: (
|
updateInboxFilters: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
filters: Partial<IInboxFilterOptions>
|
filters: Partial<TInboxIssueFilterOptions>
|
||||||
) => Promise<void>;
|
) => Promise<TInbox>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InboxFiltersStore implements IInboxFiltersStore {
|
export class InboxFilter implements IInboxFilter {
|
||||||
// observables
|
// observables
|
||||||
inboxFilters: {
|
filters: Record<string, TInboxIssueFilters> = {};
|
||||||
[inboxId: string]: { filters: IInboxFilterOptions };
|
|
||||||
} = {};
|
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
// services
|
// services
|
||||||
@ -37,12 +34,12 @@ export class InboxFiltersStore implements IInboxFiltersStore {
|
|||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
inboxFilters: observable,
|
filters: observable,
|
||||||
// computed
|
// computed
|
||||||
appliedFilters: computed,
|
inboxFilters: computed,
|
||||||
// fetch action
|
inboxAppliedFilters: computed,
|
||||||
|
// actions
|
||||||
fetchInboxFilters: action,
|
fetchInboxFilters: action,
|
||||||
// update action
|
|
||||||
updateInboxFilters: action,
|
updateInboxFilters: action,
|
||||||
});
|
});
|
||||||
// root store
|
// root store
|
||||||
@ -51,69 +48,81 @@ export class InboxFiltersStore implements IInboxFiltersStore {
|
|||||||
this.inboxService = new InboxService();
|
this.inboxService = new InboxService();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get inboxFilters() {
|
||||||
* Returns applied filters to specific inbox
|
|
||||||
*/
|
|
||||||
get appliedFilters(): IInboxQueryParams | null {
|
|
||||||
const inboxId = this.rootStore.app.router.inboxId;
|
const inboxId = this.rootStore.app.router.inboxId;
|
||||||
if (!inboxId) return null;
|
if (!inboxId) return undefined;
|
||||||
const filtersList = this.inboxFilters[inboxId]?.filters;
|
|
||||||
if (!filtersList) return null;
|
const displayFilters = this.filters[inboxId] || undefined;
|
||||||
const filteredRouteParams: IInboxQueryParams = {
|
if (isEmpty(displayFilters)) return undefined;
|
||||||
priority: filtersList.priority ? filtersList.priority.join(",") : null,
|
|
||||||
inbox_status: filtersList.inbox_status ? filtersList.inbox_status.join(",") : null,
|
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) => {
|
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(() => {
|
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 (
|
updateInboxFilters = async (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
filters: Partial<IInboxFilterOptions>
|
filters: Partial<TInboxIssueFilterOptions>
|
||||||
) => {
|
) => {
|
||||||
const newViewProps = {
|
try {
|
||||||
...this.inboxFilters[inboxId],
|
runInAction(() => {
|
||||||
filters: {
|
Object.keys(filters).forEach((_key) => {
|
||||||
...this.inboxFilters[inboxId]?.filters,
|
const _filterKey = _key as keyof TInboxIssueFilterOptions;
|
||||||
...filters,
|
set(this.filters, [inboxId, "filters", _key], filters[_filterKey]);
|
||||||
},
|
|
||||||
};
|
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 { 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
|
// services
|
||||||
import { InboxService } from "services/inbox.service";
|
import { InboxIssueService } from "services/inbox/inbox-issue.service";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "store/root.store";
|
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
|
// constants
|
||||||
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
||||||
|
|
||||||
export interface IInboxIssuesStore {
|
type TInboxIssueLoader = "fetch" | undefined;
|
||||||
|
|
||||||
|
export interface IInboxIssue {
|
||||||
// observables
|
// observables
|
||||||
issueMap: Record<string, Record<string, IInboxIssue>>; // {inboxId: {issueId: IInboxIssue}}
|
loader: TInboxIssueLoader;
|
||||||
// computed
|
inboxIssues: TInboxIssueDetailIdMap;
|
||||||
currentInboxIssueIds: string[] | null;
|
inboxIssueMap: TInboxIssueDetailMap;
|
||||||
// computed actions
|
// helper methods
|
||||||
getIssueById: (inboxId: string, issueId: string) => IInboxIssue | null;
|
getInboxIssuesByInboxId: (inboxId: string) => string[] | undefined;
|
||||||
// fetch actions
|
getInboxIssueByIssueId: (inboxId: string, issueId: string) => TInboxIssueDetail | undefined;
|
||||||
fetchIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<IInboxIssue[]>;
|
// actions
|
||||||
fetchIssueDetails: (
|
fetchInboxIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<TInboxIssueExtendedDetail[]>;
|
||||||
|
fetchInboxIssueById: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
issueId: string
|
inboxIssueId: string
|
||||||
) => Promise<IInboxIssue>;
|
) => Promise<TInboxIssueExtendedDetail[]>;
|
||||||
// CRUD actions
|
createInboxIssue: (
|
||||||
createIssue: (
|
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
data: Partial<TIssue>
|
data: Partial<TInboxIssueExtendedDetail>
|
||||||
) => Promise<IInboxIssue>;
|
) => Promise<TInboxIssueExtendedDetail>;
|
||||||
updateIssue: (
|
updateInboxIssue: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
issueId: string,
|
inboxIssueId: string,
|
||||||
data: Partial<IInboxIssue>
|
data: Partial<TInboxIssueExtendedDetail>
|
||||||
) => Promise<void>;
|
) => Promise<TInboxIssueExtendedDetail>;
|
||||||
updateIssueStatus: (
|
removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
||||||
|
updateInboxIssueStatus: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
issueId: string,
|
inboxIssueId: string,
|
||||||
data: TInboxStatus
|
data: TInboxDetailedStatus
|
||||||
) => Promise<void>;
|
) => Promise<TInboxIssueExtendedDetail>;
|
||||||
deleteIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InboxIssuesStore implements IInboxIssuesStore {
|
export class InboxIssue implements IInboxIssue {
|
||||||
// observables
|
// observables
|
||||||
issueMap: { [inboxId: string]: Record<string, IInboxIssue> } = {};
|
loader: TInboxIssueLoader = "fetch";
|
||||||
|
inboxIssues: TInboxIssueDetailIdMap = {};
|
||||||
|
inboxIssueMap: TInboxIssueDetailMap = {};
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
// services
|
// services
|
||||||
inboxService;
|
inboxIssueService;
|
||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// observables
|
// observables
|
||||||
issueMap: observable,
|
loader: observable.ref,
|
||||||
// computed
|
inboxIssues: observable,
|
||||||
currentInboxIssueIds: computed,
|
inboxIssueMap: observable,
|
||||||
// fetch actions
|
// actions
|
||||||
fetchIssues: action,
|
fetchInboxIssues: action,
|
||||||
fetchIssueDetails: action,
|
fetchInboxIssueById: action,
|
||||||
// CRUD actions
|
createInboxIssue: action,
|
||||||
createIssue: action,
|
updateInboxIssue: action,
|
||||||
updateIssue: action,
|
removeInboxIssue: action,
|
||||||
updateIssueStatus: action,
|
updateInboxIssueStatus: action,
|
||||||
deleteIssue: action,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// root store
|
// root store
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
// services
|
// services
|
||||||
this.inboxService = new InboxService();
|
this.inboxIssueService = new InboxIssueService();
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// helper methods
|
||||||
* Returns the issue IDs belong to a specific inbox issues list
|
getInboxIssuesByInboxId = computedFn((inboxId: string) => {
|
||||||
*/
|
if (!inboxId) return undefined;
|
||||||
get currentInboxIssueIds() {
|
return this.inboxIssues?.[inboxId] ?? undefined;
|
||||||
const inboxId = this.rootStore.app.router.inboxId;
|
});
|
||||||
if (!inboxId) return null;
|
|
||||||
return Object.keys(this.issueMap?.[inboxId] ?? {}) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
getInboxIssueByIssueId = computedFn((inboxId: string, issueId: string) => {
|
||||||
* Returns the issue details belongs to a specific inbox issue
|
if (!inboxId) return undefined;
|
||||||
*/
|
return this.inboxIssueMap?.[inboxId]?.[issueId] ?? undefined;
|
||||||
getIssueById = computedFn(
|
});
|
||||||
(inboxId: string, issueId: string): IInboxIssue | null => this.issueMap?.[inboxId]?.[issueId] ?? null
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
// actions
|
||||||
* Fetches issues of a specific inbox and adds it to the store
|
fetchInboxIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => {
|
||||||
* @param workspaceSlug
|
try {
|
||||||
* @param projectId
|
this.loader = "fetch";
|
||||||
* @param inboxId
|
const queryParams = this.rootStore.inbox.inboxFilter.inboxAppliedFilters ?? {};
|
||||||
* @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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const response = await this.inboxIssueService.fetchInboxIssues(workspaceSlug, projectId, inboxId, queryParams);
|
||||||
* 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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(() => {
|
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;
|
return response;
|
||||||
});
|
} catch (error) {
|
||||||
|
this.loader = undefined;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
fetchInboxIssueById = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
||||||
* Updates an issue for a specific inbox and update it in the store
|
try {
|
||||||
* @param workspaceSlug
|
const response = await this.inboxIssueService.fetchInboxIssueById(
|
||||||
* @param projectId
|
workspaceSlug,
|
||||||
* @param inboxId
|
projectId,
|
||||||
* @param issueId
|
inboxId,
|
||||||
* @param data
|
inboxIssueId
|
||||||
* @returns Promise<IInboxIssue>
|
);
|
||||||
*/
|
|
||||||
updateIssue = async (
|
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,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
issueId: string,
|
inboxIssueId: string,
|
||||||
data: Partial<IInboxIssue>
|
data: Partial<TIssue>
|
||||||
) => {
|
) => {
|
||||||
const issueDetails = this.rootStore.inboxRoot.inboxIssues.getIssueById(inboxId, issueId);
|
try {
|
||||||
return await this.inboxService
|
const response = await this.inboxIssueService.updateInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId, {
|
||||||
.patchInboxIssue(workspaceSlug, projectId, inboxId, issueId, { issue: data })
|
issue: data,
|
||||||
.then((issueResponse) => {
|
|
||||||
runInAction(() => {
|
|
||||||
set(this.issueMap, [inboxId, issueId], {
|
|
||||||
...issueDetails,
|
|
||||||
...issueResponse,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return issueResponse;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => {
|
||||||
* Updates an issue status for a specific inbox issue and update it in the store
|
try {
|
||||||
* @param workspaceSlug
|
const response = await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId);
|
||||||
* @param projectId
|
|
||||||
* @param inboxId
|
runInAction(() => {
|
||||||
* @param issueId
|
pull(this.inboxIssues[inboxId], inboxIssueId);
|
||||||
* @param data
|
delete this.inboxIssueMap[inboxId][inboxIssueId];
|
||||||
* @returns Promise<IInboxIssue>
|
});
|
||||||
*/
|
|
||||||
updateIssueStatus = async (
|
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||||
|
return response as any;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateInboxIssueStatus = async (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
issueId: string,
|
inboxIssueId: string,
|
||||||
data: TInboxStatus
|
data: TInboxDetailedStatus
|
||||||
) => {
|
) => {
|
||||||
const issueDetails = this.rootStore.inboxRoot.inboxIssues.getIssueById(inboxId, issueId);
|
try {
|
||||||
await this.inboxService.markInboxStatus(workspaceSlug, projectId, inboxId, issueId, data).then((response) => {
|
const response = await this.inboxIssueService.updateInboxIssueStatus(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
inboxId,
|
||||||
|
inboxIssueId,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.issueMap, [inboxId, issueId, "issue_inbox", 0], {
|
const { ["issue_inbox"]: issueInboxDetail, ...issue } = response;
|
||||||
...issueDetails?.issue_inbox?.[0],
|
this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]);
|
||||||
...response?.issue_inbox?.[0],
|
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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId);
|
||||||
* Deletes an issue for a specific inbox and removes it from the store
|
return response as any;
|
||||||
* @param workspaceSlug
|
} catch (error) {
|
||||||
* @param projectId
|
throw error;
|
||||||
* @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];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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 { IUserRootStore, UserRootStore } from "./user";
|
||||||
import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace";
|
import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace";
|
||||||
import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
|
import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
|
||||||
|
import { IInboxRootStore, InboxRootStore } from "./inbox/root.store";
|
||||||
import { IStateStore, StateStore } from "./state.store";
|
import { IStateStore, StateStore } from "./state.store";
|
||||||
import { IMemberRootStore, MemberRootStore } from "./member";
|
import { IMemberRootStore, MemberRootStore } from "./member";
|
||||||
import { IInboxRootStore, InboxRootStore } from "./inbox";
|
|
||||||
import { IEstimateStore, EstimateStore } from "./estimate.store";
|
import { IEstimateStore, EstimateStore } from "./estimate.store";
|
||||||
import { GlobalViewStore, IGlobalViewStore } from "./global-view.store";
|
import { GlobalViewStore, IGlobalViewStore } from "./global-view.store";
|
||||||
import { IMentionStore, MentionStore } from "./mention.store";
|
import { IMentionStore, MentionStore } from "./mention.store";
|
||||||
@ -26,12 +26,12 @@ export class RootStore {
|
|||||||
workspaceRoot: IWorkspaceRootStore;
|
workspaceRoot: IWorkspaceRootStore;
|
||||||
projectRoot: IProjectRootStore;
|
projectRoot: IProjectRootStore;
|
||||||
memberRoot: IMemberRootStore;
|
memberRoot: IMemberRootStore;
|
||||||
inboxRoot: IInboxRootStore;
|
|
||||||
cycle: ICycleStore;
|
cycle: ICycleStore;
|
||||||
module: IModuleStore;
|
module: IModuleStore;
|
||||||
projectView: IProjectViewStore;
|
projectView: IProjectViewStore;
|
||||||
globalView: IGlobalViewStore;
|
globalView: IGlobalViewStore;
|
||||||
issue: IIssueRootStore;
|
issue: IIssueRootStore;
|
||||||
|
inbox: IInboxRootStore;
|
||||||
state: IStateStore;
|
state: IStateStore;
|
||||||
label: ILabelStore;
|
label: ILabelStore;
|
||||||
estimate: IEstimateStore;
|
estimate: IEstimateStore;
|
||||||
@ -45,13 +45,13 @@ export class RootStore {
|
|||||||
this.workspaceRoot = new WorkspaceRootStore(this);
|
this.workspaceRoot = new WorkspaceRootStore(this);
|
||||||
this.projectRoot = new ProjectRootStore(this);
|
this.projectRoot = new ProjectRootStore(this);
|
||||||
this.memberRoot = new MemberRootStore(this);
|
this.memberRoot = new MemberRootStore(this);
|
||||||
this.inboxRoot = new InboxRootStore(this);
|
|
||||||
// independent stores
|
// independent stores
|
||||||
this.cycle = new CycleStore(this);
|
this.cycle = new CycleStore(this);
|
||||||
this.module = new ModulesStore(this);
|
this.module = new ModulesStore(this);
|
||||||
this.projectView = new ProjectViewStore(this);
|
this.projectView = new ProjectViewStore(this);
|
||||||
this.globalView = new GlobalViewStore(this);
|
this.globalView = new GlobalViewStore(this);
|
||||||
this.issue = new IssueRootStore(this);
|
this.issue = new IssueRootStore(this);
|
||||||
|
this.inbox = new InboxRootStore(this);
|
||||||
this.state = new StateStore(this);
|
this.state = new StateStore(this);
|
||||||
this.label = new LabelStore(this);
|
this.label = new LabelStore(this);
|
||||||
this.estimate = new EstimateStore(this);
|
this.estimate = new EstimateStore(this);
|
||||||
|
Loading…
Reference in New Issue
Block a user