diff --git a/apps/app/components/core/issues-view-filter.tsx b/apps/app/components/core/issues-view-filter.tsx index f959688af..d449d6e67 100644 --- a/apps/app/components/core/issues-view-filter.tsx +++ b/apps/app/components/core/issues-view-filter.tsx @@ -14,7 +14,7 @@ import useIssuesView from "hooks/use-issues-view"; // headless ui import { Popover, Transition } from "@headlessui/react"; // ui -import { CustomMenu } from "components/ui"; +import { CustomMenu, MultiLevelDropdown } from "components/ui"; // icons import { ChevronDownIcon, ListBulletIcon } from "@heroicons/react/24/outline"; import { Squares2X2Icon } from "@heroicons/react/20/solid"; @@ -96,59 +96,69 @@ export const IssuesFilterView: React.FC = () => { - - Filters - - } - > -

Status

- {statesList?.map((state) => ( - { - const filterStates = filters?.state ?? []; - const newFilterState = filterStates.includes(state.id) - ? filterStates.filter((id) => id !== state.id) - : [...filterStates, state.id]; - setFilters({ ...filters, state: newFilterState }); - }} - > - <>{state.name} - - ))} -

Members

- {members?.map((member) => ( - {}}> - <> - {member.member.first_name && member.member.first_name !== "" - ? member.member.first_name + " " + member.member.last_name - : member.member.email} - - - ))} -

Labels

- {issueLabels?.map((label) => ( - {}}> - <>{label.name} - - ))} -

Priority

- {PRIORITIES?.map((priority) => ( - { - if (priority === null) return; - const filterPriorities = filters?.priority ?? []; - const newFilterPriority = filterPriorities.includes(priority) - ? filterPriorities.filter((id) => id !== priority) - : [...filterPriorities, priority]; - setFilters({ ...filters, priority: newFilterPriority }); - }} - > - {priority ?? "None"} - - ))} -
+ { + setFilters({ + ...filters, + [option.key]: [ + ...((filters?.[option.key as keyof typeof filters] as any[]) ?? []), + option.value, + ], + }); + }} + direction="left" + options={[ + { + id: "priority", + label: "Priority", + value: PRIORITIES, + children: [ + ...PRIORITIES.map((priority) => ({ + id: priority ?? "none", + label: priority ?? "None", + 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: state.name, + 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: member.member.first_name, + value: { + key: "assignee", + value: member.member.id, + }, + selected: filters?.assignees?.includes(member.member.id), + })) ?? []), + ], + }, + ]} + /> {({ open }) => ( <> diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 3f9a64a9f..5e1100f80 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -458,6 +458,35 @@ export const IssuesView: React.FC = ({ type = "issue", openIssuesListModa

) ) + : key === "assignee" + ? (filters[key as keyof IIssueFilterOptions] as any)?.map( + (member: any) => ( +

+ + { + members?.find((m) => m.member.id === member)?.member + .first_name + } + + { + setFilters({ + ...filters, + [key as keyof IIssueFilterOptions]: ( + filters[key as keyof IIssueFilterOptions] as any + )?.filter((p: any) => p !== member), + }); + }} + > + + +

+ ) + ) : (filters[key as keyof IIssueFilterOptions] as any)?.join(", ")}

) diff --git a/apps/app/components/ui/index.ts b/apps/app/components/ui/index.ts index da97deb65..0e881329b 100644 --- a/apps/app/components/ui/index.ts +++ b/apps/app/components/ui/index.ts @@ -18,4 +18,5 @@ export * from "./spinner"; export * from "./tooltip"; export * from "./labels-list"; export * from "./linear-progress-indicator"; -export * from "./empty-state"; \ No newline at end of file +export * from "./empty-state"; +export * from "./multi-level-dropdown"; diff --git a/apps/app/components/ui/multi-level-dropdown.tsx b/apps/app/components/ui/multi-level-dropdown.tsx new file mode 100644 index 000000000..b7e437a54 --- /dev/null +++ b/apps/app/components/ui/multi-level-dropdown.tsx @@ -0,0 +1,137 @@ +import { Menu, Transition } from "@headlessui/react"; +import { Fragment, useState } from "react"; +import { ChevronDownIcon, ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid"; + +type MultiLevelDropdownProps = { + label: string; + options: { + id: string; + label: string; + value: any; + selected?: boolean; + children?: { + id: string; + label: string; + value: any; + selected?: boolean; + }[]; + }[]; + onSelect: (value: any) => void; + direction?: "left" | "right"; +}; + +export const MultiLevelDropdown: React.FC = (props) => { + const { label, options, onSelect, direction = "right" } = props; + + const [openChildFor, setOpenChildFor] = useState(null); + + return ( + + {({ open }) => ( + <> +
+ { + setOpenChildFor(null); + }} + className={`group flex items-center gap-2 rounded-md border bg-transparent p-2 text-xs font-medium hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${ + open ? "bg-gray-100 text-gray-900" : "text-gray-500" + }`} + > + {label} + +
+ + + {options.map((option) => ( +
+ { + if (option.children) { + if (openChildFor === option.id) { + e.stopPropagation(); + e.preventDefault(); + setOpenChildFor(null); + } else { + e.stopPropagation(); + e.preventDefault(); + setOpenChildFor(option.id); + } + } else { + onSelect(option.value); + } + }} + className="w-full" + > + {({ active }) => ( + <> +
+ {direction === "left" && option.children && ( +
+ + )} +
+ {option.children && option.id === openChildFor && ( + + {option.children.map((child) => ( +
+ + {({ active }) => ( + <> + + + )} + +
+ ))} +
+ )} +
+ ))} +
+
+ + )} +
+ ); +};