forked from github/plane
chore: due date filter (#965)
* chore: due date filter * fix: deployment error * chore: optimized code * chore: created constants for due date * chore: create seperated css file for react datepicker styling * fix: due date filter * chore: highlight selected option * fix: merge conflicts * fix: build error * chore: date range selector validation * fix: issue views overflow * refactor: due date filter modal code * refactor: multi level dropdown * chore: due date filter select default value --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
parent
4ede04d72f
commit
4c3e2c252a
@ -170,7 +170,7 @@ export const CalendarView: React.FC<Props> = ({
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||
|
||||
return calendarIssues ? (
|
||||
<div className="h-full">
|
||||
<div className="h-full overflow-y-auto">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className="h-full rounded-lg p-8 text-brand-secondary">
|
||||
<CalendarHeader
|
||||
|
186
apps/app/components/core/filters/due-date-filter-modal.tsx
Normal file
186
apps/app/components/core/filters/due-date-filter-modal.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// components
|
||||
import { DueDateFilterSelect } from "./due-date-filter-select";
|
||||
// ui
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
// helpers
|
||||
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
type TFormValues = {
|
||||
filterType: "before" | "after" | "range";
|
||||
date1: Date;
|
||||
date2: Date;
|
||||
};
|
||||
|
||||
const defaultValues: TFormValues = {
|
||||
filterType: "range",
|
||||
date1: new Date(),
|
||||
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
||||
};
|
||||
|
||||
export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) => {
|
||||
const { filters, setFilters } = useIssuesView();
|
||||
|
||||
const router = useRouter();
|
||||
const { viewId } = router.query;
|
||||
|
||||
const { handleSubmit, watch, control } = useForm<TFormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const handleFormSubmit = (formData: TFormValues) => {
|
||||
const { filterType, date1, date2 } = formData;
|
||||
|
||||
if (filterType === "range") {
|
||||
setFilters(
|
||||
{ target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
|
||||
!Boolean(viewId)
|
||||
);
|
||||
} else {
|
||||
const filteredArray = filters?.target_date?.filter((item) => {
|
||||
if (item?.includes(filterType)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
|
||||
if (filterOne)
|
||||
setFilters(
|
||||
{ target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
|
||||
!Boolean(viewId)
|
||||
);
|
||||
else
|
||||
setFilters(
|
||||
{
|
||||
target_date: [`${renderDateFormat(date1)};${filterType}`],
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const isInvalid =
|
||||
watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
|
||||
|
||||
const nextDay = new Date(watch("date1"));
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-20 flex w-full justify-center overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex transform rounded-lg border border-brand-base bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form className="space-y-4" onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="flex w-full justify-between">
|
||||
<Controller
|
||||
control={control}
|
||||
name="filterType"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DueDateFilterSelect value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<XMarkIcon
|
||||
className="border-base h-4 w-4 cursor-pointer"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-between gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="date1"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DatePicker
|
||||
selected={value}
|
||||
onChange={(val) => onChange(val)}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
calendarClassName="h-full"
|
||||
inline
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{watch("filterType") === "range" && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="date2"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DatePicker
|
||||
selected={value}
|
||||
onChange={onChange}
|
||||
dateFormat="dd-MM-yyyy"
|
||||
calendarClassName="h-full"
|
||||
minDate={nextDay}
|
||||
inline
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{watch("filterType") === "range" && (
|
||||
<h6 className="text-xs flex items-center gap-1">
|
||||
<span className="text-brand-secondary">After:</span>
|
||||
<span>{renderShortDateWithYearFormat(watch("date1"))}</span>
|
||||
<span className="text-brand-secondary ml-1">Before:</span>
|
||||
{!isInvalid && <span>{renderShortDateWithYearFormat(watch("date2"))}</span>}
|
||||
</h6>
|
||||
)}
|
||||
<div className="flex justify-end gap-4">
|
||||
<SecondaryButton className="flex items-center gap-2" onClick={handleClose}>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="flex items-center gap-2"
|
||||
disabled={isInvalid}
|
||||
>
|
||||
Apply
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
58
apps/app/components/core/filters/due-date-filter-select.tsx
Normal file
58
apps/app/components/core/filters/due-date-filter-select.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// icons
|
||||
import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "components/icons";
|
||||
// fetch-keys
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
type DueDate = {
|
||||
name: string;
|
||||
value: string;
|
||||
icon: any;
|
||||
};
|
||||
|
||||
const dueDateRange: DueDate[] = [
|
||||
{
|
||||
name: "Due date before",
|
||||
value: "before",
|
||||
icon: <CalendarBeforeIcon className="h-4 w-4 " />,
|
||||
},
|
||||
{
|
||||
name: "Due date after",
|
||||
value: "after",
|
||||
icon: <CalendarAfterIcon className="h-4 w-4 " />,
|
||||
},
|
||||
{
|
||||
name: "Due date range",
|
||||
value: "range",
|
||||
icon: <CalendarMonthIcon className="h-4 w-4 " />,
|
||||
},
|
||||
];
|
||||
|
||||
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{dueDateRange.find((item) => item.value === value)?.icon}
|
||||
<span>{dueDateRange.find((item) => item.value === value)?.name}</span>
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
>
|
||||
{dueDateRange.map((option, index) => (
|
||||
<CustomSelect.Option key={index} value={option.value}>
|
||||
<>
|
||||
<span>{option.icon}</span>
|
||||
{option.name}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
@ -17,6 +17,7 @@ import stateService from "services/state.service";
|
||||
// types
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
|
||||
export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
const router = useRouter();
|
||||
@ -60,7 +61,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
className="flex items-center gap-x-2 rounded-full border border-brand-base bg-brand-surface-2 px-2 py-1"
|
||||
>
|
||||
<span className="capitalize text-brand-secondary">
|
||||
{replaceUnderscoreIfSnakeCase(key)}:
|
||||
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||
</span>
|
||||
{filters[key as keyof IIssueFilterOptions] === null ||
|
||||
(filters[key as keyof IIssueFilterOptions]?.length ?? 0) <= 0 ? (
|
||||
@ -299,6 +300,51 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : key === "target_date" ? (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{filters.target_date?.map((date: string) => {
|
||||
if (filters.target_date.length <= 0) return null;
|
||||
|
||||
const splitDate = date.split(";");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={date}
|
||||
className="inline-flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-base px-1 py-0.5"
|
||||
>
|
||||
<div className="h-1.5 w-1.5 rounded-full" />
|
||||
<span className="capitalize">
|
||||
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||
</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters(
|
||||
{
|
||||
target_date: filters.target_date?.filter(
|
||||
(d: any) => d !== date
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
)
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
target_date: null,
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
(filters[key as keyof IIssueFilterOptions] as any)?.join(", ")
|
||||
)}
|
||||
@ -332,6 +378,7 @@ export const FilterList: React.FC<any> = ({ filters, setFilters }) => {
|
||||
assignees: null,
|
||||
labels: null,
|
||||
created_by: null,
|
||||
target_date: null,
|
||||
})
|
||||
}
|
||||
className="flex items-center gap-x-1 rounded-full border border-brand-base bg-brand-surface-2 px-3 py-1.5 text-xs"
|
4
apps/app/components/core/filters/index.ts
Normal file
4
apps/app/components/core/filters/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./due-date-filter-modal";
|
||||
export * from "./due-date-filter-select";
|
||||
export * from "./filters-list";
|
||||
export * from "./issues-view-filter";
|
@ -2,11 +2,12 @@ import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
// headless ui
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// components
|
||||
import { SelectFilters } from "components/views";
|
||||
// ui
|
||||
@ -17,15 +18,14 @@ import {
|
||||
ListBulletIcon,
|
||||
Squares2X2Icon,
|
||||
CalendarDaysIcon,
|
||||
ChartBarIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { Properties } from "types";
|
||||
// constants
|
||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
|
||||
export const IssuesFilterView: React.FC = () => {
|
||||
const router = useRouter();
|
||||
@ -109,22 +109,30 @@ export const IssuesFilterView: React.FC = () => {
|
||||
onSelect={(option) => {
|
||||
const key = option.key as keyof typeof filters;
|
||||
|
||||
if (key === "target_date") {
|
||||
const valueExists = checkIfArraysHaveSameElements(
|
||||
filters.target_date ?? [],
|
||||
option.value
|
||||
);
|
||||
|
||||
setFilters({
|
||||
target_date: valueExists ? null : option.value,
|
||||
});
|
||||
} else {
|
||||
const valueExists = filters[key]?.includes(option.value);
|
||||
|
||||
if (valueExists) {
|
||||
if (valueExists)
|
||||
setFilters(
|
||||
{
|
||||
...(filters ?? {}),
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
} else {
|
||||
else
|
||||
setFilters(
|
||||
{
|
||||
...(filters ?? {}),
|
||||
[option.key]: [...((filters[key] ?? []) as any[]), option.value],
|
||||
},
|
||||
!Boolean(viewId)
|
@ -1,17 +1,12 @@
|
||||
export * from "./board-view";
|
||||
export * from "./calendar-view";
|
||||
export * from "./filters";
|
||||
export * from "./gantt-chart-view";
|
||||
export * from "./list-view";
|
||||
export * from "./modals";
|
||||
export * from "./spreadsheet-view";
|
||||
export * from "./sidebar";
|
||||
export * from "./bulk-delete-issues-modal";
|
||||
export * from "./existing-issues-list-modal";
|
||||
export * from "./filters-list";
|
||||
export * from "./gpt-assistant-modal";
|
||||
export * from "./image-upload-modal";
|
||||
export * from "./issues-view-filter";
|
||||
export * from "./issues-view";
|
||||
export * from "./link-modal";
|
||||
export * from "./image-picker-popover";
|
||||
export * from "./feeds";
|
||||
export * from "./theme-switch";
|
||||
|
@ -38,7 +38,7 @@ export const AllLists: React.FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
{groupedByIssues && (
|
||||
<div>
|
||||
<div className="h-full overflow-y-auto">
|
||||
{Object.keys(groupedByIssues).map((singleGroup) => {
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
|
5
apps/app/components/core/modals/index.ts
Normal file
5
apps/app/components/core/modals/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./bulk-delete-issues-modal";
|
||||
export * from "./existing-issues-list-modal";
|
||||
export * from "./gpt-assistant-modal";
|
||||
export * from "./image-upload-modal";
|
||||
export * from "./link-modal";
|
26
apps/app/components/icons/calendar-after-icon.tsx
Normal file
26
apps/app/components/icons/calendar-after-icon.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const CalendarAfterIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 18 19"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3309_70901)">
|
||||
<path
|
||||
d="M10.6125 17V15.875H14.625V7.8125H3.375V11.9375H2.25V4.25C2.25 3.95 2.3625 3.6875 2.5875 3.4625C2.8125 3.2375 3.075 3.125 3.375 3.125H4.59375V2H5.8125V3.125H12.1875V2H13.4063V3.125H14.625C14.925 3.125 15.1875 3.2375 15.4125 3.4625C15.6375 3.6875 15.75 3.95 15.75 4.25V15.875C15.75 16.175 15.6375 16.4375 15.4125 16.6625C15.1875 16.8875 14.925 17 14.625 17H10.6125ZM6 18.2375L5.2125 17.45L7.33125 15.3125H0.9375V14.1875H7.33125L5.2125 12.05L6 11.2625L9.4875 14.75L6 18.2375ZM3.375 6.6875H14.625V4.25H3.375V6.6875Z"
|
||||
fill="#858E96"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3309_70901">
|
||||
<rect width="18" height="18" fill="white" transform="translate(0 0.5)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
37
apps/app/components/icons/calendar-before-icon.tsx
Normal file
37
apps/app/components/icons/calendar-before-icon.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
|
||||
import type { Props } from "./types";
|
||||
|
||||
export const CalendarBeforeIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3309_70907)">
|
||||
<path
|
||||
d="M10.6125 16.5V15.375H14.625V7.3125H3.375V11.4375H2.25V3.75C2.25 3.45 2.3625 3.1875 2.5875 2.9625C2.8125 2.7375 3.075 2.625 3.375 2.625H4.59375V1.5H5.8125V2.625H12.1875V1.5H13.4062V2.625H14.625C14.925 2.625 15.1875 2.7375 15.4125 2.9625C15.6375 3.1875 15.75 3.45 15.75 3.75V15.375C15.75 15.675 15.6375 15.9375 15.4125 16.1625C15.1875 16.3875 14.925 16.5 14.625 16.5H10.6125ZM3.375 6.1875H14.625V3.75H3.375V6.1875Z"
|
||||
fill="#858E96"
|
||||
/>
|
||||
<g clipPath="url(#clip1_3309_70907)">
|
||||
<path
|
||||
d="M3.99967 17.1667L1.33301 14.5L3.99967 11.8334L4.34967 12.1834L2.28301 14.25H8V14.75H2.28301L4.34967 16.8167L3.99967 17.1667Z"
|
||||
fill="#858E96"
|
||||
stroke="#858E96"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3309_70907">
|
||||
<rect width="18" height="18" fill="white" />
|
||||
</clipPath>
|
||||
<clipPath id="clip1_3309_70907">
|
||||
<rect width="8" height="8" fill="white" transform="translate(0 10.5)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
@ -13,7 +13,7 @@ export const CalendarMonthIcon: React.FC<Props> = ({ width = "24", height = "24"
|
||||
>
|
||||
<path
|
||||
d="M12 14C11.7167 14 11.4792 13.9042 11.2875 13.7125C11.0958 13.5208 11 13.2833 11 13C11 12.7167 11.0958 12.4792 11.2875 12.2875C11.4792 12.0958 11.7167 12 12 12C12.2833 12 12.5208 12.0958 12.7125 12.2875C12.9042 12.4792 13 12.7167 13 13C13 13.2833 12.9042 13.5208 12.7125 13.7125C12.5208 13.9042 12.2833 14 12 14ZM8 14C7.71667 14 7.47917 13.9042 7.2875 13.7125C7.09583 13.5208 7 13.2833 7 13C7 12.7167 7.09583 12.4792 7.2875 12.2875C7.47917 12.0958 7.71667 12 8 12C8.28333 12 8.52083 12.0958 8.7125 12.2875C8.90417 12.4792 9 12.7167 9 13C9 13.2833 8.90417 13.5208 8.7125 13.7125C8.52083 13.9042 8.28333 14 8 14ZM16 14C15.7167 14 15.4792 13.9042 15.2875 13.7125C15.0958 13.5208 15 13.2833 15 13C15 12.7167 15.0958 12.4792 15.2875 12.2875C15.4792 12.0958 15.7167 12 16 12C16.2833 12 16.5208 12.0958 16.7125 12.2875C16.9042 12.4792 17 12.7167 17 13C17 13.2833 16.9042 13.5208 16.7125 13.7125C16.5208 13.9042 16.2833 14 16 14ZM12 18C11.7167 18 11.4792 17.9042 11.2875 17.7125C11.0958 17.5208 11 17.2833 11 17C11 16.7167 11.0958 16.4792 11.2875 16.2875C11.4792 16.0958 11.7167 16 12 16C12.2833 16 12.5208 16.0958 12.7125 16.2875C12.9042 16.4792 13 16.7167 13 17C13 17.2833 12.9042 17.5208 12.7125 17.7125C12.5208 17.9042 12.2833 18 12 18ZM8 18C7.71667 18 7.47917 17.9042 7.2875 17.7125C7.09583 17.5208 7 17.2833 7 17C7 16.7167 7.09583 16.4792 7.2875 16.2875C7.47917 16.0958 7.71667 16 8 16C8.28333 16 8.52083 16.0958 8.7125 16.2875C8.90417 16.4792 9 16.7167 9 17C9 17.2833 8.90417 17.5208 8.7125 17.7125C8.52083 17.9042 8.28333 18 8 18ZM16 18C15.7167 18 15.4792 17.9042 15.2875 17.7125C15.0958 17.5208 15 17.2833 15 17C15 16.7167 15.0958 16.4792 15.2875 16.2875C15.4792 16.0958 15.7167 16 16 16C16.2833 16 16.5208 16.0958 16.7125 16.2875C16.9042 16.4792 17 16.7167 17 17C17 17.2833 16.9042 17.5208 16.7125 17.7125C16.5208 17.9042 16.2833 18 16 18ZM4.5 22C4.1 22 3.75 21.85 3.45 21.55C3.15 21.25 3 20.9 3 20.5V5C3 4.6 3.15 4.25 3.45 3.95C3.75 3.65 4.1 3.5 4.5 3.5H6.125V2.8C6.125 2.56667 6.2 2.375 6.35 2.225C6.5 2.075 6.69167 2 6.925 2C7.15833 2 7.35417 2.075 7.5125 2.225C7.67083 2.375 7.75 2.56667 7.75 2.8V3.5H16.25V2.8C16.25 2.56667 16.325 2.375 16.475 2.225C16.625 2.075 16.8167 2 17.05 2C17.2833 2 17.4792 2.075 17.6375 2.225C17.7958 2.375 17.875 2.56667 17.875 2.8V3.5H19.5C19.9 3.5 20.25 3.65 20.55 3.95C20.85 4.25 21 4.6 21 5V20.5C21 20.9 20.85 21.25 20.55 21.55C20.25 21.85 19.9 22 19.5 22H4.5ZM4.5 20.5H19.5V9.75H4.5V20.5ZM4.5 8.25H19.5V5H4.5V8.25ZM4.5 8.25V5V8.25Z"
|
||||
fill="#212529"
|
||||
fill="#858E96"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
@ -4,6 +4,8 @@ export * from "./backlog-state-icon";
|
||||
export * from "./blocked-icon";
|
||||
export * from "./blocker-icon";
|
||||
export * from "./bolt-icon";
|
||||
export * from "./calendar-before-icon";
|
||||
export * from "./calendar-after-icon";
|
||||
export * from "./calendar-month-icon";
|
||||
export * from "./cancel-icon";
|
||||
export * from "./cancelled-state-icon";
|
||||
|
@ -18,6 +18,7 @@ type MultiLevelDropdownProps = {
|
||||
label: string | JSX.Element;
|
||||
value: any;
|
||||
selected?: boolean;
|
||||
element?: JSX.Element;
|
||||
}[];
|
||||
}[];
|
||||
onSelect: (value: any) => void;
|
||||
@ -35,6 +36,7 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
const [openChildFor, setOpenChildFor] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
{({ open }) => (
|
||||
<>
|
||||
@ -101,7 +103,7 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
</Menu.Item>
|
||||
{option.children && option.id === openChildFor && (
|
||||
<div
|
||||
className={`absolute top-0 w-auto min-w-[144px] max-w-[192px] origin-top-right select-none overflow-y-scroll rounded-md bg-brand-surface-1 shadow-lg focus:outline-none ${
|
||||
className={`absolute top-0 w-36 origin-top-right select-none overflow-y-scroll rounded-md bg-brand-surface-1 shadow-lg focus:outline-none ${
|
||||
direction === "left"
|
||||
? "right-full -translate-x-1"
|
||||
: "left-full translate-x-1"
|
||||
@ -118,25 +120,27 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-1 p-1">
|
||||
{option.children.map((child) => (
|
||||
{option.children.map((child) => {
|
||||
if (child.element) return child.element;
|
||||
else
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelect(child.value);
|
||||
}}
|
||||
onClick={() => onSelect(child.value)}
|
||||
className={`${
|
||||
child.selected ? "bg-brand-surface-2" : ""
|
||||
} flex w-full items-center justify-between whitespace-nowrap break-words rounded px-1 py-1.5 text-left capitalize text-brand-secondary hover:bg-brand-surface-2`}
|
||||
} flex w-full items-center justify-between break-words rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2`}
|
||||
>
|
||||
{child.label}
|
||||
{child.label}{" "}
|
||||
<CheckIcon
|
||||
className={`h-3.5 w-3.5 opacity-0 ${
|
||||
child.selected ? "opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -147,5 +151,6 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@ -6,18 +8,22 @@ import useSWR from "swr";
|
||||
import stateService from "services/state.service";
|
||||
import projectService from "services/project.service";
|
||||
import issuesService from "services/issues.service";
|
||||
// components
|
||||
import { DueDateFilterModal } from "components/core";
|
||||
// ui
|
||||
import { Avatar, MultiLevelDropdown } from "components/ui";
|
||||
// icons
|
||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IQuery } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
import { DUE_DATES } from "constants/due-dates";
|
||||
|
||||
type Props = {
|
||||
filters: Partial<IIssueFilterOptions> | IQuery;
|
||||
@ -32,6 +38,8 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
direction = "right",
|
||||
height = "md",
|
||||
}) => {
|
||||
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -58,6 +66,13 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDueDateFilterModalOpen && (
|
||||
<DueDateFilterModal
|
||||
isOpen={isDueDateFilterModalOpen}
|
||||
handleClose={() => setIsDueDateFilterModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<MultiLevelDropdown
|
||||
label="Filters"
|
||||
onSelect={onSelect}
|
||||
@ -72,7 +87,7 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
...PRIORITIES.map((priority) => ({
|
||||
id: priority === null ? "null" : priority,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
{getPriorityIcon(priority)} {priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
@ -129,7 +144,7 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
},
|
||||
{
|
||||
id: "created_by",
|
||||
label: "Created By",
|
||||
label: "Created by",
|
||||
value: members,
|
||||
children: [
|
||||
...(members?.map((member) => ({
|
||||
@ -162,7 +177,8 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
<div
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000000",
|
||||
backgroundColor:
|
||||
label.color && label.color !== "" ? label.color : "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
@ -176,7 +192,37 @@ export const SelectFilters: React.FC<Props> = ({
|
||||
})) ?? []),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "target_date",
|
||||
label: "Due date",
|
||||
value: DUE_DATES,
|
||||
children: [
|
||||
...(DUE_DATES?.map((option) => ({
|
||||
id: option.name,
|
||||
label: option.name,
|
||||
value: {
|
||||
key: "target_date",
|
||||
value: option.value,
|
||||
},
|
||||
selected: checkIfArraysHaveSameElements(filters?.target_date ?? [], option.value),
|
||||
})) ?? []),
|
||||
{
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
value: "custom",
|
||||
element: (
|
||||
<button
|
||||
onClick={() => setIsDueDateFilterModalOpen(true)}
|
||||
className="w-full rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
37
apps/app/constants/due-dates.ts
Normal file
37
apps/app/constants/due-dates.ts
Normal file
@ -0,0 +1,37 @@
|
||||
// helper
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
export const DUE_DATES = [
|
||||
{
|
||||
name: "Last week",
|
||||
value: [
|
||||
`${renderDateFormat(new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000))};after`,
|
||||
`${renderDateFormat(new Date())};before`,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "2 weeks from now",
|
||||
value: [
|
||||
`${renderDateFormat(new Date())};after`,
|
||||
`${renderDateFormat(new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000))};before`,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "1 month from now",
|
||||
value: [
|
||||
`${renderDateFormat(new Date())};after`,
|
||||
`${renderDateFormat(
|
||||
new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate())
|
||||
)};before`,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "2 months from now",
|
||||
value: [
|
||||
`${renderDateFormat(new Date())};after`,
|
||||
`${renderDateFormat(
|
||||
new Date(new Date().getFullYear(), new Date().getMonth() + 2, new Date().getDate())
|
||||
)};before`,
|
||||
],
|
||||
},
|
||||
];
|
@ -1,3 +1,4 @@
|
||||
|
||||
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||
|
||||
export const GROUP_CHOICES = {
|
||||
|
@ -89,6 +89,7 @@ export const initialState: StateType = {
|
||||
issue__assignees__id: null,
|
||||
issue__labels__id: null,
|
||||
created_by: null,
|
||||
target_date: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -42,3 +42,11 @@ export const findStringWithMostCharacters = (strings: string[]) =>
|
||||
strings.reduce((longestString, currentString) =>
|
||||
currentString.length > longestString.length ? currentString : longestString
|
||||
);
|
||||
|
||||
export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | null): boolean => {
|
||||
if (!arr1 || !arr2) return false;
|
||||
if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false;
|
||||
if (arr1.length === 0 && arr2.length === 0) return true;
|
||||
|
||||
return arr1.length === arr2.length && arr1.every((e) => arr2.includes(e));
|
||||
};
|
||||
|
@ -60,6 +60,7 @@ const useIssuesView = () => {
|
||||
? filters?.issue__labels__id.join(",")
|
||||
: undefined,
|
||||
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
||||
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
||||
};
|
||||
|
||||
const { data: projectIssues } = useSWR(
|
||||
|
@ -159,7 +159,7 @@ const SingleCycle: React.FC = () => {
|
||||
>
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
<div
|
||||
className={`h-full ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
||||
className={`h-full flex flex-col ${cycleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
|
@ -99,7 +99,9 @@ const ProjectIssues: NextPage = () => {
|
||||
}
|
||||
>
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<IssuesView />
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</IssueViewContextProvider>
|
||||
);
|
||||
|
@ -164,7 +164,7 @@ const SingleModule: React.FC = () => {
|
||||
<AnalyticsProjectModal isOpen={analyticsModal} onClose={() => setAnalyticsModal(false)} />
|
||||
|
||||
<div
|
||||
className={`h-full ${moduleSidebar ? "mr-[24rem]" : ""} ${
|
||||
className={`h-full flex flex-col ${moduleSidebar ? "mr-[24rem]" : ""} ${
|
||||
analyticsModal ? "mr-[50%]" : ""
|
||||
} duration-300`}
|
||||
>
|
||||
|
@ -101,7 +101,9 @@ const SingleView: React.FC = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<IssuesView />
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</IssueViewContextProvider>
|
||||
);
|
||||
|
@ -179,25 +179,6 @@ body {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* react datepicker styling */
|
||||
.react-datepicker-wrapper input::placeholder {
|
||||
color: rgba(var(--color-text-secondary));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.react-datepicker-wrapper input:-ms-input-placeholder {
|
||||
color: rgba(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.react-datepicker-wrapper .react-datepicker__close-icon::after {
|
||||
background: transparent;
|
||||
color: rgba(var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.react-datepicker-popper {
|
||||
z-index: 30 !important;
|
||||
}
|
||||
|
||||
.conical-gradient {
|
||||
background: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
|
@ -81,7 +81,7 @@
|
||||
}
|
||||
|
||||
.react-datepicker__day-name {
|
||||
color: rgba(var(--color-text-base)) !important;
|
||||
color: rgba(var(--color-text-secondary)) !important;
|
||||
}
|
||||
|
||||
.react-datepicker__week {
|
||||
|
1
apps/app/types/issues.d.ts
vendored
1
apps/app/types/issues.d.ts
vendored
@ -239,6 +239,7 @@ export interface IIssueLite {
|
||||
export interface IIssueFilterOptions {
|
||||
type: "active" | "backlog" | null;
|
||||
assignees: string[] | null;
|
||||
target_date: string[] | null;
|
||||
state: string[] | null;
|
||||
labels: string[] | null;
|
||||
issue__assignees__id: string[] | null;
|
||||
|
Loading…
Reference in New Issue
Block a user