[WEB-406] chore: project inbox improvement (#4151)

* chore: inbox issue status pill improvement

* chore: loader inconsistancy resolved

* chore: accepted and decline inbox issue validation

* chore: removed clear all button in applied filters

* chore: inbox issue create label improvement

* chore: updated label filter

* chore: updated fetching activites and comments

* chore: inbox filters date

* chore: removed the print statement

* chore: inbox date filter updated

* chore: handled custom date filter in inbox issue query params

* chore: handled custom date filter in inbox issue single select

* chore: inbox custom date filter updated

* chore: inbox sidebar filter improvement

* chore: inbox sidebar filter improvement

* chore: duplicate issue detail

* chore: duplicate inbox issue improvement

* chore: lint issue resolved

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
guru_sainath 2024-04-10 13:52:57 +05:30 committed by GitHub
parent 3c2b2e3ed6
commit 1dac70ecbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 194 additions and 82 deletions

View File

@ -55,6 +55,9 @@ class InboxIssueSerializer(BaseSerializer):
class InboxIssueDetailSerializer(BaseSerializer):
issue = IssueDetailSerializer(read_only=True)
duplicate_issue_detail = IssueInboxSerializer(
read_only=True, source="duplicate_to"
)
class Meta:
model = InboxIssue
@ -63,6 +66,7 @@ class InboxIssueDetailSerializer(BaseSerializer):
"status",
"duplicate_to",
"snoozed_till",
"duplicate_issue_detail",
"source",
"issue",
]

View File

@ -168,9 +168,8 @@ class InboxIssueViewSet(BaseViewSet):
).distinct()
def list(self, request, slug, project_id):
workspace = Workspace.objects.filter(slug=slug).first()
inbox_id = Inbox.objects.filter(
workspace_id=workspace.id, project_id=project_id
workspace__slug=slug, project_id=project_id
).first()
filters = issue_filters(request.GET, "GET", "issue__")
inbox_issue = (
@ -264,9 +263,8 @@ class InboxIssueViewSet(BaseViewSet):
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
workspace = Workspace.objects.filter(slug=slug).first()
inbox_id = Inbox.objects.filter(
workspace_id=workspace.id, project_id=project_id
workspace__slug=slug, project_id=project_id
).first()
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
@ -279,9 +277,8 @@ class InboxIssueViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, issue_id):
workspace = Workspace.objects.filter(slug=slug).first()
inbox_id = Inbox.objects.filter(
workspace_id=workspace.id, project_id=project_id
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = InboxIssue.objects.get(
issue_id=issue_id,
@ -307,9 +304,12 @@ class InboxIssueViewSet(BaseViewSet):
# Get issue data
issue_data = request.data.pop("issue", False)
if bool(issue_data):
issue = self.get_queryset().filter(pk=inbox_issue.issue_id).first()
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
# Only allow guests and viewers to edit name and description
if project_member.role <= 10:
# viewers and guests since only viewers and guests
@ -406,9 +406,8 @@ class InboxIssueViewSet(BaseViewSet):
return Response(serializer, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, issue_id):
workspace = Workspace.objects.filter(slug=slug).first()
inbox_id = Inbox.objects.filter(
workspace_id=workspace.id, project_id=project_id
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = (
InboxIssue.objects.select_related("issue")
@ -445,9 +444,8 @@ class InboxIssueViewSet(BaseViewSet):
)
def destroy(self, request, slug, project_id, issue_id):
workspace = Workspace.objects.filter(slug=slug).first()
inbox_id = Inbox.objects.filter(
workspace_id=workspace.id, project_id=project_id
workspace__slug=slug, project_id=project_id
).first()
inbox_issue = InboxIssue.objects.get(
issue_id=issue_id,

View File

@ -52,9 +52,9 @@ def string_date_filter(
filter[f"{date_filter}__gte"] = now - timedelta(weeks=duration)
else:
if offset == "fromnow":
filter[f"{date_filter}__lte"] = now + timedelta(days=duration)
filter[f"{date_filter}__lte"] = now + timedelta(weeks=duration)
else:
filter[f"{date_filter}__lte"] = now - timedelta(days=duration)
filter[f"{date_filter}__lte"] = now - timedelta(weeks=duration)
def date_filter(filter, date_term, queries):

View File

@ -18,7 +18,7 @@ export type TInboxIssueFilter = {
} & {
status: TInboxIssueStatus[] | undefined;
priority: TIssuePriorities[] | undefined;
label: string[] | undefined;
labels: string[] | undefined;
};
// sorting filters
@ -50,21 +50,29 @@ export type TInboxIssueSortingOrderByQueryParam = {
};
export type TInboxIssuesQueryParams = {
[key in TInboxIssueFilter]: string;
[key in keyof TInboxIssueFilter]: string;
} & TInboxIssueSortingOrderByQueryParam & {
per_page: number;
cursor: string;
};
// inbox issue types
export type TInboxDuplicateIssueDetails = {
id: string;
sequence_id: string;
name: string;
};
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | null;
snoozed_till: Date | undefined;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export type TInboxIssuePaginationInfo = TPaginationInfo & {

View File

@ -178,7 +178,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
{getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
</h3>
)}
<InboxIssueStatus inboxIssue={inboxIssue} />
<InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
<div className="flex items-center justify-end w-full">
<IssueUpdateStatus isSubmitting={isSubmitting} />
</div>

View File

@ -1,13 +1,16 @@
import React from "react";
import { observer } from "mobx-react";
import { CalendarCheck2, Signal, Tag } from "lucide-react";
import { TIssue } from "@plane/types";
import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
import { useRouter } from "next/router";
import { CalendarCheck2, CopyPlus, Signal, Tag } from "lucide-react";
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
import { ControlLink, DoubleCircleIcon, Tooltip, UserGroupIcon } from "@plane/ui";
// components
import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns";
import { IssueLabel, TIssueOperations } from "@/components/issues";
// helper
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks
import { useProject } from "@/hooks/store";
type Props = {
workspaceSlug: string;
@ -15,14 +18,18 @@ type Props = {
issue: Partial<TIssue>;
issueOperations: TIssueOperations;
is_editable: boolean;
duplicateIssueDetails: TInboxDuplicateIssueDetails | undefined;
};
export const InboxIssueProperties: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, issue, issueOperations, is_editable } = props;
const { workspaceSlug, projectId, issue, issueOperations, is_editable, duplicateIssueDetails } = props;
const router = useRouter();
// store hooks
const { currentProjectDetails } = useProject();
const minDate = issue.start_date ? getDate(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
if (!issue || !issue?.id) return <></>;
return (
<div className="flex h-min w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
@ -149,6 +156,29 @@ export const InboxIssueProperties: React.FC<Props> = observer((props) => {
)}
</div>
</div>
{/* duplicate to*/}
{duplicateIssueDetails && (
<div className="flex min-h-8 gap-2">
<div className="flex w-2/5 flex-shrink-0 gap-1 pt-2 text-sm text-custom-text-300">
<CopyPlus className="h-4 w-4 flex-shrink-0" />
<span>Duplicate of</span>
</div>
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${duplicateIssueDetails?.id}`}
onClick={() => {
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${duplicateIssueDetails?.id}`);
}}
>
<Tooltip tooltipContent={`${duplicateIssueDetails?.name}`}>
<span className="flex items-center gap-1 cursor-pointer text-xs rounded px-1.5 py-1 pb-0.5 bg-custom-background-80 text-custom-text-200">
{`${currentProjectDetails?.identifier}-${duplicateIssueDetails?.sequence_id}`}
</span>
</Tooltip>
</ControlLink>
</div>
)}
</div>
</div>
</div>

View File

@ -164,6 +164,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
issue={issue}
issueOperations={issueOperations}
is_editable={is_editable}
duplicateIssueDetails={inboxIssue?.duplicate_issue_detail}
/>
<div className="pb-12">

View File

@ -35,6 +35,9 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
if (!inboxIssue) return <></>;
const isIssueAcceptedOrDeclined = [-1, 1].includes(inboxIssue.status);
return (
<>
<div className="w-full h-full overflow-hidden relative flex flex-col">
@ -51,7 +54,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
workspaceSlug={workspaceSlug}
projectId={projectId}
inboxIssue={inboxIssue}
is_editable={is_editable}
is_editable={is_editable && !isIssueAcceptedOrDeclined}
isSubmitting={isSubmitting}
setIsSubmitting={setIsSubmitting}
/>

View File

@ -2,10 +2,10 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { X } from "lucide-react";
import { TInboxIssueFilterDateKeys } from "@plane/types";
// constants
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
// constants
import { PAST_DURATION_FILTER_OPTIONS } from "@/helpers/inbox.helper";
// hooks
import { useProjectInbox } from "@/hooks/store";
@ -21,7 +21,7 @@ export const InboxIssueAppliedFiltersDate: FC<InboxIssueAppliedFiltersDate> = ob
// derived values
const filteredValues = inboxFilters?.[filterKey] || [];
const currentOptionDetail = (date: string) => {
const currentDate = DATE_BEFORE_FILTER_OPTIONS.find((d) => d.value === date);
const currentDate = PAST_DURATION_FILTER_OPTIONS.find((d) => d.value === date);
if (currentDate) return currentDate;
const dateSplit = date.split(";");
return {

View File

@ -13,13 +13,13 @@ export const InboxIssueAppliedFiltersLabel: FC = observer(() => {
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const { getLabelById } = useLabel();
// derived values
const filteredValues = inboxFilters?.label || [];
const filteredValues = inboxFilters?.labels || [];
const currentOptionDetail = (labelId: string) => getLabelById(labelId) || undefined;
const handleFilterValue = (value: string): string[] =>
filteredValues?.includes(value) ? filteredValues.filter((v) => v !== value) : [...filteredValues, value];
const clearFilter = () => handleInboxIssueFilters("label", undefined);
const clearFilter = () => handleInboxIssueFilters("labels", undefined);
if (filteredValues.length === 0) return <></>;
return (
@ -36,7 +36,7 @@ export const InboxIssueAppliedFiltersLabel: FC = observer(() => {
<div className="text-xs truncate">{optionDetail?.name}</div>
<div
className="w-3 h-3 flex-shrink-0 relative flex justify-center items-center overflow-hidden cursor-pointer text-custom-text-300 hover:text-custom-text-200 transition-all"
onClick={() => handleInboxIssueFilters("label", handleFilterValue(value))}
onClick={() => handleInboxIssueFilters("labels", handleFilterValue(value))}
>
<X className={`w-3 h-3`} />
</div>

View File

@ -28,9 +28,9 @@ export const InboxIssueAppliedFilters: FC = observer(() => {
{/* label */}
<InboxIssueAppliedFiltersLabel />
{/* created_at */}
<InboxIssueAppliedFiltersDate filterKey="created_at" label="Created At" />
<InboxIssueAppliedFiltersDate filterKey="created_at" label="Created date" />
{/* updated_at */}
<InboxIssueAppliedFiltersDate filterKey="updated_at" label="Updated At" />
<InboxIssueAppliedFiltersDate filterKey="updated_at" label="Updated date" />
</div>
);
});

View File

@ -9,7 +9,7 @@ import { useProjectInbox } from "@/hooks/store";
export const InboxIssueAppliedFiltersStatus: FC = observer(() => {
// hooks
const { currentTab, inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
// derived values
const filteredValues = inboxFilters?.status || [];
const currentOptionDetail = (status: TInboxIssueStatus) => INBOX_STATUS.find((s) => s.status === status) || undefined;
@ -17,8 +17,6 @@ export const InboxIssueAppliedFiltersStatus: FC = observer(() => {
const handleFilterValue = (value: TInboxIssueStatus): TInboxIssueStatus[] =>
filteredValues?.includes(value) ? filteredValues.filter((v) => v !== value) : [...filteredValues, value];
const clearFilter = () => handleInboxIssueFilters("status", undefined);
if (filteredValues.length === 0) return <></>;
return (
<div className="relative flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1">
@ -32,7 +30,7 @@ export const InboxIssueAppliedFiltersStatus: FC = observer(() => {
<optionDetail.icon className={`w-3 h-3 ${optionDetail?.textColor(false)}`} />
</div>
<div className="text-xs truncate">{optionDetail?.title}</div>
{currentTab === "closed" && handleFilterValue(optionDetail?.status).length >= 1 && (
{handleFilterValue(optionDetail?.status).length >= 1 && (
<div
className="w-3 h-3 flex-shrink-0 relative flex justify-center items-center overflow-hidden cursor-pointer text-custom-text-300 hover:text-custom-text-200 transition-all"
onClick={() => handleInboxIssueFilters("status", handleFilterValue(optionDetail?.status))}
@ -43,15 +41,6 @@ export const InboxIssueAppliedFiltersStatus: FC = observer(() => {
</div>
);
})}
{currentTab === "closed" && filteredValues.length > 1 && (
<div
className="w-3 h-3 flex-shrink-0 relative flex justify-center items-center overflow-hidden cursor-pointer text-custom-text-300 hover:text-custom-text-200 transition-all"
onClick={clearFilter}
>
<X className={`w-3 h-3`} />
</div>
)}
</div>
);
});

View File

@ -1,6 +1,5 @@
import { FC, useState } from "react";
import concat from "lodash/concat";
import pull from "lodash/pull";
import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import { TInboxIssueFilterDateKeys } from "@plane/types";
@ -8,7 +7,7 @@ import { TInboxIssueFilterDateKeys } from "@plane/types";
import { DateFilterModal } from "@/components/core";
import { FilterHeader, FilterOption } from "@/components/issues";
// constants
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
import { PAST_DURATION_FILTER_OPTIONS } from "@/helpers/inbox.helper";
// hooks
import { useProjectInbox } from "@/hooks/store";
@ -33,16 +32,15 @@ export const FilterDate: FC<Props> = observer((props) => {
// derived values
const filterValue: string[] = inboxFilters?.[filterKey] || [];
const appliedFiltersCount = filterValue?.length ?? 0;
const filteredOptions = DATE_BEFORE_FILTER_OPTIONS.filter((d) =>
const filteredOptions = PAST_DURATION_FILTER_OPTIONS.filter((d) =>
d.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleFilterValue = (value: string): string[] =>
filterValue?.includes(value) ? pull(filterValue, value) : uniq(concat(filterValue, value));
const handleFilterValue = (value: string): string[] => (filterValue?.includes(value) ? [] : uniq(concat(value)));
const handleCustomFilterValue = (value: string[]): string[] => {
const finalOptions: string[] = [...filterValue];
value.forEach((v) => (finalOptions?.includes(v) ? pull(finalOptions, v) : finalOptions.push(v)));
value.forEach((v) => (finalOptions?.includes(v) ? [] : finalOptions.push(v)));
return uniq(finalOptions);
};
@ -53,10 +51,13 @@ export const FilterDate: FC<Props> = observer((props) => {
const handleCustomDate = () => {
if (isCustomDateSelected()) {
const updateAppliedFilters = filterValue?.filter((f) => isDate(f.split(";")[0])) || [];
handleInboxIssueFilters(filterKey, handleCustomFilterValue(updateAppliedFilters));
} else setIsDateFilterModalOpen(true);
const updateAppliedFilters = filterValue?.filter((f) => !isDate(f.split(";")[0])) || [];
handleInboxIssueFilters(filterKey, updateAppliedFilters);
} else {
setIsDateFilterModalOpen(true);
}
};
return (
<>
{isDateFilterModalOpen && (
@ -82,10 +83,15 @@ export const FilterDate: FC<Props> = observer((props) => {
isChecked={filterValue?.includes(option.value) ? true : false}
onClick={() => handleInboxIssueFilters(filterKey, handleFilterValue(option.value))}
title={option.name}
multiple
multiple={false}
/>
))}
<FilterOption isChecked={isCustomDateSelected()} onClick={handleCustomDate} title="Custom" multiple />
<FilterOption
isChecked={isCustomDateSelected()}
onClick={handleCustomDate}
title="Custom"
multiple={false}
/>
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>

View File

@ -75,11 +75,11 @@ export const InboxIssueFilterSelection: FC = observer(() => {
</div>
{/* Created at */}
<div className="py-2">
<FilterDate filterKey="created_at" label="Created at" searchQuery={filtersSearchQuery} />
<FilterDate filterKey="created_at" label="Created date" searchQuery={filtersSearchQuery} />
</div>
{/* Updated at */}
<div className="py-2">
<FilterDate filterKey="updated_at" label="Updated at" searchQuery={filtersSearchQuery} />
<FilterDate filterKey="updated_at" label="Last updated date" searchQuery={filtersSearchQuery} />
</div>
</div>
</div>

View File

@ -24,7 +24,7 @@ export const FilterLabels: FC<Props> = observer((props) => {
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const filterValue = inboxFilters?.label || [];
const filterValue = inboxFilters?.labels || [];
const appliedFiltersCount = filterValue?.length ?? 0;
@ -56,7 +56,7 @@ export const FilterLabels: FC<Props> = observer((props) => {
<FilterOption
key={label?.id}
isChecked={filterValue?.includes(label?.id) ? true : false}
onClick={() => handleInboxIssueFilters("label", handleFilterValue(label.id))}
onClick={() => handleInboxIssueFilters("labels", handleFilterValue(label.id))}
icon={<LabelIcons color={label.color} />}
title={label.name}
/>

View File

@ -24,8 +24,8 @@ export const FilterStatus: FC<Props> = observer((props) => {
const appliedFiltersCount = filterValue?.length ?? 0;
const filteredOptions = INBOX_STATUS.filter(
(s) =>
((currentTab === "open" && [-2].includes(s.status)) ||
(currentTab === "closed" && [-1, 0, 1, 2].includes(s.status))) &&
((currentTab === "open" && [-2, 0].includes(s.status)) ||
(currentTab === "closed" && [-1, 1, 2].includes(s.status))) &&
s.key.includes(searchQuery.toLowerCase())
);
@ -33,10 +33,8 @@ export const FilterStatus: FC<Props> = observer((props) => {
filterValue?.includes(value) ? filterValue.filter((v) => v !== value) : [...filterValue, value];
const handleStatusFilterSelect = (status: TInboxIssueStatus) => {
if (currentTab === "closed") {
const selectedStatus = handleFilterValue(status);
if (selectedStatus.length >= 1) handleInboxIssueFilters("status", selectedStatus);
}
};
return (

View File

@ -32,14 +32,14 @@ export const InboxIssueStatus: React.FC<Props> = observer((props) => {
)}
>
<div className={`flex items-center gap-1`}>
<inboxIssueStatusDetail.icon size={iconSize} />
<div className="font-medium text-xs">
<inboxIssueStatusDetail.icon size={iconSize} className="flex-shrink-0" />
<div className="font-medium text-xs whitespace-nowrap">
{inboxIssue?.status === 0 && inboxIssue?.snoozed_till
? inboxIssueStatusDetail.description(inboxIssue?.snoozed_till)
: inboxIssueStatusDetail.title}
</div>
</div>
{showDescription && <div className="text-sm">{description}</div>}
{showDescription && <div className="text-sm whitespace-nowrap">{description}</div>}
</div>
);
});

View File

@ -28,7 +28,7 @@ export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => {
() => {
inboxAccessible && workspaceSlug && projectId && fetchInboxIssues(workspaceSlug.toString(), projectId.toString());
},
{ revalidateOnFocus: false }
{ revalidateOnFocus: false, revalidateIfStale: false }
);
// loader

View File

@ -99,7 +99,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
<InboxIssueAppliedFilters />
{isLoading && !inboxIssuePaginationInfo?.next_page_results ? (
{isLoading === "filter-loading" && !inboxIssuePaginationInfo?.next_page_results ? (
<InboxSidebarLoader />
) : (
<div

View File

@ -69,6 +69,7 @@ export const LabelCreate: FC<ILabelCreate> = (props) => {
const labelResponse = await labelOperations.createLabel(workspaceSlug, projectId, formData);
const currentLabels = [...(values || []), labelResponse.id];
await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels });
handleIsCreateToggle();
reset(defaultValues);
} catch (error) {
setToast({

View File

@ -0,0 +1,56 @@
import { subDays } from "date-fns";
import { renderFormattedPayloadDate } from "./date-time.helper";
export enum EPastDurationFilters {
TODAY = "today",
YESTERDAY = "yesterday",
LAST_7_DAYS = "last_7_days",
LAST_30_DAYS = "last_30_days",
}
export const getCustomDates = (duration: EPastDurationFilters): string => {
const today = new Date();
let firstDay, lastDay;
switch (duration) {
case EPastDurationFilters.TODAY:
firstDay = renderFormattedPayloadDate(today);
lastDay = renderFormattedPayloadDate(today);
return `${firstDay};after,${lastDay};before`;
case EPastDurationFilters.YESTERDAY:
const yesterday = subDays(today, 1);
firstDay = renderFormattedPayloadDate(yesterday);
lastDay = renderFormattedPayloadDate(yesterday);
return `${firstDay};after,${lastDay};before`;
case EPastDurationFilters.LAST_7_DAYS:
firstDay = renderFormattedPayloadDate(subDays(today, 7));
lastDay = renderFormattedPayloadDate(today);
return `${firstDay};after,${lastDay};before`;
case EPastDurationFilters.LAST_30_DAYS:
firstDay = renderFormattedPayloadDate(subDays(today, 30));
lastDay = renderFormattedPayloadDate(today);
return `${firstDay};after,${lastDay};before`;
}
};
export const PAST_DURATION_FILTER_OPTIONS: {
name: string;
value: string;
}[] = [
{
name: "Today",
value: EPastDurationFilters.TODAY,
},
{
name: "Yesterday",
value: EPastDurationFilters.YESTERDAY,
},
{
name: "Last 7 days",
value: EPastDurationFilters.LAST_7_DAYS,
},
{
name: "Last 30 days",
value: EPastDurationFilters.LAST_30_DAYS,
},
];

View File

@ -2,7 +2,7 @@ import set from "lodash/set";
import { makeObservable, observable, runInAction, action } from "mobx";
// services
// types
import { TIssue, TInboxIssue, TInboxIssueStatus } from "@plane/types";
import { TIssue, TInboxIssue, TInboxIssueStatus, TInboxDuplicateIssueDetails } from "@plane/types";
import { InboxIssueService } from "@/services/inbox";
export interface IInboxIssueStore {
@ -13,6 +13,7 @@ export interface IInboxIssueStore {
snoozed_till: Date | undefined;
duplicate_to: string | undefined;
created_by: string | undefined;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
// actions
updateInboxIssueStatus: (status: TInboxIssueStatus) => Promise<void>; // accept, decline
updateInboxIssueDuplicateTo: (issueId: string) => Promise<void>; // connecting the inbox issue to the project existing issue
@ -29,6 +30,7 @@ export class InboxIssueStore implements IInboxIssueStore {
snoozed_till: Date | undefined;
duplicate_to: string | undefined;
created_by: string | undefined;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined = undefined;
workspaceSlug: string;
projectId: string;
// services
@ -41,6 +43,7 @@ export class InboxIssueStore implements IInboxIssueStore {
this.snoozed_till = data?.snoozed_till ? new Date(data.snoozed_till) : undefined;
this.duplicate_to = data?.duplicate_to || undefined;
this.created_by = data?.created_by || undefined;
this.duplicate_issue_detail = data?.duplicate_issue_detail || undefined;
this.workspaceSlug = workspaceSlug;
this.projectId = projectId;
// services
@ -52,6 +55,7 @@ export class InboxIssueStore implements IInboxIssueStore {
issue: observable,
snoozed_till: observable,
duplicate_to: observable,
duplicate_issue_detail: observable,
created_by: observable,
// actions
updateInboxIssueStatus: action,

View File

@ -12,6 +12,8 @@ import {
TInboxIssuePaginationInfo,
TInboxIssueSortingOrderByQueryParam,
} from "@plane/types";
// helpers
import { EPastDurationFilters, getCustomDates } from "@/helpers/inbox.helper";
// services
import { InboxIssueService } from "@/services/inbox";
// root store
@ -110,7 +112,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
get inboxIssuesArray() {
return Object.values(this.inboxIssues || {}).filter((inbox) =>
(this.currentTab === "open" ? [-2] : [-1, 0, 1, 2]).includes(inbox.status)
(this.currentTab === "open" ? [-2, 0] : [-1, 1, 2]).includes(inbox.status)
);
}
@ -126,8 +128,16 @@ export class ProjectInboxStore implements IProjectInboxStore {
!isEmpty(inboxFilters) &&
Object.keys(inboxFilters).forEach((key) => {
const filterKey = key as keyof TInboxIssueFilter;
if (inboxFilters[filterKey] && inboxFilters[filterKey]?.length)
filters[filterKey] = inboxFilters[filterKey]?.join(",");
if (inboxFilters[filterKey] && inboxFilters[filterKey]?.length) {
if (["created_at", "updated_at"].includes(filterKey) && (inboxFilters[filterKey] || [])?.length > 0) {
const appliedDateFilters: string[] = [];
inboxFilters[filterKey]?.forEach((value) => {
const dateValue = value as EPastDurationFilters;
appliedDateFilters.push(getCustomDates(dateValue));
});
filters[filterKey] = appliedDateFilters?.join(",");
} else filters[filterKey] = inboxFilters[filterKey]?.join(",");
}
});
const sorting: TInboxIssueSortingOrderByQueryParam = {
@ -167,7 +177,9 @@ export class ProjectInboxStore implements IProjectInboxStore {
set(this, "inboxFilters", undefined);
set(this, ["inboxSorting", "order_by"], "issue__created_at");
set(this, ["inboxSorting", "sort_by"], "desc");
if (tab === "closed") set(this, ["inboxFilters", "status"], [-1, 0, 1, 2]);
set(this, ["inboxIssues"], {});
set(this, ["inboxIssuePaginationInfo"], undefined);
if (tab === "closed") set(this, ["inboxFilters", "status"], [-1, 1, 2]);
else set(this, ["inboxFilters", "status"], [-2]);
const { workspaceSlug, projectId } = this.store.app.router;
if (workspaceSlug && projectId) this.fetchInboxIssues(workspaceSlug, projectId, "filter-loading");
@ -175,12 +187,16 @@ export class ProjectInboxStore implements IProjectInboxStore {
handleInboxIssueFilters = <T extends keyof TInboxIssueFilter>(key: T, value: TInboxIssueFilter[T]) => {
set(this.inboxFilters, key, value);
set(this, ["inboxIssues"], {});
set(this, ["inboxIssuePaginationInfo"], undefined);
const { workspaceSlug, projectId } = this.store.app.router;
if (workspaceSlug && projectId) this.fetchInboxIssues(workspaceSlug, projectId, "filter-loading");
};
handleInboxIssueSorting = <T extends keyof TInboxIssueSorting>(key: T, value: TInboxIssueSorting[T]) => {
set(this.inboxSorting, key, value);
set(this, ["inboxIssues"], {});
set(this, ["inboxIssuePaginationInfo"], undefined);
const { workspaceSlug, projectId } = this.store.app.router;
if (workspaceSlug && projectId) this.fetchInboxIssues(workspaceSlug, projectId, "filter-loading");
};
@ -193,9 +209,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
fetchInboxIssues = async (workspaceSlug: string, projectId: string, loadingType: TLoader = undefined) => {
try {
if (loadingType) this.isLoading = loadingType;
else this.isLoading = "init-loading";
this.inboxIssuePaginationInfo = undefined;
this.inboxIssues = {};
else if (Object.keys(this.inboxIssues).length === 0) this.isLoading = "init-loading";
const queryParams = this.inboxIssueQueryParams(
this.inboxFilters,
@ -284,9 +298,9 @@ export class ProjectInboxStore implements IProjectInboxStore {
// fetching reactions
await this.store.issue.issueDetail.fetchReactions(workspaceSlug, projectId, issueId);
// fetching activity
await this.store.issue.issueDetail.fetchReactions(workspaceSlug, projectId, issueId);
await this.store.issue.issueDetail.fetchActivities(workspaceSlug, projectId, issueId);
// fetching comments
await this.store.issue.issueDetail.fetchReactions(workspaceSlug, projectId, issueId);
await this.store.issue.issueDetail.fetchComments(workspaceSlug, projectId, issueId);
runInAction(() => {
set(this.inboxIssues, issueId, new InboxIssueStore(workspaceSlug, projectId, inboxIssue));
});