mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-984] feat: integrated state filter in inbox issues filter (#4182)
* chore: added state filtering in the inbox filters * chore: Clearing the issues when we apply filters
This commit is contained in:
parent
3e2355e223
commit
a44a032683
1
packages/types/src/inbox.d.ts
vendored
1
packages/types/src/inbox.d.ts
vendored
@ -29,6 +29,7 @@ export type TInboxIssueFilter = {
|
|||||||
} & {
|
} & {
|
||||||
[key in TInboxIssueFilterDateKeys]: string[] | undefined;
|
[key in TInboxIssueFilterDateKeys]: string[] | undefined;
|
||||||
} & {
|
} & {
|
||||||
|
state: string[] | undefined;
|
||||||
status: TInboxIssueStatus[] | undefined;
|
status: TInboxIssueStatus[] | undefined;
|
||||||
priority: TIssuePriorities[] | undefined;
|
priority: TIssuePriorities[] | undefined;
|
||||||
labels: string[] | undefined;
|
labels: string[] | undefined;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./status";
|
export * from "./status";
|
||||||
|
export * from "./state";
|
||||||
export * from "./priority";
|
export * from "./priority";
|
||||||
export * from "./member";
|
export * from "./member";
|
||||||
export * from "./label";
|
export * from "./label";
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
InboxIssueAppliedFiltersMember,
|
InboxIssueAppliedFiltersMember,
|
||||||
InboxIssueAppliedFiltersLabel,
|
InboxIssueAppliedFiltersLabel,
|
||||||
InboxIssueAppliedFiltersDate,
|
InboxIssueAppliedFiltersDate,
|
||||||
|
InboxIssueAppliedFiltersState,
|
||||||
} from "@/components/inbox";
|
} from "@/components/inbox";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectInbox } from "@/hooks/store";
|
import { useProjectInbox } from "@/hooks/store";
|
||||||
@ -19,6 +20,8 @@ export const InboxIssueAppliedFilters: FC = observer(() => {
|
|||||||
<div className="p-3 py-2 relative flex flex-wrap items-center gap-1 border-b border-custom-border-300">
|
<div className="p-3 py-2 relative flex flex-wrap items-center gap-1 border-b border-custom-border-300">
|
||||||
{/* status */}
|
{/* status */}
|
||||||
<InboxIssueAppliedFiltersStatus />
|
<InboxIssueAppliedFiltersStatus />
|
||||||
|
{/* state */}
|
||||||
|
<InboxIssueAppliedFiltersState />
|
||||||
{/* priority */}
|
{/* priority */}
|
||||||
<InboxIssueAppliedFiltersPriority />
|
<InboxIssueAppliedFiltersPriority />
|
||||||
{/* assignees */}
|
{/* assignees */}
|
||||||
|
52
web/components/inbox/inbox-filter/applied-filters/state.tsx
Normal file
52
web/components/inbox/inbox-filter/applied-filters/state.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useProjectInbox, useProjectState } from "@/hooks/store";
|
||||||
|
|
||||||
|
export const InboxIssueAppliedFiltersState: FC = observer(() => {
|
||||||
|
// hooks
|
||||||
|
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
|
||||||
|
const { getStateById } = useProjectState();
|
||||||
|
// derived values
|
||||||
|
const filteredValues = inboxFilters?.state || [];
|
||||||
|
const currentOptionDetail = (stateId: string) => getStateById(stateId) || undefined;
|
||||||
|
|
||||||
|
const handleFilterValue = (value: string): string[] =>
|
||||||
|
filteredValues?.includes(value) ? filteredValues.filter((v) => v !== value) : [...filteredValues, value];
|
||||||
|
|
||||||
|
const clearFilter = () => handleInboxIssueFilters("state", 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">
|
||||||
|
<div className="text-xs text-custom-text-200">Status</div>
|
||||||
|
{filteredValues.map((value) => {
|
||||||
|
const optionDetail = currentOptionDetail(value);
|
||||||
|
if (!optionDetail) return <></>;
|
||||||
|
return (
|
||||||
|
<div key={value} className="relative flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||||
|
<div className="w-3 h-3 flex-shrink-0 relative flex justify-center items-center overflow-hidden">
|
||||||
|
<StateGroupIcon color={optionDetail.color} stateGroup={optionDetail.group} height="12px" width="12px" />
|
||||||
|
</div>
|
||||||
|
<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("state", handleFilterValue(optionDetail?.id))}
|
||||||
|
>
|
||||||
|
<X className={`w-3 h-3`} />
|
||||||
|
</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"
|
||||||
|
onClick={clearFilter}
|
||||||
|
>
|
||||||
|
<X className={`w-3 h-3`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -8,9 +8,10 @@ import {
|
|||||||
FilterMember,
|
FilterMember,
|
||||||
FilterDate,
|
FilterDate,
|
||||||
FilterLabels,
|
FilterLabels,
|
||||||
|
FilterState,
|
||||||
} from "@/components/inbox/inbox-filter/filters";
|
} from "@/components/inbox/inbox-filter/filters";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember, useLabel } from "@/hooks/store";
|
import { useMember, useLabel, useProjectState } from "@/hooks/store";
|
||||||
|
|
||||||
export const InboxIssueFilterSelection: FC = observer(() => {
|
export const InboxIssueFilterSelection: FC = observer(() => {
|
||||||
// hooks
|
// hooks
|
||||||
@ -18,6 +19,7 @@ export const InboxIssueFilterSelection: FC = observer(() => {
|
|||||||
project: { projectMemberIds },
|
project: { projectMemberIds },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
const { projectLabels } = useLabel();
|
const { projectLabels } = useLabel();
|
||||||
|
const { projectStates } = useProjectState();
|
||||||
// states
|
// states
|
||||||
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||||
|
|
||||||
@ -47,6 +49,10 @@ export const InboxIssueFilterSelection: FC = observer(() => {
|
|||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<FilterStatus searchQuery={filtersSearchQuery} />
|
<FilterStatus searchQuery={filtersSearchQuery} />
|
||||||
</div>
|
</div>
|
||||||
|
{/* state */}
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterState states={projectStates} searchQuery={filtersSearchQuery} />
|
||||||
|
</div>
|
||||||
{/* Priority */}
|
{/* Priority */}
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<FilterPriority searchQuery={filtersSearchQuery} />
|
<FilterPriority searchQuery={filtersSearchQuery} />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./filter-selection";
|
export * from "./filter-selection";
|
||||||
export * from "./status";
|
export * from "./status";
|
||||||
|
export * from "./state";
|
||||||
export * from "./priority";
|
export * from "./priority";
|
||||||
export * from "./labels";
|
export * from "./labels";
|
||||||
export * from "./members";
|
export * from "./members";
|
||||||
|
84
web/components/inbox/inbox-filter/filters/state.tsx
Normal file
84
web/components/inbox/inbox-filter/filters/state.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { IState } from "@plane/types";
|
||||||
|
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "@/components/issues";
|
||||||
|
// hooks
|
||||||
|
import { useProjectInbox } from "@/hooks/store";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
states: IState[] | undefined;
|
||||||
|
searchQuery: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterState: FC<Props> = observer((props) => {
|
||||||
|
const { states, searchQuery } = props;
|
||||||
|
|
||||||
|
const [itemsToRender, setItemsToRender] = useState(5);
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
|
const { inboxFilters, handleInboxIssueFilters } = useProjectInbox();
|
||||||
|
|
||||||
|
const filterValue = inboxFilters?.state || [];
|
||||||
|
|
||||||
|
const appliedFiltersCount = filterValue?.length ?? 0;
|
||||||
|
|
||||||
|
const filteredOptions = states?.filter((state) => state.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
const handleViewToggle = () => {
|
||||||
|
if (!filteredOptions) return;
|
||||||
|
|
||||||
|
if (itemsToRender === filteredOptions.length) setItemsToRender(5);
|
||||||
|
else setItemsToRender(filteredOptions.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterValue = (value: string): string[] =>
|
||||||
|
filterValue?.includes(value) ? filterValue.filter((v) => v !== value) : [...filterValue, value];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterHeader
|
||||||
|
title={`State${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{filteredOptions ? (
|
||||||
|
filteredOptions.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{filteredOptions.slice(0, itemsToRender).map((state) => (
|
||||||
|
<FilterOption
|
||||||
|
key={state?.id}
|
||||||
|
isChecked={filterValue?.includes(state?.id) ? true : false}
|
||||||
|
onClick={() => handleInboxIssueFilters("state", handleFilterValue(state.id))}
|
||||||
|
icon={<StateGroupIcon color={state.color} stateGroup={state.group} height="12px" width="12px" />}
|
||||||
|
title={state.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredOptions.length > 5 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-8 text-xs font-medium text-custom-primary-100"
|
||||||
|
onClick={handleViewToggle}
|
||||||
|
>
|
||||||
|
{itemsToRender === filteredOptions.length ? "View less" : "View all"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-2">
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -216,6 +216,8 @@ 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");
|
||||||
|
set(this, ["inboxIssues"], {});
|
||||||
|
set(this, ["inboxIssuePaginationInfo"], undefined);
|
||||||
if (tab === "closed") set(this, ["inboxFilters", "status"], [-1, 1, 2]);
|
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;
|
||||||
@ -224,12 +226,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");
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user