forked from github/plane
style: made new issue filter dropdown (#462)
This commit is contained in:
parent
a84abc60b2
commit
0f06589b83
@ -14,7 +14,7 @@ import useIssuesView from "hooks/use-issues-view";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu, MultiLevelDropdown } 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";
|
||||||
@ -96,59 +96,69 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<Squares2X2Icon className="h-4 w-4" />
|
<Squares2X2Icon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CustomMenu
|
<MultiLevelDropdown
|
||||||
label={
|
label="Filter"
|
||||||
<span className="flex items-center gap-2 rounded-md py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:outline-none">
|
onSelect={(option) => {
|
||||||
Filters
|
setFilters({
|
||||||
</span>
|
...filters,
|
||||||
}
|
[option.key]: [
|
||||||
>
|
...((filters?.[option.key as keyof typeof filters] as any[]) ?? []),
|
||||||
<h4 className="px-1 py-2 font-medium">Status</h4>
|
option.value,
|
||||||
{statesList?.map((state) => (
|
],
|
||||||
<CustomMenu.MenuItem
|
});
|
||||||
onClick={() => {
|
|
||||||
const filterStates = filters?.state ?? [];
|
|
||||||
const newFilterState = filterStates.includes(state.id)
|
|
||||||
? filterStates.filter((id) => id !== state.id)
|
|
||||||
: [...filterStates, state.id];
|
|
||||||
setFilters({ ...filters, state: newFilterState });
|
|
||||||
}}
|
}}
|
||||||
>
|
direction="left"
|
||||||
<>{state.name}</>
|
options={[
|
||||||
</CustomMenu.MenuItem>
|
{
|
||||||
))}
|
id: "priority",
|
||||||
<h4 className="px-1 py-2 font-medium">Members</h4>
|
label: "Priority",
|
||||||
{members?.map((member) => (
|
value: PRIORITIES,
|
||||||
<CustomMenu.MenuItem onClick={() => {}}>
|
children: [
|
||||||
<>
|
...PRIORITIES.map((priority) => ({
|
||||||
{member.member.first_name && member.member.first_name !== ""
|
id: priority ?? "none",
|
||||||
? member.member.first_name + " " + member.member.last_name
|
label: priority ?? "None",
|
||||||
: member.member.email}
|
value: {
|
||||||
</>
|
key: "priority",
|
||||||
</CustomMenu.MenuItem>
|
value: priority,
|
||||||
))}
|
},
|
||||||
<h4 className="px-1 py-2 font-medium">Labels</h4>
|
selected: filters?.priority?.includes(priority ?? "none"),
|
||||||
{issueLabels?.map((label) => (
|
})),
|
||||||
<CustomMenu.MenuItem onClick={() => {}}>
|
],
|
||||||
<>{label.name}</>
|
},
|
||||||
</CustomMenu.MenuItem>
|
{
|
||||||
))}
|
id: "state",
|
||||||
<h4 className="px-1 py-2 font-medium">Priority</h4>
|
label: "State",
|
||||||
{PRIORITIES?.map((priority) => (
|
value: statesList,
|
||||||
<CustomMenu.MenuItem
|
children: [
|
||||||
onClick={() => {
|
...statesList.map((state) => ({
|
||||||
if (priority === null) return;
|
id: state.id,
|
||||||
const filterPriorities = filters?.priority ?? [];
|
label: state.name,
|
||||||
const newFilterPriority = filterPriorities.includes(priority)
|
value: {
|
||||||
? filterPriorities.filter((id) => id !== priority)
|
key: "state",
|
||||||
: [...filterPriorities, priority];
|
value: state.id,
|
||||||
setFilters({ ...filters, priority: newFilterPriority });
|
},
|
||||||
}}
|
selected: filters?.state?.includes(state.id),
|
||||||
>
|
})),
|
||||||
<span className="capitalize">{priority ?? "None"}</span>
|
],
|
||||||
</CustomMenu.MenuItem>
|
},
|
||||||
))}
|
{
|
||||||
</CustomMenu>
|
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),
|
||||||
|
})) ?? []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -458,6 +458,35 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
|
|||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
: key === "assignee"
|
||||||
|
? (filters[key as keyof IIssueFilterOptions] as any)?.map(
|
||||||
|
(member: any) => (
|
||||||
|
<p
|
||||||
|
key={member}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full bg-gray-500 px-2 py-0.5 text-xs font-medium capitalize text-white"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
members?.find((m) => m.member.id === member)?.member
|
||||||
|
.first_name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
[key as keyof IIssueFilterOptions]: (
|
||||||
|
filters[key as keyof IIssueFilterOptions] as any
|
||||||
|
)?.filter((p: any) => p !== member),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)
|
||||||
: (filters[key as keyof IIssueFilterOptions] as any)?.join(", ")}
|
: (filters[key as keyof IIssueFilterOptions] as any)?.join(", ")}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
@ -19,3 +19,4 @@ export * from "./tooltip";
|
|||||||
export * from "./labels-list";
|
export * from "./labels-list";
|
||||||
export * from "./linear-progress-indicator";
|
export * from "./linear-progress-indicator";
|
||||||
export * from "./empty-state";
|
export * from "./empty-state";
|
||||||
|
export * from "./multi-level-dropdown";
|
||||||
|
137
apps/app/components/ui/multi-level-dropdown.tsx
Normal file
137
apps/app/components/ui/multi-level-dropdown.tsx
Normal file
@ -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<MultiLevelDropdownProps> = (props) => {
|
||||||
|
const { label, options, onSelect, direction = "right" } = props;
|
||||||
|
|
||||||
|
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Menu.Button
|
||||||
|
onClick={() => {
|
||||||
|
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}
|
||||||
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items
|
||||||
|
static
|
||||||
|
className="absolute right-0 mt-2 w-36 origin-top-right select-none divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<div className="relative px-1 py-1" key={option.id}>
|
||||||
|
<Menu.Item
|
||||||
|
as="button"
|
||||||
|
onClick={(e: any) => {
|
||||||
|
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 }) => (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
active || option.selected ? "bg-gray-100" : "text-gray-900"
|
||||||
|
} group flex w-full items-center justify-between rounded-md px-2 py-2 text-sm`}
|
||||||
|
>
|
||||||
|
{direction === "left" && option.children && (
|
||||||
|
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{direction === "right" && option.children && (
|
||||||
|
<ChevronRightIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
{option.children && option.id === openChildFor && (
|
||||||
|
<Menu.Items
|
||||||
|
static
|
||||||
|
className={`absolute top-0 mt-2 w-36 origin-top-right select-none divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
|
||||||
|
direction === "left"
|
||||||
|
? "right-full -translate-x-2"
|
||||||
|
: "left-full translate-x-2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.children.map((child) => (
|
||||||
|
<div className="relative px-1 py-1" key={child.id}>
|
||||||
|
<Menu.Item as="div" className="flex items-center justify-between">
|
||||||
|
{({ active }) => (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(child.value);
|
||||||
|
}}
|
||||||
|
className={`${
|
||||||
|
active || option.selected ? "bg-gray-100" : "text-gray-900"
|
||||||
|
} group flex w-full items-center rounded-md px-2 py-2 text-sm capitalize`}
|
||||||
|
>
|
||||||
|
{child.label}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Menu.Items>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user