[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): class InboxIssueDetailSerializer(BaseSerializer):
issue = IssueDetailSerializer(read_only=True) issue = IssueDetailSerializer(read_only=True)
duplicate_issue_detail = IssueInboxSerializer(
read_only=True, source="duplicate_to"
)
class Meta: class Meta:
model = InboxIssue model = InboxIssue
@ -63,6 +66,7 @@ class InboxIssueDetailSerializer(BaseSerializer):
"status", "status",
"duplicate_to", "duplicate_to",
"snoozed_till", "snoozed_till",
"duplicate_issue_detail",
"source", "source",
"issue", "issue",
] ]

View File

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

View File

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

View File

@ -18,7 +18,7 @@ export type TInboxIssueFilter = {
} & { } & {
status: TInboxIssueStatus[] | undefined; status: TInboxIssueStatus[] | undefined;
priority: TIssuePriorities[] | undefined; priority: TIssuePriorities[] | undefined;
label: string[] | undefined; labels: string[] | undefined;
}; };
// sorting filters // sorting filters
@ -50,21 +50,29 @@ export type TInboxIssueSortingOrderByQueryParam = {
}; };
export type TInboxIssuesQueryParams = { export type TInboxIssuesQueryParams = {
[key in TInboxIssueFilter]: string; [key in keyof TInboxIssueFilter]: string;
} & TInboxIssueSortingOrderByQueryParam & { } & TInboxIssueSortingOrderByQueryParam & {
per_page: number; per_page: number;
cursor: string; cursor: string;
}; };
// inbox issue types // inbox issue types
export type TInboxDuplicateIssueDetails = {
id: string;
sequence_id: string;
name: string;
};
export type TInboxIssue = { export type TInboxIssue = {
id: string; id: string;
status: TInboxIssueStatus; status: TInboxIssueStatus;
snoozed_till: Date | null; snoozed_till: Date | undefined;
duplicate_to: string | null; duplicate_to: string | undefined;
source: string; source: string;
issue: TIssue; issue: TIssue;
created_by: string; created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
}; };
export type TInboxIssuePaginationInfo = TPaginationInfo & { 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} {getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
</h3> </h3>
)} )}
<InboxIssueStatus inboxIssue={inboxIssue} /> <InboxIssueStatus inboxIssue={inboxIssue} iconSize={12} />
<div className="flex items-center justify-end w-full"> <div className="flex items-center justify-end w-full">
<IssueUpdateStatus isSubmitting={isSubmitting} /> <IssueUpdateStatus isSubmitting={isSubmitting} />
</div> </div>

View File

@ -1,13 +1,16 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CalendarCheck2, Signal, Tag } from "lucide-react"; import { useRouter } from "next/router";
import { TIssue } from "@plane/types"; import { CalendarCheck2, CopyPlus, Signal, Tag } from "lucide-react";
import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
import { ControlLink, DoubleCircleIcon, Tooltip, UserGroupIcon } from "@plane/ui";
// components // components
import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns"; import { DateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "@/components/dropdowns";
import { IssueLabel, TIssueOperations } from "@/components/issues"; import { IssueLabel, TIssueOperations } from "@/components/issues";
// helper // helper
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// hooks
import { useProject } from "@/hooks/store";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -15,14 +18,18 @@ type Props = {
issue: Partial<TIssue>; issue: Partial<TIssue>;
issueOperations: TIssueOperations; issueOperations: TIssueOperations;
is_editable: boolean; is_editable: boolean;
duplicateIssueDetails: TInboxDuplicateIssueDetails | undefined;
}; };
export const InboxIssueProperties: React.FC<Props> = observer((props) => { 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; const minDate = issue.start_date ? getDate(issue.start_date) : null;
minDate?.setDate(minDate.getDate()); minDate?.setDate(minDate.getDate());
if (!issue || !issue?.id) return <></>; if (!issue || !issue?.id) return <></>;
return ( return (
<div className="flex h-min w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden"> <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>
</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> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -13,13 +13,13 @@ export const InboxIssueAppliedFiltersLabel: FC = observer(() => {
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox(); const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
const { getLabelById } = useLabel(); const { getLabelById } = useLabel();
// derived values // derived values
const filteredValues = inboxFilters?.label || []; const filteredValues = inboxFilters?.labels || [];
const currentOptionDetail = (labelId: string) => getLabelById(labelId) || undefined; const currentOptionDetail = (labelId: string) => getLabelById(labelId) || undefined;
const handleFilterValue = (value: string): string[] => const handleFilterValue = (value: string): string[] =>
filteredValues?.includes(value) ? filteredValues.filter((v) => v !== value) : [...filteredValues, value]; 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 <></>; if (filteredValues.length === 0) return <></>;
return ( return (
@ -36,7 +36,7 @@ export const InboxIssueAppliedFiltersLabel: FC = observer(() => {
<div className="text-xs truncate">{optionDetail?.name}</div> <div className="text-xs truncate">{optionDetail?.name}</div>
<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" 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`} /> <X className={`w-3 h-3`} />
</div> </div>

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import concat from "lodash/concat"; import concat from "lodash/concat";
import pull from "lodash/pull";
import uniq from "lodash/uniq"; import uniq from "lodash/uniq";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TInboxIssueFilterDateKeys } from "@plane/types"; import { TInboxIssueFilterDateKeys } from "@plane/types";
@ -8,7 +7,7 @@ import { TInboxIssueFilterDateKeys } from "@plane/types";
import { DateFilterModal } from "@/components/core"; import { DateFilterModal } from "@/components/core";
import { FilterHeader, FilterOption } from "@/components/issues"; import { FilterHeader, FilterOption } from "@/components/issues";
// constants // constants
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters"; import { PAST_DURATION_FILTER_OPTIONS } from "@/helpers/inbox.helper";
// hooks // hooks
import { useProjectInbox } from "@/hooks/store"; import { useProjectInbox } from "@/hooks/store";
@ -33,16 +32,15 @@ export const FilterDate: FC<Props> = observer((props) => {
// derived values // derived values
const filterValue: string[] = inboxFilters?.[filterKey] || []; const filterValue: string[] = inboxFilters?.[filterKey] || [];
const appliedFiltersCount = filterValue?.length ?? 0; 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()) d.name.toLowerCase().includes(searchQuery.toLowerCase())
); );
const handleFilterValue = (value: string): string[] => const handleFilterValue = (value: string): string[] => (filterValue?.includes(value) ? [] : uniq(concat(value)));
filterValue?.includes(value) ? pull(filterValue, value) : uniq(concat(filterValue, value));
const handleCustomFilterValue = (value: string[]): string[] => { const handleCustomFilterValue = (value: string[]): string[] => {
const finalOptions: string[] = [...filterValue]; 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); return uniq(finalOptions);
}; };
@ -53,10 +51,13 @@ export const FilterDate: FC<Props> = observer((props) => {
const handleCustomDate = () => { const handleCustomDate = () => {
if (isCustomDateSelected()) { if (isCustomDateSelected()) {
const updateAppliedFilters = filterValue?.filter((f) => isDate(f.split(";")[0])) || []; const updateAppliedFilters = filterValue?.filter((f) => !isDate(f.split(";")[0])) || [];
handleInboxIssueFilters(filterKey, handleCustomFilterValue(updateAppliedFilters)); handleInboxIssueFilters(filterKey, updateAppliedFilters);
} else setIsDateFilterModalOpen(true); } else {
setIsDateFilterModalOpen(true);
}
}; };
return ( return (
<> <>
{isDateFilterModalOpen && ( {isDateFilterModalOpen && (
@ -82,10 +83,15 @@ export const FilterDate: FC<Props> = observer((props) => {
isChecked={filterValue?.includes(option.value) ? true : false} isChecked={filterValue?.includes(option.value) ? true : false}
onClick={() => handleInboxIssueFilters(filterKey, handleFilterValue(option.value))} onClick={() => handleInboxIssueFilters(filterKey, handleFilterValue(option.value))}
title={option.name} 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> <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> </div>
{/* Created at */} {/* Created at */}
<div className="py-2"> <div className="py-2">
<FilterDate filterKey="created_at" label="Created at" searchQuery={filtersSearchQuery} /> <FilterDate filterKey="created_at" label="Created date" searchQuery={filtersSearchQuery} />
</div> </div>
{/* Updated at */} {/* Updated at */}
<div className="py-2"> <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> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -69,6 +69,7 @@ export const LabelCreate: FC<ILabelCreate> = (props) => {
const labelResponse = await labelOperations.createLabel(workspaceSlug, projectId, formData); const labelResponse = await labelOperations.createLabel(workspaceSlug, projectId, formData);
const currentLabels = [...(values || []), labelResponse.id]; const currentLabels = [...(values || []), labelResponse.id];
await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels }); await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels });
handleIsCreateToggle();
reset(defaultValues); reset(defaultValues);
} catch (error) { } catch (error) {
setToast({ 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"; import { makeObservable, observable, runInAction, action } from "mobx";
// services // services
// types // types
import { TIssue, TInboxIssue, TInboxIssueStatus } from "@plane/types"; import { TIssue, TInboxIssue, TInboxIssueStatus, TInboxDuplicateIssueDetails } from "@plane/types";
import { InboxIssueService } from "@/services/inbox"; import { InboxIssueService } from "@/services/inbox";
export interface IInboxIssueStore { export interface IInboxIssueStore {
@ -13,6 +13,7 @@ export interface IInboxIssueStore {
snoozed_till: Date | undefined; snoozed_till: Date | undefined;
duplicate_to: string | undefined; duplicate_to: string | undefined;
created_by: string | undefined; created_by: string | undefined;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
// actions // actions
updateInboxIssueStatus: (status: TInboxIssueStatus) => Promise<void>; // accept, decline updateInboxIssueStatus: (status: TInboxIssueStatus) => Promise<void>; // accept, decline
updateInboxIssueDuplicateTo: (issueId: string) => Promise<void>; // connecting the inbox issue to the project existing issue 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; snoozed_till: Date | undefined;
duplicate_to: string | undefined; duplicate_to: string | undefined;
created_by: string | undefined; created_by: string | undefined;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined = undefined;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
// services // services
@ -41,6 +43,7 @@ export class InboxIssueStore implements IInboxIssueStore {
this.snoozed_till = data?.snoozed_till ? new Date(data.snoozed_till) : undefined; this.snoozed_till = data?.snoozed_till ? new Date(data.snoozed_till) : undefined;
this.duplicate_to = data?.duplicate_to || undefined; this.duplicate_to = data?.duplicate_to || undefined;
this.created_by = data?.created_by || undefined; this.created_by = data?.created_by || undefined;
this.duplicate_issue_detail = data?.duplicate_issue_detail || undefined;
this.workspaceSlug = workspaceSlug; this.workspaceSlug = workspaceSlug;
this.projectId = projectId; this.projectId = projectId;
// services // services
@ -52,6 +55,7 @@ export class InboxIssueStore implements IInboxIssueStore {
issue: observable, issue: observable,
snoozed_till: observable, snoozed_till: observable,
duplicate_to: observable, duplicate_to: observable,
duplicate_issue_detail: observable,
created_by: observable, created_by: observable,
// actions // actions
updateInboxIssueStatus: action, updateInboxIssueStatus: action,

View File

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