diff --git a/space/app/[workspace_slug]/[project_id]/layout.tsx b/space/app/[workspace_slug]/[project_id]/layout.tsx index ad713db18..aabff72ba 100644 --- a/space/app/[workspace_slug]/[project_id]/layout.tsx +++ b/space/app/[workspace_slug]/[project_id]/layout.tsx @@ -2,25 +2,24 @@ import Image from "next/image"; import { notFound } from "next/navigation"; // components import IssueNavbar from "@/components/issues/navbar"; -// services -import ProjectService from "@/services/project.service"; // assets import planeLogo from "public/plane-logo.svg"; -const projectService = new ProjectService(); - -export default async function ProjectLayout({ children, params }: { children: React.ReactNode; params: any }) { +export default async function ProjectLayout({ + children, + params, +}: { + children: React.ReactNode; + params: { workspace_slug: string; project_id: string }; +}) { const { workspace_slug, project_id } = params; - const projectSettings = await projectService.getProjectSettings(workspace_slug, project_id).catch(() => null); - if (!projectSettings) { - notFound(); - } + if (!workspace_slug || !project_id) notFound(); return (
- +
{children}
; return ; } diff --git a/space/components/issues/board-views/block-priority.tsx b/space/components/issues/board-views/block-priority.tsx index 9bfa3808b..3110930ec 100644 --- a/space/components/issues/board-views/block-priority.tsx +++ b/space/components/issues/board-views/block-priority.tsx @@ -1,11 +1,11 @@ "use client"; // types -import { issuePriorityFilter } from "@/constants/data"; -import { TIssuePriorityKey } from "types/issue"; +import { issuePriorityFilter } from "@/constants/issue"; +import { TIssueFilterPriority } from "@/types/issue"; // constants -export const IssueBlockPriority = ({ priority }: { priority: TIssuePriorityKey | null }) => { +export const IssueBlockPriority = ({ priority }: { priority: TIssueFilterPriority | null }) => { const priority_detail = priority != null ? issuePriorityFilter(priority) : null; if (priority_detail === null) return <>; diff --git a/space/components/issues/board-views/block-state.tsx b/space/components/issues/board-views/block-state.tsx index bc444445d..39b10ceb0 100644 --- a/space/components/issues/board-views/block-state.tsx +++ b/space/components/issues/board-views/block-state.tsx @@ -1,7 +1,7 @@ // ui import { StateGroupIcon } from "@plane/ui"; // constants -import { issueGroupFilter } from "@/constants/data"; +import { issueGroupFilter } from "@/constants/issue"; export const IssueBlockState = ({ state }: any) => { const stateGroup = issueGroupFilter(state.group); diff --git a/space/components/issues/board-views/kanban/block.tsx b/space/components/issues/board-views/kanban/block.tsx index 4ecee992c..9b7c87b8d 100644 --- a/space/components/issues/board-views/kanban/block.tsx +++ b/space/components/issues/board-views/kanban/block.tsx @@ -21,7 +21,7 @@ type IssueKanBanBlockProps = { export const IssueKanBanBlock: FC = observer((props) => { const { workspaceSlug, projectId, params, issue } = props; - const { board, priorities, states, labels } = params; + const { board, priority, states, labels } = params; // store const { project } = useProject(); const { setPeekId } = useIssueDetails(); @@ -33,7 +33,7 @@ export const IssueKanBanBlock: FC = observer((props) => { setPeekId(issue.id); const params: any = { board: board, peekId: issue.id }; if (states && states.length > 0) params.states = states; - if (priorities && priorities.length > 0) params.priorities = priorities; + if (priority && priority.length > 0) params.priority = priority; if (labels && labels.length > 0) params.labels = labels; router.push(`/${workspaceSlug}/${projectId}?${searchParams}`); }; diff --git a/space/components/issues/board-views/kanban/header.tsx b/space/components/issues/board-views/kanban/header.tsx index 9e88b6322..baf5612b3 100644 --- a/space/components/issues/board-views/kanban/header.tsx +++ b/space/components/issues/board-views/kanban/header.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // ui import { StateGroupIcon } from "@plane/ui"; // constants -import { issueGroupFilter } from "@/constants/data"; +import { issueGroupFilter } from "@/constants/issue"; // mobx hook // import { useIssue } from "@/hooks/store"; // interfaces diff --git a/space/components/issues/board-views/list/block.tsx b/space/components/issues/board-views/list/block.tsx index 2bc9e8133..364bf1231 100644 --- a/space/components/issues/board-views/list/block.tsx +++ b/space/components/issues/board-views/list/block.tsx @@ -21,7 +21,7 @@ type IssueListBlockProps = { export const IssueListBlock: FC = observer((props) => { const { workspaceSlug, projectId, issue } = props; - const { board, states, priorities, labels } = useParams(); + const { board, states, priority, labels } = useParams(); const searchParams = useSearchParams(); // store const { project } = useProject(); @@ -33,7 +33,7 @@ export const IssueListBlock: FC = observer((props) => { setPeekId(issue.id); const params: any = { board: board, peekId: issue.id }; if (states && states.length > 0) params.states = states; - if (priorities && priorities.length > 0) params.priorities = priorities; + if (priority && priority.length > 0) params.priority = priority; if (labels && labels.length > 0) params.labels = labels; router.push(`/${workspaceSlug}/${projectId}?${searchParams}`); // router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`); diff --git a/space/components/issues/board-views/list/header.tsx b/space/components/issues/board-views/list/header.tsx index f13a17f5a..2f8f6c018 100644 --- a/space/components/issues/board-views/list/header.tsx +++ b/space/components/issues/board-views/list/header.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; // ui import { StateGroupIcon } from "@plane/ui"; // constants -import { issueGroupFilter } from "@/constants/data"; +import { issueGroupFilter } from "@/constants/issue"; // mobx hook // import { useIssue } from "@/hooks/store"; // types diff --git a/space/components/issues/filters/applied-filters/filters-list.tsx b/space/components/issues/filters/applied-filters/filters-list.tsx index a0d703c2c..cec23d301 100644 --- a/space/components/issues/filters/applied-filters/filters-list.tsx +++ b/space/components/issues/filters/applied-filters/filters-list.tsx @@ -1,30 +1,34 @@ +"use client"; + // icons +import { observer } from "mobx-react-lite"; import { X } from "lucide-react"; // types -import { IIssueLabel, IIssueState, IIssueFilterOptions } from "@/types/issue"; +import { IIssueLabel, IIssueState, TFilters } from "@/types/issue"; // components import { AppliedPriorityFilters } from "./priority"; import { AppliedStateFilters } from "./state"; type Props = { - appliedFilters: IIssueFilterOptions; + appliedFilters: TFilters; handleRemoveAllFilters: () => void; - handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; + handleRemoveFilter: (key: keyof TFilters, value: string | null) => void; labels?: IIssueLabel[] | undefined; states?: IIssueState[] | undefined; }; export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); -export const AppliedFiltersList: React.FC = (props) => { +export const AppliedFiltersList: React.FC = observer((props) => { const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter, states } = props; return (
{Object.entries(appliedFilters).map(([key, value]) => { - const filterKey = key as keyof IIssueFilterOptions; + const filterKey = key as keyof TFilters; + const filterValue = value as TFilters[keyof TFilters]; - if (!value) return; + if (!filterValue) return; return (
= (props) => { {replaceUnderscoreIfSnakeCase(filterKey)}
{filterKey === "priority" && ( - handleRemoveFilter("priority", val)} values={value} /> + handleRemoveFilter("priority", val)} + values={filterValue ?? []} + /> )} {/* {filterKey === "labels" && labels && ( @@ -49,7 +56,7 @@ export const AppliedFiltersList: React.FC = (props) => { handleRemoveFilter("state", val)} states={states} - values={value} + values={filterValue ?? []} /> )} @@ -74,4 +81,4 @@ export const AppliedFiltersList: React.FC = (props) => {
); -}; +}); diff --git a/space/components/issues/filters/applied-filters/label.tsx b/space/components/issues/filters/applied-filters/label.tsx index c4a9d9ca7..86f65f867 100644 --- a/space/components/issues/filters/applied-filters/label.tsx +++ b/space/components/issues/filters/applied-filters/label.tsx @@ -1,6 +1,8 @@ +"use client"; + import { X } from "lucide-react"; // types -import { IIssueLabel } from "types/issue"; +import { IIssueLabel } from "@/types/issue"; type Props = { handleRemove: (val: string) => void; diff --git a/space/components/issues/filters/applied-filters/priority.tsx b/space/components/issues/filters/applied-filters/priority.tsx index bbe72e404..6fdf5c653 100644 --- a/space/components/issues/filters/applied-filters/priority.tsx +++ b/space/components/issues/filters/applied-filters/priority.tsx @@ -1,3 +1,5 @@ +"use client"; + import { X } from "lucide-react"; import { PriorityIcon } from "@plane/ui"; diff --git a/space/components/issues/filters/applied-filters/root.tsx b/space/components/issues/filters/applied-filters/root.tsx index 1e32ea363..9dd1eb013 100644 --- a/space/components/issues/filters/applied-filters/root.tsx +++ b/space/components/issues/filters/applied-filters/root.tsx @@ -1,27 +1,33 @@ "use client"; import { FC, useCallback } from "react"; +import cloneDeep from "lodash/cloneDeep"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/navigation"; // hooks -import { useIssue, useProject, useIssueFilter } from "@/hooks/store"; +import { useIssue, useIssueFilter } from "@/hooks/store"; // store -import { IIssueFilterOptions } from "@/types/issue"; +import { TIssueQueryFilters } from "@/types/issue"; // components import { AppliedFiltersList } from "./filters-list"; -// TODO: fix component types -export const IssueAppliedFilters: FC = observer((props: any) => { - const router = useRouter(); - const { workspaceSlug, projectId } = props; - const { states, labels } = useIssue(); - const { activeLayout } = useProject(); - const { issueFilters, updateFilters } = useIssueFilter(); +type TIssueAppliedFilters = { + workspaceSlug: string; + projectId: string; +}; +export const IssueAppliedFilters: FC = observer((props) => { + const router = useRouter(); + // props + const { workspaceSlug, projectId } = props; + // hooks + const { issueFilters, initIssueFilters, updateIssueFilters } = useIssueFilter(); + const { states, labels } = useIssue(); + + const activeLayout = issueFilters?.display_filters?.layout || undefined; const userFilters = issueFilters?.filters || {}; const appliedFilters: any = {}; - Object.entries(userFilters).forEach(([key, value]) => { if (!value) return; if (Array.isArray(value) && value.length === 0) return; @@ -29,48 +35,50 @@ export const IssueAppliedFilters: FC = observer((props: any) => { }); 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 ?? []; + (key: keyof TIssueQueryFilters, 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: activeLayout || "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(",") }; - } - console.log("params", params); - // TODO: fix this redirection - // router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true }); + if (priority.length > 0) params = { ...params, priority: priority.join(",") }; + if (state.length > 0) params = { ...params, states: state.join(",") }; + if (labels.length > 0) params = { ...params, labels: labels.join(",") }; + params = new URLSearchParams(params).toString(); + + router.push(`/${workspaceSlug}/${projectId}?${params}`); }, [workspaceSlug, projectId, activeLayout, issueFilters, router] ); - const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { - if (!projectId) return; - if (!value) { - updateFilters(projectId, { [key]: null }); - return; - } + const handleFilters = useCallback( + (key: keyof TIssueQueryFilters, value: string | null) => { + if (!projectId) return; - let newValues = issueFilters?.filters?.[key] ?? []; - newValues = newValues.filter((val) => val !== value); + let newValues = cloneDeep(issueFilters?.filters?.[key]) ?? []; - updateFilters(projectId, { [key]: newValues }); - updateRouteParams(key, newValues); - }; + if (value === null) newValues = []; + else if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1); + + updateIssueFilters(projectId, "filters", key, newValues); + updateRouteParams(key, newValues); + }, + [projectId, issueFilters, updateIssueFilters, updateRouteParams] + ); const handleRemoveAllFilters = () => { if (!projectId) return; - const newFilters: IIssueFilterOptions = {}; - Object.keys(userFilters).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + initIssueFilters(projectId, { + display_filters: { layout: activeLayout || "list" }, + filters: { + state: [], + priority: [], + labels: [], + }, }); - updateFilters(projectId, { ...newFilters }); - updateRouteParams(null, null, true); + router.push(`/${workspaceSlug}/${projectId}?${`board=${activeLayout || "list"}`}`); }; if (Object.keys(appliedFilters).length === 0) return null; @@ -79,7 +87,7 @@ export const IssueAppliedFilters: FC = observer((props: any) => {
void; @@ -10,7 +12,7 @@ type Props = { values: string[]; }; -export const AppliedStateFilters: React.FC = (props) => { +export const AppliedStateFilters: React.FC = observer((props) => { const { handleRemove, states, values } = props; return ( @@ -36,4 +38,4 @@ export const AppliedStateFilters: React.FC = (props) => { })} ); -}; +}); diff --git a/space/components/issues/filters/helpers/dropdown.tsx b/space/components/issues/filters/helpers/dropdown.tsx index d98dee7dd..a5e257e07 100644 --- a/space/components/issues/filters/helpers/dropdown.tsx +++ b/space/components/issues/filters/helpers/dropdown.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { Fragment, useState } from "react"; import { Placement } from "@popperjs/core"; import { usePopper } from "react-popper"; diff --git a/space/components/issues/filters/helpers/filter-header.tsx b/space/components/issues/filters/helpers/filter-header.tsx index a2d2f0ac9..52d766516 100644 --- a/space/components/issues/filters/helpers/filter-header.tsx +++ b/space/components/issues/filters/helpers/filter-header.tsx @@ -1,3 +1,5 @@ +"use client"; + import React from "react"; // lucide icons import { ChevronDown, ChevronUp } from "lucide-react"; diff --git a/space/components/issues/filters/helpers/filter-option.tsx b/space/components/issues/filters/helpers/filter-option.tsx index f46d962ea..ba782b2c3 100644 --- a/space/components/issues/filters/helpers/filter-option.tsx +++ b/space/components/issues/filters/helpers/filter-option.tsx @@ -1,3 +1,5 @@ +"use client"; + import React from "react"; // lucide icons import { Check } from "lucide-react"; diff --git a/space/components/issues/filters/labels.tsx b/space/components/issues/filters/labels.tsx index e5cb12696..51c9d038a 100644 --- a/space/components/issues/filters/labels.tsx +++ b/space/components/issues/filters/labels.tsx @@ -1,11 +1,11 @@ -import React, { useState } from "react"; +"use client"; -// components -// ui +import React, { useState } from "react"; import { Loader } from "@plane/ui"; +// components +import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers"; // types -import { IIssueLabel } from "types/issue"; -import { FilterHeader, FilterOption } from "./helpers"; +import { IIssueLabel } from "@/types/issue"; const LabelIcons = ({ color }: { color: string }) => ( diff --git a/space/components/issues/filters/priority.tsx b/space/components/issues/filters/priority.tsx index 98c35eea6..d41448a8f 100644 --- a/space/components/issues/filters/priority.tsx +++ b/space/components/issues/filters/priority.tsx @@ -1,9 +1,11 @@ +"use client"; + import React, { useState } from "react"; import { observer } from "mobx-react-lite"; // ui import { PriorityIcon } from "@plane/ui"; // components -import { issuePriorityFilters } from "@/constants/data"; +import { issuePriorityFilters } from "@/constants/issue"; import { FilterHeader, FilterOption } from "./helpers"; // constants diff --git a/space/components/issues/filters/root.tsx b/space/components/issues/filters/root.tsx index 1e9951236..08af9c4b8 100644 --- a/space/components/issues/filters/root.tsx +++ b/space/components/issues/filters/root.tsx @@ -1,12 +1,15 @@ +"use client"; + import { FC, useCallback } from "react"; +import cloneDeep from "lodash/cloneDeep"; import { observer } from "mobx-react-lite"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; // hooks -import { useIssue, useIssueFilter, useProject } from "@/hooks/store"; +import { useIssue, useIssueFilter } from "@/hooks/store"; // types -import { IIssueFilterOptions } from "@/types/issue"; +import { TIssueQueryFilters } from "@/types/issue"; // components import { FiltersDropdown } from "./helpers/dropdown"; import { FilterSelection } from "./selection"; @@ -17,48 +20,44 @@ type IssueFiltersDropdownProps = { }; export const IssueFiltersDropdown: FC = observer((props) => { - const { workspaceSlug, projectId } = props; - const searchParams = useSearchParams(); const router = useRouter(); - // store hooks - const { activeLayout } = useProject(); + const { workspaceSlug, projectId } = props; + // hooks + const { issueFilters, updateIssueFilters } = useIssueFilter(); const { states, labels } = useIssue(); - const { issueFilters, updateFilters } = useIssueFilter(); + + const activeLayout = issueFilters?.display_filters?.layout || undefined; const updateRouteParams = useCallback( - (key: keyof IIssueFilterOptions, value: string[]) => { + (key: keyof TIssueQueryFilters, 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: activeLayout || "list" }; - if (priority.length > 0) params = { ...params, priorities: priority.join(",") }; - if (state.length > 0) params = { ...params, states: state.join(",") }; + if (priority.length > 0) params = { ...params, priority: priority.join(",") }; + if (state.length > 0) params = { ...params, state: state.join(",") }; if (labels.length > 0) params = { ...params, labels: labels.join(",") }; - console.log("params", params); - router.push(`/${workspaceSlug}/${projectId}?${searchParams}`); + params = new URLSearchParams(params).toString(); + + router.push(`/${workspaceSlug}/${projectId}?${params}`); }, [workspaceSlug, projectId, activeLayout, issueFilters, router] ); const handleFilters = useCallback( - (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + (key: keyof TIssueQueryFilters, value: string) => { + if (!projectId || !value) return; - 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); - } + const newValues = cloneDeep(issueFilters?.filters?.[key]) ?? []; - updateFilters(projectId, { [key]: newValues }); + if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + + updateIssueFilters(projectId, "filters", key, newValues); updateRouteParams(key, newValues); }, - [projectId, issueFilters, updateFilters, updateRouteParams] + [projectId, issueFilters, updateIssueFilters, updateRouteParams] ); return ( @@ -67,7 +66,7 @@ export const IssueFiltersDropdown: FC = observer((pro diff --git a/space/components/issues/filters/selection.tsx b/space/components/issues/filters/selection.tsx index 9d7d89666..a1180b0ee 100644 --- a/space/components/issues/filters/selection.tsx +++ b/space/components/issues/filters/selection.tsx @@ -1,16 +1,17 @@ +"use client"; + import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { Search, X } from "lucide-react"; // types -import { IIssueState, IIssueLabel, IIssueFilterOptions } from "@/types/issue"; -import { ILayoutDisplayFiltersOptions } from "@/types/issue-filters"; +import { IIssueState, IIssueLabel, IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue"; // components import { FilterPriority, FilterState } from "./"; type Props = { filters: IIssueFilterOptions; handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void; - layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; + layoutDisplayFiltersOptions: TIssueFilterKeys[]; labels?: IIssueLabel[] | undefined; states?: IIssueState[] | undefined; }; @@ -20,7 +21,7 @@ export const FilterSelection: React.FC = observer((props) => { const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); - const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter); + const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions.includes(filter); return (
diff --git a/space/components/issues/filters/state.tsx b/space/components/issues/filters/state.tsx index 734abef55..24b6bb5c8 100644 --- a/space/components/issues/filters/state.tsx +++ b/space/components/issues/filters/state.tsx @@ -1,10 +1,11 @@ +"use client"; + import React, { useState } from "react"; -// components -// ui import { Loader, StateGroupIcon } from "@plane/ui"; +// components +import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers"; // types -import { IIssueState } from "types/issue"; -import { FilterHeader, FilterOption } from "./helpers"; +import { IIssueState } from "@/types/issue"; type Props = { appliedFilters: string[] | null; diff --git a/space/components/issues/navbar/controls.tsx b/space/components/issues/navbar/controls.tsx index d2adcea40..b996a54cc 100644 --- a/space/components/issues/navbar/controls.tsx +++ b/space/components/issues/navbar/controls.tsx @@ -3,49 +3,48 @@ import { useEffect, FC } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; -import { useRouter, useParams, useSearchParams, usePathname } from "next/navigation"; -import useSWR from "swr"; +import { useRouter, useSearchParams, usePathname } from "next/navigation"; // ui import { Avatar, Button } from "@plane/ui"; // components import { IssueFiltersDropdown } from "@/components/issues/filters"; +import { NavbarIssueBoardView } from "@/components/issues/navbar/issue-board-view"; +import { NavbarTheme } from "@/components/issues/navbar/theme"; // hooks -import { useProject, useUser, useIssueFilter } from "@/hooks/store"; +import { useProject, useUser, useIssueFilter, useIssueDetails } from "@/hooks/store"; // types -import { TIssueBoardKeys } from "@/types/issue"; -// components -import { NavbarIssueBoardView } from "./issue-board-view"; -import { NavbarTheme } from "./theme"; +import { TIssueLayout } from "@/types/issue"; export type NavbarControlsProps = { workspaceSlug: string; projectId: string; - projectSettings: any; }; export const NavbarControls: FC = observer((props) => { - const { workspaceSlug, projectId, projectSettings } = props; - const { views } = projectSettings; + // props + const { workspaceSlug, projectId } = props; // router const router = useRouter(); - const { board, labels, states, priorities, peekId } = useParams(); - const searchParams = useSearchParams(); const pathName = usePathname(); - // store - const { updateFilters } = useIssueFilter(); - const { settings, activeLayout, hydrate, setActiveLayout } = useProject(); - hydrate(projectSettings); - - const { data: user, fetchCurrentUser } = useUser(); - - useSWR("CURRENT_USER", () => fetchCurrentUser(), { errorRetryCount: 2 }); - - console.log("user", user); + const searchParams = useSearchParams(); + // query params + const board = searchParams.get("board") || undefined; + const labels = searchParams.get("labels") || undefined; + const state = searchParams.get("state") || undefined; + const priority = searchParams.get("priority") || undefined; + const peekId = searchParams.get("peekId") || undefined; + // hooks + const { issueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter(); + const { settings } = useProject(); + const { data: user } = useUser(); + const { setPeekId } = useIssueDetails(); + // derived values + const activeLayout = issueFilters?.display_filters?.layout || undefined; useEffect(() => { if (workspaceSlug && projectId && settings) { const viewsAcceptable: string[] = []; - const currentBoard: TIssueBoardKeys | null = null; + let currentBoard: TIssueLayout | null = null; if (settings?.views?.list) viewsAcceptable.push("list"); if (settings?.views?.kanban) viewsAcceptable.push("kanban"); @@ -53,59 +52,66 @@ export const NavbarControls: FC = observer((props) => { if (settings?.views?.gantt) viewsAcceptable.push("gantt"); if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet"); - // if (board) { - // if (viewsAcceptable.includes(board.toString())) { - // currentBoard = board.toString() as TIssueBoardKeys; - // } else { - // if (viewsAcceptable && viewsAcceptable.length > 0) { - // currentBoard = viewsAcceptable[0] as TIssueBoardKeys; - // } - // } - // } else { - // if (viewsAcceptable && viewsAcceptable.length > 0) { - // currentBoard = viewsAcceptable[0] as TIssueBoardKeys; - // } - // } + if (board) { + if (viewsAcceptable.includes(board.toString())) currentBoard = board.toString() as TIssueLayout; + else { + if (viewsAcceptable && viewsAcceptable.length > 0) currentBoard = viewsAcceptable[0] as TIssueLayout; + } + } else { + if (viewsAcceptable && viewsAcceptable.length > 0) currentBoard = viewsAcceptable[0] as TIssueLayout; + } if (currentBoard) { - if (activeLayout === null || activeLayout !== currentBoard) { - let params: any = { board: currentBoard }; - if (peekId && peekId.length > 0) params = { ...params, peekId: peekId }; - 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 }; - console.log("params", params); - 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 (activeLayout === undefined || activeLayout !== currentBoard) { + let queryParams: any = { board: currentBoard }; + const params: any = { display_filters: { layout: currentBoard }, filters: {} }; - if (storeParams) updateFilters(projectId, storeParams); - setActiveLayout(currentBoard); - router.push(`/${workspaceSlug}/${projectId}?${searchParams}`); + if (peekId && peekId.length > 0) { + queryParams = { ...queryParams, peekId: peekId }; + setPeekId(peekId); + } + if (priority && priority.length > 0) { + queryParams = { ...queryParams, priority: priority }; + params.filters = { ...params.filters, priority: priority.split(",") }; + } + if (state && state.length > 0) { + queryParams = { ...queryParams, state: state }; + params.filters = { ...params.filters, state: state.split(",") }; + } + if (labels && labels.length > 0) { + queryParams = { ...queryParams, labels: labels }; + params.filters = { ...params.filters, labels: labels.split(",") }; + } + + if (!isIssueFiltersUpdated(params)) { + initIssueFilters(projectId, params); + queryParams = new URLSearchParams(queryParams).toString(); + router.push(`/${workspaceSlug}/${projectId}?${queryParams}`); + } } } } }, [ - board, workspaceSlug, projectId, - router, - updateFilters, + board, labels, - states, - priorities, + state, + priority, peekId, settings, activeLayout, - setActiveLayout, - searchParams, + router, + initIssueFilters, + setPeekId, + isIssueFiltersUpdated, ]); + return ( <> {/* issue views */}
- +
{/* issue filters */} diff --git a/space/components/issues/navbar/index.tsx b/space/components/issues/navbar/index.tsx index c356230d4..f5d60b8b0 100644 --- a/space/components/issues/navbar/index.tsx +++ b/space/components/issues/navbar/index.tsx @@ -1,43 +1,44 @@ "use client"; import { FC } from "react"; +import { observer } from "mobx-react-lite"; import { Briefcase } from "lucide-react"; // components import { ProjectLogo } from "@/components/common"; -import { NavbarControls } from "./controls"; +import { NavbarControls } from "@/components/issues/navbar/controls"; +// hooks +import { useProject } from "@/hooks/store"; type IssueNavbarProps = { - projectSettings: any; workspaceSlug: string; projectId: string; }; -const IssueNavbar: FC = (props) => { - const { projectSettings, workspaceSlug, projectId } = props; - const { project_details } = projectSettings; +const IssueNavbar: FC = observer((props) => { + const { workspaceSlug, projectId } = props; + // hooks + const { project } = useProject(); return (
{/* project detail */}
- {project_details ? ( + {project ? ( - + ) : ( )} -
- {project_details?.name || `...`} -
+
{project?.name || `...`}
- +
); -}; +}); export default IssueNavbar; diff --git a/space/components/issues/navbar/issue-board-view.tsx b/space/components/issues/navbar/issue-board-view.tsx index d2eb53398..4581d1800 100644 --- a/space/components/issues/navbar/issue-board-view.tsx +++ b/space/components/issues/navbar/issue-board-view.tsx @@ -2,31 +2,53 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; +import { useRouter, useSearchParams } from "next/navigation"; // constants -import { issueViews } from "@/constants/data"; +import { issueLayoutViews } from "@/constants/issue"; // hooks -import { useProject } from "@/hooks/store"; +import { useIssueFilter } from "@/hooks/store"; // mobx -import { TIssueBoardKeys } from "@/types/issue"; +import { TIssueLayout } from "@/types/issue"; type NavbarIssueBoardViewProps = { - layouts: Record; + workspaceSlug: string; + projectId: string; }; export const NavbarIssueBoardView: FC = observer((props) => { - const { layouts } = props; + const router = useRouter(); + const searchParams = useSearchParams(); + // query params + const labels = searchParams.get("labels") || undefined; + const state = searchParams.get("state") || undefined; + const priority = searchParams.get("priority") || undefined; + const peekId = searchParams.get("peekId") || undefined; + // props + const { workspaceSlug, projectId } = props; + // hooks + const { layoutOptions, issueFilters, updateIssueFilters } = useIssueFilter(); - const { activeLayout, setActiveLayout } = useProject(); + // derived values + const activeLayout = issueFilters?.display_filters?.layout || undefined; - const handleCurrentBoardView = (boardView: string) => { - setActiveLayout(boardView as TIssueBoardKeys); + const handleCurrentBoardView = (boardView: TIssueLayout) => { + updateIssueFilters(projectId, "display_filters", "layout", boardView); + + let queryParams: any = { board: boardView }; + if (peekId && peekId.length > 0) queryParams = { ...queryParams, peekId: peekId }; + if (priority && priority.length > 0) queryParams = { ...queryParams, priority: priority }; + if (state && state.length > 0) queryParams = { ...queryParams, state: state }; + if (labels && labels.length > 0) queryParams = { ...queryParams, labels: labels }; + queryParams = new URLSearchParams(queryParams).toString(); + router.push(`/${workspaceSlug}/${projectId}?${queryParams}`); }; return ( <> - {layouts && - Object.keys(layouts).map((layoutKey: string) => { - if (layouts[layoutKey as TIssueBoardKeys]) { + {issueLayoutViews && + Object.keys(issueLayoutViews).map((key: string) => { + const layoutKey = key as TIssueLayout; + if (layoutOptions[layoutKey]) { return (
= observer((pro > - {issueViews[layoutKey]?.icon} + {issueLayoutViews[layoutKey]?.icon}
); diff --git a/space/components/issues/peek-overview/issue-properties.tsx b/space/components/issues/peek-overview/issue-properties.tsx index d31e8dd6d..32d39b540 100644 --- a/space/components/issues/peek-overview/issue-properties.tsx +++ b/space/components/issues/peek-overview/issue-properties.tsx @@ -4,7 +4,7 @@ import { StateGroupIcon } from "@plane/ui"; // icons import { Icon } from "@/components/ui"; // helpers -import { issueGroupFilter, issuePriorityFilter } from "@/constants/data"; +import { issueGroupFilter, issuePriorityFilter } from "@/constants/issue"; import { renderFullDate } from "@/helpers/date-time.helper"; import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper"; // types diff --git a/space/components/issues/peek-overview/layout.tsx b/space/components/issues/peek-overview/layout.tsx index aa4163610..a70021372 100644 --- a/space/components/issues/peek-overview/layout.tsx +++ b/space/components/issues/peek-overview/layout.tsx @@ -10,7 +10,7 @@ import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overv import { useIssue, useIssueDetails } from "@/hooks/store"; export const IssuePeekOverview: React.FC = observer((props: any) => { - const { workspaceSlug, projectId, peekId, board, priorities, states, labels } = props; + const { workspaceSlug, projectId, peekId, board, priority, states, labels } = props; // states const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); @@ -33,7 +33,7 @@ export const IssuePeekOverview: React.FC = observer((props: any) => { const params: any = { board: board }; if (states && states.length > 0) params.states = states; - if (priorities && priorities.length > 0) params.priorities = priorities; + if (priority && priority.length > 0) params.priority = priority; if (labels && labels.length > 0) params.labels = labels; // TODO: fix this redirection // router.push( encodeURI(`/${workspaceSlug?.toString()}/${projectId}`, ) { pathname: `/${workspaceSlug?.toString()}/${projectId}`, query: { ...params } }); diff --git a/space/components/views/project-details.tsx b/space/components/views/project-details.tsx index f0fff758c..1209ac9c0 100644 --- a/space/components/views/project-details.tsx +++ b/space/components/views/project-details.tsx @@ -3,7 +3,7 @@ import { FC, useEffect } from "react"; import { observer } from "mobx-react-lite"; import Image from "next/image"; -import { useParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import useSWR from "swr"; // components import { IssueCalendarView } from "@/components/issues/board-views/calendar"; @@ -14,33 +14,44 @@ import { IssueSpreadsheetView } from "@/components/issues/board-views/spreadshee import { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root"; import { IssuePeekOverview } from "@/components/issues/peek-overview"; // mobx store -import { useIssue, useUser, useProject, useIssueDetails } from "@/hooks/store"; +import { useIssue, useUser, useIssueDetails, useIssueFilter, useProject } from "@/hooks/store"; // assets import SomethingWentWrongImage from "public/something-went-wrong.svg"; type ProjectDetailsViewProps = { workspaceSlug: string; projectId: string; - peekId: string; + peekId: string | undefined; }; export const ProjectDetailsView: FC = observer((props) => { - const { workspaceSlug, projectId, peekId } = props; // router - const params = useParams(); - // store hooks + const searchParams = useSearchParams(); + // query params + const states = searchParams.get("states") || undefined; + const priority = searchParams.get("priority") || undefined; + const labels = searchParams.get("labels") || undefined; + + const { workspaceSlug, projectId, peekId } = props; + // hooks + const { fetchProjectSettings } = useProject(); + const { issueFilters } = useIssueFilter(); + const { loader, issues, error } = useIssue(); const { fetchPublicIssues } = useIssue(); - const { activeLayout } = useProject(); - // fetching public issues - useSWR( - workspaceSlug && projectId ? "PROJECT_PUBLIC_ISSUES" : null, - workspaceSlug && projectId ? () => fetchPublicIssues(workspaceSlug, projectId, params) : null - ); - // store hooks - const issueStore = useIssue(); const issueDetailStore = useIssueDetails(); const { data: currentUser, fetchCurrentUser } = useUser(); + useSWR( + workspaceSlug && projectId ? "WORKSPACE_PROJECT_SETTINGS" : null, + workspaceSlug && projectId ? () => fetchProjectSettings(workspaceSlug, projectId) : null + ); + useSWR( + (workspaceSlug && projectId) || states || priority || labels ? "WORKSPACE_PROJECT_PUBLIC_ISSUES" : null, + (workspaceSlug && projectId) || states || priority || labels + ? () => fetchPublicIssues(workspaceSlug, projectId, { states, priority, labels }) + : null + ); + useEffect(() => { if (!currentUser) { fetchCurrentUser(); @@ -53,15 +64,18 @@ export const ProjectDetailsView: FC = observer((props) } }, [peekId, issueDetailStore, projectId, workspaceSlug]); + // derived values + const activeLayout = issueFilters?.display_filters?.layout || undefined; + return (
{workspaceSlug && } - {issueStore?.loader && !issueStore.issues ? ( + {loader && !issues ? (
Loading...
) : ( <> - {issueStore?.error ? ( + {error ? (
@@ -77,7 +91,7 @@ export const ProjectDetailsView: FC = observer((props) activeLayout && (
{/* applied filters */} - + {activeLayout === "list" && (
diff --git a/space/constants/data.ts b/space/constants/data.ts deleted file mode 100644 index bb9030696..000000000 --- a/space/constants/data.ts +++ /dev/null @@ -1,119 +0,0 @@ -// interfaces -import { - // priority - TIssuePriorityKey, - // state groups - TIssueGroupKey, - IIssuePriorityFilters, - IIssueGroup, -} from "types/issue"; - -// all issue views -export const issueViews: any = { - list: { - title: "List View", - icon: "format_list_bulleted", - className: "", - }, - kanban: { - title: "Board View", - icon: "grid_view", - className: "", - }, -}; - -// issue priority filters -export const issuePriorityFilters: IIssuePriorityFilters[] = [ - { - key: "urgent", - title: "Urgent", - className: "bg-red-500 border-red-500 text-white", - icon: "error", - }, - { - key: "high", - title: "High", - className: "text-orange-500 border-custom-border-300", - icon: "signal_cellular_alt", - }, - { - key: "medium", - title: "Medium", - className: "text-yellow-500 border-custom-border-300", - icon: "signal_cellular_alt_2_bar", - }, - { - key: "low", - title: "Low", - className: "text-green-500 border-custom-border-300", - icon: "signal_cellular_alt_1_bar", - }, - { - key: "none", - title: "None", - className: "text-gray-500 border-custom-border-300", - icon: "block", - }, -]; - -export const issuePriorityFilter = (priorityKey: TIssuePriorityKey): IIssuePriorityFilters | null => { - const currentIssuePriority: IIssuePriorityFilters | undefined | null = - issuePriorityFilters && issuePriorityFilters.length > 0 - ? issuePriorityFilters.find((_priority) => _priority.key === priorityKey) - : null; - - if (currentIssuePriority === undefined || currentIssuePriority === null) return null; - return { ...currentIssuePriority }; -}; - -// issue group filters -export const issueGroupColors: { - [key: string]: string; -} = { - backlog: "#d9d9d9", - unstarted: "#3f76ff", - started: "#f59e0b", - completed: "#16a34a", - cancelled: "#dc2626", -}; - -export const issueGroups: IIssueGroup[] = [ - { - key: "backlog", - title: "Backlog", - color: "#d9d9d9", - className: `text-[#d9d9d9] bg-[#d9d9d9]/10`, - }, - { - key: "unstarted", - title: "Unstarted", - color: "#3f76ff", - className: `text-[#3f76ff] bg-[#3f76ff]/10`, - }, - { - key: "started", - title: "Started", - color: "#f59e0b", - className: `text-[#f59e0b] bg-[#f59e0b]/10`, - }, - { - key: "completed", - title: "Completed", - color: "#16a34a", - className: `text-[#16a34a] bg-[#16a34a]/10`, - }, - { - key: "cancelled", - title: "Cancelled", - color: "#dc2626", - className: `text-[#dc2626] bg-[#dc2626]/10`, - }, -]; - -export const issueGroupFilter = (issueKey: TIssueGroupKey): IIssueGroup | null => { - const currentIssueStateGroup: IIssueGroup | undefined | null = - issueGroups && issueGroups.length > 0 ? issueGroups.find((group) => group.key === issueKey) : null; - - if (currentIssueStateGroup === undefined || currentIssueStateGroup === null) return null; - return { ...currentIssueStateGroup }; -}; diff --git a/space/constants/issue.ts b/space/constants/issue.ts index 147d840fc..fb9c78fcd 100644 --- a/space/constants/issue.ts +++ b/space/constants/issue.ts @@ -1,20 +1,138 @@ -import { ILayoutDisplayFiltersOptions } from "@/types/issue-filters"; +// interfaces +import { + TIssueLayout, + TIssueLayoutViews, + TIssueFilterKeys, + TIssueFilterPriority, + TIssueFilterPriorityObject, + TIssueFilterState, + TIssueFilterStateObject, +} from "types/issue"; -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, - }, +// issue filters +export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]> } = { + list: { + filters: ["priority", "state", "labels"], + }, + kanban: { + filters: ["priority", "state", "labels"], + }, + calendar: { + filters: ["priority", "state", "labels"], + }, + spreadsheet: { + filters: ["priority", "state", "labels"], + }, + gantt: { + filters: ["priority", "state", "labels"], }, }; + +export const issueLayoutViews: Partial = { + list: { + title: "List View", + icon: "format_list_bulleted", + className: "", + }, + kanban: { + title: "Board View", + icon: "grid_view", + className: "", + }, +}; + +// issue priority filters +export const issuePriorityFilters: TIssueFilterPriorityObject[] = [ + { + key: "urgent", + title: "Urgent", + className: "bg-red-500 border-red-500 text-white", + icon: "error", + }, + { + key: "high", + title: "High", + className: "text-orange-500 border-custom-border-300", + icon: "signal_cellular_alt", + }, + { + key: "medium", + title: "Medium", + className: "text-yellow-500 border-custom-border-300", + icon: "signal_cellular_alt_2_bar", + }, + { + key: "low", + title: "Low", + className: "text-green-500 border-custom-border-300", + icon: "signal_cellular_alt_1_bar", + }, + { + key: "none", + title: "None", + className: "text-gray-500 border-custom-border-300", + icon: "block", + }, +]; + +export const issuePriorityFilter = (priorityKey: TIssueFilterPriority): TIssueFilterPriorityObject | undefined => { + const currentIssuePriority: TIssueFilterPriorityObject | undefined = + issuePriorityFilters && issuePriorityFilters.length > 0 + ? issuePriorityFilters.find((_priority) => _priority.key === priorityKey) + : undefined; + + if (currentIssuePriority) return currentIssuePriority; + return undefined; +}; + +// issue group filters +export const issueGroupColors: { + [key in TIssueFilterState]: string; +} = { + backlog: "#d9d9d9", + unstarted: "#3f76ff", + started: "#f59e0b", + completed: "#16a34a", + cancelled: "#dc2626", +}; + +export const issueGroups: TIssueFilterStateObject[] = [ + { + key: "backlog", + title: "Backlog", + color: "#d9d9d9", + className: `text-[#d9d9d9] bg-[#d9d9d9]/10`, + }, + { + key: "unstarted", + title: "Unstarted", + color: "#3f76ff", + className: `text-[#3f76ff] bg-[#3f76ff]/10`, + }, + { + key: "started", + title: "Started", + color: "#f59e0b", + className: `text-[#f59e0b] bg-[#f59e0b]/10`, + }, + { + key: "completed", + title: "Completed", + color: "#16a34a", + className: `text-[#16a34a] bg-[#16a34a]/10`, + }, + { + key: "cancelled", + title: "Cancelled", + color: "#dc2626", + className: `text-[#dc2626] bg-[#dc2626]/10`, + }, +]; + +export const issueGroupFilter = (issueKey: TIssueFilterState): TIssueFilterStateObject | undefined => { + const currentIssueStateGroup: TIssueFilterStateObject | undefined = + issueGroups && issueGroups.length > 0 ? issueGroups.find((group) => group.key === issueKey) : undefined; + + if (currentIssueStateGroup) return currentIssueStateGroup; + return undefined; +}; diff --git a/space/lib/wrappers/auth-wrapper.tsx b/space/lib/wrappers/auth-wrapper.tsx index ba1fae2e5..840ce4ba2 100644 --- a/space/lib/wrappers/auth-wrapper.tsx +++ b/space/lib/wrappers/auth-wrapper.tsx @@ -20,8 +20,6 @@ export const AuthWrapper: FC = observer((props) => { const { isLoading, data: currentUser, fetchCurrentUser } = useUser(); const { data: currentUserProfile } = useUserProfile(); - console; - const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchCurrentUser(), { revalidateOnFocus: false, }); diff --git a/space/package.json b/space/package.json index 48fe001b7..9c49da818 100644 --- a/space/package.json +++ b/space/package.json @@ -17,12 +17,12 @@ "@emotion/styled": "^11.11.0", "@headlessui/react": "^1.7.13", "@mui/material": "^5.14.1", + "@plane/constants": "*", "@plane/document-editor": "*", "@plane/lite-text-editor": "*", "@plane/rich-text-editor": "*", "@plane/types": "*", "@plane/ui": "*", - "@plane/constants": "*", "@sentry/nextjs": "^7.108.0", "axios": "^1.3.4", "clsx": "^2.0.0", @@ -34,6 +34,7 @@ "lucide-react": "^0.378.0", "mobx": "^6.10.0", "mobx-react-lite": "^4.0.3", + "mobx-utils": "^6.0.8", "next": "^14.2.3", "next-themes": "^0.2.1", "nprogress": "^0.2.0", diff --git a/space/services/api.service.ts b/space/services/api.service.ts index e4f7c8631..e13ebbdcc 100644 --- a/space/services/api.service.ts +++ b/space/services/api.service.ts @@ -29,7 +29,7 @@ export abstract class APIService { } get(url: string, params = {}) { - return this.axiosInstance.get(url, { params }); + return this.axiosInstance.get(url, params); } post(url: string, data: any, config = {}) { diff --git a/space/store/issue-filters.store.ts b/space/store/issue-filters.store.ts index d137753be..b7b311af4 100644 --- a/space/store/issue-filters.store.ts +++ b/space/store/issue-filters.store.ts @@ -1,131 +1,162 @@ +import cloneDeep from "lodash/cloneDeep"; +import isEqual from "lodash/isEqual"; +import set from "lodash/set"; import { action, makeObservable, observable, runInAction, computed } from "mobx"; +import { computedFn } from "mobx-utils"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; // store import { RootStore } from "@/store/root.store"; // types -import { TIssueBoardKeys, IIssueFilterOptions, TIssueParams } from "@/types/issue"; - -interface IFiltersOptions { - filters: IIssueFilterOptions; -} +import { + TIssueLayoutOptions, + TIssueFilters, + TIssueQueryFilters, + TIssueQueryFiltersParams, + TIssueFilterKeys, +} from "@/types/issue"; export interface IIssueFilterStore { // observables - projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined; + layoutOptions: TIssueLayoutOptions; + filters: { [projectId: string]: TIssueFilters } | undefined; // computed - issueFilters: IFiltersOptions | undefined; - appliedFilters: TIssueParams[] | undefined; - // helpers - issueDisplayFilters: (projectId: string) => IFiltersOptions | undefined; + issueFilters: TIssueFilters | undefined; + appliedFilters: TIssueQueryFiltersParams | undefined; + isIssueFiltersUpdated: (filters: TIssueFilters) => boolean; // actions - updateFilters: (projectId: string, filters: IIssueFilterOptions) => Promise; + updateLayoutOptions: (layout: TIssueLayoutOptions) => void; + initIssueFilters: (projectId: string, filters: TIssueFilters) => void; + updateIssueFilters: ( + projectId: string, + filterKind: K, + filterKey: keyof TIssueFilters[K], + filters: TIssueFilters[K][typeof filterKey] + ) => Promise; } export class IssueFilterStore implements IIssueFilterStore { // observables - projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined = undefined; - // root store - rootStore; + layoutOptions: TIssueLayoutOptions = { + list: true, + kanban: false, + calendar: false, + gantt: false, + spreadsheet: false, + }; + filters: { [projectId: string]: TIssueFilters } | undefined = undefined; - constructor(_rootStore: RootStore) { + constructor(private store: RootStore) { makeObservable(this, { // observables - projectIssueFilters: observable.ref, + layoutOptions: observable, + filters: observable, // computed issueFilters: computed, appliedFilters: computed, // actions - updateFilters: action, + updateLayoutOptions: action, + initIssueFilters: action, + updateIssueFilters: action, }); - // root store - this.rootStore = _rootStore; } // helper methods - computedFilter = (filters: any, filteredParams: any) => { - const computedFilters: any = {}; + computedFilter = (filters: TIssueQueryFilters, filteredParams: TIssueFilterKeys[]) => { + const computedFilters: TIssueQueryFiltersParams = {}; + 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(","); + const currentFilterKey = key as TIssueFilterKeys; + + if (filters[currentFilterKey] != undefined && filteredParams.includes(currentFilterKey)) { + if (Array.isArray(filters[currentFilterKey])) + computedFilters[currentFilterKey] = filters[currentFilterKey]?.join(","); + else if (filters[currentFilterKey] && typeof filters[currentFilterKey] === "string") + computedFilters[currentFilterKey] = filters[currentFilterKey]?.toString(); + else if (typeof filters[currentFilterKey] === "boolean") + computedFilters[currentFilterKey] = filters[currentFilterKey]?.toString(); + } }); return computedFilters; }; - // helpers - issueDisplayFilters = (projectId: string) => { + // computed + get issueFilters() { + const projectId = this.store.project.project?.id; if (!projectId) return undefined; - return this.projectIssueFilters?.[projectId] || undefined; - }; - handleIssueQueryParamsByLayout = (layout: TIssueBoardKeys | undefined, viewType: "issues"): TIssueParams[] | null => { - const queryParams: TIssueParams[] = []; + const currentFilters = this.filters?.[projectId]; + if (!currentFilters) return undefined; - if (!layout) return null; + return currentFilters; + } - const layoutOptions = ISSUE_DISPLAY_FILTERS_BY_LAYOUT[viewType][layout]; + get appliedFilters() { + const currentIssueFilters = this.issueFilters; + if (!currentIssueFilters) return undefined; - // add filters query params - layoutOptions.filters.forEach((option: any) => { - queryParams.push(option); - }); + const currentLayout = currentIssueFilters?.display_filters?.layout; + if (!currentLayout) return undefined; - return queryParams; - }; + const currentFilters: TIssueQueryFilters = { + priority: currentIssueFilters?.filters?.priority || undefined, + state: currentIssueFilters?.filters?.state || undefined, + labels: currentIssueFilters?.filters?.labels || undefined, + }; + const filteredParams = ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[currentLayout]?.filters || []; + const currentFilterQueryParams: TIssueQueryFiltersParams = this.computedFilter(currentFilters, filteredParams); + + return currentFilterQueryParams; + } + + isIssueFiltersUpdated = computedFn((userFilters: TIssueFilters) => { + if (!this.issueFilters) return false; + const currentUserFilters = cloneDeep(userFilters?.filters || {}); + const currentIssueFilters = cloneDeep(this.issueFilters?.filters || {}); + return isEqual(currentUserFilters, currentIssueFilters); + }); // actions - updateFilters = async (projectId: string, filters: IIssueFilterOptions) => { + updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options); + + initIssueFilters = async (projectId: string, initFilters: TIssueFilters) => { try { - let issueFilters = { ...this.projectIssueFilters }; - if (!issueFilters) issueFilters = {}; - if (!issueFilters[projectId]) issueFilters[projectId] = { filters: {} }; + if (!projectId) return; + if (this.filters === undefined) runInAction(() => (this.filters = {})); + if (this.filters && initFilters) set(this.filters, [projectId], initFilters); - const newFilters = { - filters: { ...issueFilters[projectId].filters }, - }; + const workspaceSlug = this.store.project.workspace?.slug; + const currentAppliedFilters = this.appliedFilters; - newFilters.filters = { ...newFilters.filters, ...filters }; - - issueFilters[projectId] = { - filters: newFilters.filters, - }; - - runInAction(() => { - this.projectIssueFilters = issueFilters; - }); - - return newFilters; + if (!workspaceSlug) return; + await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters); } catch (error) { throw error; } }; - get issueFilters() { - const projectId = this.rootStore.project.project?.id; - if (!projectId) return undefined; + updateIssueFilters = async ( + projectId: string, + filterKind: K, + filterKey: keyof TIssueFilters[K], + filterValue: TIssueFilters[K][typeof filterKey] + ) => { + try { + if (!projectId || !filterKind || !filterKey || !filterValue) return; + if (this.filters === undefined) runInAction(() => (this.filters = {})); - const issueFilters = this.issueDisplayFilters(projectId); - if (!issueFilters) return undefined; + runInAction(() => { + if (this.filters) set(this.filters, [projectId, filterKind, filterKey], filterValue); + }); - return issueFilters; - } + const workspaceSlug = this.store.project.workspace?.slug; + const currentAppliedFilters = this.appliedFilters; - get appliedFilters() { - const userFilters = this.issueFilters; - const layout = this.rootStore.project?.activeLayout; - 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 = this.handleIssueQueryParamsByLayout(layout, "issues"); - if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); - - return filteredRouteParams; - } + if (!workspaceSlug) return; + await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters); + } catch (error) { + throw error; + } + }; } diff --git a/space/store/issue.store.ts b/space/store/issue.store.ts index bbaf47f79..7967aafb1 100644 --- a/space/store/issue.store.ts +++ b/space/store/issue.store.ts @@ -21,7 +21,7 @@ export interface IIssueStore { // service issueService: any; // actions - fetchPublicIssues: (workspace_slug: string, project_slug: string, params: any) => void; + fetchPublicIssues: (workspace_slug: string, project_slug: string, params: any) => Promise; getCountOfIssuesByState: (state: string) => number; getFilteredIssuesByState: (state: string) => IIssue[]; } diff --git a/space/store/project.store.ts b/space/store/project.store.ts index e382b6792..70a9c1043 100644 --- a/space/store/project.store.ts +++ b/space/store/project.store.ts @@ -2,50 +2,40 @@ import { observable, action, makeObservable, runInAction } from "mobx"; // service import ProjectService from "@/services/project.service"; +// store types +import { RootStore } from "@/store/root.store"; // types -import { TIssueBoardKeys } from "@/types/issue"; import { IWorkspace, IProject, IProjectSettings } from "@/types/project"; export interface IProjectStore { + // observables loader: boolean; error: any | null; workspace: IWorkspace | null; project: IProject | null; settings: IProjectSettings | null; - activeLayout: TIssueBoardKeys; - layoutOptions: Record; canReact: boolean; canComment: boolean; canVote: boolean; + // actions fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise; - setActiveLayout: (value: TIssueBoardKeys) => void; hydrate: (projectSettings: any) => void; } export class ProjectStore implements IProjectStore { + // observables loader: boolean = false; error: any | null = null; - // data workspace: IWorkspace | null = null; project: IProject | null = null; settings: IProjectSettings | null = null; - activeLayout: TIssueBoardKeys = "list"; - layoutOptions: Record = { - list: true, - kanban: true, - calendar: false, - gantt: false, - spreadsheet: false, - }; canReact: boolean = false; canComment: boolean = false; canVote: boolean = false; - // root store - rootStore; // service projectService; - constructor(_rootStore: any | null = null) { + constructor(private store: RootStore) { makeObservable(this, { // loaders and error observables loader: observable, @@ -54,36 +44,29 @@ export class ProjectStore implements IProjectStore { workspace: observable, project: observable, settings: observable, - layoutOptions: observable, - activeLayout: observable.ref, canReact: observable.ref, canComment: observable.ref, canVote: observable.ref, // actions fetchProjectSettings: action, - setActiveLayout: action, + hydrate: action, // computed }); - this.rootStore = _rootStore; + // services this.projectService = new ProjectService(); } hydrate = (projectSettings: any) => { - const { workspace_detail, project_details, views, votes, comments, reactions } = projectSettings; + const { workspace_detail, project_details, votes, comments, reactions } = projectSettings; this.workspace = workspace_detail; this.project = project_details; - this.layoutOptions = views; this.canComment = comments; this.canVote = votes; this.canReact = reactions; }; - setActiveLayout = (boardValue: TIssueBoardKeys) => { - this.activeLayout = boardValue; - }; - fetchProjectSettings = async (workspace_slug: string, project_slug: string) => { try { this.loader = true; @@ -94,12 +77,11 @@ export class ProjectStore implements IProjectStore { if (response) { const currentProject: IProject = { ...response?.project_details }; const currentWorkspace: IWorkspace = { ...response?.workspace_detail }; - const currentViewOptions = { ...response?.views }; const currentDeploySettings = { ...response }; + this.store.issueFilter.updateLayoutOptions(response?.views); runInAction(() => { this.project = currentProject; this.workspace = currentWorkspace; - this.layoutOptions = currentViewOptions; this.settings = currentDeploySettings; this.loader = false; }); diff --git a/space/types/issue-filters.d.ts b/space/types/issue-filters.d.ts deleted file mode 100644 index 0ec82f40e..000000000 --- a/space/types/issue-filters.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ILayoutDisplayFiltersOptions { - filters: (keyof IIssueFilterOptions)[]; - display_properties: boolean | null; - display_filters: null; - extra_options: null; -} diff --git a/space/types/issue.d.ts b/space/types/issue.d.ts index 2b7d3e673..9735bedc2 100644 --- a/space/types/issue.d.ts +++ b/space/types/issue.d.ts @@ -1,30 +1,47 @@ -export type TIssueBoardKeys = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; +export type TIssueLayout = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; +export type TIssueLayoutOptions = { + [key in TIssueLayout]: boolean; +}; +export type TIssueLayoutViews = { + [key in TIssueLayout]: { title: string; icon: string; className: string }; +}; -export interface IIssueBoardViews { - key: TIssueBoardKeys; +export type TIssueFilterPriority = "urgent" | "high" | "medium" | "low" | "none"; +export type TIssueFilterPriorityObject = { + key: TIssueFilterPriority; title: string; - icon: string; - className: string; -} - -export type TIssuePriorityKey = "urgent" | "high" | "medium" | "low" | "none"; -export type TIssuePriorityTitle = "Urgent" | "High" | "Medium" | "Low" | "None"; -export interface IIssuePriorityFilters { - key: TIssuePriorityKey; - title: TIssuePriorityTitle; className: string; icon: string; -} +}; -export type TIssueGroupKey = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; -export type TIssueGroupTitle = "Backlog" | "Unstarted" | "Started" | "Completed" | "Cancelled"; - -export interface IIssueGroup { - key: TIssueGroupKey; - title: TIssueGroupTitle; +export type TIssueFilterState = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; +export type TIssueFilterStateObject = { + key: TIssueFilterState; + title: string; color: string; className: string; -} +}; + +export type TIssueFilterKeys = "priority" | "state" | "labels"; + +export type TDisplayFilters = { + layout: TIssueLayout; +}; + +export type TFilters = { + state: TIssueFilterState[]; + priority: TIssueFilterPriority[]; + labels: string[]; +}; + +export type TIssueFilters = { + display_filters: TDisplayFilters; + filters: TFilters; +}; + +export type TIssueQueryFilters = Partial; + +export type TIssueQueryFiltersParams = Partial>; export interface IIssue { id: string;