diff --git a/web/components/core/filters/date-filter-modal.tsx b/web/components/core/filters/date-filter-modal.tsx index eb285fd6f..4a1ec0e8b 100644 --- a/web/components/core/filters/date-filter-modal.tsx +++ b/web/components/core/filters/date-filter-modal.tsx @@ -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 = ({ - title, - field, - filters, - handleClose, - isOpen, - onSelect, -}) => { +export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, onSelect }) => { const { handleSubmit, watch, control } = useForm({ defaultValues, }); @@ -52,32 +39,13 @@ export const DateFilterModal: React.FC = ({ 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 = ({ )} /> - +
= ({ Cancel - + Apply
diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 086c58cd4..d3d18729d 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -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) => { + if (!workspaceSlug || !projectId) return; + + issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { + display_filters: { + ...updatedDisplayFilter, + }, + }); + }, + [issueFilterStore, projectId, workspaceSlug] + ); return (
handleLayoutChange(layout)} selectedLayout={issueFilterStore.userDisplayFilters.layout ?? "list"} /> - - - - - - + + + + + +
); }); diff --git a/web/components/issue-layouts/display-filters/display-filters-selection.tsx b/web/components/issue-layouts/display-filters/display-filters-selection.tsx deleted file mode 100644 index 01394548d..000000000 --- a/web/components/issue-layouts/display-filters/display-filters-selection.tsx +++ /dev/null @@ -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 ( -
- {/* display properties */} - {ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.display_properties[ - issueFilterStore.userDisplayFilters.layout ?? "list" - ] && ( -
- -
- )} - - {/* group by */} - {isDisplayFilterEnabled("group_by") && ( -
- -
- )} - - {/* sub-group by */} - {isDisplayFilterEnabled("sub_group_by") && ( -
- -
- )} - - {/* order by */} - {isDisplayFilterEnabled("order_by") && ( -
- -
- )} - - {/* issue type */} - {isDisplayFilterEnabled("issue_type") && ( -
- -
- )} - - {/* Options */} - {ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.extra_options[issueFilterStore.userDisplayFilters.layout ?? "list"] - .access && ( -
- -
- )} -
- ); -}); diff --git a/web/components/issue-layouts/display-filters/extra-options.tsx b/web/components/issue-layouts/display-filters/extra-options.tsx deleted file mode 100644 index 8b2354435..000000000 --- a/web/components/issue-layouts/display-filters/extra-options.tsx +++ /dev/null @@ -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 ( - <> - setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {ISSUE_EXTRA_OPTIONS.map((option) => { - if (!isExtraOptionEnabled(option.key)) return null; - - return ( - - ); - })} -
- )} - - ); -}); diff --git a/web/components/issue-layouts/display-filters/group-by.tsx b/web/components/issue-layouts/display-filters/group-by.tsx deleted file mode 100644 index 5ad584b58..000000000 --- a/web/components/issue-layouts/display-filters/group-by.tsx +++ /dev/null @@ -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 ( - <> - setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {ISSUE_GROUP_BY_OPTIONS.map((groupBy) => ( - handleGroupBy(groupBy.key)} - title={groupBy.title} - multiple={false} - /> - ))} -
- )} - - ); -}); diff --git a/web/components/issue-layouts/display-filters/issue-type.tsx b/web/components/issue-layouts/display-filters/issue-type.tsx deleted file mode 100644 index c2fe34857..000000000 --- a/web/components/issue-layouts/display-filters/issue-type.tsx +++ /dev/null @@ -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 ( - <> - setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {ISSUE_FILTER_OPTIONS.map((issueType) => ( - handleIssueType(issueType?.key)} - title={issueType.title} - multiple={false} - /> - ))} -
- )} - - ); -}); diff --git a/web/components/issue-layouts/display-filters/order-by.tsx b/web/components/issue-layouts/display-filters/order-by.tsx deleted file mode 100644 index b355dbd1b..000000000 --- a/web/components/issue-layouts/display-filters/order-by.tsx +++ /dev/null @@ -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 ( - <> - setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {ISSUE_ORDER_BY_OPTIONS.map((orderBy) => ( - handleOrderBy(orderBy.key)} - title={orderBy.title} - multiple={false} - /> - ))} -
- )} - - ); -}); diff --git a/web/components/issue-layouts/display-filters/sub-group-by.tsx b/web/components/issue-layouts/display-filters/sub-group-by.tsx deleted file mode 100644 index ad83067ec..000000000 --- a/web/components/issue-layouts/display-filters/sub-group-by.tsx +++ /dev/null @@ -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 ( - <> - setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {ISSUE_GROUP_BY_OPTIONS.map((subGroupBy) => { - if ( - issueFilterStore.userDisplayFilters.group_by !== null && - subGroupBy.key === issueFilterStore.userDisplayFilters.group_by - ) - return null; - - return ( - handleSubGroupBy(subGroupBy.key)} - title={subGroupBy.title} - multiple={false} - /> - ); - })} -
- )} - - ); -}); diff --git a/web/components/issue-layouts/filters/filters-selection.tsx b/web/components/issue-layouts/filters/filters-selection.tsx deleted file mode 100644 index 71aa144bf..000000000 --- a/web/components/issue-layouts/filters/filters-selection.tsx +++ /dev/null @@ -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 = 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 ( -
-
-
- - issueFilterStore.updateFiltersSearchQuery(e.target.value)} - autoFocus - /> - {issueFilterStore.filtersSearchQuery !== "" && ( - - )} -
-
-
- {/* priority */} -
- - {isViewMoreVisible("priority") && ( - - )} -
- - {/* state group */} -
- - {isViewMoreVisible("state_group") && ( - - )} -
- - {/* state */} -
- - {isViewMoreVisible("state") && ( - - )} -
- - {/* assignees */} -
- - {isViewMoreVisible("assignees") && ( - - )} -
- - {/* created_by */} -
- - {isViewMoreVisible("created_by") && ( - - )} -
- - {/* labels */} -
- - {isViewMoreVisible("labels") && ( - - )} -
- - {/* start_date */} - {/*
- -
*/} - - {/* due_date */} - {/*
- -
*/} -
-
- ); -}); diff --git a/web/components/issue-layouts/filters/start-date.tsx b/web/components/issue-layouts/filters/start-date.tsx deleted file mode 100644 index c93091a11..000000000 --- a/web/components/issue-layouts/filters/start-date.tsx +++ /dev/null @@ -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 ( - <> - 0 ? ` (${appliedFiltersCount})` : ""}`} - isPreviewEnabled={previewEnabled} - handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {issueFilterStore?.userFilters?.start_date && - issueFilterStore?.userFilters?.start_date.length > 0 && - issueFilterStore?.userFilters?.start_date.map((_startDate) => ( - - ))} -
- )} - - ); -}); diff --git a/web/components/issue-layouts/filters/state-group.tsx b/web/components/issue-layouts/filters/state-group.tsx deleted file mode 100644 index 2601eb308..000000000 --- a/web/components/issue-layouts/filters/state-group.tsx +++ /dev/null @@ -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 = 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 ( - <> - 0 ? ` (${appliedFiltersCount})` : ""}`} - isPreviewEnabled={previewEnabled} - handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {filteredOptions.length > 0 ? ( - filteredOptions - .slice(0, itemsToRender) - .map((stateGroup) => ( - handleUpdateStateGroup(stateGroup.key)} - icon={} - title={stateGroup.title} - /> - )) - ) : ( -

No matches found

- )} -
- )} - - ); -}); diff --git a/web/components/issue-layouts/filters/target-date.tsx b/web/components/issue-layouts/filters/target-date.tsx deleted file mode 100644 index 7a00b12a4..000000000 --- a/web/components/issue-layouts/filters/target-date.tsx +++ /dev/null @@ -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 ( - <> - 0 ? ` (${appliedFiltersCount})` : ""}`} - isPreviewEnabled={previewEnabled} - handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} - /> - {previewEnabled && ( -
- {issueFilterStore?.userFilters?.target_date && - issueFilterStore?.userFilters?.target_date.length > 0 && - issueFilterStore?.userFilters?.target_date.map((_targetDate) => ( - - ))} -
- )} - - ); -}); diff --git a/web/components/issue-layouts/helpers/dropdown.tsx b/web/components/issue-layouts/helpers/dropdown.tsx deleted file mode 100644 index 33e813d99..000000000 --- a/web/components/issue-layouts/helpers/dropdown.tsx +++ /dev/null @@ -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) => ( - - {({ open }) => { - if (open) { - } - return ( - <> - -
{title}
-
- -
-
- - -
{children}
-
-
- - ); - }} -
-); diff --git a/web/components/issue-layouts/index.ts b/web/components/issue-layouts/index.ts index 61375f0ff..e69de29bb 100644 --- a/web/components/issue-layouts/index.ts +++ b/web/components/issue-layouts/index.ts @@ -1,4 +0,0 @@ -export * from "./display-filters"; -export * from "./filters"; -export * from "./helpers"; -export * from "./layout-selection"; diff --git a/web/components/issue-layouts/root.tsx b/web/components/issue-layouts/root.tsx index 250f3204a..38029c633 100644 --- a/web/components/issue-layouts/root.tsx +++ b/web/components/issue-layouts/root.tsx @@ -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"; diff --git a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx index b92fe96b9..a6e410e09 100644 --- a/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx +++ b/web/components/issues/issue-layouts/calendar/dropdowns/months-dropdown.tsx @@ -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()} - ${ diff --git a/web/components/issues/issue-layouts/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/header/display-filters/display-filters-selection.tsx new file mode 100644 index 000000000..8c4d5d392 --- /dev/null +++ b/web/components/issues/issue-layouts/header/display-filters/display-filters-selection.tsx @@ -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) => void; + layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions; +}; + +export const DisplayFiltersSelection: React.FC = observer((props) => { + const { displayFilters, handleDisplayFiltersUpdate, layoutDisplayFiltersOptions } = props; + + const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) => + Object.keys(layoutDisplayFiltersOptions.display_filters).includes(displayFilter); + + return ( +
+ {/* display properties */} + {layoutDisplayFiltersOptions.display_properties && ( +
+ +
+ )} + + {/* group by */} + {isDisplayFilterEnabled("group_by") && ( +
+ + handleDisplayFiltersUpdate({ + group_by: val, + }) + } + /> +
+ )} + + {/* sub-group by */} + {isDisplayFilterEnabled("sub_group_by") && displayFilters.group_by !== null && ( +
+ + handleDisplayFiltersUpdate({ + sub_group_by: val, + }) + } + subGroupByOptions={layoutDisplayFiltersOptions.display_filters.sub_group_by ?? []} + /> +
+ )} + + {/* order by */} + {isDisplayFilterEnabled("order_by") && ( +
+ + handleDisplayFiltersUpdate({ + order_by: val, + }) + } + /> +
+ )} + + {/* issue type */} + {isDisplayFilterEnabled("type") && ( +
+ + handleDisplayFiltersUpdate({ + type: val, + }) + } + /> +
+ )} + + {/* Options */} + {layoutDisplayFiltersOptions.extra_options.access && ( +
+ + handleDisplayFiltersUpdate({ + [key]: val, + }) + } + enabledExtraOptions={layoutDisplayFiltersOptions.extra_options.values} + /> +
+ )} +
+ ); +}); diff --git a/web/components/issue-layouts/display-filters/display-properties.tsx b/web/components/issues/issue-layouts/header/display-filters/display-properties.tsx similarity index 100% rename from web/components/issue-layouts/display-filters/display-properties.tsx rename to web/components/issues/issue-layouts/header/display-filters/display-properties.tsx diff --git a/web/components/issues/issue-layouts/header/display-filters/extra-options.tsx b/web/components/issues/issue-layouts/header/display-filters/extra-options.tsx new file mode 100644 index 000000000..111d25772 --- /dev/null +++ b/web/components/issues/issue-layouts/header/display-filters/extra-options.tsx @@ -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 = observer((props) => { + const { selectedExtraOptions, handleUpdate, enabledExtraOptions } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + + const isExtraOptionEnabled = (option: TIssueExtraOptions) => enabledExtraOptions.includes(option); + + return ( + <> + setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {ISSUE_EXTRA_OPTIONS.map((option) => { + if (!isExtraOptionEnabled(option.key)) return null; + + return ( + handleUpdate(option.key, !selectedExtraOptions?.[option.key])} + title={option.title} + /> + ); + })} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/header/display-filters/group-by.tsx new file mode 100644 index 000000000..04820f94c --- /dev/null +++ b/web/components/issues/issue-layouts/header/display-filters/group-by.tsx @@ -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 = observer((props) => { + const { selectedGroupBy, selectedSubGroupBy, groupByOptions, handleUpdate } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + + return ( + <> + setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => { + if (selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy) return null; + + return ( + handleUpdate(groupBy.key)} + title={groupBy.title} + multiple={false} + /> + ); + })} +
+ )} + + ); +}); diff --git a/web/components/issue-layouts/display-filters/index.ts b/web/components/issues/issue-layouts/header/display-filters/index.ts similarity index 100% rename from web/components/issue-layouts/display-filters/index.ts rename to web/components/issues/issue-layouts/header/display-filters/index.ts diff --git a/web/components/issues/issue-layouts/header/display-filters/issue-type.tsx b/web/components/issues/issue-layouts/header/display-filters/issue-type.tsx new file mode 100644 index 000000000..8732bc739 --- /dev/null +++ b/web/components/issues/issue-layouts/header/display-filters/issue-type.tsx @@ -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 = observer((props) => { + const { selectedIssueType, handleUpdate } = props; + + const [previewEnabled, setPreviewEnabled] = React.useState(true); + + return ( + <> + setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {ISSUE_FILTER_OPTIONS.map((issueType) => ( + handleUpdate(issueType?.key)} + title={issueType.title} + multiple={false} + /> + ))} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/header/display-filters/order-by.tsx b/web/components/issues/issue-layouts/header/display-filters/order-by.tsx new file mode 100644 index 000000000..0387e9abb --- /dev/null +++ b/web/components/issues/issue-layouts/header/display-filters/order-by.tsx @@ -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 = observer((props) => { + const { selectedOrderBy, handleUpdate } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + + return ( + <> + setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {ISSUE_ORDER_BY_OPTIONS.map((orderBy) => ( + handleUpdate(orderBy.key)} + title={orderBy.title} + multiple={false} + /> + ))} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/header/display-filters/sub-group-by.tsx b/web/components/issues/issue-layouts/header/display-filters/sub-group-by.tsx new file mode 100644 index 000000000..83f09092d --- /dev/null +++ b/web/components/issues/issue-layouts/header/display-filters/sub-group-by.tsx @@ -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 = observer((props) => { + const { selectedGroupBy, selectedSubGroupBy, handleUpdate, subGroupByOptions } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + + return ( + <> + setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {ISSUE_GROUP_BY_OPTIONS.filter((option) => subGroupByOptions.includes(option.key)).map((subGroupBy) => { + if (selectedGroupBy !== null && subGroupBy.key === selectedGroupBy) return null; + + return ( + handleUpdate(subGroupBy.key)} + title={subGroupBy.title} + multiple={false} + /> + ); + })} +
+ )} + + ); +}); diff --git a/web/components/issue-layouts/filters/assignees.tsx b/web/components/issues/issue-layouts/header/filters/assignees.tsx similarity index 51% rename from web/components/issue-layouts/filters/assignees.tsx rename to web/components/issues/issue-layouts/header/filters/assignees.tsx index f79db0e33..252890d52 100644 --- a/web/components/issue-layouts/filters/assignees.tsx +++ b/web/components/issues/issue-layouts/header/filters/assignees.tsx @@ -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 = 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 = observer((props) => {
{filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions - .slice(0, itemsToRender) - .map((member) => ( + <> + {filteredOptions.slice(0, itemsToRender).map((member) => ( handleUpdateAssignees(member.member?.id)} + isChecked={appliedFilters?.includes(member.member?.id) ? true : false} + onClick={() => handleUpdate(member.member?.id)} icon={} title={member.member?.display_name} /> - )) + ))} + {viewButtons} + ) : (

No matches found

) diff --git a/web/components/issue-layouts/filters/created-by.tsx b/web/components/issues/issue-layouts/header/filters/created-by.tsx similarity index 54% rename from web/components/issue-layouts/filters/created-by.tsx rename to web/components/issues/issue-layouts/header/filters/created-by.tsx index c34b461b1..791e1e931 100644 --- a/web/components/issue-layouts/filters/created-by.tsx +++ b/web/components/issues/issue-layouts/header/filters/created-by.tsx @@ -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 = 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 = observer((props) => {
{filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions - .slice(0, itemsToRender) - .map((member) => ( + <> + {filteredOptions.slice(0, itemsToRender).map((member) => ( handleUpdateCreatedBy(member.member?.id)} + isChecked={appliedFilters?.includes(member.member?.id) ? true : false} + onClick={() => handleUpdate(member.member?.id)} icon={} title={member.member?.display_name} /> - )) + ))} + {viewButtons} + ) : (

No matches found

) diff --git a/web/components/issues/issue-layouts/header/filters/filters-selection.tsx b/web/components/issues/issue-layouts/header/filters/filters-selection.tsx new file mode 100644 index 000000000..cf65b7a70 --- /dev/null +++ b/web/components/issues/issue-layouts/header/filters/filters-selection.tsx @@ -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 = 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 ( +
+
+
+ + setFiltersSearchQuery(e.target.value)} + autoFocus + /> + {filtersSearchQuery !== "" && ( + + )} +
+
+
+ {/* priority */} + {isFilterEnabled("priority") && ( +
+ handleFiltersUpdate("priority", val)} + itemsToRender={filtersToRender.priority?.currentLength ?? 0} + searchQuery={filtersSearchQuery} + viewButtons={ +
+ {/* TODO: handle view more and less in a better way */} + {isViewMoreVisible("priority") && ( + + )} + {isViewLessVisible("priority") && ( + + )} +
+ } + /> +
+ )} + + {/* state group */} + {isFilterEnabled("state_group") && ( +
+ handleFiltersUpdate("state_group", val)} + itemsToRender={filtersToRender.state_group?.currentLength ?? 0} + searchQuery={filtersSearchQuery} + /> +
+ {isViewMoreVisible("state_group") && ( + + )} + {isViewLessVisible("state_group") && ( + + )} +
+
+ )} + + {/* state */} + {isFilterEnabled("state") && ( +
+ handleFiltersUpdate("state", val)} + itemsToRender={filtersToRender.state?.currentLength ?? 0} + searchQuery={filtersSearchQuery} + projectId={projectId} + /> +
+ {isViewMoreVisible("state") && ( + + )} + {isViewLessVisible("state") && ( + + )} +
+
+ )} + + {/* assignees */} + {isFilterEnabled("assignees") && ( +
+ handleFiltersUpdate("assignees", val)} + itemsToRender={filtersToRender.assignees?.currentLength ?? 0} + projectId={projectId} + searchQuery={filtersSearchQuery} + viewButtons={ +
+ {isViewMoreVisible("assignees") && ( + + )} + {isViewLessVisible("assignees") && ( + + )} +
+ } + /> +
+ )} + + {/* created_by */} + {isFilterEnabled("created_by") && ( +
+ handleFiltersUpdate("created_by", val)} + itemsToRender={filtersToRender.created_by?.currentLength ?? 0} + projectId={projectId} + searchQuery={filtersSearchQuery} + viewButtons={ +
+ {isViewMoreVisible("created_by") && ( + + )} + {isViewLessVisible("created_by") && ( + + )} +
+ } + /> +
+ )} + + {/* labels */} + {isFilterEnabled("labels") && ( +
+ handleFiltersUpdate("labels", val)} + itemsToRender={filtersToRender.labels?.currentLength ?? 0} + projectId={projectId} + searchQuery={filtersSearchQuery} + viewButtons={ +
+ {isViewMoreVisible("labels") && ( + + )} + {isViewLessVisible("labels") && ( + + )} +
+ } + /> +
+ )} + + {/* start_date */} + {isFilterEnabled("start_date") && ( +
+ handleFiltersUpdate("start_date", val)} + itemsToRender={filtersToRender.start_date?.currentLength ?? 0} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* target_date */} + {isFilterEnabled("target_date") && ( +
+ handleFiltersUpdate("target_date", val)} + itemsToRender={filtersToRender.target_date?.currentLength ?? 0} + searchQuery={filtersSearchQuery} + /> +
+ )} +
+
+ ); +}); diff --git a/web/components/issue-layouts/filters/index.ts b/web/components/issues/issue-layouts/header/filters/index.ts similarity index 100% rename from web/components/issue-layouts/filters/index.ts rename to web/components/issues/issue-layouts/header/filters/index.ts diff --git a/web/components/issue-layouts/filters/labels.tsx b/web/components/issues/issue-layouts/header/filters/labels.tsx similarity index 56% rename from web/components/issue-layouts/filters/labels.tsx rename to web/components/issues/issue-layouts/header/filters/labels.tsx index b71c82dc2..407bd707f 100644 --- a/web/components/issue-layouts/filters/labels.tsx +++ b/web/components/issues/issue-layouts/header/filters/labels.tsx @@ -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 }) => ( ); -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 = 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 = observer((props) => {
{filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions - .slice(0, itemsToRender) - .map((label) => ( + <> + {filteredOptions.slice(0, itemsToRender).map((label) => ( handleUpdateLabels(label?.id)} + isChecked={appliedFilters?.includes(label?.id) ? true : false} + onClick={() => handleUpdate(label?.id)} icon={} title={label.name} /> - )) + ))} + {viewButtons} + ) : (

No matches found

) diff --git a/web/components/issue-layouts/filters/priority.tsx b/web/components/issues/issue-layouts/header/filters/priority.tsx similarity index 66% rename from web/components/issue-layouts/filters/priority.tsx rename to web/components/issues/issue-layouts/header/filters/priority.tsx index 659f76f32..4eff176e0 100644 --- a/web/components/issue-layouts/filters/priority.tsx +++ b/web/components/issues/issue-layouts/header/filters/priority.tsx @@ -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 = 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 = observer((props) => { {previewEnabled && (
{filteredOptions.length > 0 ? ( - filteredOptions - .slice(0, itemsToRender) - .map((priority) => ( + <> + {filteredOptions.slice(0, itemsToRender).map((priority) => ( handleUpdatePriority(priority.key)} + isChecked={appliedFilters?.includes(priority.key) ? true : false} + onClick={() => handleUpdate(priority.key)} icon={} title={priority.title} /> - )) + ))} + {viewButtons} + ) : (

No matches found

)} diff --git a/web/components/issues/issue-layouts/header/filters/start-date.tsx b/web/components/issues/issue-layouts/header/filters/start-date.tsx new file mode 100644 index 000000000..ad5ed955a --- /dev/null +++ b/web/components/issues/issue-layouts/header/filters/start-date.tsx @@ -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 = 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 && ( + setIsDateFilterModalOpen(false)} + isOpen={isDateFilterModalOpen} + onSelect={(val) => handleUpdate(val)} + title="Start date" + /> + )} + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((option) => ( + handleUpdate(option.value)} + title={option.name} + multiple={false} + /> + ))} + setIsDateFilterModalOpen(true)} + title="Custom" + multiple={false} + /> + + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/header/filters/state-group.tsx b/web/components/issues/issue-layouts/header/filters/state-group.tsx new file mode 100644 index 000000000..fd22cd10d --- /dev/null +++ b/web/components/issues/issue-layouts/header/filters/state-group.tsx @@ -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 = 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 ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + filteredOptions + .slice(0, itemsToRender) + .map((stateGroup) => ( + handleUpdate(stateGroup.key)} + icon={} + title={stateGroup.title} + /> + )) + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/issue-layouts/filters/state.tsx b/web/components/issues/issue-layouts/header/filters/state.tsx similarity index 56% rename from web/components/issue-layouts/filters/state.tsx rename to web/components/issues/issue-layouts/header/filters/state.tsx index a9bace648..6a1232588 100644 --- a/web/components/issue-layouts/filters/state.tsx +++ b/web/components/issues/issue-layouts/header/filters/state.tsx @@ -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 = 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 = observer((props) => { {filteredOptions.slice(0, itemsToRender).map((state) => ( handleUpdateState(state?.id)} - icon={} - title={state?.name} + isChecked={appliedFilters?.includes(state.id) ? true : false} + onClick={() => handleUpdate(state.id)} + icon={} + title={state.name} /> ))} diff --git a/web/components/issues/issue-layouts/header/filters/target-date.tsx b/web/components/issues/issue-layouts/header/filters/target-date.tsx new file mode 100644 index 000000000..c6ee45b04 --- /dev/null +++ b/web/components/issues/issue-layouts/header/filters/target-date.tsx @@ -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 = 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 && ( + setIsDateFilterModalOpen(false)} + isOpen={isDateFilterModalOpen} + onSelect={(val) => handleUpdate(val)} + title="Due date" + /> + )} + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((option) => ( + handleUpdate(option.value)} + title={option.name} + multiple={false} + /> + ))} + setIsDateFilterModalOpen(true)} + title="Custom" + multiple={false} + /> + + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/header/helpers/dropdown.tsx b/web/components/issues/issue-layouts/header/helpers/dropdown.tsx new file mode 100644 index 000000000..00c589a76 --- /dev/null +++ b/web/components/issues/issue-layouts/header/helpers/dropdown.tsx @@ -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) => { + const { children, title = "Dropdown" } = props; + + return ( + + {({ open }) => { + if (open) { + } + return ( + <> + +
{title}
+
+ +
+
+ + +
{children}
+
+
+ + ); + }} +
+ ); +}; diff --git a/web/components/issue-layouts/helpers/filter-header.tsx b/web/components/issues/issue-layouts/header/helpers/filter-header.tsx similarity index 87% rename from web/components/issue-layouts/helpers/filter-header.tsx rename to web/components/issues/issue-layouts/header/helpers/filter-header.tsx index 6dd0c7a02..4513b0795 100644 --- a/web/components/issue-layouts/helpers/filter-header.tsx +++ b/web/components/issues/issue-layouts/header/helpers/filter-header.tsx @@ -9,7 +9,7 @@ interface IFilterHeader { } export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => ( -
+
{title}