mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: calendar filters (#908)
* feat: hiding unnecessary filters for calendar view * feat: filters for calendar view * feat: module and cycle calendar view filters
This commit is contained in:
parent
2ba4594b29
commit
9129a6cde2
@ -26,7 +26,7 @@ import {
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { CustomMenu } from "components/ui";
|
||||
import { CustomMenu, Spinner } from "components/ui";
|
||||
// icon
|
||||
import {
|
||||
CheckIcon,
|
||||
@ -35,6 +35,8 @@ import {
|
||||
ChevronRightIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
@ -67,6 +69,8 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { params } = useIssuesView();
|
||||
|
||||
const [calendarDateRange, setCalendarDateRange] = useState<ICalendarRange>({
|
||||
startDate: startOfWeek(currentDate),
|
||||
endDate: lastDayOfWeek(currentDate),
|
||||
@ -82,11 +86,13 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
workspaceSlug && projectId ? PROJECT_CALENDAR_ISSUES(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
issuesService.getIssuesWithParams(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
targetDateFilter
|
||||
)
|
||||
issuesService.getIssuesWithParams(workspaceSlug as string, projectId as string, {
|
||||
...params,
|
||||
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
|
||||
calendarDateRange.endDate
|
||||
)};before`,
|
||||
group_by: null,
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
@ -100,7 +106,13 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycleId as string,
|
||||
targetDateFilter
|
||||
{
|
||||
...params,
|
||||
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
|
||||
calendarDateRange.endDate
|
||||
)};before`,
|
||||
group_by: null,
|
||||
}
|
||||
)
|
||||
: null
|
||||
);
|
||||
@ -115,7 +127,13 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
moduleId as string,
|
||||
targetDateFilter
|
||||
{
|
||||
...params,
|
||||
target_date: `${renderDateFormat(calendarDateRange.startDate)};after,${renderDateFormat(
|
||||
calendarDateRange.endDate
|
||||
)};before`,
|
||||
group_by: null,
|
||||
}
|
||||
)
|
||||
: null
|
||||
);
|
||||
@ -132,7 +150,11 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
|
||||
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
|
||||
|
||||
const calendarIssues = cycleCalendarIssues ?? moduleCalendarIssues ?? projectCalendarIssues;
|
||||
const calendarIssues = cycleId
|
||||
? cycleCalendarIssues
|
||||
: moduleId
|
||||
? moduleCalendarIssues
|
||||
: projectCalendarIssues;
|
||||
|
||||
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
||||
const filterIssue =
|
||||
@ -203,11 +225,11 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
return calendarIssues ? (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className="h-full overflow-y-auto rounded-lg text-gray-600 -m-2">
|
||||
<div className="-m-2 h-full overflow-y-auto rounded-lg text-gray-600">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="relative flex h-full w-full gap-2 items-center justify-start text-sm ">
|
||||
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
||||
<Popover className="flex h-full items-center justify-start rounded-lg">
|
||||
{({ open }) => (
|
||||
<>
|
||||
@ -227,8 +249,8 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute top-10 left-0 z-20 w-full max-w-xs flex flex-col transform overflow-hidden bg-brand-surface-2 shadow-lg rounded-[10px]">
|
||||
<div className="flex justify-center items-center text-sm gap-5 px-2 py-2">
|
||||
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-brand-surface-2 shadow-lg">
|
||||
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
||||
{yearOptions.map((year) => (
|
||||
<button
|
||||
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
||||
@ -236,19 +258,19 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
isSameYear(year.value, currentDate)
|
||||
? "text-sm font-medium text-gray-800"
|
||||
: "text-xs text-gray-400 "
|
||||
} hover:text-sm hover:text-gray-800 hover:font-medium`}
|
||||
} hover:text-sm hover:font-medium hover:text-gray-800`}
|
||||
>
|
||||
{year.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 px-2 border-t border-brand-base">
|
||||
<div className="grid grid-cols-4 border-t border-brand-base px-2">
|
||||
{monthOptions.map((month) => (
|
||||
<button
|
||||
onClick={() =>
|
||||
updateDate(updateDateWithMonth(month.value, currentDate))
|
||||
}
|
||||
className={`text-gray-400 text-xs px-2 py-2 hover:font-medium hover:text-gray-800 ${
|
||||
className={`px-2 py-2 text-xs text-gray-400 hover:font-medium hover:text-gray-800 ${
|
||||
isSameMonth(month.value, currentDate)
|
||||
? "font-medium text-gray-800"
|
||||
: ""
|
||||
@ -300,7 +322,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full gap-2 items-center justify-end">
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<button
|
||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 px-4 py-1.5 text-sm hover:bg-brand-surface-1 hover:text-brand-base focus:outline-none"
|
||||
onClick={() => {
|
||||
@ -317,6 +339,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
>
|
||||
Today{" "}
|
||||
</button>
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div
|
||||
@ -397,7 +420,7 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
{weeks.map((date, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center justify-start p-1.5 gap-2 border-brand-base bg-brand-surface-1 text-base font-medium text-gray-600 ${
|
||||
className={`flex items-center justify-start gap-2 border-brand-base bg-brand-surface-1 p-1.5 text-base font-medium text-gray-600 ${
|
||||
!isMonthlyView
|
||||
? showWeekEnds
|
||||
? (index + 1) % 7 === 0
|
||||
@ -480,5 +503,9 @@ export const CalendarView: React.FC<Props> = ({ addIssueToDate }) => {
|
||||
</div>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -137,50 +137,54 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-brand-surface-2 p-3 shadow-lg">
|
||||
<div className="relative divide-y-2 divide-brand-base">
|
||||
<div className="space-y-4 pb-3 text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) =>
|
||||
issueView === "kanban" && option.key === null ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
{issueView !== "calendar" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) =>
|
||||
issueView === "kanban" && option.key === null ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
width="lg"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
groupByProperty === "priority" && option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setOrderBy(option.key);
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Issue type</h4>
|
||||
<CustomMenu
|
||||
@ -204,62 +208,69 @@ export const IssuesFilterView: React.FC = () => {
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Show empty states</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
showEmptyGroups ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={showEmptyGroups}
|
||||
onClick={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||
>
|
||||
<span className="sr-only">Show empty groups</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-brand-accent"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-brand-secondary">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
return (
|
||||
{issueView !== "calendar" && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-brand-secondary">Show empty states</h4>
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-theme bg-theme text-white"
|
||||
: "border-gray-300"
|
||||
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||
showEmptyGroups ? "bg-green-500" : "bg-brand-surface-2"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
role="switch"
|
||||
aria-checked={showEmptyGroups}
|
||||
onClick={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||
>
|
||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||
<span className="sr-only">Show empty groups</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-brand-surface-2 shadow ring-0 transition duration-200 ease-in-out ${
|
||||
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex justify-end gap-x-3">
|
||||
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||
Reset to default
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-brand-accent"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{issueView !== "calendar" && (
|
||||
<div className="space-y-2 py-3">
|
||||
<h4 className="text-sm text-brand-secondary">Display Properties</h4>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{Object.keys(properties).map((key) => {
|
||||
if (key === "estimate" && !isEstimateActive) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-theme bg-theme text-white"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{key === "key" ? "ID" : replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
|
@ -396,41 +396,40 @@ export const IssuesView: React.FC<Props> = ({
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
{issueView !== "calendar" && (
|
||||
<>
|
||||
<div
|
||||
className={`flex items-center justify-between gap-2 ${
|
||||
issueView === "list" && areFiltersApplied ? "mt-6 px-8" : "-mt-2"
|
||||
}`}
|
||||
>
|
||||
<FilterList filters={filters} setFilters={setFilters} />
|
||||
{areFiltersApplied && (
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
setFilters({}, true);
|
||||
setToastAlert({
|
||||
title: "View updated",
|
||||
message: "Your view has been updated",
|
||||
type: "success",
|
||||
});
|
||||
} else
|
||||
setCreateViewModal({
|
||||
query: filters,
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
{!viewId && <PlusIcon className="h-4 w-4" />}
|
||||
{viewId ? "Update" : "Save"} view
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={`flex items-center justify-between gap-2 ${
|
||||
issueView === "list" && areFiltersApplied ? "mt-6 px-8" : "-mt-2"
|
||||
}`}
|
||||
>
|
||||
<FilterList filters={filters} setFilters={setFilters} />
|
||||
{areFiltersApplied && (
|
||||
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} />
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (viewId) {
|
||||
setFilters({}, true);
|
||||
setToastAlert({
|
||||
title: "View updated",
|
||||
message: "Your view has been updated",
|
||||
type: "success",
|
||||
});
|
||||
} else
|
||||
setCreateViewModal({
|
||||
query: filters,
|
||||
});
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
{!viewId && <PlusIcon className="h-4 w-4" />}
|
||||
{viewId ? "Update" : "Save"} view
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{areFiltersApplied && (
|
||||
<div className={` ${issueView === "list" ? "mt-4" : "my-4"} border-t`} />
|
||||
)}
|
||||
</>
|
||||
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<StrictModeDroppable droppableId="trashBox">
|
||||
{(provided, snapshot) => (
|
||||
|
@ -297,6 +297,14 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
||||
},
|
||||
});
|
||||
}
|
||||
if (property === "calendar") {
|
||||
dispatch({
|
||||
type: "SET_GROUP_BY_PROPERTY",
|
||||
payload: {
|
||||
groupByProperty: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user