feat: start date filter added across the platform (#1955)

Co-authored-by: Aaryan Khandelwal <aaryan610@Aaryans-MacBook-Pro.local>
This commit is contained in:
Aaryan Khandelwal 2023-08-24 19:45:23 +05:30 committed by GitHub
parent 802e6b3e8e
commit d18ac83909
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 216 additions and 64 deletions

View File

@ -11,15 +11,18 @@ import { Dialog, Transition } from "@headlessui/react";
// hooks
import useIssuesView from "hooks/use-issues-view";
// components
import { DueDateFilterSelect } from "./due-date-filter-select";
import { DateFilterSelect } from "./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";
import { IIssueFilterOptions } from "types";
type Props = {
title: string;
field: keyof IIssueFilterOptions;
isOpen: boolean;
handleClose: () => void;
};
@ -36,7 +39,7 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
};
export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) => {
export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleClose }) => {
const { filters, setFilters } = useIssuesView();
const router = useRouter();
@ -51,11 +54,11 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
if (filterType === "range") {
setFilters(
{ target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
{ [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
!Boolean(viewId)
);
} else {
const filteredArray = filters?.target_date?.filter((item) => {
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
if (item?.includes(filterType)) return false;
return true;
@ -64,13 +67,13 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne)
setFilters(
{ target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
{ [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
!Boolean(viewId)
);
else
setFilters(
{
target_date: [`${renderDateFormat(date1)};${filterType}`],
[field]: [`${renderDateFormat(date1)};${filterType}`],
},
!Boolean(viewId)
);
@ -116,7 +119,7 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
control={control}
name="filterType"
render={({ field: { value, onChange } }) => (
<DueDateFilterSelect value={value} onChange={onChange} />
<DateFilterSelect title={title} value={value} onChange={onChange} />
)}
/>
<XMarkIcon

View File

@ -7,6 +7,7 @@ import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "compon
// fetch-keys
type Props = {
title: string;
value: string;
onChange: (value: string) => void;
};
@ -19,29 +20,31 @@ type DueDate = {
const dueDateRange: DueDate[] = [
{
name: "Due date before",
name: "before",
value: "before",
icon: <CalendarBeforeIcon className="h-4 w-4 " />,
},
{
name: "Due date after",
name: "after",
value: "after",
icon: <CalendarAfterIcon className="h-4 w-4 " />,
},
{
name: "Due date range",
name: "range",
value: "range",
icon: <CalendarMonthIcon className="h-4 w-4 " />,
},
];
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
export const DateFilterSelect: React.FC<Props> = ({ title, 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>
<span>
{title} {dueDateRange.find((item) => item.value === value)?.name}
</span>
</div>
}
onChange={onChange}
@ -50,7 +53,7 @@ export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
<CustomSelect.Option key={index} value={option.value}>
<>
<span>{option.icon}</span>
{option.name}
{title} {option.name}
</>
</CustomSelect.Option>
))}

View File

@ -240,6 +240,34 @@ export const FiltersList: React.FC<Props> = ({
</div>
);
})
: key === "start_date"
? filters.start_date?.map((date: string) => {
if (filters.start_date && filters.start_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-custom-border-200 bg-custom-background-100 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({
start_date: filters.start_date?.filter((d: any) => d !== date),
})
}
>
<XMarkIcon className="h-3 w-3" />
</span>
</div>
);
})
: key === "target_date"
? filters.target_date?.map((date: string) => {
if (filters.target_date && filters.target_date.length <= 0) return null;

View File

@ -1,4 +1,4 @@
export * from "./due-date-filter-modal";
export * from "./due-date-filter-select";
export * from "./date-filter-modal";
export * from "./date-filter-select";
export * from "./filters-list";
export * from "./issues-view-filter";

View File

@ -119,14 +119,11 @@ 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
);
if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(filters[key] ?? [], option.value);
setFilters({
target_date: valueExists ? null : option.value,
[key]: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);

View File

@ -478,6 +478,7 @@ export const IssuesView: React.FC<Props> = ({
labels: null,
priority: null,
state: null,
start_date: null,
target_date: null,
type: null,
})

View File

@ -7,7 +7,7 @@ import useSWR from "swr";
// services
import issuesService from "services/issues.service";
// components
import { DueDateFilterModal } from "components/core";
import { DateFilterModal } from "components/core";
// ui
import { MultiLevelDropdown } from "components/ui";
// icons
@ -20,7 +20,7 @@ import { IIssueFilterOptions, IQuery } from "types";
import { WORKSPACE_LABELS } from "constants/fetch-keys";
// constants
import { GROUP_CHOICES, PRIORITIES } from "constants/project";
import { DUE_DATES } from "constants/due-dates";
import { DATE_FILTER_OPTIONS } from "constants/filters";
type Props = {
filters: Partial<IIssueFilterOptions> | IQuery;
@ -35,7 +35,14 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
direction = "right",
height = "md",
}) => {
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
const [dateFilterType, setDateFilterType] = useState<{
title: string;
type: "start_date" | "target_date";
}>({
title: "",
type: "start_date",
});
const [fetchLabels, setFetchLabels] = useState(false);
const router = useRouter();
@ -50,10 +57,12 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
return (
<>
{isDueDateFilterModalOpen && (
<DueDateFilterModal
isOpen={isDueDateFilterModalOpen}
handleClose={() => setIsDueDateFilterModalOpen(false)}
{isDateFilterModalOpen && (
<DateFilterModal
title={dateFilterType.title}
field={dateFilterType.type}
isOpen={isDateFilterModalOpen}
handleClose={() => setIsDateFilterModalOpen(false)}
/>
)}
<MultiLevelDropdown
@ -132,12 +141,48 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
})),
},
{
id: "target_date",
label: "Due date",
value: DUE_DATES,
id: "start_date",
label: "Start date",
value: DATE_FILTER_OPTIONS,
hasChildren: true,
children: [
...(DUE_DATES?.map((option) => ({
...(DATE_FILTER_OPTIONS?.map((option) => ({
id: option.name,
label: option.name,
value: {
key: "start_date",
value: option.value,
},
selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value),
})) ?? []),
{
id: "custom",
label: "Custom",
value: "custom",
element: (
<button
onClick={() => {
setIsDateFilterModalOpen(true);
setDateFilterType({
title: "Start date",
type: "start_date",
});
}}
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
>
Custom
</button>
),
},
],
},
{
id: "target_date",
label: "Due date",
value: DATE_FILTER_OPTIONS,
hasChildren: true,
children: [
...(DATE_FILTER_OPTIONS?.map((option) => ({
id: option.name,
label: option.name,
value: {
@ -152,7 +197,13 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
value: "custom",
element: (
<button
onClick={() => setIsDueDateFilterModalOpen(true)}
onClick={() => {
setIsDateFilterModalOpen(true);
setDateFilterType({
title: "Due date",
type: "target_date",
});
}}
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
>
Custom

View File

@ -89,14 +89,11 @@ export const MyIssuesViewOptions: React.FC = () => {
onSelect={(option) => {
const key = option.key as keyof typeof filters;
if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters?.target_date ?? [],
option.value
);
if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
setFilters({
target_date: valueExists ? null : option.value,
[key]: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);

View File

@ -249,6 +249,7 @@ export const MyIssuesView: React.FC<Props> = ({
labels: null,
priority: null,
state_group: null,
start_date: null,
target_date: null,
type: null,
})

View File

@ -115,14 +115,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
onSelect={(option) => {
const key = option.key as keyof typeof filters;
if (key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters?.target_date ?? [],
option.value
);
if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
setFilters({
target_date: valueExists ? null : option.value,
[key]: valueExists ? null : option.value,
});
} else {
const valueExists = filters[key]?.includes(option.value);

View File

@ -263,6 +263,7 @@ export const ProfileIssuesView = () => {
labels: null,
priority: null,
state_group: null,
start_date: null,
target_date: null,
type: null,
})

View File

@ -94,6 +94,7 @@ export const ViewForm: React.FC<Props> = ({
labels: null,
priority: null,
state: null,
start_date: null,
target_date: null,
type: null,
});
@ -155,14 +156,15 @@ export const ViewForm: React.FC<Props> = ({
onSelect={(option) => {
const key = option.key as keyof typeof filters;
if (key === "target_date") {
if (key === "start_date" || key === "target_date") {
const valueExists = checkIfArraysHaveSameElements(
filters?.target_date ?? [],
filters?.[key] ?? [],
option.value
);
setValue("query", {
target_date: valueExists ? null : option.value,
...filters,
[key]: valueExists ? null : option.value,
} as IQuery);
} else {
if (!filters?.[key]?.includes(option.value))

View File

@ -9,7 +9,7 @@ import stateService from "services/state.service";
import projectService from "services/project.service";
import issuesService from "services/issues.service";
// components
import { DueDateFilterModal } from "components/core";
import { DateFilterModal } from "components/core";
// ui
import { Avatar, MultiLevelDropdown } from "components/ui";
// icons
@ -23,7 +23,7 @@ import { IIssueFilterOptions, IQuery } from "types";
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";
import { DATE_FILTER_OPTIONS } from "constants/filters";
type Props = {
filters: Partial<IIssueFilterOptions> | IQuery;
@ -38,7 +38,14 @@ export const SelectFilters: React.FC<Props> = ({
direction = "right",
height = "md",
}) => {
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
const [dateFilterType, setDateFilterType] = useState<{
title: string;
type: "start_date" | "target_date";
}>({
title: "",
type: "start_date",
});
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -67,10 +74,12 @@ export const SelectFilters: React.FC<Props> = ({
return (
<>
{isDueDateFilterModalOpen && (
<DueDateFilterModal
isOpen={isDueDateFilterModalOpen}
handleClose={() => setIsDueDateFilterModalOpen(false)}
{isDateFilterModalOpen && (
<DateFilterModal
title={dateFilterType.title}
field={dateFilterType.type}
isOpen={isDateFilterModalOpen}
handleClose={() => setIsDateFilterModalOpen(false)}
/>
)}
<MultiLevelDropdown
@ -183,12 +192,48 @@ export const SelectFilters: React.FC<Props> = ({
})),
},
{
id: "target_date",
label: "Due date",
value: DUE_DATES,
id: "start_date",
label: "Start date",
value: DATE_FILTER_OPTIONS,
hasChildren: true,
children: [
...DUE_DATES.map((option) => ({
...DATE_FILTER_OPTIONS.map((option) => ({
id: option.name,
label: option.name,
value: {
key: "start_date",
value: option.value,
},
selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value),
})),
{
id: "custom",
label: "Custom",
value: "custom",
element: (
<button
onClick={() => {
setIsDateFilterModalOpen(true);
setDateFilterType({
title: "Start date",
type: "start_date",
});
}}
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
>
Custom
</button>
),
},
],
},
{
id: "target_date",
label: "Due date",
value: DATE_FILTER_OPTIONS,
hasChildren: true,
children: [
...DATE_FILTER_OPTIONS.map((option) => ({
id: option.name,
label: option.name,
value: {
@ -203,7 +248,13 @@ export const SelectFilters: React.FC<Props> = ({
value: "custom",
element: (
<button
onClick={() => setIsDueDateFilterModalOpen(true)}
onClick={() => {
setIsDateFilterModalOpen(true);
setDateFilterType({
title: "Due date",
type: "target_date",
});
}}
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
>
Custom

View File

@ -8,6 +8,7 @@ const paramsToKey = (params: any) => {
assignees,
created_by,
labels,
start_date,
target_date,
sub_issue,
start_target_date,
@ -19,6 +20,7 @@ const paramsToKey = (params: any) => {
let createdByKey = created_by ? created_by.split(",") : [];
let labelsKey = labels ? labels.split(",") : [];
const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE";
const startDateKey = start_date ?? "";
const targetDateKey = target_date ?? "";
const type = params.type ? params.type.toUpperCase() : "NULL";
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
@ -31,7 +33,7 @@ const paramsToKey = (params: any) => {
createdByKey = createdByKey.sort().join("_");
labelsKey = labelsKey.sort().join("_");
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`;
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`;
};
const inboxParamsToKey = (params: any) => {
@ -48,7 +50,16 @@ const inboxParamsToKey = (params: any) => {
};
const myIssuesParamsToKey = (params: any) => {
const { assignees, created_by, labels, priority, state_group, subscriber, target_date } = params;
const {
assignees,
created_by,
labels,
priority,
state_group,
subscriber,
start_date,
target_date,
} = params;
let assigneesKey = assignees ? assignees.split(",") : [];
let createdByKey = created_by ? created_by.split(",") : [];
@ -56,6 +67,7 @@ const myIssuesParamsToKey = (params: any) => {
let subscriberKey = subscriber ? subscriber.split(",") : [];
let priorityKey = priority ? priority.split(",") : [];
let labelsKey = labels ? labels.split(",") : [];
const startDateKey = start_date ?? "";
const targetDateKey = target_date ?? "";
const type = params.type ? params.type.toUpperCase() : "NULL";
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
@ -69,7 +81,7 @@ const myIssuesParamsToKey = (params: any) => {
priorityKey = priorityKey.sort().join("_");
labelsKey = labelsKey.sort().join("_");
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`;
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}`;
};
export const CURRENT_USER = "CURRENT_USER";

View File

@ -1,7 +1,7 @@
// helper
import { renderDateFormat } from "helpers/date-time.helper";
export const DUE_DATES = [
export const DATE_FILTER_OPTIONS = [
{
name: "Last week",
value: [

View File

@ -94,6 +94,7 @@ export const initialState: StateType = {
state_group: null,
subscriber: null,
created_by: null,
start_date: null,
target_date: null,
},
};

View File

@ -72,6 +72,7 @@ export const initialState: StateType = {
state_group: null,
subscriber: null,
created_by: null,
start_date: null,
target_date: null,
},
properties: {

View File

@ -26,6 +26,7 @@ const initialValues: IWorkspaceViewProps = {
priority: null,
state_group: null,
subscriber: null,
start_date: null,
target_date: null,
type: null,
},

View File

@ -27,6 +27,7 @@ const useMyIssues = (workspaceSlug: string | undefined) => {
priority: filters?.priority ? filters?.priority.join(",") : undefined,
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined,
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
type: filters?.type ? filters?.type : undefined,
};

View File

@ -58,6 +58,7 @@ const useIssuesView = () => {
type: filters?.type ? filters?.type : undefined,
labels: filters?.labels ? filters?.labels.join(",") : undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
sub_issue: showSubIssues,
};

View File

@ -44,6 +44,7 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
order_by: orderBy,
priority: filters?.priority ? filters?.priority.join(",") : undefined,
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
type: filters?.type ? filters?.type : undefined,
subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined,

View File

@ -215,6 +215,7 @@ export interface IIssueLite {
export interface IIssueFilterOptions {
type: "active" | "backlog" | null;
assignees: string[] | null;
start_date: string[] | null;
target_date: string[] | null;
state: string[] | null;
state_group: TStateGroups[] | null;

View File

@ -20,6 +20,7 @@ export interface IQuery {
labels: string[] | null;
priority: string[] | null;
state: string[] | null;
start_date: string[] | null;
target_date: string[] | null;
type: "active" | "backlog" | null;
}