forked from github/plane
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 useSWR from "swr";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { PRIORITIES } from "constants/project";
|
||||
import { SelectFilters } from "components/views";
|
||||
// ui
|
||||
import { Avatar, CustomMenu, MultiLevelDropdown } from "components/ui";
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||
import { Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { IIssueLabels, Properties } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/fetch-keys";
|
||||
import { Properties } from "types";
|
||||
// constants
|
||||
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
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
@ -101,8 +68,8 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<Squares2X2Icon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<MultiLevelDropdown
|
||||
label="Filters"
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
setFilters(
|
||||
{
|
||||
@ -116,71 +83,6 @@ export const IssuesFilterView: React.FC = () => {
|
||||
);
|
||||
}}
|
||||
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">
|
||||
{({ open }) => (
|
||||
|
@ -24,6 +24,7 @@ export * from "./layer-diagonal-icon";
|
||||
export * from "./lock-icon";
|
||||
export * from "./menu-icon";
|
||||
export * from "./plus-icon";
|
||||
export * from "./priority-icon";
|
||||
export * from "./question-mark-circle-icon";
|
||||
export * from "./setting-icon";
|
||||
export * from "./signal-cellular-icon";
|
||||
|
@ -6,14 +6,7 @@ import useSWR from "swr";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
// ui
|
||||
import {
|
||||
Avatar,
|
||||
Input,
|
||||
MultiLevelDropdown,
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
TextArea,
|
||||
} from "components/ui";
|
||||
import { Avatar, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||
// types
|
||||
import { IView } from "types";
|
||||
// constant
|
||||
@ -23,11 +16,12 @@ import { getStatesList } from "helpers/state.helper";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
import projectService from "services/project.service";
|
||||
// components
|
||||
import { SelectFilters } from "components/views";
|
||||
// icons
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||
// components
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
handleFormSubmit: (values: IView) => Promise<void>;
|
||||
@ -139,8 +133,8 @@ export const ViewForm: React.FC<Props> = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<MultiLevelDropdown
|
||||
label="Filters"
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
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>
|
||||
@ -257,8 +185,9 @@ export const ViewForm: React.FC<Props> = ({
|
||||
else if (queryKey === "assignees")
|
||||
return (
|
||||
<div className="flex gap-3" key={key}>
|
||||
{filters.assignees?.map((assigneeID) => {
|
||||
const member = members?.find((member) => member.member.id === assigneeID);
|
||||
{filters.assignees?.map((assigneeId) => {
|
||||
const member = members?.find((member) => member.member.id === assigneeId);
|
||||
|
||||
if (!member) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs" key={member.member.id}>
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./delete-view-modal";
|
||||
export * from "./form";
|
||||
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" />
|
||||
|
Loading…
Reference in New Issue
Block a user