forked from github/plane
refactor: filters list component (#1687)
* refactor: filters list component * fix: build error
This commit is contained in:
parent
0b86080166
commit
c54b8b9a15
@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
@ -12,11 +10,13 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite } from "types";
|
||||
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
filters: any;
|
||||
setFilters: any;
|
||||
filters: Partial<IIssueFilterOptions>;
|
||||
setFilters: (updatedFilter: Partial<IIssueFilterOptions>) => void;
|
||||
clearAllFilters: (...args: any) => void;
|
||||
labels: IIssueLabels[] | undefined;
|
||||
members: IUserLite[] | undefined;
|
||||
@ -31,9 +31,6 @@ export const FiltersList: React.FC<Props> = ({
|
||||
members,
|
||||
states,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { viewId } = router.query;
|
||||
|
||||
if (!filters) return <></>;
|
||||
|
||||
const nullFilters = Object.keys(filters).filter(
|
||||
@ -42,24 +39,26 @@ export const FiltersList: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
|
||||
{Object.keys(filters).map((key) => {
|
||||
if (filters[key as keyof typeof filters] !== null)
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
||||
>
|
||||
<span className="capitalize text-custom-text-200">
|
||||
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||
</span>
|
||||
{filters[key as keyof IIssueFilterOptions] === null ||
|
||||
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
|
||||
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
|
||||
) : Array.isArray(filters[key as keyof IIssueFilterOptions]) ? (
|
||||
<div className="space-x-2">
|
||||
{key === "state" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.state?.map((stateId: any) => {
|
||||
{Object.keys(filters).map((filterKey) => {
|
||||
const key = filterKey as keyof typeof filters;
|
||||
|
||||
if (filters[key] === null) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
||||
>
|
||||
<span className="capitalize text-custom-text-200">
|
||||
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||
</span>
|
||||
{filters[key] === null || (filters[key]?.length ?? 0) <= 0 ? (
|
||||
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
|
||||
) : Array.isArray(filters[key]) ? (
|
||||
<div className="space-x-2">
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{key === "state"
|
||||
? filters.state?.map((stateId: string) => {
|
||||
const state = states?.find((s) => s.id === stateId);
|
||||
|
||||
return (
|
||||
@ -83,33 +82,46 @@ export const FiltersList: React.FC<Props> = ({
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters(
|
||||
{
|
||||
state: filters.state?.filter((s: any) => s !== stateId),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
)
|
||||
setFilters({
|
||||
state: filters.state?.filter((s: any) => s !== stateId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
state: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : key === "priority" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.priority?.map((priority: any) => (
|
||||
})
|
||||
: key === "state_group"
|
||||
? filters.state_group?.map((stateGroup) => {
|
||||
const group = stateGroup as TStateGroups;
|
||||
|
||||
return (
|
||||
<p
|
||||
key={group}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||
style={{
|
||||
color: STATE_GROUP_COLORS[group],
|
||||
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
|
||||
}}
|
||||
>
|
||||
<span>{getStateGroupIcon(group, "16", "16")}</span>
|
||||
<span>{group}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
state_group: filters.state_group?.filter((g) => g !== group),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})
|
||||
: key === "priority"
|
||||
? filters.priority?.map((priority: any) => (
|
||||
<p
|
||||
key={priority}
|
||||
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
|
||||
@ -129,32 +141,17 @@ export const FiltersList: React.FC<Props> = ({
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters(
|
||||
{
|
||||
priority: filters.priority?.filter((p: any) => p !== priority),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
)
|
||||
setFilters({
|
||||
priority: filters.priority?.filter((p: any) => p !== priority),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
priority: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : key === "assignees" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.assignees?.map((memberId: string) => {
|
||||
))
|
||||
: key === "assignees"
|
||||
? filters.assignees?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
@ -167,35 +164,18 @@ export const FiltersList: React.FC<Props> = ({
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters(
|
||||
{
|
||||
assignees: filters.assignees?.filter(
|
||||
(p: any) => p !== memberId
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
)
|
||||
setFilters({
|
||||
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
assignees: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : key === "created_by" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.created_by?.map((memberId: string) => {
|
||||
})
|
||||
: key === "created_by"
|
||||
? filters.created_by?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
@ -208,35 +188,20 @@ export const FiltersList: React.FC<Props> = ({
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters(
|
||||
{
|
||||
created_by: filters.created_by?.filter(
|
||||
(p: any) => p !== memberId
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
)
|
||||
setFilters({
|
||||
created_by: filters.created_by?.filter(
|
||||
(p: any) => p !== memberId
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
created_by: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : key === "labels" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.labels?.map((labelId: string) => {
|
||||
})
|
||||
: key === "labels"
|
||||
? filters.labels?.map((labelId: string) => {
|
||||
const label = labels?.find((l) => l.id === labelId);
|
||||
|
||||
if (!label) return null;
|
||||
@ -260,12 +225,9 @@ export const FiltersList: React.FC<Props> = ({
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters(
|
||||
{
|
||||
labels: filters.labels?.filter((l: any) => l !== labelId),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
)
|
||||
setFilters({
|
||||
labels: filters.labels?.filter((l: any) => l !== labelId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon
|
||||
@ -277,22 +239,10 @@ export const FiltersList: React.FC<Props> = ({
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
labels: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : key === "target_date" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.target_date?.map((date: string) => {
|
||||
if (filters.target_date.length <= 0) return null;
|
||||
})
|
||||
: key === "target_date"
|
||||
? filters.target_date?.map((date: string) => {
|
||||
if (filters.target_date && filters.target_date.length <= 0) return null;
|
||||
|
||||
const splitDate = date.split(";");
|
||||
|
||||
@ -308,39 +258,17 @@ export const FiltersList: React.FC<Props> = ({
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters(
|
||||
{
|
||||
target_date: filters.target_date?.filter(
|
||||
(d: any) => d !== date
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
)
|
||||
setFilters({
|
||||
target_date: filters.target_date?.filter((d: any) => d !== date),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
target_date: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
(filters[key as keyof IIssueFilterOptions] as any)?.join(", ")
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-x-1 capitalize">
|
||||
{filters[key as keyof typeof filters]}
|
||||
})
|
||||
: (filters[key] as any)?.join(", ")}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@ -352,9 +280,24 @@ export const FiltersList: React.FC<Props> = ({
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-x-1 capitalize">
|
||||
{filters[key as keyof typeof filters]}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
[key]: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
||||
<button
|
||||
|
@ -466,7 +466,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||
<FiltersList
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
setFilters={(updatedFilter) => setFilters(updatedFilter, !Boolean(viewId))}
|
||||
labels={labels}
|
||||
members={members?.map((m) => m.member)}
|
||||
states={states}
|
||||
|
@ -200,17 +200,15 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
},
|
||||
[makeIssueCopy, handleEditIssue, handleDeleteIssue]
|
||||
);
|
||||
const filtersToShow = { ...filters };
|
||||
delete filtersToShow?.assignees;
|
||||
delete filtersToShow?.created_by;
|
||||
|
||||
const nullFilters = Object.keys(filtersToShow).filter(
|
||||
(key) => filtersToShow[key as keyof IIssueFilterOptions] === null
|
||||
const filtersToDisplay = { ...filters, assignees: null, created_by: null };
|
||||
|
||||
const nullFilters = Object.keys(filtersToDisplay).filter(
|
||||
(key) => filtersToDisplay[key as keyof IIssueFilterOptions] === null
|
||||
);
|
||||
|
||||
const areFiltersApplied =
|
||||
Object.keys(filtersToShow).length > 0 &&
|
||||
nullFilters.length !== Object.keys(filtersToShow).length;
|
||||
Object.keys(filtersToDisplay).length > 0 &&
|
||||
nullFilters.length !== Object.keys(filtersToDisplay).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -242,7 +240,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-2 px-5 pt-3 pb-0">
|
||||
<FiltersList
|
||||
filters={filtersToShow}
|
||||
filters={filtersToDisplay}
|
||||
setFilters={setFilters}
|
||||
labels={labels}
|
||||
members={undefined}
|
||||
|
@ -91,6 +91,7 @@ export const initialState: StateType = {
|
||||
assignees: null,
|
||||
labels: null,
|
||||
state: null,
|
||||
state_group: null,
|
||||
created_by: null,
|
||||
target_date: null,
|
||||
},
|
||||
|
@ -88,18 +88,24 @@ const useMyIssuesFilters = (workspaceSlug: string | undefined) => {
|
||||
);
|
||||
|
||||
const issueView = (myWorkspace?.view_props ?? initialValues).issueView;
|
||||
const groupBy = (myWorkspace?.view_props ?? initialValues).groupByProperty;
|
||||
const orderBy = (myWorkspace?.view_props ?? initialValues).orderBy;
|
||||
const showEmptyGroups = (myWorkspace?.view_props ?? initialValues).showEmptyGroups;
|
||||
const filters = (myWorkspace?.view_props ?? initialValues).filters;
|
||||
|
||||
const setIssueView = useCallback(
|
||||
(newView: TIssueViewOptions) => {
|
||||
console.log("newView", newView);
|
||||
|
||||
saveData({
|
||||
const payload: Partial<IWorkspaceViewProps> = {
|
||||
issueView: newView,
|
||||
});
|
||||
};
|
||||
|
||||
if (newView === "kanban" && groupBy === null) payload.groupByProperty = "state_detail.group";
|
||||
|
||||
saveData(payload);
|
||||
},
|
||||
[saveData]
|
||||
[groupBy, saveData]
|
||||
);
|
||||
|
||||
const groupBy = (myWorkspace?.view_props ?? initialValues).groupByProperty;
|
||||
const setGroupBy = useCallback(
|
||||
(newGroup: TIssueGroupByOptions) => {
|
||||
saveData({
|
||||
@ -109,7 +115,6 @@ const useMyIssuesFilters = (workspaceSlug: string | undefined) => {
|
||||
[saveData]
|
||||
);
|
||||
|
||||
const orderBy = (myWorkspace?.view_props ?? initialValues).orderBy;
|
||||
const setOrderBy = useCallback(
|
||||
(newOrderBy: TIssueOrderByOptions) => {
|
||||
saveData({
|
||||
@ -119,7 +124,6 @@ const useMyIssuesFilters = (workspaceSlug: string | undefined) => {
|
||||
[saveData]
|
||||
);
|
||||
|
||||
const showEmptyGroups = (myWorkspace?.view_props ?? initialValues).showEmptyGroups;
|
||||
const setShowEmptyGroups = useCallback(() => {
|
||||
if (!myWorkspace) return;
|
||||
|
||||
@ -142,9 +146,8 @@ const useMyIssuesFilters = (workspaceSlug: string | undefined) => {
|
||||
[myWorkspace, saveData]
|
||||
);
|
||||
|
||||
const filters = (myWorkspace?.view_props ?? initialValues).filters;
|
||||
const setFilters = useCallback(
|
||||
(updatedFilter: Partial<IIssueFilterOptions & { state_group: string[] | null }>) => {
|
||||
(updatedFilter: Partial<IIssueFilterOptions>) => {
|
||||
if (!myWorkspace) return;
|
||||
|
||||
saveData({
|
||||
|
2
apps/app/types/issues.d.ts
vendored
2
apps/app/types/issues.d.ts
vendored
@ -8,6 +8,7 @@ import type {
|
||||
IProjectLite,
|
||||
IWorkspaceLite,
|
||||
IStateLite,
|
||||
TStateGroups,
|
||||
} from "types";
|
||||
|
||||
export interface IIssueCycle {
|
||||
@ -231,6 +232,7 @@ export interface IIssueFilterOptions {
|
||||
assignees: string[] | null;
|
||||
target_date: string[] | null;
|
||||
state: string[] | null;
|
||||
state_group: TStateGroups[] | null;
|
||||
labels: string[] | null;
|
||||
priority: string[] | null;
|
||||
created_by: string[] | null;
|
||||
|
4
apps/app/types/projects.d.ts
vendored
4
apps/app/types/projects.d.ts
vendored
@ -6,7 +6,7 @@ import type {
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
TIssueViewOptions,
|
||||
TStateGroup,
|
||||
TStateGroups,
|
||||
} from "./";
|
||||
|
||||
export interface IProject {
|
||||
@ -140,7 +140,7 @@ export interface ISearchIssueResponse {
|
||||
project__name: string;
|
||||
sequence_id: number;
|
||||
state__color: string;
|
||||
state__group: TStateGroup;
|
||||
state__group: TStateGroups;
|
||||
state__name: string;
|
||||
workspace__slug: string;
|
||||
}
|
||||
|
6
apps/app/types/state.d.ts
vendored
6
apps/app/types/state.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
import { IProject, IProjectLite, IWorkspaceLite } from "types";
|
||||
|
||||
export type TStateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
|
||||
export interface IState {
|
||||
readonly id: string;
|
||||
@ -9,7 +9,7 @@ export interface IState {
|
||||
readonly created_by: string;
|
||||
default: boolean;
|
||||
description: string;
|
||||
group: TStateGroup;
|
||||
group: TStateGroups;
|
||||
name: string;
|
||||
project: string;
|
||||
readonly project_detail: IProjectLite;
|
||||
@ -23,7 +23,7 @@ export interface IState {
|
||||
|
||||
export interface IStateLite {
|
||||
color: string;
|
||||
group: TStateGroup;
|
||||
group: TStateGroups;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
6
apps/app/types/workspace.d.ts
vendored
6
apps/app/types/workspace.d.ts
vendored
@ -67,11 +67,7 @@ export interface IWorkspaceViewProps {
|
||||
issueView: TIssueViewOptions;
|
||||
groupByProperty: TIssueGroupByOptions;
|
||||
orderBy: TIssueOrderByOptions;
|
||||
filters: Partial<
|
||||
IIssueFilterOptions & {
|
||||
state_group: string[] | null;
|
||||
}
|
||||
>;
|
||||
filters: Partial<IIssueFilterOptions>;
|
||||
showEmptyGroups: boolean;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user