mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
style: filter issues dropdown (#466)
This commit is contained in:
parent
0f06589b83
commit
23c468786d
@ -13,21 +13,24 @@ 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
|
||||||
|
import { PRIORITIES } from "constants/project";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, MultiLevelDropdown } from "components/ui";
|
import { Avatar, 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";
|
||||||
|
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";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueLabels, Properties } from "types";
|
import { IIssueLabels, Properties } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATE_LIST } from "constants/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";
|
||||||
import { PRIORITIES } from "constants/project";
|
|
||||||
|
|
||||||
export const IssuesFilterView: React.FC = () => {
|
export const IssuesFilterView: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -97,7 +100,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MultiLevelDropdown
|
<MultiLevelDropdown
|
||||||
label="Filter"
|
label="Filters"
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
setFilters({
|
setFilters({
|
||||||
...filters,
|
...filters,
|
||||||
@ -116,7 +119,11 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
children: [
|
children: [
|
||||||
...PRIORITIES.map((priority) => ({
|
...PRIORITIES.map((priority) => ({
|
||||||
id: priority ?? "none",
|
id: priority ?? "none",
|
||||||
label: priority ?? "None",
|
label: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
value: {
|
value: {
|
||||||
key: "priority",
|
key: "priority",
|
||||||
value: priority,
|
value: priority,
|
||||||
@ -132,7 +139,11 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
children: [
|
children: [
|
||||||
...statesList.map((state) => ({
|
...statesList.map((state) => ({
|
||||||
id: state.id,
|
id: state.id,
|
||||||
label: state.name,
|
label: (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
value: {
|
value: {
|
||||||
key: "state",
|
key: "state",
|
||||||
value: state.id,
|
value: state.id,
|
||||||
@ -148,7 +159,14 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
children: [
|
children: [
|
||||||
...(members?.map((member) => ({
|
...(members?.map((member) => ({
|
||||||
id: member.member.id,
|
id: member.member.id,
|
||||||
label: member.member.first_name,
|
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: {
|
value: {
|
||||||
key: "assignee",
|
key: "assignee",
|
||||||
value: member.member.id,
|
value: member.member.id,
|
||||||
@ -163,12 +181,12 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
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 ${
|
className={`group flex items-center gap-2 rounded-md border bg-transparent px-3 py-1.5 text-xs hover:bg-gray-100 hover:text-gray-900 focus:outline-none ${
|
||||||
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
|
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
|
@ -42,6 +42,7 @@ import {
|
|||||||
STATE_LIST,
|
STATE_LIST,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
import { EmptySpace, EmptySpaceItem } from "components/ui";
|
import { EmptySpace, EmptySpaceItem } from "components/ui";
|
||||||
|
import { PrimaryButton } from "components/ui/button/primary-button";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
type?: "issue" | "cycle" | "module";
|
||||||
@ -496,20 +497,21 @@ export const IssuesView: React.FC<Props> = ({ type = "issue", openIssuesListModa
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{Object.keys(filters).length > 0 && (
|
||||||
<button
|
<PrimaryButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCreateViewModal({
|
setCreateViewModal({
|
||||||
query: filters,
|
query: filters,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="flex items-center gap-x-0.5 text-sm"
|
className="flex items-center gap-4 text-sm"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-3 w-3" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
<span>Save view</span>
|
Save view
|
||||||
</button>
|
</PrimaryButton>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
<StrictModeDroppable droppableId="trashBox">
|
<StrictModeDroppable droppableId="trashBox">
|
||||||
|
@ -18,7 +18,7 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
|
|||||||
label={
|
label={
|
||||||
<div className="flex items-center justify-center gap-2 text-xs">
|
<div className="flex items-center justify-center gap-2 text-xs">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
{getPriorityIcon(value, `${value ? "text-xs" : "text-xs text-gray-500"}`)}
|
{getPriorityIcon(value, `text-xs ${value ? "" : "text-gray-500"}`)}
|
||||||
</span>
|
</span>
|
||||||
<span className={`${value ? "text-gray-600" : "text-gray-500"} capitalize`}>
|
<span className={`${value ? "text-gray-600" : "text-gray-500"} capitalize`}>
|
||||||
{value ?? "Priority"}
|
{value ?? "Priority"}
|
||||||
|
@ -136,8 +136,12 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
className={`${sidebarCollapse ? "" : "ml-[2.25rem]"} flex flex-col gap-y-1`}
|
className={`${sidebarCollapse ? "" : "ml-[2.25rem]"} flex flex-col gap-y-1`}
|
||||||
>
|
>
|
||||||
{navigation(workspaceSlug as string, project?.id).map((item) => {
|
{navigation(workspaceSlug as string, project?.id).map((item) => {
|
||||||
if (item.name === "Cycles" && !project.cycle_view) return;
|
if (
|
||||||
if (item.name === "Modules" && !project.module_view) return;
|
(item.name === "Cycles" && !project.cycle_view) ||
|
||||||
|
(item.name === "Modules" && !project.module_view) ||
|
||||||
|
(item.name === "Views" && !project.issue_views_view)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={item.name} href={item.href}>
|
<Link key={item.name} href={item.href}>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { ChevronDownIcon, ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
|
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
type MultiLevelDropdownProps = {
|
type MultiLevelDropdownProps = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -11,7 +12,7 @@ type MultiLevelDropdownProps = {
|
|||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
children?: {
|
children?: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string | JSX.Element;
|
||||||
value: any;
|
value: any;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
}[];
|
}[];
|
||||||
@ -20,26 +21,27 @@ type MultiLevelDropdownProps = {
|
|||||||
direction?: "left" | "right";
|
direction?: "left" | "right";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = (props) => {
|
export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||||
const { label, options, onSelect, direction = "right" } = props;
|
label,
|
||||||
|
options,
|
||||||
|
onSelect,
|
||||||
|
direction = "right",
|
||||||
|
}) => {
|
||||||
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
onClick={() => {
|
onClick={() => setOpenChildFor(null)}
|
||||||
setOpenChildFor(null);
|
className={`group flex items-center justify-between gap-2 rounded-md border px-3 py-1.5 text-xs shadow-sm duration-300 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
||||||
}}
|
|
||||||
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"
|
open ? "bg-gray-100 text-gray-900" : "text-gray-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</div>
|
</div>
|
||||||
<Transition
|
<Transition
|
||||||
@ -53,10 +55,10 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = (props) =>
|
|||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
static
|
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"
|
className="absolute right-0 mt-1 w-36 origin-top-right select-none rounded-md bg-white text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
>
|
>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<div className="relative px-1 py-1" key={option.id}>
|
<div className="relative p-1" key={option.id}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
as="button"
|
as="button"
|
||||||
onClick={(e: any) => {
|
onClick={(e: any) => {
|
||||||
@ -81,7 +83,9 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = (props) =>
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active || option.selected ? "bg-gray-100" : "text-gray-900"
|
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`}
|
} flex items-center gap-1 rounded px-1 py-1.5 ${
|
||||||
|
direction === "right" ? "justify-between" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{direction === "left" && option.children && (
|
{direction === "left" && option.children && (
|
||||||
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
<ChevronLeftIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
@ -97,33 +101,31 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = (props) =>
|
|||||||
{option.children && option.id === openChildFor && (
|
{option.children && option.id === openChildFor && (
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
static
|
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 ${
|
className={`absolute top-0 w-36 origin-top-right select-none rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none ${
|
||||||
direction === "left"
|
direction === "left"
|
||||||
? "right-full -translate-x-2"
|
? "right-full -translate-x-1"
|
||||||
: "left-full translate-x-2"
|
: "left-full translate-x-1"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{option.children.map((child) => (
|
<div className="p-1">
|
||||||
<div className="relative px-1 py-1" key={child.id}>
|
{option.children.map((child) => (
|
||||||
<Menu.Item as="div" className="flex items-center justify-between">
|
<Menu.Item
|
||||||
{({ active }) => (
|
key={child.id}
|
||||||
<>
|
as="button"
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => {
|
||||||
onClick={() => {
|
onSelect(child.value);
|
||||||
onSelect(child.value);
|
}}
|
||||||
}}
|
className={({ active }) =>
|
||||||
className={`${
|
`${
|
||||||
active || option.selected ? "bg-gray-100" : "text-gray-900"
|
active || option.selected ? "bg-gray-100" : "text-gray-900"
|
||||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm capitalize`}
|
} flex w-full items-center rounded px-1 py-1.5 capitalize`
|
||||||
>
|
}
|
||||||
{child.label}
|
>
|
||||||
</button>
|
{child.label}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,7 @@ import { IProject, UserAuth } from "types";
|
|||||||
import type { NextPage, GetServerSidePropsContext } from "next";
|
import type { NextPage, GetServerSidePropsContext } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
import { PROJECTS_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||||
import { ContrastIcon, PeopleGroupIcon } from "components/icons";
|
import { ContrastIcon, PeopleGroupIcon, ViewListIcon } from "components/icons";
|
||||||
|
|
||||||
const FeaturesSettings: NextPage<UserAuth> = (props) => {
|
const FeaturesSettings: NextPage<UserAuth> = (props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -93,7 +93,7 @@ const FeaturesSettings: NextPage<UserAuth> = (props) => {
|
|||||||
<section className="space-y-8">
|
<section className="space-y-8">
|
||||||
<h3 className="text-2xl font-semibold">Features</h3>
|
<h3 className="text-2xl font-semibold">Features</h3>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border bg-white p-6">
|
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border bg-white p-5">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<ContrastIcon color="#3f76ff" width={28} height={28} className="flex-shrink-0" />
|
<ContrastIcon color="#3f76ff" width={28} height={28} className="flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
@ -122,7 +122,7 @@ const FeaturesSettings: NextPage<UserAuth> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border bg-white p-6">
|
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border bg-white p-5">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<PeopleGroupIcon color="#ff6b00" width={28} height={28} className="flex-shrink-0" />
|
<PeopleGroupIcon color="#ff6b00" width={28} height={28} className="flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
@ -151,6 +151,35 @@ const FeaturesSettings: NextPage<UserAuth> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-x-8 gap-y-2 rounded-[10px] border bg-white p-5">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<ViewListIcon color="#ff6b00" width={28} height={28} className="flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h4 className="-mt-1.5 text-xl font-semibold">Views</h4>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
Modules are enabled for all the projects in this workspace. Access it from the
|
||||||
|
navigation bar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
|
projectDetails?.issue_views_view ? "bg-green-500" : "bg-gray-200"
|
||||||
|
}`}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={projectDetails?.issue_views_view}
|
||||||
|
onClick={() => handleSubmit({ issue_views_view: !projectDetails?.issue_views_view })}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Use views</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
projectDetails?.issue_views_view ? "translate-x-5" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<a href="https://plane.so/" target="_blank" rel="noreferrer">
|
<a href="https://plane.so/" target="_blank" rel="noreferrer">
|
||||||
|
1
apps/app/types/projects.d.ts
vendored
1
apps/app/types/projects.d.ts
vendored
@ -11,6 +11,7 @@ export interface IProject {
|
|||||||
id: string;
|
id: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
is_favorite: boolean;
|
is_favorite: boolean;
|
||||||
|
issue_views_view: boolean;
|
||||||
module_view: boolean;
|
module_view: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
network: number;
|
network: number;
|
||||||
|
Loading…
Reference in New Issue
Block a user