From 8dee7e51cad6caefbe402808ec80a86fe3fd95ac Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 5 Dec 2023 17:26:57 +0530 Subject: [PATCH] chore: workspace profile issues, kanabn DND upgrade, implemented filters in plaen deploy (#2991) --- .../issues/board-views/kanban/block.tsx | 2 +- .../issues/board-views/kanban/header.tsx | 2 +- .../issues/board-views/kanban/index.tsx | 8 +- .../issues/filters-render/index.tsx | 53 --------- .../label/filter-label-block.tsx | 43 ------- .../issues/filters-render/label/index.tsx | 51 -------- .../priority/filter-priority-block.tsx | 42 ------- .../issues/filters-render/priority/index.tsx | 53 --------- .../state/filter-state-block.tsx | 34 ------ .../issues/filters-render/state/index.tsx | 51 -------- .../filters/applied-filters/filters-list.tsx | 80 +++++++++++++ .../issues/filters/applied-filters/label.tsx | 42 +++++++ .../filters/applied-filters/priority.tsx | 31 +++++ .../issues/filters/applied-filters/root.tsx | 90 ++++++++++++++ .../issues/filters/applied-filters/state.tsx | 39 +++++++ .../issues/filters/helpers/dropdown.tsx | 72 ++++++++++++ .../issues/filters/helpers/filter-header.tsx | 22 ++++ .../issues/filters/helpers/filter-option.tsx | 35 ++++++ .../issues/filters/helpers/index.ts | 3 + space/components/issues/filters/index.ts | 11 ++ space/components/issues/filters/labels.tsx | 83 +++++++++++++ space/components/issues/filters/priority.tsx | 51 ++++++++ space/components/issues/filters/root.tsx | 77 ++++++++++++ space/components/issues/filters/selection.tsx | 86 ++++++++++++++ space/components/issues/filters/state.tsx | 78 +++++++++++++ space/components/issues/navbar/index.tsx | 48 ++++++-- .../issues/navbar/issue-board-view.tsx | 3 +- .../components/issues/navbar/issue-filter.tsx | 110 ------------------ space/components/views/project-details.tsx | 8 +- space/layouts/project-layout.tsx | 1 - space/store/issue.ts | 2 +- space/store/issues/base-issue-filter.store.ts | 29 +++++ space/store/issues/helpers.ts | 52 +++++++++ space/store/issues/issue-filters.store.ts | 106 +++++++++++++++++ space/store/issues/types.ts | 36 ++++++ space/store/project.ts | 9 +- space/store/root.ts | 3 + .../issues/issue-layouts/kanban/block.tsx | 39 +++++-- web/components/issues/modal.tsx | 2 +- web/components/profile/profile-issues.tsx | 6 +- .../profile/[userId]/assigned.tsx | 3 +- .../issues/base-issue-kanban-helper.store.ts | 2 - web/store/issues/profile/issue.store.ts | 2 - 43 files changed, 1117 insertions(+), 483 deletions(-) delete mode 100644 space/components/issues/filters-render/index.tsx delete mode 100644 space/components/issues/filters-render/label/filter-label-block.tsx delete mode 100644 space/components/issues/filters-render/label/index.tsx delete mode 100644 space/components/issues/filters-render/priority/filter-priority-block.tsx delete mode 100644 space/components/issues/filters-render/priority/index.tsx delete mode 100644 space/components/issues/filters-render/state/filter-state-block.tsx delete mode 100644 space/components/issues/filters-render/state/index.tsx create mode 100644 space/components/issues/filters/applied-filters/filters-list.tsx create mode 100644 space/components/issues/filters/applied-filters/label.tsx create mode 100644 space/components/issues/filters/applied-filters/priority.tsx create mode 100644 space/components/issues/filters/applied-filters/root.tsx create mode 100644 space/components/issues/filters/applied-filters/state.tsx create mode 100644 space/components/issues/filters/helpers/dropdown.tsx create mode 100644 space/components/issues/filters/helpers/filter-header.tsx create mode 100644 space/components/issues/filters/helpers/filter-option.tsx create mode 100644 space/components/issues/filters/helpers/index.ts create mode 100644 space/components/issues/filters/index.ts create mode 100644 space/components/issues/filters/labels.tsx create mode 100644 space/components/issues/filters/priority.tsx create mode 100644 space/components/issues/filters/root.tsx create mode 100644 space/components/issues/filters/selection.tsx create mode 100644 space/components/issues/filters/state.tsx delete mode 100644 space/components/issues/navbar/issue-filter.tsx create mode 100644 space/store/issues/base-issue-filter.store.ts create mode 100644 space/store/issues/helpers.ts create mode 100644 space/store/issues/issue-filters.store.ts create mode 100644 space/store/issues/types.ts diff --git a/space/components/issues/board-views/kanban/block.tsx b/space/components/issues/board-views/kanban/block.tsx index e44f1dba0..34e4cb3f1 100644 --- a/space/components/issues/board-views/kanban/block.tsx +++ b/space/components/issues/board-views/kanban/block.tsx @@ -13,7 +13,7 @@ import { IIssue } from "types/issue"; import { RootStore } from "store/root"; import { useRouter } from "next/router"; -export const IssueListBlock = observer(({ issue }: { issue: IIssue }) => { +export const IssueKanBanBlock = observer(({ issue }: { issue: IIssue }) => { const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore(); // router diff --git a/space/components/issues/board-views/kanban/header.tsx b/space/components/issues/board-views/kanban/header.tsx index 8f2f28496..488d94b59 100644 --- a/space/components/issues/board-views/kanban/header.tsx +++ b/space/components/issues/board-views/kanban/header.tsx @@ -10,7 +10,7 @@ import { StateGroupIcon } from "@plane/ui"; import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; -export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { +export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) => { const store: RootStore = useMobxStore(); const stateGroup = issueGroupFilter(state.group); diff --git a/space/components/issues/board-views/kanban/index.tsx b/space/components/issues/board-views/kanban/index.tsx index b45b037d2..cc00f931e 100644 --- a/space/components/issues/board-views/kanban/index.tsx +++ b/space/components/issues/board-views/kanban/index.tsx @@ -3,8 +3,8 @@ // mobx react lite import { observer } from "mobx-react-lite"; // components -import { IssueListHeader } from "components/issues/board-views/kanban/header"; -import { IssueListBlock } from "components/issues/board-views/kanban/block"; +import { IssueKanBanHeader } from "components/issues/board-views/kanban/header"; +import { IssueKanBanBlock } from "components/issues/board-views/kanban/block"; // ui import { Icon } from "components/ui"; // interfaces @@ -23,14 +23,14 @@ export const IssueKanbanView = observer(() => { store?.issue?.states.map((_state: IIssueState) => (
- +
{store.issue.getFilteredIssuesByState(_state.id) && store.issue.getFilteredIssuesByState(_state.id).length > 0 ? (
{store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( - + ))}
) : ( diff --git a/space/components/issues/filters-render/index.tsx b/space/components/issues/filters-render/index.tsx deleted file mode 100644 index d797d1506..000000000 --- a/space/components/issues/filters-render/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import IssueStateFilter from "./state"; -import IssueLabelFilter from "./label"; -import IssuePriorityFilter from "./priority"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const clearAllFilters = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "all", - // removeAll: true, - // }) - // ); - }; - - // if (store.issue.getIfFiltersIsEmpty()) return null; - - return ( -
-
- {/* state */} - {/* {store.issue.checkIfFilterExistsForKey("state") && } */} - {/* labels */} - {/* {store.issue.checkIfFilterExistsForKey("label") && } */} - {/* priority */} - {/* {store.issue.checkIfFilterExistsForKey("priority") && } */} - {/* clear all filters */} -
-
Clear all filters
-
- close -
-
-
-
- ); -}); - -export default IssueFilter; diff --git a/space/components/issues/filters-render/label/filter-label-block.tsx b/space/components/issues/filters-render/label/filter-label-block.tsx deleted file mode 100644 index a54fb65e4..000000000 --- a/space/components/issues/filters-render/label/filter-label-block.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssueLabel } from "types/issue"; - -export const RenderIssueLabel = observer(({ label }: { label: IIssueLabel }) => { - const store = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const removeLabelFromFilter = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "label", - // value: label?.id, - // }) - // ); - }; - - return ( -
-
- -
{label?.name}
-
- close -
-
- ); -}); diff --git a/space/components/issues/filters-render/label/index.tsx b/space/components/issues/filters-render/label/index.tsx deleted file mode 100644 index 1d9a4f990..000000000 --- a/space/components/issues/filters-render/label/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { RenderIssueLabel } from "./filter-label-block"; -// interfaces -import { IIssueLabel } from "types/issue"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueLabelFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const clearLabelFilters = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "label", - // removeAll: true, - // }) - // ); - }; - - return ( - <> -
-
Labels
-
- {/* {store?.issue?.labels && - store?.issue?.labels.map( - (_label: IIssueLabel, _index: number) => - store.issue.getUserSelectedFilter("label", _label.id) && ( - - ) - )} */} -
-
- close -
-
- - ); -}); - -export default IssueLabelFilter; diff --git a/space/components/issues/filters-render/priority/filter-priority-block.tsx b/space/components/issues/filters-render/priority/filter-priority-block.tsx deleted file mode 100644 index 5fd1ef1a7..000000000 --- a/space/components/issues/filters-render/priority/filter-priority-block.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// interfaces -import { IIssuePriorityFilters } from "types/issue"; - -export const RenderIssuePriority = observer(({ priority }: { priority: IIssuePriorityFilters }) => { - const store = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const removePriorityFromFilter = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "priority", - // value: priority?.key, - // }) - // ); - }; - - return ( -
-
- {priority?.icon} -
-
{priority?.title}
-
- close -
-
- ); -}); diff --git a/space/components/issues/filters-render/priority/index.tsx b/space/components/issues/filters-render/priority/index.tsx deleted file mode 100644 index 100ba1761..000000000 --- a/space/components/issues/filters-render/priority/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -// components -import { RenderIssuePriority } from "./filter-priority-block"; -// interfaces -import { IIssuePriorityFilters } from "types/issue"; -// constants -import { issuePriorityFilters } from "constants/data"; - -const IssuePriorityFilter = observer(() => { - const store = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const clearPriorityFilters = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "priority", - // removeAll: true, - // }) - // ); - }; - - return ( - <> -
-
Priority
-
- {/* {issuePriorityFilters.map( - (_priority: IIssuePriorityFilters, _index: number) => - store.issue.getUserSelectedFilter("priority", _priority.key) && ( - - ) - )} */} -
-
{ - clearPriorityFilters(); - }} - > - close -
-
- - ); -}); - -export default IssuePriorityFilter; diff --git a/space/components/issues/filters-render/state/filter-state-block.tsx b/space/components/issues/filters-render/state/filter-state-block.tsx deleted file mode 100644 index b9c8ed4ec..000000000 --- a/space/components/issues/filters-render/state/filter-state-block.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { observer } from "mobx-react-lite"; -// interfaces -import { IIssueState } from "types/issue"; -// constants -import { issueGroupFilter } from "constants/data"; - -export const RenderIssueState = observer(({ state }: { state: IIssueState }) => { - const stateGroup = issueGroupFilter(state.group); - - const removeStateFromFilter = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "state", - // value: state?.id, - // }) - // ); - }; - - if (stateGroup === null) return <>; - return ( -
-
- {/* */} -
-
{state?.name}
-
- close -
-
- ); -}); diff --git a/space/components/issues/filters-render/state/index.tsx b/space/components/issues/filters-render/state/index.tsx deleted file mode 100644 index 0198c5215..000000000 --- a/space/components/issues/filters-render/state/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useRouter } from "next/router"; -// mobx react lite -import { observer } from "mobx-react-lite"; -// components -import { RenderIssueState } from "./filter-state-block"; -// interfaces -import { IIssueState } from "types/issue"; -// mobx hook -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -const IssueStateFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; - - const clearStateFilters = () => { - // router.replace( - // store.issue.getURLDefinition(workspace_slug, project_slug, { - // key: "state", - // removeAll: true, - // }) - // ); - }; - - return ( - <> -
-
State
-
- {/* {store?.issue?.states && - store?.issue?.states.map( - (_state: IIssueState, _index: number) => - store.issue.getUserSelectedFilter("state", _state.id) && ( - - ) - )} */} -
-
- close -
-
- - ); -}); - -export default IssueStateFilter; diff --git a/space/components/issues/filters/applied-filters/filters-list.tsx b/space/components/issues/filters/applied-filters/filters-list.tsx new file mode 100644 index 000000000..898898232 --- /dev/null +++ b/space/components/issues/filters/applied-filters/filters-list.tsx @@ -0,0 +1,80 @@ +// components +import { AppliedLabelsFilters } from "./label"; +import { AppliedPriorityFilters } from "./priority"; +import { AppliedStateFilters } from "./state"; +// icons +import { X } from "lucide-react"; +// helpers +import { IIssueFilterOptions } from "store/issues/types"; +import { IIssueLabel, IIssueState } from "types/issue"; +// types + +type Props = { + appliedFilters: IIssueFilterOptions; + handleRemoveAllFilters: () => void; + handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; + labels?: IIssueLabel[] | undefined; + states?: IIssueState[] | undefined; +}; + +export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); + +export const AppliedFiltersList: React.FC = (props) => { + const { appliedFilters, handleRemoveAllFilters, handleRemoveFilter, labels, states } = props; + + return ( +
+ {Object.entries(appliedFilters).map(([key, value]) => { + const filterKey = key as keyof IIssueFilterOptions; + + if (!value) return; + + return ( +
+ {replaceUnderscoreIfSnakeCase(filterKey)} +
+ {filterKey === "priority" && ( + handleRemoveFilter("priority", val)} values={value} /> + )} + + {filterKey === "labels" && labels && ( + handleRemoveFilter("labels", val)} + labels={labels} + values={value} + /> + )} + + {filterKey === "state" && states && ( + handleRemoveFilter("state", val)} + states={states} + values={value} + /> + )} + + +
+
+ ); + })} + +
+ ); +}; diff --git a/space/components/issues/filters/applied-filters/label.tsx b/space/components/issues/filters/applied-filters/label.tsx new file mode 100644 index 000000000..ecf824210 --- /dev/null +++ b/space/components/issues/filters/applied-filters/label.tsx @@ -0,0 +1,42 @@ +import { X } from "lucide-react"; +// types +import { IIssueLabel } from "types/issue"; + +type Props = { + handleRemove: (val: string) => void; + labels: IIssueLabel[] | undefined; + values: string[]; +}; + +export const AppliedLabelsFilters: React.FC = (props) => { + const { handleRemove, labels, values } = props; + + return ( + <> + {values.map((labelId) => { + const labelDetails = labels?.find((l) => l.id === labelId); + + if (!labelDetails) return null; + + return ( +
+ + {labelDetails.name} + +
+ ); + })} + + ); +}; diff --git a/space/components/issues/filters/applied-filters/priority.tsx b/space/components/issues/filters/applied-filters/priority.tsx new file mode 100644 index 000000000..f051abf2d --- /dev/null +++ b/space/components/issues/filters/applied-filters/priority.tsx @@ -0,0 +1,31 @@ +import { PriorityIcon } from "@plane/ui"; +import { X } from "lucide-react"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; +}; + +export const AppliedPriorityFilters: React.FC = (props) => { + const { handleRemove, values } = props; + + return ( + <> + {values && + values.length > 0 && + values.map((priority) => ( +
+ + {priority} + +
+ ))} + + ); +}; diff --git a/space/components/issues/filters/applied-filters/root.tsx b/space/components/issues/filters/applied-filters/root.tsx new file mode 100644 index 000000000..3f77dcc06 --- /dev/null +++ b/space/components/issues/filters/applied-filters/root.tsx @@ -0,0 +1,90 @@ +import { FC, useCallback } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// components +import { AppliedFiltersList } from "./filters-list"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; +import { IIssueFilterOptions } from "store/issues/types"; + +export const IssueAppliedFilters: FC = observer(() => { + const router = useRouter(); + const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as { + workspace_slug: string; + project_slug: string; + }; + + const { + issuesFilter: { issueFilters, updateFilters }, + issue: { states, labels }, + project: { activeBoard }, + }: RootStore = useMobxStore(); + + const userFilters = issueFilters?.filters || {}; + + const appliedFilters: IIssueFilterOptions = {}; + Object.entries(userFilters).forEach(([key, value]) => { + if (!value) return; + if (Array.isArray(value) && value.length === 0) return; + appliedFilters[key as keyof IIssueFilterOptions] = value; + }); + + const updateRouteParams = useCallback( + (key: keyof IIssueFilterOptions | null, value: string[] | null, clearFields: boolean = false) => { + const state = key === "state" ? value || [] : issueFilters?.filters?.state ?? []; + const priority = key === "priority" ? value || [] : issueFilters?.filters?.priority ?? []; + const labels = key === "labels" ? value || [] : issueFilters?.filters?.labels ?? []; + + let params: any = { board: activeBoard || "list" }; + if (!clearFields) { + if (priority.length > 0) params = { ...params, priorities: priority.join(",") }; + if (state.length > 0) params = { ...params, states: state.join(",") }; + if (labels.length > 0) params = { ...params, labels: labels.join(",") }; + } + + router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true }); + }, + [workspaceSlug, projectId, activeBoard, issueFilters, router] + ); + + const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { + if (!projectId) return; + if (!value) { + updateFilters(projectId, { [key]: null }); + return; + } + + let newValues = issueFilters?.filters?.[key] ?? []; + newValues = newValues.filter((val) => val !== value); + + updateFilters(projectId, { [key]: newValues }); + updateRouteParams(key, newValues); + }; + + const handleRemoveAllFilters = () => { + if (!projectId) return; + + const newFilters: IIssueFilterOptions = {}; + Object.keys(userFilters).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = null; + }); + + updateFilters(projectId, { ...newFilters }); + updateRouteParams(null, null, true); + }; + + if (Object.keys(appliedFilters).length === 0) return null; + + return ( +
+ +
+ ); +}); diff --git a/space/components/issues/filters/applied-filters/state.tsx b/space/components/issues/filters/applied-filters/state.tsx new file mode 100644 index 000000000..f238197b8 --- /dev/null +++ b/space/components/issues/filters/applied-filters/state.tsx @@ -0,0 +1,39 @@ +import { X } from "lucide-react"; +import { StateGroupIcon } from "@plane/ui"; +// icons +import { IIssueState } from "types/issue"; +// types + +type Props = { + handleRemove: (val: string) => void; + states: IIssueState[]; + values: string[]; +}; + +export const AppliedStateFilters: React.FC = (props) => { + const { handleRemove, states, values } = props; + + return ( + <> + {values.map((stateId) => { + const stateDetails = states?.find((s) => s.id === stateId); + + if (!stateDetails) return null; + + return ( +
+ + {stateDetails.name} + +
+ ); + })} + + ); +}; diff --git a/space/components/issues/filters/helpers/dropdown.tsx b/space/components/issues/filters/helpers/dropdown.tsx new file mode 100644 index 000000000..0f93b75c9 --- /dev/null +++ b/space/components/issues/filters/helpers/dropdown.tsx @@ -0,0 +1,72 @@ +import React, { Fragment, useState } from "react"; +import { usePopper } from "react-popper"; +import { Popover, Transition } from "@headlessui/react"; +import { Placement } from "@popperjs/core"; +// ui +import { Button } from "@plane/ui"; +// icons +import { ChevronUp } from "lucide-react"; + +type Props = { + children: React.ReactNode; + title?: string; + placement?: Placement; +}; + +export const FiltersDropdown: React.FC = (props) => { + const { children, title = "Dropdown", placement } = props; + + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "auto", + }); + + return ( + + {({ open }) => { + if (open) { + } + return ( + <> + + + + + +
+
{children}
+
+
+
+ + ); + }} +
+ ); +}; diff --git a/space/components/issues/filters/helpers/filter-header.tsx b/space/components/issues/filters/helpers/filter-header.tsx new file mode 100644 index 000000000..4513b0795 --- /dev/null +++ b/space/components/issues/filters/helpers/filter-header.tsx @@ -0,0 +1,22 @@ +import React from "react"; +// lucide icons +import { ChevronDown, ChevronUp } from "lucide-react"; + +interface IFilterHeader { + title: string; + isPreviewEnabled: boolean; + handleIsPreviewEnabled: () => void; +} + +export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => ( +
+
{title}
+ +
+); diff --git a/space/components/issues/filters/helpers/filter-option.tsx b/space/components/issues/filters/helpers/filter-option.tsx new file mode 100644 index 000000000..4b6f1b041 --- /dev/null +++ b/space/components/issues/filters/helpers/filter-option.tsx @@ -0,0 +1,35 @@ +import React from "react"; +// lucide icons +import { Check } from "lucide-react"; + +type Props = { + icon?: React.ReactNode; + isChecked: boolean; + title: React.ReactNode; + onClick?: () => void; + multiple?: boolean; +}; + +export const FilterOption: React.FC = (props) => { + const { icon, isChecked, multiple = true, onClick, title } = props; + + return ( + + ); +}; diff --git a/space/components/issues/filters/helpers/index.ts b/space/components/issues/filters/helpers/index.ts new file mode 100644 index 000000000..ef38d9884 --- /dev/null +++ b/space/components/issues/filters/helpers/index.ts @@ -0,0 +1,3 @@ +export * from "./dropdown"; +export * from "./filter-header"; +export * from "./filter-option"; diff --git a/space/components/issues/filters/index.ts b/space/components/issues/filters/index.ts new file mode 100644 index 000000000..56a01386d --- /dev/null +++ b/space/components/issues/filters/index.ts @@ -0,0 +1,11 @@ +// filters +export * from "./root"; +export * from "./selection"; + +// properties +export * from "./state"; +export * from "./priority"; +export * from "./labels"; + +// helpers +export * from "./helpers"; diff --git a/space/components/issues/filters/labels.tsx b/space/components/issues/filters/labels.tsx new file mode 100644 index 000000000..4b8aa3b4f --- /dev/null +++ b/space/components/issues/filters/labels.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; + +// components +import { FilterHeader, FilterOption } from "./helpers"; +// ui +import { Loader } from "@plane/ui"; +// types +import { IIssueLabel } from "types/issue"; + +const LabelIcons = ({ color }: { color: string }) => ( + +); + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + labels: IIssueLabel[] | undefined; + searchQuery: string; +}; + +export const FilterLabels: React.FC = (props) => { + const { appliedFilters, handleUpdate, labels, searchQuery } = props; + + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase())); + + const handleViewToggle = () => { + if (!filteredOptions) return; + + if (itemsToRender === filteredOptions.length) setItemsToRender(5); + else setItemsToRender(filteredOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((label) => ( + handleUpdate(label?.id)} + icon={} + title={label.name} + /> + ))} + {filteredOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}; diff --git a/space/components/issues/filters/priority.tsx b/space/components/issues/filters/priority.tsx new file mode 100644 index 000000000..94a7f6a8c --- /dev/null +++ b/space/components/issues/filters/priority.tsx @@ -0,0 +1,51 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +// ui +import { PriorityIcon } from "@plane/ui"; +// components +import { FilterHeader, FilterOption } from "./helpers"; +// constants +import { issuePriorityFilters } from "constants/data"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterPriority: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = issuePriorityFilters.filter((p) => p.key.includes(searchQuery.toLowerCase())); + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + filteredOptions.map((priority) => ( + handleUpdate(priority.key)} + icon={} + title={priority.title} + /> + )) + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/space/components/issues/filters/root.tsx b/space/components/issues/filters/root.tsx new file mode 100644 index 000000000..eb9946a24 --- /dev/null +++ b/space/components/issues/filters/root.tsx @@ -0,0 +1,77 @@ +import { FC, useCallback } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// components +import { FiltersDropdown } from "./helpers/dropdown"; +import { FilterSelection } from "./selection"; +// types +import { IIssueFilterOptions } from "store/issues/types"; +// helpers +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "store/issues/helpers"; +// store +import { RootStore } from "store/root"; +import { useMobxStore } from "lib/mobx/store-provider"; + +export const IssueFiltersDropdown: FC = observer(() => { + const router = useRouter(); + const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as { + workspace_slug: string; + project_slug: string; + }; + + const { + project: { activeBoard }, + issue: { states, labels }, + issuesFilter: { issueFilters, updateFilters }, + }: RootStore = useMobxStore(); + + const updateRouteParams = useCallback( + (key: keyof IIssueFilterOptions, value: string[]) => { + const state = key === "state" ? value : issueFilters?.filters?.state ?? []; + const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? []; + const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? []; + + let params: any = { board: activeBoard || "list" }; + if (priority.length > 0) params = { ...params, priorities: priority.join(",") }; + if (state.length > 0) params = { ...params, states: state.join(",") }; + if (labels.length > 0) params = { ...params, labels: labels.join(",") }; + + router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true }); + }, + [workspaceSlug, projectId, activeBoard, issueFilters, router] + ); + + const handleFilters = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!projectId) return; + const newValues = issueFilters?.filters?.[key] ?? []; + + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters(projectId, { [key]: newValues }); + updateRouteParams(key, newValues); + }, + [projectId, issueFilters, updateFilters, updateRouteParams] + ); + + return ( +
+ + + +
+ ); +}); diff --git a/space/components/issues/filters/selection.tsx b/space/components/issues/filters/selection.tsx new file mode 100644 index 000000000..e479a7d59 --- /dev/null +++ b/space/components/issues/filters/selection.tsx @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Search, X } from "lucide-react"; +// components +import { FilterLabels, FilterPriority, FilterState } from "./"; +// types + +// filter helpers +import { ILayoutDisplayFiltersOptions } from "store/issues/helpers"; +import { IIssueFilterOptions } from "store/issues/types"; +import { IIssueState, IIssueLabel } from "types/issue"; + +type Props = { + filters: IIssueFilterOptions; + handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void; + layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; + labels?: IIssueLabel[] | undefined; + states?: IIssueState[] | undefined; +}; + +export const FilterSelection: React.FC = observer((props) => { + const { filters, handleFilters, layoutDisplayFiltersOptions, labels, states } = props; + + const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); + + const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter); + + return ( +
+
+
+ + setFiltersSearchQuery(e.target.value)} + autoFocus + /> + {filtersSearchQuery !== "" && ( + + )} +
+
+
+ {/* priority */} + {isFilterEnabled("priority") && ( +
+ handleFilters("priority", val)} + searchQuery={filtersSearchQuery} + /> +
+ )} + + {/* state */} + {isFilterEnabled("state") && ( +
+ handleFilters("state", val)} + searchQuery={filtersSearchQuery} + states={states} + /> +
+ )} + + {/* labels */} + {isFilterEnabled("labels") && ( +
+ handleFilters("labels", val)} + labels={labels} + searchQuery={filtersSearchQuery} + /> +
+ )} +
+
+ ); +}); diff --git a/space/components/issues/filters/state.tsx b/space/components/issues/filters/state.tsx new file mode 100644 index 000000000..1175a5ed6 --- /dev/null +++ b/space/components/issues/filters/state.tsx @@ -0,0 +1,78 @@ +import React, { useState } from "react"; +// components +import { FilterHeader, FilterOption } from "./helpers"; +// ui +import { Loader, StateGroupIcon } from "@plane/ui"; +// types +import { IIssueState } from "types/issue"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; + states: IIssueState[] | undefined; +}; + +export const FilterState: React.FC = (props) => { + const { appliedFilters, handleUpdate, searchQuery, states } = props; + + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = states?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + + const handleViewToggle = () => { + if (!filteredOptions) return; + + if (itemsToRender === filteredOptions.length) setItemsToRender(5); + else setItemsToRender(filteredOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + <> + {filteredOptions.slice(0, itemsToRender).map((state) => ( + handleUpdate(state.id)} + icon={} + title={state.name} + /> + ))} + {filteredOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}; diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index e4ce36050..d6491bc26 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -9,6 +9,7 @@ import { observer } from "mobx-react-lite"; // import { NavbarSearch } from "./search"; import { NavbarIssueBoardView } from "./issue-board-view"; import { NavbarTheme } from "./theme"; +import { IssueFiltersDropdown } from "components/issues/filters"; // ui import { Avatar, Button } from "@plane/ui"; import { Briefcase } from "lucide-react"; @@ -16,6 +17,7 @@ import { Briefcase } from "lucide-react"; import { useMobxStore } from "lib/mobx/store-provider"; // store import { RootStore } from "store/root"; +import { TIssueBoardKeys } from "types/issue"; const renderEmoji = (emoji: string | { name: string; color: string }) => { if (!emoji) return; @@ -30,10 +32,21 @@ const renderEmoji = (emoji: string | { name: string; color: string }) => { }; const IssueNavbar = observer(() => { - const { project: projectStore, user: userStore }: RootStore = useMobxStore(); + const { + project: projectStore, + user: userStore, + issuesFilter: { updateFilters }, + }: RootStore = useMobxStore(); // router const router = useRouter(); - const { workspace_slug, project_slug, board } = router.query; + const { workspace_slug, project_slug, board, states, priorities, labels } = router.query as { + workspace_slug: string; + project_slug: string; + board: string; + states: string; + priorities: string; + labels: string; + }; const user = userStore?.currentUser; @@ -46,7 +59,7 @@ const IssueNavbar = observer(() => { useEffect(() => { if (workspace_slug && project_slug && projectStore?.deploySettings) { const viewsAcceptable: string[] = []; - let currentBoard: string | null = null; + let currentBoard: TIssueBoardKeys | null = null; if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("list"); if (projectStore?.deploySettings?.views?.kanban) viewsAcceptable.push("kanban"); @@ -56,31 +69,41 @@ const IssueNavbar = observer(() => { if (board) { if (viewsAcceptable.includes(board.toString())) { - currentBoard = board.toString(); + currentBoard = board.toString() as TIssueBoardKeys; } else { if (viewsAcceptable && viewsAcceptable.length > 0) { - currentBoard = viewsAcceptable[0]; + currentBoard = viewsAcceptable[0] as TIssueBoardKeys; } } } else { if (viewsAcceptable && viewsAcceptable.length > 0) { - currentBoard = viewsAcceptable[0]; + currentBoard = viewsAcceptable[0] as TIssueBoardKeys; } } if (currentBoard) { if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) { + let params: any = { board: currentBoard }; + if (priorities && priorities.length > 0) params = { ...params, priorities: priorities }; + if (states && states.length > 0) params = { ...params, states: states }; + if (labels && labels.length > 0) params = { ...params, labels: labels }; + + let storeParams: any = {}; + if (priorities && priorities.length > 0) storeParams = { ...storeParams, priority: priorities.split(",") }; + if (states && states.length > 0) storeParams = { ...storeParams, state: states.split(",") }; + if (labels && labels.length > 0) storeParams = { ...storeParams, labels: labels.split(",") }; + + if (storeParams) updateFilters(project_slug, storeParams); + projectStore.setActiveBoard(currentBoard); router.push({ pathname: `/${workspace_slug}/${project_slug}`, - query: { - board: currentBoard, - }, + query: { ...params }, }); } } } - }, [board, workspace_slug, project_slug, router, projectStore, projectStore?.deploySettings]); + }, [board, workspace_slug, project_slug, router, projectStore, projectStore?.deploySettings, updateFilters]); return (
@@ -120,6 +143,11 @@ const IssueNavbar = observer(() => {
+ {/* issue filters */} +
+ +
+ {/* theming */}
diff --git a/space/components/issues/navbar/issue-board-view.tsx b/space/components/issues/navbar/issue-board-view.tsx index 16b09229a..906d3543d 100644 --- a/space/components/issues/navbar/issue-board-view.tsx +++ b/space/components/issues/navbar/issue-board-view.tsx @@ -5,6 +5,7 @@ import { issueViews } from "constants/data"; // mobx import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; +import { TIssueBoardKeys } from "types/issue"; export const NavbarIssueBoardView = observer(() => { const { @@ -15,7 +16,7 @@ export const NavbarIssueBoardView = observer(() => { const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string }; const handleCurrentBoardView = (boardView: string) => { - setActiveBoard(boardView); + setActiveBoard(boardView as TIssueBoardKeys); router.push(`/${workspace_slug}/${project_slug}?board=${boardView}`); }; diff --git a/space/components/issues/navbar/issue-filter.tsx b/space/components/issues/navbar/issue-filter.tsx deleted file mode 100644 index 83d5159d6..000000000 --- a/space/components/issues/navbar/issue-filter.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import { ChevronDown } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; -// components -import { Dropdown } from "components/ui/dropdown"; -// constants -import { issueGroupFilter } from "constants/data"; - -const PRIORITIES = ["urgent", "high", "medium", "low"]; - -export const NavbarIssueFilter = observer(() => { - const store: RootStore = useMobxStore(); - - const router = useRouter(); - const pathName = router.asPath; - - const handleOnSelect = (key: "states" | "labels" | "priorities", value: string) => { - // if (key === "states") { - // store.issue.userSelectedStates = store.issue.userSelectedStates.includes(value) - // ? store.issue.userSelectedStates.filter((s) => s !== value) - // : [...store.issue.userSelectedStates, value]; - // } else if (key === "labels") { - // store.issue.userSelectedLabels = store.issue.userSelectedLabels.includes(value) - // ? store.issue.userSelectedLabels.filter((l) => l !== value) - // : [...store.issue.userSelectedLabels, value]; - // } else if (key === "priorities") { - // store.issue.userSelectedPriorities = store.issue.userSelectedPriorities.includes(value) - // ? store.issue.userSelectedPriorities.filter((p) => p !== value) - // : [...store.issue.userSelectedPriorities, value]; - // } - // const paramsCommaSeparated = `${`board=${store.issue.currentIssueBoardView || "list"}`}${ - // store.issue.userSelectedPriorities.length > 0 ? `&priorities=${store.issue.userSelectedPriorities.join(",")}` : "" - // }${store.issue.userSelectedStates.length > 0 ? `&states=${store.issue.userSelectedStates.join(",")}` : ""}${ - // store.issue.userSelectedLabels.length > 0 ? `&labels=${store.issue.userSelectedLabels.join(",")}` : "" - // }`; - // router.replace(`${pathName}?${paramsCommaSeparated}`); - }; - - return ( - - Filters -
) : ( projectStore?.activeBoard && ( - <> +
+ {/* applied filters */} + + {projectStore?.activeBoard === "list" && (
@@ -85,7 +89,7 @@ export const ProjectDetailsView = observer(() => { {projectStore?.activeBoard === "calendar" && } {projectStore?.activeBoard === "spreadsheet" && } {projectStore?.activeBoard === "gantt" && } - +
) )} diff --git a/space/layouts/project-layout.tsx b/space/layouts/project-layout.tsx index 1a0b7899e..c8bdfd9a1 100644 --- a/space/layouts/project-layout.tsx +++ b/space/layouts/project-layout.tsx @@ -1,4 +1,3 @@ -import Link from "next/link"; import Image from "next/image"; // mobx diff --git a/space/store/issue.ts b/space/store/issue.ts index d47336984..02dd3cdd0 100644 --- a/space/store/issue.ts +++ b/space/store/issue.ts @@ -1,4 +1,4 @@ -import { observable, action, computed, makeObservable, runInAction, reaction } from "mobx"; +import { observable, action, computed, makeObservable, runInAction } from "mobx"; // services import IssueService from "services/issue.service"; // store diff --git a/space/store/issues/base-issue-filter.store.ts b/space/store/issues/base-issue-filter.store.ts new file mode 100644 index 000000000..2cd2e3bc9 --- /dev/null +++ b/space/store/issues/base-issue-filter.store.ts @@ -0,0 +1,29 @@ +// types +import { RootStore } from "store/root"; + +export interface IIssueFilterBaseStore { + // helper methods + computedFilter(filters: any, filteredParams: any): any; +} + +export class IssueFilterBaseStore implements IIssueFilterBaseStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + // root store + this.rootStore = _rootStore; + } + + // helper methods + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; +} diff --git a/space/store/issues/helpers.ts b/space/store/issues/helpers.ts new file mode 100644 index 000000000..a862ca6e0 --- /dev/null +++ b/space/store/issues/helpers.ts @@ -0,0 +1,52 @@ +import { TIssueBoardKeys } from "types/issue"; +import { IIssueFilterOptions, TIssueParams } from "./types"; + +export const isNil = (value: any) => { + if (value === undefined || value === null) return true; + + return false; +}; + +export interface ILayoutDisplayFiltersOptions { + filters: (keyof IIssueFilterOptions)[]; + display_properties: boolean | null; + display_filters: null; + extra_options: null; +} + +export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { + [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; +} = { + issues: { + list: { + filters: ["priority", "state", "labels"], + display_properties: null, + display_filters: null, + extra_options: null, + }, + kanban: { + filters: ["priority", "state", "labels"], + display_properties: null, + display_filters: null, + extra_options: null, + }, + }, +}; + +export const handleIssueQueryParamsByLayout = ( + layout: TIssueBoardKeys | undefined, + viewType: "issues" +): TIssueParams[] | null => { + const queryParams: TIssueParams[] = []; + + if (!layout) return null; + + const layoutOptions = ISSUE_DISPLAY_FILTERS_BY_LAYOUT[viewType][layout]; + + // add filters query params + layoutOptions.filters.forEach((option) => { + queryParams.push(option); + }); + + return queryParams; +}; diff --git a/space/store/issues/issue-filters.store.ts b/space/store/issues/issue-filters.store.ts new file mode 100644 index 000000000..f2408e290 --- /dev/null +++ b/space/store/issues/issue-filters.store.ts @@ -0,0 +1,106 @@ +import { action, makeObservable, observable, runInAction, computed } from "mobx"; +// types +import { RootStore } from "store/root"; +import { IIssueFilterOptions, TIssueParams } from "./types"; +import { handleIssueQueryParamsByLayout } from "./helpers"; +import { IssueFilterBaseStore } from "./base-issue-filter.store"; + +interface IFiltersOptions { + filters: IIssueFilterOptions; +} + +export interface IIssuesFilterStore { + // observables + projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined; + // computed + issueFilters: IFiltersOptions | undefined; + appliedFilters: TIssueParams[] | undefined; + // helpers + issueDisplayFilters: (projectId: string) => IFiltersOptions | undefined; + // actions + updateFilters: (projectId: string, filters: IIssueFilterOptions) => Promise; +} + +export class IssuesFilterStore extends IssueFilterBaseStore implements IIssuesFilterStore { + // observables + projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined = undefined; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observables + projectIssueFilters: observable.ref, + // computed + issueFilters: computed, + appliedFilters: computed, + // actions + updateFilters: action, + }); + // root store + this.rootStore = _rootStore; + } + + // helpers + issueDisplayFilters = (projectId: string) => { + if (!projectId) return undefined; + return this.projectIssueFilters?.[projectId] || undefined; + }; + + // actions + + updateFilters = async (projectId: string, filters: IIssueFilterOptions) => { + try { + let _projectIssueFilters = { ...this.projectIssueFilters }; + if (!_projectIssueFilters) _projectIssueFilters = {}; + if (!_projectIssueFilters[projectId]) _projectIssueFilters[projectId] = { filters: {} }; + + const _filters = { + filters: { ..._projectIssueFilters[projectId].filters }, + }; + + _filters.filters = { ..._filters.filters, ...filters }; + + _projectIssueFilters[projectId] = { + filters: _filters.filters, + }; + + runInAction(() => { + this.projectIssueFilters = _projectIssueFilters; + }); + + return _filters; + } catch (error) { + throw error; + } + }; + + get issueFilters() { + const projectId = this.rootStore.project.project?.id; + if (!projectId) return undefined; + + const issueFilters = this.issueDisplayFilters(projectId); + if (!issueFilters) return undefined; + + return issueFilters; + } + + get appliedFilters() { + const userFilters = this.issueFilters; + const layout = this.rootStore.project?.activeBoard; + if (!userFilters || !layout) return undefined; + + let filteredRouteParams: any = { + priority: userFilters?.filters?.priority || undefined, + state: userFilters?.filters?.state || undefined, + labels: userFilters?.filters?.labels || undefined, + }; + + const filteredParams = handleIssueQueryParamsByLayout(layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + return filteredRouteParams; + } +} diff --git a/space/store/issues/types.ts b/space/store/issues/types.ts new file mode 100644 index 000000000..d1de0a5ea --- /dev/null +++ b/space/store/issues/types.ts @@ -0,0 +1,36 @@ +import { IIssue } from "types/issue"; + +export type TIssueGroupByOptions = "state" | "priority" | "labels" | null; + +export type TIssueParams = "priority" | "state" | "labels"; + +export interface IIssueFilterOptions { + state?: string[] | null; + labels?: string[] | null; + priority?: string[] | null; +} + +// issues +export interface IGroupedIssues { + [group_id: string]: string[]; +} + +export interface ISubGroupedIssues { + [sub_grouped_id: string]: { + [group_id: string]: string[]; + }; +} + +export type TUnGroupedIssues = string[]; + +export interface IIssueResponse { + [issue_id: string]: IIssue; +} + +export type TLoader = "init-loader" | "mutation" | undefined; + +export interface ViewFlags { + enableQuickAdd: boolean; + enableIssueCreation: boolean; + enableInlineEditing: boolean; +} diff --git a/space/store/project.ts b/space/store/project.ts index ddd589f9a..76b4d06cb 100644 --- a/space/store/project.ts +++ b/space/store/project.ts @@ -2,6 +2,7 @@ import { observable, action, makeObservable, runInAction } from "mobx"; // service import ProjectService from "services/project.service"; +import { TIssueBoardKeys } from "types/issue"; // types import { IWorkspace, IProject, IProjectSettings } from "types/project"; @@ -12,9 +13,9 @@ export interface IProjectStore { project: IProject | null; deploySettings: IProjectSettings | null; viewOptions: any; - activeBoard: string | null; + activeBoard: TIssueBoardKeys | null; fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise; - setActiveBoard: (value: string) => void; + setActiveBoard: (value: TIssueBoardKeys) => void; } class ProjectStore implements IProjectStore { @@ -25,7 +26,7 @@ class ProjectStore implements IProjectStore { project: IProject | null = null; deploySettings: IProjectSettings | null = null; viewOptions: any = null; - activeBoard: string | null = null; + activeBoard: TIssueBoardKeys | null = null; // root store rootStore; // service @@ -80,7 +81,7 @@ class ProjectStore implements IProjectStore { } }; - setActiveBoard = (boardValue: string) => { + setActiveBoard = (boardValue: TIssueBoardKeys) => { this.activeBoard = boardValue; }; } diff --git a/space/store/root.ts b/space/store/root.ts index 22b951d20..5a9e0bca1 100644 --- a/space/store/root.ts +++ b/space/store/root.ts @@ -6,6 +6,7 @@ import IssueStore, { IIssueStore } from "./issue"; import ProjectStore, { IProjectStore } from "./project"; import IssueDetailStore, { IIssueDetailStore } from "./issue_details"; import { IMentionsStore, MentionsStore } from "./mentions.store"; +import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store"; enableStaticRendering(typeof window === "undefined"); @@ -15,6 +16,7 @@ export class RootStore { issueDetails: IIssueDetailStore; project: IProjectStore; mentionsStore: IMentionsStore; + issuesFilter: IIssuesFilterStore; constructor() { this.user = new UserStore(this); @@ -22,5 +24,6 @@ export class RootStore { this.project = new ProjectStore(this); this.issueDetails = new IssueDetailStore(this); this.mentionsStore = new MentionsStore(this); + this.issuesFilter = new IssuesFilterStore(this); } } diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 316d88144..4b00361b0 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -1,4 +1,6 @@ +import { memo } from "react"; import { Draggable } from "@hello-pangea/dnd"; +import isEqual from "lodash/isEqual"; // components import { KanBanProperties } from "./properties"; // ui @@ -21,7 +23,7 @@ interface IssueBlockProps { isReadOnly: boolean; } -export const KanbanIssueBlock: React.FC = (props) => { +export const KanBanIssueMemoBlock: React.FC = (props) => { const { sub_group_id, columnId, @@ -63,30 +65,36 @@ export const KanbanIssueBlock: React.FC = (props) => { {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef} - onClick={handleIssuePeekOverview} > {issue.tempId !== undefined && (
)} -
- {quickActions( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !columnId && columnId === "null" ? null : columnId, - issue - )} -
{displayProperties && displayProperties?.key && ( -
- {issue.project_detail.identifier}-{issue.sequence_id} +
+
+ {issue.project_detail.identifier}-{issue.sequence_id} +
+
+ {quickActions( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !columnId && columnId === "null" ? null : columnId, + issue + )} +
)} -
{issue.name}
+
+ {issue.name} +
= (props) => { ); }; + +const validateMemo = (prevProps: IssueBlockProps, nextProps: IssueBlockProps) => { + if (prevProps.issue != nextProps.issue) return true; + return false; +}; + +export const KanbanIssueBlock = memo(KanBanIssueMemoBlock, validateMemo); diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index 3d5780c06..65ae1b12f 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -240,7 +240,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop if (handleSubmit) { await handleSubmit(res); } else { - currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId); + if (viewId) currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId); if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res, payload.cycle); if (payload.module && payload.module !== "") await addIssueToModule(res, payload.module); diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index 11974caaa..cfb8cdbe6 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -16,6 +16,8 @@ interface IProfileIssuesPage { } export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { + const { type } = props; + const router = useRouter(); const { workspaceSlug, userId } = router.query as { workspaceSlug: string; @@ -28,11 +30,11 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { }: RootStore = useMobxStore(); useSWR( - workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${props.type}` : null, + workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${type}` : null, async () => { if (workspaceSlug && userId) { await fetchFilters(workspaceSlug); - await fetchIssues(workspaceSlug, userId, getIssues ? "mutation" : "init-loader", props.type); + await fetchIssues(workspaceSlug, userId, getIssues ? "mutation" : "init-loader", type); } } ); diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index a2c09dd98..cd2eb09bc 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -1,5 +1,4 @@ import React, { ReactElement } from "react"; -import { observer } from "mobx-react-lite"; // layouts import { AppLayout } from "layouts/app-layout"; import { ProfileAuthWrapper } from "layouts/user-profile-layout"; @@ -9,7 +8,7 @@ import { UserProfileHeader } from "components/headers"; import { NextPageWithLayout } from "types/app"; import { ProfileIssuesPage } from "components/profile/profile-issues"; -const ProfileAssignedIssuesPage: NextPageWithLayout = observer(() => ); +const ProfileAssignedIssuesPage: NextPageWithLayout = () => ; ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) { return ( diff --git a/web/store/issues/base-issue-kanban-helper.store.ts b/web/store/issues/base-issue-kanban-helper.store.ts index 62b25fe22..e21c85e84 100644 --- a/web/store/issues/base-issue-kanban-helper.store.ts +++ b/web/store/issues/base-issue-kanban-helper.store.ts @@ -118,8 +118,6 @@ export class KanBanHelpers implements IKanBanHelpers { const [removed] = sourceIssues.splice(source.index, 1); - console.log("removed", removed); - if (removed) { if (viewId) store?.removeIssue(workspaceSlug, projectId, removed, viewId); else store?.removeIssue(workspaceSlug, projectId, removed); diff --git a/web/store/issues/profile/issue.store.ts b/web/store/issues/profile/issue.store.ts index 37b9f3085..eb02796c6 100644 --- a/web/store/issues/profile/issue.store.ts +++ b/web/store/issues/profile/issue.store.ts @@ -28,7 +28,6 @@ export interface IProfileIssuesStore { workspaceSlug: string, userId: string, loadType: TLoader, - _?: string, type?: "assigned" | "created" | "subscribed" ) => Promise; createIssue: (workspaceSlug: string, userId: string, data: Partial) => Promise; @@ -151,7 +150,6 @@ export class ProfileIssuesStore extends IssueBaseStore implements IProfileIssues workspaceSlug: string, userId: string, loadType: TLoader = "init-loader", - _?: string, type?: "assigned" | "created" | "subscribed" ) => { try {