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:
Anmol Singh Bhatia 2023-04-21 01:42:09 +05:30 committed by GitHub
parent 2ba4594b29
commit 9129a6cde2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 190 additions and 145 deletions

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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) => (

View File

@ -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;