mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: new global select filters component (#501)
This commit is contained in:
parent
10e5ba7b3e
commit
ad2fa91a2b
@ -2,33 +2,22 @@ import React from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// services
|
|
||||||
import projectService from "services/project.service";
|
|
||||||
import issuesService from "services/issues.service";
|
|
||||||
import stateService from "services/state.service";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// components
|
// components
|
||||||
import { PRIORITIES } from "constants/project";
|
import { SelectFilters } from "components/views";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, CustomMenu, MultiLevelDropdown } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||||
import { Squares2X2Icon } from "@heroicons/react/20/solid";
|
import { Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||||
import { getStateGroupIcon } from "components/icons";
|
|
||||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
import { getStatesList } from "helpers/state.helper";
|
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels, Properties } from "types";
|
import { Properties } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys";
|
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
@ -57,28 +46,6 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
projectId as string
|
projectId as string
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: states } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
const statesList = getStatesList(states ?? {});
|
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
|
||||||
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
@ -101,8 +68,8 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<Squares2X2Icon className="h-4 w-4" />
|
<Squares2X2Icon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MultiLevelDropdown
|
<SelectFilters
|
||||||
label="Filters"
|
filters={filters}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
setFilters(
|
setFilters(
|
||||||
{
|
{
|
||||||
@ -116,71 +83,6 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
direction="left"
|
direction="left"
|
||||||
options={[
|
|
||||||
{
|
|
||||||
id: "priority",
|
|
||||||
label: "Priority",
|
|
||||||
value: PRIORITIES,
|
|
||||||
children: [
|
|
||||||
...PRIORITIES.map((priority) => ({
|
|
||||||
id: priority ?? "none",
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: {
|
|
||||||
key: "priority",
|
|
||||||
value: priority,
|
|
||||||
},
|
|
||||||
selected: filters?.priority?.includes(priority ?? "none"),
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "state",
|
|
||||||
label: "State",
|
|
||||||
value: statesList,
|
|
||||||
children: [
|
|
||||||
...statesList.map((state) => ({
|
|
||||||
id: state.id,
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: {
|
|
||||||
key: "state",
|
|
||||||
value: state.id,
|
|
||||||
},
|
|
||||||
selected: filters?.state?.includes(state.id),
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "assignees",
|
|
||||||
label: "Assignees",
|
|
||||||
value: members,
|
|
||||||
children: [
|
|
||||||
...(members?.map((member) => ({
|
|
||||||
id: member.member.id,
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar user={member.member} />
|
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
|
||||||
? member.member.first_name
|
|
||||||
: member.member.email}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: {
|
|
||||||
key: "assignees",
|
|
||||||
value: member.member.id,
|
|
||||||
},
|
|
||||||
selected: filters?.assignees?.includes(member.member.id),
|
|
||||||
})) ?? []),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
|
@ -24,6 +24,7 @@ export * from "./layer-diagonal-icon";
|
|||||||
export * from "./lock-icon";
|
export * from "./lock-icon";
|
||||||
export * from "./menu-icon";
|
export * from "./menu-icon";
|
||||||
export * from "./plus-icon";
|
export * from "./plus-icon";
|
||||||
|
export * from "./priority-icon";
|
||||||
export * from "./question-mark-circle-icon";
|
export * from "./question-mark-circle-icon";
|
||||||
export * from "./setting-icon";
|
export * from "./setting-icon";
|
||||||
export * from "./signal-cellular-icon";
|
export * from "./signal-cellular-icon";
|
||||||
|
@ -6,14 +6,7 @@ import useSWR from "swr";
|
|||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// ui
|
// ui
|
||||||
import {
|
import { Avatar, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||||
Avatar,
|
|
||||||
Input,
|
|
||||||
MultiLevelDropdown,
|
|
||||||
PrimaryButton,
|
|
||||||
SecondaryButton,
|
|
||||||
TextArea,
|
|
||||||
} from "components/ui";
|
|
||||||
// types
|
// types
|
||||||
import { IView } from "types";
|
import { IView } from "types";
|
||||||
// constant
|
// constant
|
||||||
@ -23,11 +16,12 @@ import { getStatesList } from "helpers/state.helper";
|
|||||||
// services
|
// services
|
||||||
import stateService from "services/state.service";
|
import stateService from "services/state.service";
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
|
// components
|
||||||
|
import { SelectFilters } from "components/views";
|
||||||
// icons
|
// icons
|
||||||
import { getStateGroupIcon } from "components/icons";
|
import { getStateGroupIcon } from "components/icons";
|
||||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||||
// components
|
// components
|
||||||
import { PRIORITIES } from "constants/project";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: IView) => Promise<void>;
|
handleFormSubmit: (values: IView) => Promise<void>;
|
||||||
@ -139,8 +133,8 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<MultiLevelDropdown
|
<SelectFilters
|
||||||
label="Filters"
|
filters={filters}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
const key = option.key as keyof typeof filters;
|
||||||
|
|
||||||
@ -156,72 +150,6 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
direction="right"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
id: "priority",
|
|
||||||
label: "Priority",
|
|
||||||
value: PRIORITIES,
|
|
||||||
children: [
|
|
||||||
...PRIORITIES.map((priority) => ({
|
|
||||||
id: priority ?? "none",
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: {
|
|
||||||
key: "priority",
|
|
||||||
value: priority,
|
|
||||||
},
|
|
||||||
selected: filters?.priority?.includes(priority ?? "none"),
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "state",
|
|
||||||
label: "State",
|
|
||||||
value: statesList,
|
|
||||||
children: [
|
|
||||||
...statesList.map((state) => ({
|
|
||||||
id: state.id,
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: {
|
|
||||||
key: "state",
|
|
||||||
value: state.id,
|
|
||||||
},
|
|
||||||
selected: filters?.state?.includes(state.id),
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "assignee",
|
|
||||||
label: "Assignee",
|
|
||||||
value: members,
|
|
||||||
children: [
|
|
||||||
...(members?.map((member) => ({
|
|
||||||
id: member.member.id,
|
|
||||||
label: (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar user={member.member} />
|
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
|
||||||
? member.member.first_name
|
|
||||||
: member.member.email}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
value: {
|
|
||||||
key: "assignees",
|
|
||||||
value: member.member.id,
|
|
||||||
},
|
|
||||||
selected: filters?.assignees?.includes(member.member.id),
|
|
||||||
})) ?? []),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -257,8 +185,9 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
else if (queryKey === "assignees")
|
else if (queryKey === "assignees")
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3" key={key}>
|
<div className="flex gap-3" key={key}>
|
||||||
{filters.assignees?.map((assigneeID) => {
|
{filters.assignees?.map((assigneeId) => {
|
||||||
const member = members?.find((member) => member.member.id === assigneeID);
|
const member = members?.find((member) => member.member.id === assigneeId);
|
||||||
|
|
||||||
if (!member) return null;
|
if (!member) return null;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 text-xs" key={member.member.id}>
|
<div className="flex items-center gap-2 text-xs" key={member.member.id}>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./delete-view-modal";
|
export * from "./delete-view-modal";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
|
export * from "./select-filters";
|
||||||
|
118
apps/app/components/views/select-filters.tsx
Normal file
118
apps/app/components/views/select-filters.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import stateService from "services/state.service";
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
// ui
|
||||||
|
import { Avatar, MultiLevelDropdown } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||||
|
// helpers
|
||||||
|
import { getStatesList } from "helpers/state.helper";
|
||||||
|
// types
|
||||||
|
import { IIssueFilterOptions, IQuery } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys";
|
||||||
|
// constants
|
||||||
|
import { PRIORITIES } from "constants/project";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filters: IIssueFilterOptions | IQuery;
|
||||||
|
onSelect: (option: any) => void;
|
||||||
|
direction?: "left" | "right";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectFilters: React.FC<Props> = ({ filters, onSelect, direction = "right" }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { data: states } = useSWR(
|
||||||
|
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
const statesList = getStatesList(states ?? {});
|
||||||
|
|
||||||
|
const { data: members } = useSWR(
|
||||||
|
projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiLevelDropdown
|
||||||
|
label="Filters"
|
||||||
|
onSelect={onSelect}
|
||||||
|
direction={direction}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
id: "priority",
|
||||||
|
label: "Priority",
|
||||||
|
value: PRIORITIES,
|
||||||
|
children: [
|
||||||
|
...PRIORITIES.map((priority) => ({
|
||||||
|
id: priority ?? "none",
|
||||||
|
label: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
key: "priority",
|
||||||
|
value: priority,
|
||||||
|
},
|
||||||
|
selected: filters?.priority?.includes(priority ?? "none"),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "state",
|
||||||
|
label: "State",
|
||||||
|
value: statesList,
|
||||||
|
children: [
|
||||||
|
...statesList.map((state) => ({
|
||||||
|
id: state.id,
|
||||||
|
label: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
key: "state",
|
||||||
|
value: state.id,
|
||||||
|
},
|
||||||
|
selected: filters?.state?.includes(state.id),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "assignees",
|
||||||
|
label: "Assignees",
|
||||||
|
value: members,
|
||||||
|
children: [
|
||||||
|
...(members?.map((member) => ({
|
||||||
|
id: member.member.id,
|
||||||
|
label: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar user={member.member} />
|
||||||
|
{member.member.first_name && member.member.first_name !== ""
|
||||||
|
? member.member.first_name
|
||||||
|
: member.member.email}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: {
|
||||||
|
key: "assignees",
|
||||||
|
value: member.member.id,
|
||||||
|
},
|
||||||
|
selected: filters?.assignees?.includes(member.member.id),
|
||||||
|
})) ?? []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -116,7 +116,7 @@ const ProjectViews: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Loader>
|
<Loader className="space-y-3">
|
||||||
<Loader.Item height="30px" />
|
<Loader.Item height="30px" />
|
||||||
<Loader.Item height="30px" />
|
<Loader.Item height="30px" />
|
||||||
<Loader.Item height="30px" />
|
<Loader.Item height="30px" />
|
||||||
|
Loading…
Reference in New Issue
Block a user