forked from github/plane
[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:
parent
3c2b2e3ed6
commit
1dac70ecbe
@ -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",
|
||||
]
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
16
packages/types/src/inbox.d.ts
vendored
16
packages/types/src/inbox.d.ts
vendored
@ -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 & {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
56
web/helpers/inbox.helper.ts
Normal file
56
web/helpers/inbox.helper.ts
Normal 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,
|
||||
},
|
||||
];
|
@ -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,
|
||||
|
@ -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));
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user