refactor: filter components, constants and helper functions (#2297)

* refactor: filters and display filters to accept handlers as props

* refactor: filters and display filters folder structure

* refactor: change issue layout options constant structure

* chore: display filters validations

* chore: view less filters functionality

* fix: display filters validation

* refactor: wrap functions around useCallback

* chore: start and target date filter options added

* refactor: query params generator function

* fix: query params generator function
This commit is contained in:
Aaryan Khandelwal 2023-09-29 13:09:38 +05:30 committed by GitHub
parent b70047b1d5
commit 479c145b02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1298 additions and 1340 deletions

View File

@ -1,11 +1,8 @@
import { Fragment } from "react";
// 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";
// components
import { DateFilterSelect } from "./date-filter-select";
// ui
@ -14,15 +11,12 @@ import { PrimaryButton, SecondaryButton } from "components/ui";
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;
filters: IIssueFilterOptions;
handleClose: () => void;
isOpen: boolean;
onSelect: (option: any) => void;
onSelect: (val: string[]) => void;
};
type TFormValues = {
@ -37,14 +31,7 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
};
export const DateFilterModal: React.FC<Props> = ({
title,
field,
filters,
handleClose,
isOpen,
onSelect,
}) => {
export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, onSelect }) => {
const { handleSubmit, watch, control } = useForm<TFormValues>({
defaultValues,
});
@ -52,32 +39,13 @@ export const DateFilterModal: React.FC<Props> = ({
const handleFormSubmit = (formData: TFormValues) => {
const { filterType, date1, date2 } = formData;
if (filterType === "range") {
onSelect({
key: field,
value: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`],
});
} else {
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
if (item?.includes(filterType)) return false;
if (filterType === "range") onSelect([`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`]);
else onSelect([`${renderDateFormat(date1)};${filterType}`]);
return true;
});
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
if (filterOne)
onSelect({ key: field, value: [filterOne, `${renderDateFormat(date1)};${filterType}`] });
else
onSelect({
key: field,
value: [`${renderDateFormat(date1)};${filterType}`],
});
}
handleClose();
};
const isInvalid =
watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
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);
@ -117,10 +85,7 @@ export const DateFilterModal: React.FC<Props> = ({
<DateFilterSelect title={title} value={value} onChange={onChange} />
)}
/>
<XMarkIcon
className="border-base h-4 w-4 cursor-pointer"
onClick={handleClose}
/>
<XMarkIcon className="border-base h-4 w-4 cursor-pointer" onClick={handleClose} />
</div>
<div className="flex w-full justify-between gap-4">
<Controller
@ -165,11 +130,7 @@ export const DateFilterModal: React.FC<Props> = ({
<SecondaryButton className="flex items-center gap-2" onClick={handleClose}>
Cancel
</SecondaryButton>
<PrimaryButton
type="submit"
className="flex items-center gap-2"
disabled={isInvalid}
>
<PrimaryButton type="submit" className="flex items-center gap-2" disabled={isInvalid}>
Apply
</PrimaryButton>
</div>

View File

@ -1,42 +1,98 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DisplayFiltersSelection, FilterSelection, IssueDropdown, LayoutSelection } from "components/issue-layouts";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
// types
import { TIssueLayouts } from "types";
import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssueLayouts } from "types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
export const ProjectIssuesHeader = observer(() => {
export const ProjectIssuesHeader: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { issueFilter: issueFilterStore } = useMobxStore();
const handleLayoutChange = (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
},
});
};
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilterStore.userFilters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (issueFilterStore.userFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: newValues,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
return (
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["calendar", "gantt_chart", "kanban", "list", "spreadsheet"]}
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={issueFilterStore.userDisplayFilters.layout ?? "list"}
/>
<IssueDropdown title="Filters">
<FilterSelection workspaceSlug={workspaceSlug?.toString() ?? ""} projectId={projectId?.toString() ?? ""} />
</IssueDropdown>
<IssueDropdown title="View">
<DisplayFiltersSelection />
</IssueDropdown>
<FiltersDropdown title="Filters">
<FilterSelection
filters={issueFilterStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[issueFilterStore.userDisplayFilters.layout ?? "list"]
}
projectId={projectId?.toString() ?? ""}
/>
</FiltersDropdown>
<FiltersDropdown title="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
layoutDisplayFiltersOptions={
ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[issueFilterStore.userDisplayFilters.layout ?? "list"]
}
/>
</FiltersDropdown>
</div>
);
});

View File

@ -1,74 +0,0 @@
import React from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import {
FilterDisplayProperties,
FilterExtraOptions,
FilterGroupBy,
FilterIssueType,
FilterOrderBy,
FilterSubGroupBy,
} from "components/issue-layouts";
// helpers
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
export const DisplayFiltersSelection = observer(() => {
const { issueFilter: issueFilterStore } = useMobxStore();
const isDisplayFilterEnabled = (displayFilter: string) =>
ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.display_filters[
issueFilterStore.userDisplayFilters.layout ?? "list"
].includes(displayFilter);
return (
<div className="w-full h-full overflow-hidden overflow-y-auto relative px-2.5 divide-y divide-custom-border-200">
{/* display properties */}
{ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.display_properties[
issueFilterStore.userDisplayFilters.layout ?? "list"
] && (
<div className="py-2">
<FilterDisplayProperties />
</div>
)}
{/* group by */}
{isDisplayFilterEnabled("group_by") && (
<div className="py-2">
<FilterGroupBy />
</div>
)}
{/* sub-group by */}
{isDisplayFilterEnabled("sub_group_by") && (
<div className="py-2">
<FilterSubGroupBy />
</div>
)}
{/* order by */}
{isDisplayFilterEnabled("order_by") && (
<div className="py-2">
<FilterOrderBy />
</div>
)}
{/* issue type */}
{isDisplayFilterEnabled("issue_type") && (
<div className="py-2">
<FilterIssueType />
</div>
)}
{/* Options */}
{ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.extra_options[issueFilterStore.userDisplayFilters.layout ?? "list"]
.access && (
<div className="py-2">
<FilterExtraOptions />
</div>
)}
</div>
);
});

View File

@ -1,47 +0,0 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// constants
import { ISSUE_EXTRA_OPTIONS, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
export const FilterExtraOptions = observer(() => {
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const isExtraOptionEnabled = (option: string) =>
ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.extra_options[
issueFilterStore.userDisplayFilters.layout ?? "list"
].values.includes(option);
return (
<>
<FilterHeader
title="Extra Options"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_EXTRA_OPTIONS.map((option) => {
if (!isExtraOptionEnabled(option.key)) return null;
return (
<FilterOption
key={option.key}
isChecked={issueFilterStore?.userDisplayFilters?.[option.key] ? true : false}
title={option.title}
/>
);
})}
</div>
)}
</>
);
});

View File

@ -1,56 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// types
import { TIssueGroupByOptions } from "types";
// constants
import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue";
export const FilterGroupBy = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleGroupBy = (value: TIssueGroupByOptions) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
group_by: value,
},
});
};
return (
<>
<FilterHeader
title="Group by"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_GROUP_BY_OPTIONS.map((groupBy) => (
<FilterOption
key={groupBy?.key}
isChecked={issueFilterStore?.userDisplayFilters?.group_by === groupBy?.key ? true : false}
onClick={() => handleGroupBy(groupBy.key)}
title={groupBy.title}
multiple={false}
/>
))}
</div>
)}
</>
);
});

View File

@ -1,56 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// types
import { TIssueTypeFilters } from "types";
// constants
import { ISSUE_FILTER_OPTIONS } from "constants/issue";
export const FilterIssueType = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleIssueType = (value: TIssueTypeFilters) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
type: value,
},
});
};
return (
<>
<FilterHeader
title="Issue Type"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_FILTER_OPTIONS.map((issueType) => (
<FilterOption
key={issueType?.key}
isChecked={issueFilterStore?.userDisplayFilters?.type === issueType?.key ? true : false}
onClick={() => handleIssueType(issueType?.key)}
title={issueType.title}
multiple={false}
/>
))}
</div>
)}
</>
);
});

View File

@ -1,56 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// types
import { TIssueOrderByOptions } from "types";
// constants
import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue";
export const FilterOrderBy = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleOrderBy = (value: TIssueOrderByOptions) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
order_by: value,
},
});
};
return (
<>
<FilterHeader
title="Order by"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_ORDER_BY_OPTIONS.map((orderBy) => (
<FilterOption
key={orderBy?.key}
isChecked={issueFilterStore?.userDisplayFilters?.order_by === orderBy?.key ? true : false}
onClick={() => handleOrderBy(orderBy.key)}
title={orderBy.title}
multiple={false}
/>
))}
</div>
)}
</>
);
});

View File

@ -1,64 +0,0 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// types
import { TIssueGroupByOptions } from "types";
// constants
import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue";
export const FilterSubGroupBy = observer(() => {
const [previewEnabled, setPreviewEnabled] = useState(true);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const handleSubGroupBy = (value: TIssueGroupByOptions) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
sub_group_by: value,
},
});
};
return (
<>
<FilterHeader
title="Sub-group by"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_GROUP_BY_OPTIONS.map((subGroupBy) => {
if (
issueFilterStore.userDisplayFilters.group_by !== null &&
subGroupBy.key === issueFilterStore.userDisplayFilters.group_by
)
return null;
return (
<FilterOption
key={subGroupBy?.key}
isChecked={issueFilterStore?.userDisplayFilters?.sub_group_by === subGroupBy?.key ? true : false}
onClick={() => handleSubGroupBy(subGroupBy.key)}
title={subGroupBy.title}
multiple={false}
/>
);
})}
</div>
)}
</>
);
});

View File

@ -1,230 +0,0 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import {
FilterAssignees,
FilterCreatedBy,
FilterLabels,
FilterPriority,
FilterState,
FilterStateGroup,
} from "components/issue-layouts";
// icons
import { Search, X } from "lucide-react";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { IIssueFilterOptions } from "types";
// constants
import { ISSUE_PRIORITIES, ISSUE_STATE_GROUPS } from "constants/issue";
type Props = {
workspaceSlug: string;
projectId: string;
};
export const FilterSelection: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = props;
const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore();
const statesList = getStatesList(projectStore.states?.[projectId?.toString() ?? ""]);
const [filtersToRender, setFiltersToRender] = useState<{
[key in keyof IIssueFilterOptions]: {
currentLength: number;
totalLength: number;
};
}>({
assignees: {
currentLength: 5,
totalLength: projectStore.members?.[projectId]?.length ?? 0,
},
created_by: {
currentLength: 5,
totalLength: projectStore.members?.[projectId]?.length ?? 0,
},
labels: {
currentLength: 5,
totalLength: projectStore.labels?.[projectId]?.length ?? 0,
},
priority: {
currentLength: 5,
totalLength: ISSUE_PRIORITIES.length,
},
state_group: {
currentLength: 5,
totalLength: ISSUE_STATE_GROUPS.length,
},
state: {
currentLength: 5,
totalLength: statesList?.length ?? 0,
},
});
const handleViewMore = (filterName: keyof IIssueFilterOptions) => {
const filterDetails = filtersToRender[filterName];
if (!filterDetails) return;
if (filterDetails.currentLength <= filterDetails.totalLength)
setFiltersToRender((prev) => ({
...prev,
[filterName]: {
...prev[filterName],
currentLength: filterDetails.currentLength + 5,
},
}));
};
const isViewMoreVisible = (filterName: keyof IIssueFilterOptions): boolean => {
const filterDetails = filtersToRender[filterName];
if (!filterDetails) return false;
return filterDetails.currentLength < filterDetails.totalLength;
};
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<div className="p-2.5 bg-custom-background-100">
<div className="bg-custom-background-90 border-[0.5px] border-custom-border-200 text-xs rounded flex items-center gap-1.5 px-1.5 py-1">
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
<input
type="text"
className="bg-custom-background-90 placeholder:text-custom-text-400 w-full outline-none"
placeholder="Search"
value={issueFilterStore.filtersSearchQuery}
onChange={(e) => issueFilterStore.updateFiltersSearchQuery(e.target.value)}
autoFocus
/>
{issueFilterStore.filtersSearchQuery !== "" && (
<button
type="button"
className="grid place-items-center"
onClick={() => issueFilterStore.updateFiltersSearchQuery("")}
>
<X className="text-custom-text-300" size={12} strokeWidth={2} />
</button>
)}
</div>
</div>
<div className="w-full h-full divide-y divide-custom-border-20 px-2.5 overflow-y-auto">
{/* priority */}
<div className="py-2">
<FilterPriority
workspaceSlug={workspaceSlug}
projectId={projectId}
itemsToRender={filtersToRender.priority?.currentLength ?? 0}
/>
{isViewMoreVisible("priority") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("priority")}
>
View more
</button>
)}
</div>
{/* state group */}
<div className="py-2">
<FilterStateGroup
workspaceSlug={workspaceSlug}
projectId={projectId}
itemsToRender={filtersToRender.state_group?.currentLength ?? 0}
/>
{isViewMoreVisible("state_group") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("state_group")}
>
View more
</button>
)}
</div>
{/* state */}
<div className="py-2">
<FilterState
workspaceSlug={workspaceSlug}
projectId={projectId}
itemsToRender={filtersToRender.state?.currentLength ?? 0}
/>
{isViewMoreVisible("state") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("state")}
>
View more
</button>
)}
</div>
{/* assignees */}
<div className="py-2">
<FilterAssignees
workspaceSlug={workspaceSlug}
projectId={projectId}
itemsToRender={filtersToRender.assignees?.currentLength ?? 0}
/>
{isViewMoreVisible("assignees") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("assignees")}
>
View more
</button>
)}
</div>
{/* created_by */}
<div className="py-2">
<FilterCreatedBy
workspaceSlug={workspaceSlug}
projectId={projectId}
itemsToRender={filtersToRender.created_by?.currentLength ?? 0}
/>
{isViewMoreVisible("created_by") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("created_by")}
>
View more
</button>
)}
</div>
{/* labels */}
<div className="py-2">
<FilterLabels
workspaceSlug={workspaceSlug}
projectId={projectId}
itemsToRender={filtersToRender.labels?.currentLength ?? 0}
/>
{isViewMoreVisible("labels") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("labels")}
>
View more
</button>
)}
</div>
{/* start_date */}
{/* <div>
<FilterStartDate />
</div> */}
{/* due_date */}
{/* <div>
<FilterTargetDate />
</div> */}
</div>
</div>
);
});

View File

@ -1,40 +0,0 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
export const FilterStartDate = observer(() => {
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const appliedFiltersCount = issueFilterStore.userFilters?.start_date?.length ?? 0;
return (
<>
<FilterHeader
title={`Start date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{issueFilterStore?.userFilters?.start_date &&
issueFilterStore?.userFilters?.start_date.length > 0 &&
issueFilterStore?.userFilters?.start_date.map((_startDate) => (
<FilterOption
key={_startDate?.key}
isChecked={issueFilterStore?.userFilters?.start_date?.includes(_startDate?.key) ? true : false}
title={_startDate.title}
multiple={false}
/>
))}
</div>
)}
</>
);
});

View File

@ -1,70 +0,0 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// icons
import { StateGroupIcon } from "components/icons";
// constants
import { ISSUE_STATE_GROUPS } from "constants/issue";
type Props = { workspaceSlug: string; projectId: string; itemsToRender: number };
export const FilterStateGroup: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, itemsToRender } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const handleUpdateStateGroup = (value: string) => {
const newValues = issueFilterStore.userFilters?.state_group ?? [];
if (issueFilterStore.userFilters?.state_group?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
state_group: newValues,
},
});
};
const appliedFiltersCount = issueFilterStore.userFilters?.state_group?.length ?? 0;
const filteredOptions = ISSUE_STATE_GROUPS.filter((s) =>
s.key.includes(issueFilterStore.filtersSearchQuery.toLowerCase())
);
return (
<>
<FilterHeader
title={`State group${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions.length > 0 ? (
filteredOptions
.slice(0, itemsToRender)
.map((stateGroup) => (
<FilterOption
key={stateGroup.key}
isChecked={issueFilterStore.userFilters?.state_group?.includes(stateGroup.key) ? true : false}
onClick={() => handleUpdateStateGroup(stateGroup.key)}
icon={<StateGroupIcon stateGroup={stateGroup.key} />}
title={stateGroup.title}
/>
))
) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p>
)}
</div>
)}
</>
);
});

View File

@ -1,40 +0,0 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
export const FilterTargetDate = observer(() => {
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const appliedFiltersCount = issueFilterStore.userFilters?.target_date?.length ?? 0;
return (
<>
<FilterHeader
title={`Target date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div className="space-y-[2px] pt-1">
{issueFilterStore?.userFilters?.target_date &&
issueFilterStore?.userFilters?.target_date.length > 0 &&
issueFilterStore?.userFilters?.target_date.map((_targetDate) => (
<FilterOption
key={_targetDate?.key}
isChecked={issueFilterStore?.userFilters?.target_date?.includes(_targetDate?.key) ? true : false}
title={_targetDate.title}
multiple={false}
/>
))}
</div>
)}
</>
);
});

View File

@ -1,47 +0,0 @@
import { Fragment } from "react";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// icons
import { ChevronUp } from "lucide-react";
interface IIssueDropdown {
children: React.ReactNode;
title?: string;
}
export const IssueDropdown = ({ children, title = "Dropdown" }: IIssueDropdown) => (
<Popover className="relative">
{({ open }) => {
if (open) {
}
return (
<>
<Popover.Button
className={`outline-none border border-custom-border-200 text-xs rounded flex items-center gap-2 px-2 py-1.5 hover:bg-custom-background-80 ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
<div className="font-medium">{title}</div>
<div className={`w-3.5 h-3.5 flex items-center justify-center transition-all ${open ? "" : "rotate-180"}`}>
<ChevronUp width={14} strokeWidth={2} />
</div>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded overflow-hidden">
<div className="w-[18.75rem] max-h-[37.5rem] flex flex-col overflow-hidden">{children}</div>
</Popover.Panel>
</Transition>
</>
);
}}
</Popover>
);

View File

@ -1,4 +0,0 @@
export * from "./display-filters";
export * from "./filters";
export * from "./helpers";
export * from "./layout-selection";

View File

@ -1,9 +1,9 @@
import React from "react";
// components
import { LayoutSelection } from "./layout-selection";
import { IssueDropdown } from "./helpers/dropdown";
import { FilterSelection } from "./filters/filters-selection";
import { DisplayFiltersSelection } from "./display-filters";
import { LayoutSelection } from "../issues/issue-layouts/header/layout-selection";
import { IssueDropdown } from "../issues/issue-layouts/header/helpers/dropdown";
import { FilterSelection } from "../issues/issue-layouts/header/filters/filters-selection";
import { DisplayFiltersSelection } from "../issues/issue-layouts/header/display-filters";
import { FilterPreview } from "./filters-preview";

View File

@ -26,7 +26,7 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
const lastDay = new Date(daysList[daysList.length - 1]);
if (firstDay.getMonth() === lastDay.getMonth() && firstDay.getFullYear() === lastDay.getFullYear())
return `${MONTHS_LIST[firstDay.getMonth() + 1].shortTitle} ${firstDay.getFullYear()}`;
return `${MONTHS_LIST[firstDay.getMonth() + 1].title} ${firstDay.getFullYear()}`;
if (firstDay.getFullYear() !== lastDay.getFullYear()) {
return `${MONTHS_LIST[firstDay.getMonth() + 1].shortTitle} ${firstDay.getFullYear()} - ${

View File

@ -0,0 +1,117 @@
import React from "react";
import { observer } from "mobx-react-lite";
// components
import {
FilterDisplayProperties,
FilterExtraOptions,
FilterGroupBy,
FilterIssueType,
FilterOrderBy,
FilterSubGroupBy,
} from "components/issues";
// types
import { IIssueDisplayFilterOptions } from "types";
import { ILayoutDisplayFiltersOptions } from "constants/issue";
type Props = {
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => void;
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions;
};
export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
const { displayFilters, handleDisplayFiltersUpdate, layoutDisplayFiltersOptions } = props;
const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) =>
Object.keys(layoutDisplayFiltersOptions.display_filters).includes(displayFilter);
return (
<div className="w-full h-full overflow-hidden overflow-y-auto relative px-2.5 divide-y divide-custom-border-200">
{/* display properties */}
{layoutDisplayFiltersOptions.display_properties && (
<div className="py-2">
<FilterDisplayProperties />
</div>
)}
{/* group by */}
{isDisplayFilterEnabled("group_by") && (
<div className="py-2">
<FilterGroupBy
selectedGroupBy={displayFilters.group_by}
selectedSubGroupBy={displayFilters.sub_group_by}
groupByOptions={layoutDisplayFiltersOptions.display_filters.group_by ?? []}
handleUpdate={(val) =>
handleDisplayFiltersUpdate({
group_by: val,
})
}
/>
</div>
)}
{/* sub-group by */}
{isDisplayFilterEnabled("sub_group_by") && displayFilters.group_by !== null && (
<div className="py-2">
<FilterSubGroupBy
selectedGroupBy={displayFilters.group_by}
selectedSubGroupBy={displayFilters.sub_group_by}
handleUpdate={(val) =>
handleDisplayFiltersUpdate({
sub_group_by: val,
})
}
subGroupByOptions={layoutDisplayFiltersOptions.display_filters.sub_group_by ?? []}
/>
</div>
)}
{/* order by */}
{isDisplayFilterEnabled("order_by") && (
<div className="py-2">
<FilterOrderBy
selectedOrderBy={displayFilters.order_by}
handleUpdate={(val) =>
handleDisplayFiltersUpdate({
order_by: val,
})
}
/>
</div>
)}
{/* issue type */}
{isDisplayFilterEnabled("type") && (
<div className="py-2">
<FilterIssueType
selectedIssueType={displayFilters.type}
handleUpdate={(val) =>
handleDisplayFiltersUpdate({
type: val,
})
}
/>
</div>
)}
{/* Options */}
{layoutDisplayFiltersOptions.extra_options.access && (
<div className="py-2">
<FilterExtraOptions
selectedExtraOptions={{
show_empty_groups: displayFilters.show_empty_groups ?? false,
sub_issue: displayFilters.sub_issue ?? false,
}}
handleUpdate={(key, val) =>
handleDisplayFiltersUpdate({
[key]: val,
})
}
enabledExtraOptions={layoutDisplayFiltersOptions.extra_options.values}
/>
</div>
)}
</div>
);
});

View File

@ -0,0 +1,52 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// types
import { IIssueDisplayFilterOptions, TIssueExtraOptions } from "types";
// constants
import { ISSUE_EXTRA_OPTIONS } from "constants/issue";
type Props = {
selectedExtraOptions: {
sub_issue: boolean;
show_empty_groups: boolean;
};
handleUpdate: (key: keyof IIssueDisplayFilterOptions, val: boolean) => void;
enabledExtraOptions: TIssueExtraOptions[];
};
export const FilterExtraOptions: React.FC<Props> = observer((props) => {
const { selectedExtraOptions, handleUpdate, enabledExtraOptions } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const isExtraOptionEnabled = (option: TIssueExtraOptions) => enabledExtraOptions.includes(option);
return (
<>
<FilterHeader
title="Extra Options"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_EXTRA_OPTIONS.map((option) => {
if (!isExtraOptionEnabled(option.key)) return null;
return (
<FilterOption
key={option.key}
isChecked={selectedExtraOptions?.[option.key] ? true : false}
onClick={() => handleUpdate(option.key, !selectedExtraOptions?.[option.key])}
title={option.title}
/>
);
})}
</div>
)}
</>
);
});

View File

@ -0,0 +1,49 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// types
import { TIssueGroupByOptions } from "types";
// constants
import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue";
type Props = {
selectedGroupBy: TIssueGroupByOptions | undefined;
selectedSubGroupBy: TIssueGroupByOptions | undefined;
groupByOptions: TIssueGroupByOptions[];
handleUpdate: (val: TIssueGroupByOptions) => void;
};
export const FilterGroupBy: React.FC<Props> = observer((props) => {
const { selectedGroupBy, selectedSubGroupBy, groupByOptions, handleUpdate } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
return (
<>
<FilterHeader
title="Group by"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => {
if (selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy) return null;
return (
<FilterOption
key={groupBy?.key}
isChecked={selectedGroupBy === groupBy?.key ? true : false}
onClick={() => handleUpdate(groupBy.key)}
title={groupBy.title}
multiple={false}
/>
);
})}
</div>
)}
</>
);
});

View File

@ -0,0 +1,43 @@
import React from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// types
import { TIssueTypeFilters } from "types";
// constants
import { ISSUE_FILTER_OPTIONS } from "constants/issue";
type Props = {
selectedIssueType: TIssueTypeFilters | undefined;
handleUpdate: (val: TIssueTypeFilters) => void;
};
export const FilterIssueType: React.FC<Props> = observer((props) => {
const { selectedIssueType, handleUpdate } = props;
const [previewEnabled, setPreviewEnabled] = React.useState(true);
return (
<>
<FilterHeader
title="Issue Type"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_FILTER_OPTIONS.map((issueType) => (
<FilterOption
key={issueType?.key}
isChecked={selectedIssueType === issueType?.key ? true : false}
onClick={() => handleUpdate(issueType?.key)}
title={issueType.title}
multiple={false}
/>
))}
</div>
)}
</>
);
});

View File

@ -0,0 +1,43 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// types
import { TIssueOrderByOptions } from "types";
// constants
import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue";
type Props = {
selectedOrderBy: TIssueOrderByOptions | undefined;
handleUpdate: (val: TIssueOrderByOptions) => void;
};
export const FilterOrderBy: React.FC<Props> = observer((props) => {
const { selectedOrderBy, handleUpdate } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
return (
<>
<FilterHeader
title="Order by"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_ORDER_BY_OPTIONS.map((orderBy) => (
<FilterOption
key={orderBy?.key}
isChecked={selectedOrderBy === orderBy?.key ? true : false}
onClick={() => handleUpdate(orderBy.key)}
title={orderBy.title}
multiple={false}
/>
))}
</div>
)}
</>
);
});

View File

@ -0,0 +1,49 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// types
import { TIssueGroupByOptions } from "types";
// constants
import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue";
type Props = {
selectedGroupBy: TIssueGroupByOptions | undefined;
selectedSubGroupBy: TIssueGroupByOptions | undefined;
handleUpdate: (val: TIssueGroupByOptions) => void;
subGroupByOptions: TIssueGroupByOptions[];
};
export const FilterSubGroupBy: React.FC<Props> = observer((props) => {
const { selectedGroupBy, selectedSubGroupBy, handleUpdate, subGroupByOptions } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
return (
<>
<FilterHeader
title="Sub-group by"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{ISSUE_GROUP_BY_OPTIONS.filter((option) => subGroupByOptions.includes(option.key)).map((subGroupBy) => {
if (selectedGroupBy !== null && subGroupBy.key === selectedGroupBy) return null;
return (
<FilterOption
key={subGroupBy?.key}
isChecked={selectedSubGroupBy === subGroupBy?.key ? true : false}
onClick={() => handleUpdate(subGroupBy.key)}
title={subGroupBy.title}
multiple={false}
/>
);
})}
</div>
)}
</>
);
});

View File

@ -1,40 +1,34 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
import { FilterHeader, FilterOption } from "components/issues";
// ui
import { Avatar, Loader } from "components/ui";
type Props = { workspaceSlug: string; projectId: string; itemsToRender: number };
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
itemsToRender: number;
projectId: string;
searchQuery: string;
viewButtons: React.ReactNode;
};
export const FilterAssignees: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, itemsToRender } = props;
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore, project: projectStore } = store;
const { project: projectStore } = store;
const handleUpdateAssignees = (value: string) => {
const newValues = issueFilterStore.userFilters?.assignees ?? [];
if (issueFilterStore.userFilters?.assignees?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
assignees: newValues,
},
});
};
const appliedFiltersCount = issueFilterStore.userFilters?.assignees?.length ?? 0;
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = projectStore.members?.[projectId?.toString() ?? ""]?.filter((member) =>
member.member.display_name.toLowerCase().includes(issueFilterStore.filtersSearchQuery.toLowerCase())
member.member.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
@ -48,22 +42,18 @@ export const FilterAssignees: React.FC<Props> = observer((props) => {
<div>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions
.slice(0, itemsToRender)
.map((member) => (
<>
{filteredOptions.slice(0, itemsToRender).map((member) => (
<FilterOption
key={`assignees-${member?.member?.id}`}
isChecked={
issueFilterStore?.userFilters?.assignees != null &&
issueFilterStore?.userFilters?.assignees.includes(member.member?.id)
? true
: false
}
onClick={() => handleUpdateAssignees(member.member?.id)}
isChecked={appliedFilters?.includes(member.member?.id) ? true : false}
onClick={() => handleUpdate(member.member?.id)}
icon={<Avatar user={member.member} height="18px" width="18px" />}
title={member.member?.display_name}
/>
))
))}
{viewButtons}
</>
) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p>
)

View File

@ -1,40 +1,34 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
import { FilterHeader, FilterOption } from "components/issues";
// ui
import { Avatar, Loader } from "components/ui";
type Props = { workspaceSlug: string; projectId: string; itemsToRender: number };
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
itemsToRender: number;
projectId: string;
searchQuery: string;
viewButtons: React.ReactNode;
};
export const FilterCreatedBy: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, itemsToRender } = props;
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore, project: projectStore } = store;
const { project: projectStore } = store;
const handleUpdateCreatedBy = (value: string) => {
const newValues = issueFilterStore.userFilters?.created_by ?? [];
if (issueFilterStore.userFilters?.created_by?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
created_by: newValues,
},
});
};
const appliedFiltersCount = issueFilterStore.userFilters?.created_by?.length ?? 0;
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = projectStore.members?.[projectId?.toString() ?? ""]?.filter((member) =>
member.member.display_name.toLowerCase().includes(issueFilterStore.filtersSearchQuery.toLowerCase())
member.member.display_name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
@ -48,17 +42,18 @@ export const FilterCreatedBy: React.FC<Props> = observer((props) => {
<div>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions
.slice(0, itemsToRender)
.map((member) => (
<>
{filteredOptions.slice(0, itemsToRender).map((member) => (
<FilterOption
key={`created-by-${member.member?.id}`}
isChecked={issueFilterStore?.userFilters?.created_by?.includes(member.member?.id) ? true : false}
onClick={() => handleUpdateCreatedBy(member.member?.id)}
isChecked={appliedFilters?.includes(member.member?.id) ? true : false}
onClick={() => handleUpdate(member.member?.id)}
icon={<Avatar user={member.member} height="18px" width="18px" />}
title={member.member?.display_name}
/>
))
))}
{viewButtons}
</>
) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p>
)

View File

@ -0,0 +1,367 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import {
FilterAssignees,
FilterCreatedBy,
FilterLabels,
FilterPriority,
FilterStartDate,
FilterState,
FilterStateGroup,
FilterTargetDate,
} from "components/issues";
// icons
import { Search, X } from "lucide-react";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { IIssueFilterOptions } from "types";
// constants
import { ILayoutDisplayFiltersOptions, ISSUE_PRIORITIES, ISSUE_STATE_GROUPS } from "constants/issue";
import { DATE_FILTER_OPTIONS } from "constants/filters";
type Props = {
filters: IIssueFilterOptions;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions;
projectId: string;
};
export const FilterSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, projectId } = props;
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
const { project: projectStore } = useMobxStore();
const statesList = getStatesList(projectStore.states?.[projectId?.toString() ?? ""]);
const [filtersToRender, setFiltersToRender] = useState<{
[key in keyof IIssueFilterOptions]: {
currentLength: number;
totalLength: number;
};
}>({
assignees: {
currentLength: 5,
totalLength: projectStore.members?.[projectId]?.length ?? 0,
},
created_by: {
currentLength: 5,
totalLength: projectStore.members?.[projectId]?.length ?? 0,
},
labels: {
currentLength: 5,
totalLength: projectStore.labels?.[projectId]?.length ?? 0,
},
priority: {
currentLength: 5,
totalLength: ISSUE_PRIORITIES.length,
},
state_group: {
currentLength: 5,
totalLength: ISSUE_STATE_GROUPS.length,
},
state: {
currentLength: 5,
totalLength: statesList?.length ?? 0,
},
start_date: {
currentLength: 5,
totalLength: DATE_FILTER_OPTIONS.length + 1,
},
target_date: {
currentLength: 5,
totalLength: DATE_FILTER_OPTIONS.length + 1,
},
});
const handleViewMore = (filterName: keyof IIssueFilterOptions) => {
const filterDetails = filtersToRender[filterName];
if (!filterDetails) return;
if (filterDetails.currentLength <= filterDetails.totalLength)
setFiltersToRender((prev) => ({
...prev,
[filterName]: {
...prev[filterName],
currentLength: filterDetails.currentLength + 5,
},
}));
};
const handleViewLess = (filterName: keyof IIssueFilterOptions) => {
const filterDetails = filtersToRender[filterName];
if (!filterDetails) return;
setFiltersToRender((prev) => ({
...prev,
[filterName]: {
...prev[filterName],
currentLength: 5,
},
}));
};
const isViewMoreVisible = (filterName: keyof IIssueFilterOptions): boolean => {
const filterDetails = filtersToRender[filterName];
if (!filterDetails) return false;
return filterDetails.currentLength < filterDetails.totalLength;
};
const isViewLessVisible = (filterName: keyof IIssueFilterOptions): boolean => {
const filterDetails = filtersToRender[filterName];
if (!filterDetails) return false;
return filterDetails.currentLength > 5;
};
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions.filters.includes(filter);
return (
<div className="w-full h-full flex flex-col overflow-hidden">
<div className="p-2.5 pb-0 bg-custom-background-100">
<div className="bg-custom-background-90 border-[0.5px] border-custom-border-200 text-xs rounded flex items-center gap-1.5 px-1.5 py-1">
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
<input
type="text"
className="bg-custom-background-90 placeholder:text-custom-text-400 w-full outline-none"
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
<X className="text-custom-text-300" size={12} strokeWidth={2} />
</button>
)}
</div>
</div>
<div className="w-full h-full divide-y divide-custom-border-20 px-2.5 overflow-y-auto">
{/* priority */}
{isFilterEnabled("priority") && (
<div className="py-2">
<FilterPriority
appliedFilters={filters.priority ?? null}
handleUpdate={(val) => handleFiltersUpdate("priority", val)}
itemsToRender={filtersToRender.priority?.currentLength ?? 0}
searchQuery={filtersSearchQuery}
viewButtons={
<div className="flex items-center gap-2 ml-7 mt-1">
{/* TODO: handle view more and less in a better way */}
{isViewMoreVisible("priority") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewMore("priority")}
>
View more
</button>
)}
{isViewLessVisible("priority") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewLess("priority")}
>
View less
</button>
)}
</div>
}
/>
</div>
)}
{/* state group */}
{isFilterEnabled("state_group") && (
<div className="py-2">
<FilterStateGroup
appliedFilters={filters.state_group ?? null}
handleUpdate={(val) => handleFiltersUpdate("state_group", val)}
itemsToRender={filtersToRender.state_group?.currentLength ?? 0}
searchQuery={filtersSearchQuery}
/>
<div className="flex items-center gap-2 ml-7 mt-1">
{isViewMoreVisible("state_group") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("state_group")}
>
View more
</button>
)}
{isViewLessVisible("state_group") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewLess("state_group")}
>
View less
</button>
)}
</div>
</div>
)}
{/* state */}
{isFilterEnabled("state") && (
<div className="py-2">
<FilterState
appliedFilters={filters.state ?? null}
handleUpdate={(val) => handleFiltersUpdate("state", val)}
itemsToRender={filtersToRender.state?.currentLength ?? 0}
searchQuery={filtersSearchQuery}
projectId={projectId}
/>
<div className="flex items-center gap-2 ml-7 mt-1">
{isViewMoreVisible("state") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("state")}
>
View more
</button>
)}
{isViewLessVisible("state") && (
<button className="text-custom-primary-100 text-xs font-medium" onClick={() => handleViewLess("state")}>
View less
</button>
)}
</div>
</div>
)}
{/* assignees */}
{isFilterEnabled("assignees") && (
<div className="py-2">
<FilterAssignees
appliedFilters={filters.assignees ?? null}
handleUpdate={(val) => handleFiltersUpdate("assignees", val)}
itemsToRender={filtersToRender.assignees?.currentLength ?? 0}
projectId={projectId}
searchQuery={filtersSearchQuery}
viewButtons={
<div className="flex items-center gap-2 ml-7 mt-1">
{isViewMoreVisible("assignees") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("assignees")}
>
View more
</button>
)}
{isViewLessVisible("assignees") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewLess("assignees")}
>
View less
</button>
)}
</div>
}
/>
</div>
)}
{/* created_by */}
{isFilterEnabled("created_by") && (
<div className="py-2">
<FilterCreatedBy
appliedFilters={filters.created_by ?? null}
handleUpdate={(val) => handleFiltersUpdate("created_by", val)}
itemsToRender={filtersToRender.created_by?.currentLength ?? 0}
projectId={projectId}
searchQuery={filtersSearchQuery}
viewButtons={
<div className="flex items-center gap-2 ml-7 mt-1">
{isViewMoreVisible("created_by") && (
<button
className="text-custom-primary-100 text-xs font-medium ml-7"
onClick={() => handleViewMore("created_by")}
>
View more
</button>
)}
{isViewLessVisible("created_by") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewLess("created_by")}
>
View less
</button>
)}
</div>
}
/>
</div>
)}
{/* labels */}
{isFilterEnabled("labels") && (
<div className="py-2">
<FilterLabels
appliedFilters={filters.labels ?? null}
handleUpdate={(val) => handleFiltersUpdate("labels", val)}
itemsToRender={filtersToRender.labels?.currentLength ?? 0}
projectId={projectId}
searchQuery={filtersSearchQuery}
viewButtons={
<div className="flex items-center gap-2 ml-7 mt-1">
{isViewMoreVisible("labels") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewMore("labels")}
>
View more
</button>
)}
{isViewLessVisible("labels") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewLess("labels")}
>
View less
</button>
)}
</div>
}
/>
</div>
)}
{/* start_date */}
{isFilterEnabled("start_date") && (
<div className="py-2">
<FilterStartDate
appliedFilters={filters.start_date ?? null}
handleUpdate={(val) => handleFiltersUpdate("start_date", val)}
itemsToRender={filtersToRender.start_date?.currentLength ?? 0}
searchQuery={filtersSearchQuery}
/>
</div>
)}
{/* target_date */}
{isFilterEnabled("target_date") && (
<div className="py-2">
<FilterTargetDate
appliedFilters={filters.target_date ?? null}
handleUpdate={(val) => handleFiltersUpdate("target_date", val)}
itemsToRender={filtersToRender.target_date?.currentLength ?? 0}
searchQuery={filtersSearchQuery}
/>
</div>
)}
</div>
</div>
);
});

View File

@ -1,10 +1,10 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
import { FilterHeader, FilterOption } from "components/issues";
// ui
import { Loader } from "components/ui";
@ -12,33 +12,27 @@ const LabelIcons = ({ color }: { color: string }) => (
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
);
type Props = { workspaceSlug: string; projectId: string; itemsToRender: number };
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
itemsToRender: number;
projectId: string;
searchQuery: string;
viewButtons: React.ReactNode;
};
export const FilterLabels: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, itemsToRender } = props;
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore, project: projectStore } = store;
const { project: projectStore } = store;
const handleUpdateLabels = (value: string) => {
const newValues = issueFilterStore.userFilters?.labels ?? [];
if (issueFilterStore.userFilters?.labels?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
labels: newValues,
},
});
};
const appliedFiltersCount = issueFilterStore.userFilters?.labels?.length ?? 0;
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = projectStore.labels?.[projectId?.toString() ?? ""]?.filter((label) =>
label.name.toLowerCase().includes(issueFilterStore.filtersSearchQuery.toLowerCase())
label.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
@ -52,17 +46,18 @@ export const FilterLabels: React.FC<Props> = observer((props) => {
<div>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions
.slice(0, itemsToRender)
.map((label) => (
<>
{filteredOptions.slice(0, itemsToRender).map((label) => (
<FilterOption
key={label?.id}
isChecked={issueFilterStore?.userFilters?.labels?.includes(label?.id) ? true : false}
onClick={() => handleUpdateLabels(label?.id)}
isChecked={appliedFilters?.includes(label?.id) ? true : false}
onClick={() => handleUpdate(label?.id)}
icon={<LabelIcons color={label.color} />}
title={label.name}
/>
))
))}
{viewButtons}
</>
) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p>
)

View File

@ -1,10 +1,8 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
import { FilterHeader, FilterOption } from "components/issues";
// icons
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
// constants
@ -50,34 +48,22 @@ const PriorityIcons = ({
);
};
type Props = { workspaceSlug: string; projectId: string; itemsToRender: number };
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
itemsToRender: number;
searchQuery: string;
viewButtons: React.ReactNode;
};
export const FilterPriority: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, itemsToRender } = props;
const { appliedFilters, handleUpdate, itemsToRender, searchQuery, viewButtons } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const appliedFiltersCount = appliedFilters?.length ?? 0;
const handleUpdatePriority = (value: string) => {
const newValues = issueFilterStore.userFilters?.priority ?? [];
if (issueFilterStore.userFilters?.priority?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
priority: newValues,
},
});
};
const appliedFiltersCount = issueFilterStore.userFilters?.priority?.length ?? 0;
const filteredOptions = ISSUE_PRIORITIES.filter((p) =>
p.key.includes(issueFilterStore.filtersSearchQuery.toLowerCase())
);
const filteredOptions = ISSUE_PRIORITIES.filter((p) => p.key.includes(searchQuery.toLowerCase()));
return (
<>
@ -89,17 +75,18 @@ export const FilterPriority: React.FC<Props> = observer((props) => {
{previewEnabled && (
<div>
{filteredOptions.length > 0 ? (
filteredOptions
.slice(0, itemsToRender)
.map((priority) => (
<>
{filteredOptions.slice(0, itemsToRender).map((priority) => (
<FilterOption
key={priority.key}
isChecked={issueFilterStore.userFilters?.priority?.includes(priority.key) ? true : false}
onClick={() => handleUpdatePriority(priority.key)}
isChecked={appliedFilters?.includes(priority.key) ? true : false}
onClick={() => handleUpdate(priority.key)}
icon={<PriorityIcons priority={priority.key} />}
title={priority.title}
/>
))
))}
{viewButtons}
</>
) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p>
)}

View File

@ -0,0 +1,69 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
import { DateFilterModal } from "components/core";
// constants
import { DATE_FILTER_OPTIONS } from "constants/filters";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string | string[]) => void;
itemsToRender: number;
searchQuery: string;
};
export const FilterStartDate: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, itemsToRender, searchQuery } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase()));
return (
<>
{isDateFilterModalOpen && (
<DateFilterModal
handleClose={() => setIsDateFilterModalOpen(false)}
isOpen={isDateFilterModalOpen}
onSelect={(val) => handleUpdate(val)}
title="Start date"
/>
)}
<FilterHeader
title={`Start date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions.length > 0 ? (
<>
{filteredOptions.slice(0, itemsToRender).map((option) => (
<FilterOption
key={option.value}
isChecked={appliedFilters?.includes(option.value) ? true : false}
onClick={() => handleUpdate(option.value)}
title={option.name}
multiple={false}
/>
))}
<FilterOption
isChecked={false}
onClick={() => setIsDateFilterModalOpen(true)}
title="Custom"
multiple={false}
/>
</>
) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p>
)}
</div>
)}
</>
);
});

View File

@ -0,0 +1,55 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
// icons
import { StateGroupIcon } from "components/icons";
// constants
import { ISSUE_STATE_GROUPS } from "constants/issue";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
itemsToRender: number;
searchQuery: string;
};
export const FilterStateGroup: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, itemsToRender, searchQuery } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = ISSUE_STATE_GROUPS.filter((s) => s.key.includes(searchQuery.toLowerCase()));
return (
<>
<FilterHeader
title={`State group${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions.length > 0 ? (
filteredOptions
.slice(0, itemsToRender)
.map((stateGroup) => (
<FilterOption
key={stateGroup.key}
isChecked={appliedFilters?.includes(stateGroup.key) ? true : false}
onClick={() => handleUpdate(stateGroup.key)}
icon={<StateGroupIcon stateGroup={stateGroup.key} />}
title={stateGroup.title}
/>
))
) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p>
)}
</div>
)}
</>
);
});

View File

@ -1,10 +1,10 @@
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
import { FilterHeader, FilterOption } from "components/issues";
// ui
import { Loader } from "components/ui";
// icons
@ -12,37 +12,28 @@ import { StateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
type Props = { workspaceSlug: string; projectId: string; itemsToRender: number };
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
itemsToRender: number;
projectId: string;
searchQuery: string;
};
export const FilterState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, itemsToRender } = props;
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore, project: projectStore } = store;
const { project: projectStore } = store;
const statesByGroups = projectStore.states?.[projectId?.toString() ?? ""];
const statesList = getStatesList(statesByGroups);
const handleUpdateState = (value: string) => {
const newValues = issueFilterStore.userFilters?.state ?? [];
const appliedFiltersCount = appliedFilters?.length ?? 0;
if (issueFilterStore.userFilters?.state?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
state: newValues,
},
});
};
const appliedFiltersCount = issueFilterStore.userFilters?.state?.length ?? 0;
const filteredOptions = statesList?.filter((s) =>
s.name.toLowerCase().includes(issueFilterStore.filtersSearchQuery.toLowerCase())
);
const filteredOptions = statesList?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase()));
return (
<>
@ -59,10 +50,10 @@ export const FilterState: React.FC<Props> = observer((props) => {
{filteredOptions.slice(0, itemsToRender).map((state) => (
<FilterOption
key={state.id}
isChecked={issueFilterStore?.userFilters?.state?.includes(state?.id) ? true : false}
onClick={() => handleUpdateState(state?.id)}
icon={<StateGroupIcon stateGroup={state?.group} color={state?.color} />}
title={state?.name}
isChecked={appliedFilters?.includes(state.id) ? true : false}
onClick={() => handleUpdate(state.id)}
icon={<StateGroupIcon stateGroup={state.group} color={state.color} />}
title={state.name}
/>
))}
</>

View File

@ -0,0 +1,69 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { FilterHeader, FilterOption } from "components/issues";
import { DateFilterModal } from "components/core";
// constants
import { DATE_FILTER_OPTIONS } from "constants/filters";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string | string[]) => void;
itemsToRender: number;
searchQuery: string;
};
export const FilterTargetDate: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, itemsToRender, searchQuery } = props;
const [previewEnabled, setPreviewEnabled] = useState(true);
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase()));
return (
<>
{isDateFilterModalOpen && (
<DateFilterModal
handleClose={() => setIsDateFilterModalOpen(false)}
isOpen={isDateFilterModalOpen}
onSelect={(val) => handleUpdate(val)}
title="Due date"
/>
)}
<FilterHeader
title={`Target date${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions.length > 0 ? (
<>
{filteredOptions.slice(0, itemsToRender).map((option) => (
<FilterOption
key={option.value}
isChecked={appliedFilters?.includes(option.value) ? true : false}
onClick={() => handleUpdate(option.value)}
title={option.name}
multiple={false}
/>
))}
<FilterOption
isChecked={false}
onClick={() => setIsDateFilterModalOpen(true)}
title="Custom"
multiple={false}
/>
</>
) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p>
)}
</div>
)}
</>
);
});

View File

@ -0,0 +1,53 @@
import React, { Fragment } from "react";
// headless ui
import { Popover, Transition } from "@headlessui/react";
// icons
import { ChevronUp } from "lucide-react";
type Props = {
children: React.ReactNode;
title?: string;
};
export const FiltersDropdown: React.FC<Props> = (props) => {
const { children, title = "Dropdown" } = props;
return (
<Popover className="relative">
{({ open }) => {
if (open) {
}
return (
<>
<Popover.Button
className={`outline-none border border-custom-border-200 text-xs rounded flex items-center gap-2 px-2 py-1.5 hover:bg-custom-background-80 ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
>
<div className="font-medium">{title}</div>
<div
className={`w-3.5 h-3.5 flex items-center justify-center transition-all ${open ? "" : "rotate-180"}`}
>
<ChevronUp width={14} strokeWidth={2} />
</div>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded overflow-hidden">
<div className="w-[18.75rem] max-h-[37.5rem] flex flex-col overflow-hidden">{children}</div>
</Popover.Panel>
</Transition>
</>
);
}}
</Popover>
);
};

View File

@ -9,7 +9,7 @@ interface IFilterHeader {
}
export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => (
<div className="flex items-center justify-between gap-2 pb-1 bg-custom-background-100 sticky top-0">
<div className="flex items-center justify-between gap-2 bg-custom-background-100 sticky top-0">
<div className="text-custom-text-300 text-xs font-medium flex-grow truncate">{title}</div>
<button
type="button"

View File

@ -0,0 +1,4 @@
export * from "./display-filters";
export * from "./filters";
export * from "./helpers";
export * from "./layout-selection";

View File

@ -1,2 +1,3 @@
export * from "./calendar";
export * from "./header";
export * from "./kanban";

View File

@ -1,37 +1,18 @@
// helper
import { renderDateFormat } from "helpers/date-time.helper";
export const DATE_FILTER_OPTIONS = [
{
name: "Last week",
value: [
`${renderDateFormat(new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000))};after`,
`${renderDateFormat(new Date())};before`,
],
name: "1 week from now",
value: "1_weeks;after;fromnow",
},
{
name: "2 weeks from now",
value: [
`${renderDateFormat(new Date())};after`,
`${renderDateFormat(new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000))};before`,
],
value: "2_weeks;after;fromnow",
},
{
name: "1 month from now",
value: [
`${renderDateFormat(new Date())};after`,
`${renderDateFormat(
new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate())
)};before`,
],
value: "1_months;after;fromnow",
},
{
name: "2 months from now",
value: [
`${renderDateFormat(new Date())};after`,
`${renderDateFormat(
new Date(new Date().getFullYear(), new Date().getMonth() + 2, new Date().getDate())
)};before`,
],
value: "2_months;after;fromnow",
},
];

View File

@ -2,8 +2,9 @@
import { Calendar, GanttChart, Kanban, List, Sheet } from "lucide-react";
// types
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
TIssueExtraOptions,
TIssueGroupByOptions,
TIssueLayouts,
TIssueOrderByOptions,
@ -107,12 +108,11 @@ export const ISSUE_DISPLAY_PROPERTIES: {
];
export const ISSUE_EXTRA_OPTIONS: {
key: keyof IIssueDisplayFilterOptions;
key: TIssueExtraOptions;
title: string;
}[] = [
{ key: "sub_issue", title: "Show sub-issues" }, // in spreadsheet its always false
{ key: "show_empty_groups", title: "Show empty states" }, // filter on front-end
{ key: "start_target_date", title: "Start target Date" }, // gantt always be true
];
export const ISSUE_LAYOUTS: {
@ -201,113 +201,114 @@ export const ISSUE_GANTT_DISPLAY_FILTERS = [
{ key: "sub_issue", title: "Sub Issue" },
];
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key: string]: {
layout: TIssueLayouts[];
filters: {
[key in TIssueLayouts]: string[];
};
display_properties: {
[key in TIssueLayouts]: boolean;
};
display_filters: {
[key in TIssueLayouts]: string[];
};
extra_options: {
[key in TIssueLayouts]: {
access: boolean;
values: string[];
};
};
export interface ILayoutDisplayFiltersOptions {
filters: (keyof IIssueFilterOptions)[];
display_properties: boolean;
display_filters: {
group_by?: TIssueGroupByOptions[];
sub_group_by?: TIssueGroupByOptions[];
order_by?: TIssueOrderByOptions[];
type?: TIssueTypeFilters[];
};
extra_options: {
access: boolean;
values: TIssueExtraOptions[];
};
}
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions };
} = {
my_issues: {
layout: ["list", "kanban"],
filters: {
list: ["priority", "state_group", "labels", "start_date", "due_date"],
kanban: ["priority", "state_group", "labels", "start_date", "due_date"],
calendar: [],
spreadsheet: [],
gantt_chart: [],
},
display_properties: {
list: true,
kanban: true,
calendar: true,
spreadsheet: true,
gantt_chart: false,
},
display_filters: {
list: ["group_by", "sub_group_by", "order_by", "issue_type"],
kanban: ["group_by", "sub_group_by", "order_by", "issue_type"],
calendar: ["issue_type"],
spreadsheet: ["issue_type"],
gantt_chart: ["order_by", "issue_type"],
},
extra_options: {
list: {
list: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_properties: true,
display_filters: {
group_by: ["state_detail.group", "project", "priority", "labels", null],
sub_group_by: ["state_detail.group", "project", "priority", "labels", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
kanban: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
kanban: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_properties: true,
display_filters: {
group_by: ["state_detail.group", "project", "priority", "labels", null],
sub_group_by: ["state_detail.group", "project", "priority", "labels", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
calendar: {
access: false,
values: [],
},
spreadsheet: {
access: false,
values: [],
},
gantt_chart: {
extra_options: {
access: false,
values: [],
},
},
},
issues: {
layout: ["list", "kanban", "calendar", "spreadsheet", "gantt_chart"],
filters: {
list: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
kanban: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
calendar: ["priority", "state", "assignees", "created_by", "labels"],
spreadsheet: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
gantt_chart: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
},
display_properties: {
list: true,
kanban: true,
calendar: true,
spreadsheet: true,
gantt_chart: false,
},
display_filters: {
list: ["group_by", "sub_group_by", "order_by", "issue_type"],
kanban: ["group_by", "sub_group_by", "order_by", "issue_type"],
calendar: ["issue_type"],
spreadsheet: ["issue_type"],
gantt_chart: ["order_by", "issue_type"],
},
extra_options: {
list: {
list: {
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
display_properties: true,
display_filters: {
group_by: ["state", "priority", "labels", "assignees", "created_by", null],
sub_group_by: ["state", "priority", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
kanban: {
},
kanban: {
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
display_properties: true,
display_filters: {
group_by: ["state", "priority", "labels", "assignees", "created_by", null],
sub_group_by: ["state", "priority", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
calendar: {
},
calendar: {
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date"],
display_properties: true,
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
spreadsheet: {
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
display_properties: true,
display_filters: {
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: false,
values: [],
},
spreadsheet: {
access: false,
values: [],
},
gantt_chart: {
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
display_properties: false,
display_filters: {
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
gantt_chart: {
extra_options: {
access: true,
values: ["sub_issue"],
},

View File

@ -1,7 +1,9 @@
// helpers
import { orderArrayBy } from "helpers/array.helper";
import { renderDateFormat } from "helpers/date-time.helper";
// types
import { IIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
type THandleIssuesMutation = (
formData: Partial<IIssue>,
@ -79,218 +81,35 @@ export const handleIssuesMutation: THandleIssuesMutation = (
}
};
export const handleIssueQueryParamsByLayout = (_layout: TIssueLayouts | undefined): TIssueParams[] | null => {
if (_layout === "list")
return [
"priority",
"state_group",
"state",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"group_by",
"sub_group_by",
"order_by",
"type",
"sub_issue",
"show_empty_groups",
];
if (_layout === "kanban")
return [
"priority",
"state_group",
"state",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"group_by",
"sub_group_by",
"order_by",
"type",
"sub_issue",
"show_empty_groups",
];
if (_layout === "calendar")
return [
"priority",
"state_group",
"state",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"type",
];
if (_layout === "spreadsheet")
return [
"priority",
"state_group",
"state",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"type",
"sub_issue",
];
if (_layout === "gantt_chart")
return [
"priority",
"state",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"order_by",
"type",
"sub_issue",
"start_target_date",
];
export const handleIssueQueryParamsByLayout = (
layout: TIssueLayouts | undefined,
viewType: "my_issues" | "issues"
): TIssueParams[] | null => {
const queryParams: TIssueParams[] = [];
return null;
};
export const handleIssueParamsDateFormat = (key: string, start_date: any | null, target_date: any | null) => {
if (key === "last_week")
return `${renderDateFormat(new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000))};after,${renderDateFormat(
new Date()
)};before`;
if (key === "2_weeks_from_now")
return `${renderDateFormat(new Date())};after,
${renderDateFormat(new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000))};before`;
if (key === "1_month_from_now")
return `${renderDateFormat(new Date())};after,${renderDateFormat(
new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate())
)};before`;
if (key === "2_months_from_now")
return `${renderDateFormat(new Date())};after,${renderDateFormat(
new Date(new Date().getFullYear(), new Date().getMonth() + 2, new Date().getDate())
)};before`;
if (key === "custom" && start_date && target_date)
return `${renderDateFormat(start_date)};after,${renderDateFormat(target_date)};before`;
};
export const issueFilterVisibilityData: {
[key: string]: {
layout: TIssueLayouts[];
filters: {
[key in TIssueLayouts]: string[];
};
display_properties: {
[key in TIssueLayouts]: boolean;
};
display_filters: {
[key in TIssueLayouts]: string[];
};
extra_options: {
[key in TIssueLayouts]: {
access: boolean;
values: string[];
};
};
};
} = {
my_issues: {
layout: ["list", "kanban"],
filters: {
list: ["priority", "state_group", "labels", "start_date", "due_date"],
kanban: ["priority", "state_group", "labels", "start_date", "due_date"],
calendar: [],
spreadsheet: [],
gantt_chart: [],
},
display_properties: {
list: true,
kanban: true,
calendar: true,
spreadsheet: true,
gantt_chart: false,
},
display_filters: {
list: ["group_by", "order_by", "issue_type"],
kanban: ["group_by", "order_by", "issue_type"],
calendar: ["issue_type"],
spreadsheet: ["issue_type"],
gantt_chart: ["order_by", "issue_type"],
},
extra_options: {
list: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
kanban: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
calendar: {
access: false,
values: [],
},
spreadsheet: {
access: false,
values: [],
},
gantt_chart: {
access: false,
values: [],
},
},
},
issues: {
layout: ["list", "kanban", "calendar", "spreadsheet", "gantt_chart"],
filters: {
list: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
kanban: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
calendar: ["priority", "state", "assignees", "created_by", "labels"],
spreadsheet: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
gantt_chart: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
},
display_properties: {
list: true,
kanban: true,
calendar: true,
spreadsheet: true,
gantt_chart: false,
},
display_filters: {
list: ["group_by", "order_by", "issue_type"],
kanban: ["group_by", "order_by", "issue_type"],
calendar: ["issue_type"],
spreadsheet: ["issue_type"],
gantt_chart: ["order_by", "issue_type"],
},
extra_options: {
list: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
kanban: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
calendar: {
access: false,
values: [],
},
spreadsheet: {
access: false,
values: [],
},
gantt_chart: {
access: true,
values: ["sub_issue"],
},
},
},
if (!layout) return null;
const layoutOptions = ISSUE_DISPLAY_FILTERS_BY_LAYOUT[viewType][layout];
// add filters query params
layoutOptions.filters.forEach((option) => {
queryParams.push(option);
});
// add display filters query params
Object.keys(layoutOptions.display_filters).forEach((option) => {
queryParams.push(option as TIssueParams);
});
// add extra options query params
if (layoutOptions.extra_options.access) {
layoutOptions.extra_options.values.forEach((option) => {
queryParams.push(option);
});
}
// add start_target_date query param for the gantt_chart layout
if (layout === "gantt_chart") queryParams.push("start_target_date");
return queryParams;
};

View File

@ -23,7 +23,6 @@ export interface IIssueFilterStore {
userFilters: IIssueFilterOptions;
defaultDisplayFilters: IIssueDisplayFilterOptions;
defaultFilters: IIssueFilterOptions;
filtersSearchQuery: string;
// action
fetchUserProjectFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
@ -37,7 +36,6 @@ export interface IIssueFilterStore {
projectId: string,
properties: Partial<IIssueDisplayProperties>
) => Promise<void>;
updateFiltersSearchQuery: (query: string) => void;
// computed
appliedFilters: TIssueParams[] | null;
@ -68,7 +66,6 @@ class IssueFilterStore implements IIssueFilterStore {
created_on: true,
updated_on: true,
};
filtersSearchQuery: string = "";
// root store
rootStore;
@ -88,13 +85,11 @@ class IssueFilterStore implements IIssueFilterStore {
userDisplayProperties: observable.ref,
userDisplayFilters: observable.ref,
userFilters: observable.ref,
filtersSearchQuery: observable.ref,
// actions
fetchUserProjectFilters: action,
updateUserFilters: action,
updateDisplayProperties: action,
updateFiltersSearchQuery: action,
// computed
appliedFilters: computed,
@ -162,7 +157,7 @@ class IssueFilterStore implements IIssueFilterStore {
if (this.userDisplayFilters.layout === "calendar") filteredRouteParams.target_date = this.calendarLayoutDateRange();
const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout);
const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "issues");
if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams);
return filteredRouteParams;
@ -202,13 +197,16 @@ class IssueFilterStore implements IIssueFilterStore {
},
};
// set sub_group_by to null if group_by is set to null
if (newViewProps.display_filters.group_by === null) newViewProps.display_filters.sub_group_by = null;
try {
runInAction(() => {
this.userFilters = newViewProps.filters;
this.userDisplayFilters = newViewProps.display_filters;
});
await this.projectService.setProjectView(workspaceSlug, projectId, {
this.projectService.setProjectView(workspaceSlug, projectId, {
view_props: newViewProps,
});
} catch (error) {
@ -248,12 +246,6 @@ class IssueFilterStore implements IIssueFilterStore {
console.log("Failed to update user filters in issue filter store", error);
}
};
updateFiltersSearchQuery: (query: string) => void = (query) => {
runInAction(() => {
this.filtersSearchQuery = query;
});
};
}
export default IssueFilterStore;

View File

@ -30,12 +30,15 @@ export type TIssueOrderByOptions =
export type TIssueTypeFilters = "active" | "backlog" | null;
export type TIssueExtraOptions = "show_empty_groups" | "sub_issue";
export type TIssueParams =
| "priority"
| "state_group"
| "state"
| "assignees"
| "created_by"
| "subscriber"
| "labels"
| "start_date"
| "target_date"