mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
b70047b1d5
commit
479c145b02
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
@ -1,4 +0,0 @@
|
||||
export * from "./display-filters";
|
||||
export * from "./filters";
|
||||
export * from "./helpers";
|
||||
export * from "./layout-selection";
|
@ -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";
|
||||
|
||||
|
@ -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()} - ${
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)}
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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"
|
4
web/components/issues/issue-layouts/header/index.ts
Normal file
4
web/components/issues/issue-layouts/header/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./display-filters";
|
||||
export * from "./filters";
|
||||
export * from "./helpers";
|
||||
export * from "./layout-selection";
|
@ -1,2 +1,3 @@
|
||||
export * from "./calendar";
|
||||
export * from "./header";
|
||||
export * from "./kanban";
|
||||
|
@ -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",
|
||||
},
|
||||
];
|
||||
|
@ -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"],
|
||||
},
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
3
web/types/view-props.d.ts
vendored
3
web/types/view-props.d.ts
vendored
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user