diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index c35737cb5..0d72f9192 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -111,6 +111,7 @@ from .inbox import ( InboxSerializer, InboxIssueSerializer, IssueStateInboxSerializer, + InboxIssueLiteSerializer, ) from .analytic import AnalyticViewSerializer diff --git a/apiserver/plane/app/serializers/base.py b/apiserver/plane/app/serializers/base.py index 89683ffe5..446fdb6d5 100644 --- a/apiserver/plane/app/serializers/base.py +++ b/apiserver/plane/app/serializers/base.py @@ -60,6 +60,7 @@ class DynamicBaseSerializer(BaseSerializer): CycleIssueSerializer, IssueFlatSerializer, IssueRelationSerializer, + InboxIssueLiteSerializer ) # Expansion mapper @@ -80,9 +81,10 @@ class DynamicBaseSerializer(BaseSerializer): "issue_cycle": CycleIssueSerializer, "parent": IssueSerializer, "issue_relation": IssueRelationSerializer, + "issue_inbox" : InboxIssueLiteSerializer, } - self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation"] else False) + self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle", "issue_relation", "issue_inbox"] else False) return self.fields @@ -103,6 +105,7 @@ class DynamicBaseSerializer(BaseSerializer): LabelSerializer, CycleIssueSerializer, IssueRelationSerializer, + InboxIssueLiteSerializer ) # Expansion mapper @@ -122,7 +125,8 @@ class DynamicBaseSerializer(BaseSerializer): "labels": LabelSerializer, "issue_cycle": CycleIssueSerializer, "parent": IssueSerializer, - "issue_relation": IssueRelationSerializer + "issue_relation": IssueRelationSerializer, + "issue_inbox" : InboxIssueLiteSerializer, } # Check if field in expansion then expand the field if expand in expansion: diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 0f8e68656..3bacdae4c 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -88,39 +88,24 @@ class InboxIssueViewSet(BaseViewSet): ] def get_queryset(self): - return self.filter_queryset( - super() - .get_queryset() - .filter( - Q(snoozed_till__gte=timezone.now()) - | Q(snoozed_till__isnull=True), - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - inbox_id=self.kwargs.get("inbox_id"), - ) - .select_related("issue", "workspace", "project") - ) - - def list(self, request, slug, project_id, inbox_id): - filters = issue_filters(request.query_params, "GET") - issues = ( + return ( Issue.objects.filter( - issue_inbox__inbox_id=inbox_id, - workspace__slug=slug, - project_id=project_id, + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + issue_inbox__inbox_id=self.kwargs.get("inbox_id") ) - .filter(**filters) .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels") - .order_by("issue_inbox__snoozed_till", "issue_inbox__status") - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") + .prefetch_related("labels", "assignees") + .prefetch_related( + Prefetch( + "issue_inbox", + queryset=InboxIssue.objects.only( + "status", "duplicate_to", "snoozed_till", "source" + ), ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") ) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() @@ -135,16 +120,20 @@ class InboxIssueViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .prefetch_related( - Prefetch( - "issue_inbox", - queryset=InboxIssue.objects.only( - "status", "duplicate_to", "snoozed_till", "source" - ), + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") ) - ) - issues_data = IssueStateInboxSerializer(issues, many=True).data + ).distinct() + + def list(self, request, slug, project_id, inbox_id): + filters = issue_filters(request.query_params, "GET") + issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status") + issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data return Response( issues_data, status=status.HTTP_200_OK, @@ -211,7 +200,8 @@ class InboxIssueViewSet(BaseViewSet): source=request.data.get("source", "in-app"), ) - serializer = IssueStateInboxSerializer(issue) + issue = (self.get_queryset().filter(pk=issue.id).first()) + serializer = IssueSerializer(issue ,expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, slug, project_id, inbox_id, issue_id): @@ -331,22 +321,20 @@ class InboxIssueViewSet(BaseViewSet): if state is not None: issue.state = state issue.save() - + issue = (self.get_queryset().filter(pk=issue_id).first()) + serializer = IssueSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST ) else: - return Response( - InboxIssueSerializer(inbox_issue).data, - status=status.HTTP_200_OK, - ) + issue = (self.get_queryset().filter(pk=issue_id).first()) + serializer = IssueSerializer(issue ,expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, inbox_id, issue_id): - issue = Issue.objects.get( - pk=issue_id, workspace__slug=slug, project_id=project_id - ) - serializer = IssueStateInboxSerializer(issue) + issue = self.get_queryset().filter(pk=issue_id).first() + serializer = IssueSerializer(issue, expand=self.expand,) return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, inbox_id, issue_id): diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 2895661f8..e411e0e39 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -68,7 +68,7 @@ from plane.bgtasks.project_invitation_task import project_invitation class ProjectViewSet(WebhookMixin, BaseViewSet): - serializer_class = ProjectSerializer + serializer_class = ProjectListSerializer model = Project webhook_event = "project" @@ -76,11 +76,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): ProjectBasePermission, ] - def get_serializer_class(self, *args, **kwargs): - if self.action in ["update", "partial_update"]: - return ProjectSerializer - return ProjectDetailSerializer - def get_queryset(self): return self.filter_queryset( super() diff --git a/packages/types/src/inbox/inbox-issue.d.ts b/packages/types/src/inbox/inbox-issue.d.ts new file mode 100644 index 000000000..c7d33f75b --- /dev/null +++ b/packages/types/src/inbox/inbox-issue.d.ts @@ -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 +>; // inbox_id -> issue_id -> TInboxIssueDetail + +export type TInboxIssueDetailIdMap = Record; // 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; diff --git a/packages/types/src/inbox/inbox.d.ts b/packages/types/src/inbox/inbox.d.ts new file mode 100644 index 000000000..1b4e23e0f --- /dev/null +++ b/packages/types/src/inbox/inbox.d.ts @@ -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; // inbox_id -> TInbox + +export type TInboxDetailIdMap = Record; // project_id -> inbox_id[] diff --git a/packages/types/src/inbox/root.d.ts b/packages/types/src/inbox/root.d.ts new file mode 100644 index 000000000..2f10c088d --- /dev/null +++ b/packages/types/src/inbox/root.d.ts @@ -0,0 +1,2 @@ +export * from "./inbox"; +export * from "./inbox-issue"; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 209aa6794..6e8ded942 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -13,7 +13,11 @@ export * from "./pages"; export * from "./ai"; export * from "./estimate"; export * from "./importer"; + +// FIXME: Remove this after development and the refactor/mobx-store-issue branch is stable export * from "./inbox"; +export * from "./inbox/root"; + export * from "./analytics"; export * from "./calendar"; export * from "./notifications"; diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index ae16eff89..b09aa9dc3 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -51,12 +51,14 @@ export const ProjectInboxHeader: FC = observer(() => { -
- setCreateIssueModal(false)} /> - -
+ {currentProjectDetails?.inbox_view && ( +
+ setCreateIssueModal(false)} /> + +
+ )} ); }); diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index c756b4410..6929da92f 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -32,7 +32,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const { issuesFilter: { issueFilters, updateFilters }, } = useIssues(EIssuesStoreType.PROJECT); - const { inboxesList, isInboxEnabled, getInboxId } = useInbox(); const { commandPalette: { toggleCreateIssueModal }, eventTracker: { setTrackElement }, @@ -43,6 +42,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const { currentProjectDetails } = useProject(); const { projectStates } = useProjectState(); const { projectLabels } = useLabel(); + const { getInboxesByProjectId, getInboxById } = useInbox(); const activeLayout = issueFilters?.displayFilters?.layout; @@ -89,7 +89,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => { [workspaceSlug, projectId, updateFilters] ); - const inboxDetails = projectId ? inboxesList?.[projectId]?.[0] : undefined; + const inboxesMap = currentProjectDetails?.inbox_view ? getInboxesByProjectId(currentProjectDetails.id) : undefined; + const inboxDetails = inboxesMap && inboxesMap.length > 0 ? getInboxById(inboxesMap[0]) : undefined; + const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL; const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); @@ -190,14 +192,15 @@ export const ProjectIssuesHeader: React.FC = observer(() => { handleDisplayPropertiesUpdate={handleDisplayProperties} /> - {projectId && isInboxEnabled && inboxDetails && ( - + + {currentProjectDetails?.inbox_view && inboxDetails && ( + diff --git a/web/components/inbox/actions-header.tsx b/web/components/inbox/actions-header.tsx deleted file mode 100644 index cab4be600..000000000 --- a/web/components/inbox/actions-header.tsx +++ /dev/null @@ -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 && ( - <> - setSelectDuplicateIssue(false)} - value={issue?.issue_inbox[0].duplicate_to} - onSubmit={(dupIssueId) => { - markInboxStatus({ - status: 2, - duplicate_to: dupIssueId, - }).finally(() => setSelectDuplicateIssue(false)); - }} - /> - setAcceptIssueModal(false)} - onSubmit={async () => { - await markInboxStatus({ - status: 1, - }).finally(() => setAcceptIssueModal(false)); - }} - /> - setDeclineIssueModal(false)} - onSubmit={async () => { - await markInboxStatus({ - status: -1, - }).finally(() => setDeclineIssueModal(false)); - }} - /> - setDeleteIssueModal(false)} /> - - )} -
-
-
- -

Inbox

-
- -
- {inboxIssueId && ( -
- {/*
- - -
- {currentIssueIndex + 1}/{issuesList?.length ?? 0} -
-
*/} -
- {isAllowed && (issueStatus === 0 || issueStatus === -2) && ( -
- - - - - - {({ close }) => ( -
- { - if (!val) return; - setDate(val); - }} - dateFormat="dd-MM-yyyy" - minDate={tomorrow} - inline - /> - -
- )} -
-
-
- )} - {isAllowed && issueStatus === -2 && ( -
- -
- )} - {isAllowed && (issueStatus === 0 || issueStatus === -2) && ( -
- -
- )} - {isAllowed && issueStatus === -2 && ( -
- -
- )} - {(isAllowed || currentUser?.id === issue?.created_by) && ( -
- -
- )} -
-
- )} -
- - ); -}); diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx new file mode 100644 index 000000000..25b444de0 --- /dev/null +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -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; + removeInboxIssue: () => Promise; +}; + +export const InboxIssueActionsHeader: FC = 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 && ( + <> + setSelectDuplicateIssue(false)} + value={issueStatus.duplicate_to} + onSubmit={(dupIssueId) => { + inboxIssueOperations + .updateInboxIssueStatus({ + status: 2, + duplicate_to: dupIssueId, + }) + .finally(() => setSelectDuplicateIssue(false)); + }} + /> + + setAcceptIssueModal(false)} + onSubmit={async () => { + await inboxIssueOperations + .updateInboxIssueStatus({ + status: 1, + }) + .finally(() => setAcceptIssueModal(false)); + }} + /> + + setDeclineIssueModal(false)} + onSubmit={async () => { + await inboxIssueOperations + .updateInboxIssueStatus({ + status: -1, + }) + .finally(() => setDeclineIssueModal(false)); + }} + /> + + setDeleteIssueModal(false)} + onSubmit={async () => { + await inboxIssueOperations.removeInboxIssue().finally(() => setDeclineIssueModal(false)); + }} + /> + + )} + + {inboxIssueId && ( +
+
+ + +
+ {currentIssueIndex + 1}/{inboxIssues?.length ?? 0} +
+
+ +
+ {isAllowed && (issueStatus.status === 0 || issueStatus.status === -2) && ( +
+ + + + + + {({ close }) => ( +
+ { + if (!val) return; + setDate(val); + }} + dateFormat="dd-MM-yyyy" + minDate={tomorrow} + inline + /> + +
+ )} +
+
+
+ )} + + {isAllowed && issueStatus.status === -2 && ( +
+ +
+ )} + + {isAllowed && (issueStatus.status === 0 || issueStatus.status === -2) && ( +
+ +
+ )} + + {isAllowed && issueStatus.status === -2 && ( +
+ +
+ )} + + {(isAllowed || currentUser?.id === issue?.created_by) && ( +
+ +
+ )} +
+
+ )} + + ); +}); diff --git a/web/components/inbox/inbox-issue-status.tsx b/web/components/inbox/inbox-issue-status.tsx new file mode 100644 index 000000000..301583b4b --- /dev/null +++ b/web/components/inbox/inbox-issue-status.tsx @@ -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) => { + 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 ( +
+ + {showDescription ? ( + inboxIssueStatusDetail.description( + workspaceSlug, + projectId, + inboxIssueDetail.duplicate_to ?? "", + new Date(inboxIssueDetail.snoozed_till ?? "") + ) + ) : ( + {inboxIssueStatusDetail.title} + )} +
+ ); +}; diff --git a/web/components/inbox/index.ts b/web/components/inbox/index.ts index ef1a9e92d..ae267f54c 100644 --- a/web/components/inbox/index.ts +++ b/web/components/inbox/index.ts @@ -1,8 +1,12 @@ export * from "./modals"; -export * from "./actions-header"; -export * from "./filters-dropdown"; -export * from "./filters-list"; -export * from "./issue-activity"; -export * from "./issue-card"; -export * from "./issues-list-sidebar"; -export * from "./main-content"; + +export * from "./inbox-issue-actions"; +export * from "./inbox-issue-status"; + +export * from "./sidebar/root"; + +export * from "./sidebar/filter/filter-selection"; +export * from "./sidebar/filter/applied-filters"; + +export * from "./sidebar/inbox-list"; +export * from "./sidebar/inbox-list-item"; diff --git a/web/components/inbox/issue-activity.tsx b/web/components/inbox/issue-activity.tsx deleted file mode 100644 index a3b3978e5..000000000 --- a/web/components/inbox/issue-activity.tsx +++ /dev/null @@ -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 = 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) => { - 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 ( -
- {/* FIXME: Replace this with the new issue activity component --issue-detail-- */} - {/*

Comments/Activity

- - */} -
- ); -}); diff --git a/web/components/inbox/issue-card.tsx b/web/components/inbox/issue-card.tsx deleted file mode 100644 index b6adeb2a1..000000000 --- a/web/components/inbox/issue-card.tsx +++ /dev/null @@ -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) => { - 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 ( - -
-
-

- {getProjectById(issue.project_id)?.identifier}-{issue.sequence_id} -

-
{issue.name}
-
-
- - - - -
- - {renderFormattedDate(issue.created_at ?? "")} -
-
-
-
s.value === issueStatus)?.textColor - }`} - > - {issueStatus === -2 ? ( - <> - - Pending - - ) : issueStatus === -1 ? ( - <> - - Declined - - ) : issueStatus === 0 ? ( - <> - - - {new Date(issue.issue_inbox[0].snoozed_till ?? "") < new Date() ? "Snoozed date passed" : "Snoozed"} - - - ) : issueStatus === 1 ? ( - <> - - Accepted - - ) : ( - <> - - Duplicate - - )} -
-
- - ); -}; diff --git a/web/components/inbox/issues-list-sidebar.tsx b/web/components/inbox/issues-list-sidebar.tsx deleted file mode 100644 index 18d2f3d5d..000000000 --- a/web/components/inbox/issues-list-sidebar.tsx +++ /dev/null @@ -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 ( -
- - {issuesList ? ( - issuesList.length > 0 ? ( -
- {issuesList.map((id) => ( - - ))} -
- ) : ( -
- {/* TODO: add filtersLength logic here */} - {/* {filtersLength > 0 && "No issues found for the selected filters. Try changing the filters."} */} -
- ) - ) : ( - - - - - - - )} -
- ); -}); diff --git a/web/components/inbox/main-content.tsx b/web/components/inbox/main-content.tsx deleted file mode 100644 index 97346d0a0..000000000 --- a/web/components/inbox/main-content.tsx +++ /dev/null @@ -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 = { - 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({ - 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) => { - 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 ( -
-
-
- - {issuesList && issuesList.length > 0 ? ( - - {issuesList?.length} issues found. Select an issue from the sidebar to view its details. - - ) : ( - No issues found - )} -
-
-
- ); - - const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - - return ( - <> - {issueDetails ? ( -
-
-
- {issueStatus === -2 ? ( - <> - -

This issue is still pending.

- - ) : issueStatus === -1 ? ( - <> - -

This issue has been declined.

- - ) : issueStatus === 0 ? ( - <> - - {new Date(issueDetails.issue_inbox[0].snoozed_till ?? "") < new Date() ? ( -

- This issue was snoozed till {renderFormattedDate(issueDetails.issue_inbox[0].snoozed_till ?? "")}. -

- ) : ( -

- This issue has been snoozed till{" "} - {renderFormattedDate(issueDetails.issue_inbox[0].snoozed_till ?? "")}. -

- )} - - ) : issueStatus === 1 ? ( - <> - -

This issue has been accepted.

- - ) : issueStatus === 2 ? ( - <> - -

- This issue has been marked as a duplicate of - - this issue - - . -

- - ) : null} -
-
- {currentIssueState && ( - - )} - -
- - {/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */} - {/*
- 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} - /> -
*/} - - {/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */} - {/* {workspaceSlug && projectId && ( - - )} */} - -
- -
- {/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */} - {/* */} -
-
- ) : ( - -
- - - - -
-
- - - - -
-
- )} - - ); -}); diff --git a/web/components/inbox/modals/accept-issue-modal.tsx b/web/components/inbox/modals/accept-issue-modal.tsx index bffeffed1..5ec63ea8a 100644 --- a/web/components/inbox/modals/accept-issue-modal.tsx +++ b/web/components/inbox/modals/accept-issue-modal.tsx @@ -1,16 +1,15 @@ import React, { useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; - // icons import { CheckCircle } from "lucide-react"; // ui import { Button } from "@plane/ui"; // types -import type { IInboxIssue } from "@plane/types"; +import type { TIssue } from "@plane/types"; import { useProject } from "hooks/store"; type Props = { - data: IInboxIssue; + data: TIssue; isOpen: boolean; onClose: () => void; onSubmit: () => Promise; @@ -28,7 +27,6 @@ export const AcceptIssueModal: React.FC = ({ isOpen, onClose, data, onSub const handleAccept = () => { setIsAccepting(true); - onSubmit().finally(() => setIsAccepting(false)); }; diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index 358a6f1ec..e152c1b03 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -58,7 +58,9 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; // store hooks - const { createIssue } = useInboxIssues(); + const { + issues: { createInboxIssue }, + } = useInboxIssues(); const { config: { envConfig }, eventTracker: { postHogEventTracker }, @@ -85,10 +87,10 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { const handleFormSubmit = async (formData: Partial) => { if (!workspaceSlug || !projectId || !inboxId) return; - await createIssue(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), formData) + await createInboxIssue(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), formData) .then((res) => { if (!createMore) { - router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`); + router.push(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.id}`); handleClose(); } else reset(defaultValues); postHogEventTracker( diff --git a/web/components/inbox/modals/decline-issue-modal.tsx b/web/components/inbox/modals/decline-issue-modal.tsx index ec4a06f2a..a69c8d0e1 100644 --- a/web/components/inbox/modals/decline-issue-modal.tsx +++ b/web/components/inbox/modals/decline-issue-modal.tsx @@ -1,16 +1,15 @@ import React, { useState } from "react"; import { Dialog, Transition } from "@headlessui/react"; - // icons import { AlertTriangle } from "lucide-react"; // ui import { Button } from "@plane/ui"; // types -import type { IInboxIssue } from "@plane/types"; +import type { TIssue } from "@plane/types"; import { useProject } from "hooks/store"; type Props = { - data: IInboxIssue; + data: TIssue; isOpen: boolean; onClose: () => void; onSubmit: () => Promise; @@ -28,7 +27,6 @@ export const DeclineIssueModal: React.FC = ({ isOpen, onClose, data, onSu const handleDecline = () => { setIsDeclining(true); - onSubmit().finally(() => setIsDeclining(false)); }; diff --git a/web/components/inbox/modals/delete-issue-modal.tsx b/web/components/inbox/modals/delete-issue-modal.tsx index 1dbf2541c..c06621c03 100644 --- a/web/components/inbox/modals/delete-issue-modal.tsx +++ b/web/components/inbox/modals/delete-issue-modal.tsx @@ -1,39 +1,27 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; // hooks -import { useApplication, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; +import { useProject } from "hooks/store"; // icons import { AlertTriangle } from "lucide-react"; // ui import { Button } from "@plane/ui"; // types -import type { IInboxIssue } from "@plane/types"; -import { useInboxIssues } from "hooks/store/use-inbox-issues"; +import type { TIssue } from "@plane/types"; type Props = { - data: IInboxIssue; + data: TIssue; isOpen: boolean; onClose: () => void; + onSubmit: () => Promise; }; -export const DeleteInboxIssueModal: React.FC = observer(({ isOpen, onClose, data }) => { +export const DeleteInboxIssueModal: React.FC = observer(({ isOpen, onClose, onSubmit, data }) => { // states const [isDeleting, setIsDeleting] = useState(false); - // router - const router = useRouter(); - const { workspaceSlug, projectId, inboxId } = router.query; - // store hooks - const { deleteIssue } = useInboxIssues(); - const { - eventTracker: { postHogEventTracker }, - } = useApplication(); - const { currentWorkspace } = useWorkspace(); - const { getProjectById } = useProject(); - const { setToastAlert } = useToast(); + const { getProjectById } = useProject(); const handleClose = () => { setIsDeleting(false); @@ -41,59 +29,13 @@ export const DeleteInboxIssueModal: React.FC = observer(({ isOpen, onClos }; const handleDelete = () => { - if (!workspaceSlug || !projectId || !inboxId) return; - setIsDeleting(true); - - deleteIssue(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), data.issue_inbox[0].id) - .then(() => { - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue deleted successfully.", - }); - postHogEventTracker( - "ISSUE_DELETED", - { - state: "SUCCESS", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - // remove inboxIssueId from the url - router.push({ - pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, - }); - - handleClose(); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be deleted. Please try again.", - }); - postHogEventTracker( - "ISSUE_DELETED", - { - state: "FAILED", - }, - { - isGrouping: true, - groupType: "Workspace_metrics", - groupId: currentWorkspace?.id!, - } - ); - }) - .finally(() => setIsDeleting(false)); + onSubmit().finally(() => setIsDeleting(false)); }; return ( - + = observer(({ isOpen, onClos

-