[WEB-1319] fix: handled issue filters mutation and updated the useParams with useSearchParams (#4473)

* chore: updated issue filters in space

* chore: persisting the query params even when we switch layouts

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
guru_sainath 2024-05-16 13:07:47 +05:30 committed by GitHub
parent 8ecc461fb1
commit 2bf2e98b00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 594 additions and 493 deletions

View File

@ -2,25 +2,24 @@ import Image from "next/image";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
// components // components
import IssueNavbar from "@/components/issues/navbar"; import IssueNavbar from "@/components/issues/navbar";
// services
import ProjectService from "@/services/project.service";
// assets // assets
import planeLogo from "public/plane-logo.svg"; import planeLogo from "public/plane-logo.svg";
const projectService = new ProjectService(); export default async function ProjectLayout({
children,
export default async function ProjectLayout({ children, params }: { children: React.ReactNode; params: any }) { params,
}: {
children: React.ReactNode;
params: { workspace_slug: string; project_id: string };
}) {
const { workspace_slug, project_id } = params; const { workspace_slug, project_id } = params;
const projectSettings = await projectService.getProjectSettings(workspace_slug, project_id).catch(() => null);
if (!projectSettings) { if (!workspace_slug || !project_id) notFound();
notFound();
}
return ( return (
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden"> <div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100"> <div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
<IssueNavbar projectSettings={projectSettings} workspaceSlug={workspace_slug} projectId={project_id} /> <IssueNavbar workspaceSlug={workspace_slug?.toString()} projectId={project_id?.toString()} />
</div> </div>
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div> <div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
<a <a

View File

@ -1,9 +1,16 @@
"use client"; "use client";
import { useSearchParams } from "next/navigation";
// components // components
import { ProjectDetailsView } from "@/components/views"; import { ProjectDetailsView } from "@/components/views";
export default function WorkspaceProjectPage({ params }: { params: any }) { export default function WorkspaceProjectPage({ params }: { params: { workspace_slug: any; project_id: any } }) {
const { workspace_slug, project_id, peekId } = params; const { workspace_slug, project_id } = params;
const searchParams = useSearchParams();
const peekId = searchParams.get("peekId") || undefined;
if (!workspace_slug || !project_id) return <></>;
return <ProjectDetailsView workspaceSlug={workspace_slug} projectId={project_id} peekId={peekId} />; return <ProjectDetailsView workspaceSlug={workspace_slug} projectId={project_id} peekId={peekId} />;
} }

View File

@ -1,11 +1,11 @@
"use client"; "use client";
// types // types
import { issuePriorityFilter } from "@/constants/data"; import { issuePriorityFilter } from "@/constants/issue";
import { TIssuePriorityKey } from "types/issue"; import { TIssueFilterPriority } from "@/types/issue";
// constants // constants
export const IssueBlockPriority = ({ priority }: { priority: TIssuePriorityKey | null }) => { export const IssueBlockPriority = ({ priority }: { priority: TIssueFilterPriority | null }) => {
const priority_detail = priority != null ? issuePriorityFilter(priority) : null; const priority_detail = priority != null ? issuePriorityFilter(priority) : null;
if (priority_detail === null) return <></>; if (priority_detail === null) return <></>;

View File

@ -1,7 +1,7 @@
// ui // ui
import { StateGroupIcon } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// constants // constants
import { issueGroupFilter } from "@/constants/data"; import { issueGroupFilter } from "@/constants/issue";
export const IssueBlockState = ({ state }: any) => { export const IssueBlockState = ({ state }: any) => {
const stateGroup = issueGroupFilter(state.group); const stateGroup = issueGroupFilter(state.group);

View File

@ -21,7 +21,7 @@ type IssueKanBanBlockProps = {
export const IssueKanBanBlock: FC<IssueKanBanBlockProps> = observer((props) => { export const IssueKanBanBlock: FC<IssueKanBanBlockProps> = observer((props) => {
const { workspaceSlug, projectId, params, issue } = props; const { workspaceSlug, projectId, params, issue } = props;
const { board, priorities, states, labels } = params; const { board, priority, states, labels } = params;
// store // store
const { project } = useProject(); const { project } = useProject();
const { setPeekId } = useIssueDetails(); const { setPeekId } = useIssueDetails();
@ -33,7 +33,7 @@ export const IssueKanBanBlock: FC<IssueKanBanBlockProps> = observer((props) => {
setPeekId(issue.id); setPeekId(issue.id);
const params: any = { board: board, peekId: issue.id }; const params: any = { board: board, peekId: issue.id };
if (states && states.length > 0) params.states = states; 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; if (labels && labels.length > 0) params.labels = labels;
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`); router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
}; };

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
// ui // ui
import { StateGroupIcon } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// constants // constants
import { issueGroupFilter } from "@/constants/data"; import { issueGroupFilter } from "@/constants/issue";
// mobx hook // mobx hook
// import { useIssue } from "@/hooks/store"; // import { useIssue } from "@/hooks/store";
// interfaces // interfaces

View File

@ -21,7 +21,7 @@ type IssueListBlockProps = {
export const IssueListBlock: FC<IssueListBlockProps> = observer((props) => { export const IssueListBlock: FC<IssueListBlockProps> = observer((props) => {
const { workspaceSlug, projectId, issue } = props; const { workspaceSlug, projectId, issue } = props;
const { board, states, priorities, labels } = useParams<any>(); const { board, states, priority, labels } = useParams<any>();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
// store // store
const { project } = useProject(); const { project } = useProject();
@ -33,7 +33,7 @@ export const IssueListBlock: FC<IssueListBlockProps> = observer((props) => {
setPeekId(issue.id); setPeekId(issue.id);
const params: any = { board: board, peekId: issue.id }; const params: any = { board: board, peekId: issue.id };
if (states && states.length > 0) params.states = states; 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; if (labels && labels.length > 0) params.labels = labels;
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`); router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
// router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`); // router.push(`/${workspace_slug?.toString()}/${project_slug}?board=${board?.toString()}&peekId=${issue.id}`);

View File

@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
// ui // ui
import { StateGroupIcon } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// constants // constants
import { issueGroupFilter } from "@/constants/data"; import { issueGroupFilter } from "@/constants/issue";
// mobx hook // mobx hook
// import { useIssue } from "@/hooks/store"; // import { useIssue } from "@/hooks/store";
// types // types

View File

@ -1,30 +1,34 @@
"use client";
// icons // icons
import { observer } from "mobx-react-lite";
import { X } from "lucide-react"; import { X } from "lucide-react";
// types // types
import { IIssueLabel, IIssueState, IIssueFilterOptions } from "@/types/issue"; import { IIssueLabel, IIssueState, TFilters } from "@/types/issue";
// components // components
import { AppliedPriorityFilters } from "./priority"; import { AppliedPriorityFilters } from "./priority";
import { AppliedStateFilters } from "./state"; import { AppliedStateFilters } from "./state";
type Props = { type Props = {
appliedFilters: IIssueFilterOptions; appliedFilters: TFilters;
handleRemoveAllFilters: () => void; handleRemoveAllFilters: () => void;
handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; handleRemoveFilter: (key: keyof TFilters, value: string | null) => void;
labels?: IIssueLabel[] | undefined; labels?: IIssueLabel[] | undefined;
states?: IIssueState[] | undefined; states?: IIssueState[] | undefined;
}; };
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
export const AppliedFiltersList: React.FC<Props> = (props) => { export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter, states } = props; const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter, states } = props;
return ( return (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100"> <div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
{Object.entries(appliedFilters).map(([key, value]) => { {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 ( return (
<div <div
@ -34,7 +38,10 @@ export const AppliedFiltersList: React.FC<Props> = (props) => {
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span> <span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
<div className="flex flex-wrap items-center gap-1"> <div className="flex flex-wrap items-center gap-1">
{filterKey === "priority" && ( {filterKey === "priority" && (
<AppliedPriorityFilters handleRemove={(val) => handleRemoveFilter("priority", val)} values={value} /> <AppliedPriorityFilters
handleRemove={(val) => handleRemoveFilter("priority", val)}
values={filterValue ?? []}
/>
)} )}
{/* {filterKey === "labels" && labels && ( {/* {filterKey === "labels" && labels && (
@ -49,7 +56,7 @@ export const AppliedFiltersList: React.FC<Props> = (props) => {
<AppliedStateFilters <AppliedStateFilters
handleRemove={(val) => handleRemoveFilter("state", val)} handleRemove={(val) => handleRemoveFilter("state", val)}
states={states} states={states}
values={value} values={filterValue ?? []}
/> />
)} )}
@ -74,4 +81,4 @@ export const AppliedFiltersList: React.FC<Props> = (props) => {
</button> </button>
</div> </div>
); );
}; });

View File

@ -1,6 +1,8 @@
"use client";
import { X } from "lucide-react"; import { X } from "lucide-react";
// types // types
import { IIssueLabel } from "types/issue"; import { IIssueLabel } from "@/types/issue";
type Props = { type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;

View File

@ -1,3 +1,5 @@
"use client";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { PriorityIcon } from "@plane/ui"; import { PriorityIcon } from "@plane/ui";

View File

@ -1,27 +1,33 @@
"use client"; "use client";
import { FC, useCallback } from "react"; import { FC, useCallback } from "react";
import cloneDeep from "lodash/cloneDeep";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
// hooks // hooks
import { useIssue, useProject, useIssueFilter } from "@/hooks/store"; import { useIssue, useIssueFilter } from "@/hooks/store";
// store // store
import { IIssueFilterOptions } from "@/types/issue"; import { TIssueQueryFilters } from "@/types/issue";
// components // components
import { AppliedFiltersList } from "./filters-list"; import { AppliedFiltersList } from "./filters-list";
// TODO: fix component types type TIssueAppliedFilters = {
export const IssueAppliedFilters: FC = observer((props: any) => { workspaceSlug: string;
const router = useRouter(); projectId: string;
const { workspaceSlug, projectId } = props; };
const { states, labels } = useIssue();
const { activeLayout } = useProject();
const { issueFilters, updateFilters } = useIssueFilter();
export const IssueAppliedFilters: FC<TIssueAppliedFilters> = 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 userFilters = issueFilters?.filters || {};
const appliedFilters: any = {}; const appliedFilters: any = {};
Object.entries(userFilters).forEach(([key, value]) => { Object.entries(userFilters).forEach(([key, value]) => {
if (!value) return; if (!value) return;
if (Array.isArray(value) && value.length === 0) return; if (Array.isArray(value) && value.length === 0) return;
@ -29,48 +35,50 @@ export const IssueAppliedFilters: FC = observer((props: any) => {
}); });
const updateRouteParams = useCallback( const updateRouteParams = useCallback(
(key: keyof IIssueFilterOptions | null, value: string[] | null, clearFields: boolean = false) => { (key: keyof TIssueQueryFilters, value: string[]) => {
const state = key === "state" ? value || [] : issueFilters?.filters?.state ?? []; const state = key === "state" ? value : issueFilters?.filters?.state ?? [];
const priority = key === "priority" ? value || [] : issueFilters?.filters?.priority ?? []; const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? [];
const labels = key === "labels" ? value || [] : issueFilters?.filters?.labels ?? []; const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? [];
let params: any = { board: activeLayout || "list" }; let params: any = { board: activeLayout || "list" };
if (!clearFields) { if (priority.length > 0) params = { ...params, priority: priority.join(",") };
if (priority.length > 0) params = { ...params, priorities: priority.join(",") }; if (state.length > 0) params = { ...params, states: state.join(",") };
if (state.length > 0) params = { ...params, states: state.join(",") }; if (labels.length > 0) params = { ...params, labels: labels.join(",") };
if (labels.length > 0) params = { ...params, labels: labels.join(",") }; params = new URLSearchParams(params).toString();
}
console.log("params", params); router.push(`/${workspaceSlug}/${projectId}?${params}`);
// TODO: fix this redirection
// router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
}, },
[workspaceSlug, projectId, activeLayout, issueFilters, router] [workspaceSlug, projectId, activeLayout, issueFilters, router]
); );
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => { const handleFilters = useCallback(
if (!projectId) return; (key: keyof TIssueQueryFilters, value: string | null) => {
if (!value) { if (!projectId) return;
updateFilters(projectId, { [key]: null });
return;
}
let newValues = issueFilters?.filters?.[key] ?? []; let newValues = cloneDeep(issueFilters?.filters?.[key]) ?? [];
newValues = newValues.filter((val) => val !== value);
updateFilters(projectId, { [key]: newValues }); if (value === null) newValues = [];
updateRouteParams(key, 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 = () => { const handleRemoveAllFilters = () => {
if (!projectId) return; if (!projectId) return;
const newFilters: IIssueFilterOptions = {}; initIssueFilters(projectId, {
Object.keys(userFilters).forEach((key) => { display_filters: { layout: activeLayout || "list" },
newFilters[key as keyof IIssueFilterOptions] = null; filters: {
state: [],
priority: [],
labels: [],
},
}); });
updateFilters(projectId, { ...newFilters }); router.push(`/${workspaceSlug}/${projectId}?${`board=${activeLayout || "list"}`}`);
updateRouteParams(null, null, true);
}; };
if (Object.keys(appliedFilters).length === 0) return null; if (Object.keys(appliedFilters).length === 0) return null;
@ -79,7 +87,7 @@ export const IssueAppliedFilters: FC = observer((props: any) => {
<div className="border-b border-custom-border-200 p-5 py-3"> <div className="border-b border-custom-border-200 p-5 py-3">
<AppliedFiltersList <AppliedFiltersList
appliedFilters={appliedFilters || {}} appliedFilters={appliedFilters || {}}
handleRemoveFilter={handleRemoveFilter as any} handleRemoveFilter={handleFilters as any}
handleRemoveAllFilters={handleRemoveAllFilters} handleRemoveAllFilters={handleRemoveAllFilters}
labels={labels ?? []} labels={labels ?? []}
states={states ?? []} states={states ?? []}

View File

@ -1,8 +1,10 @@
"use client";
import { observer } from "mobx-react-lite";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { StateGroupIcon } from "@plane/ui"; import { StateGroupIcon } from "@plane/ui";
// icons
import { IIssueState } from "types/issue";
// types // types
import { IIssueState } from "@/types/issue";
type Props = { type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;
@ -10,7 +12,7 @@ type Props = {
values: string[]; values: string[];
}; };
export const AppliedStateFilters: React.FC<Props> = (props) => { export const AppliedStateFilters: React.FC<Props> = observer((props) => {
const { handleRemove, states, values } = props; const { handleRemove, states, values } = props;
return ( return (
@ -36,4 +38,4 @@ export const AppliedStateFilters: React.FC<Props> = (props) => {
})} })}
</> </>
); );
}; });

View File

@ -1,3 +1,5 @@
"use client";
import React, { Fragment, useState } from "react"; import React, { Fragment, useState } from "react";
import { Placement } from "@popperjs/core"; import { Placement } from "@popperjs/core";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";

View File

@ -1,3 +1,5 @@
"use client";
import React from "react"; import React from "react";
// lucide icons // lucide icons
import { ChevronDown, ChevronUp } from "lucide-react"; import { ChevronDown, ChevronUp } from "lucide-react";

View File

@ -1,3 +1,5 @@
"use client";
import React from "react"; import React from "react";
// lucide icons // lucide icons
import { Check } from "lucide-react"; import { Check } from "lucide-react";

View File

@ -1,11 +1,11 @@
import React, { useState } from "react"; "use client";
// components import React, { useState } from "react";
// ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers";
// types // types
import { IIssueLabel } from "types/issue"; import { IIssueLabel } from "@/types/issue";
import { FilterHeader, FilterOption } from "./helpers";
const LabelIcons = ({ color }: { color: string }) => ( const LabelIcons = ({ color }: { color: string }) => (
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} /> <span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} />

View File

@ -1,9 +1,11 @@
"use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// ui // ui
import { PriorityIcon } from "@plane/ui"; import { PriorityIcon } from "@plane/ui";
// components // components
import { issuePriorityFilters } from "@/constants/data"; import { issuePriorityFilters } from "@/constants/issue";
import { FilterHeader, FilterOption } from "./helpers"; import { FilterHeader, FilterOption } from "./helpers";
// constants // constants

View File

@ -1,12 +1,15 @@
"use client";
import { FC, useCallback } from "react"; import { FC, useCallback } from "react";
import cloneDeep from "lodash/cloneDeep";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// hooks // hooks
import { useIssue, useIssueFilter, useProject } from "@/hooks/store"; import { useIssue, useIssueFilter } from "@/hooks/store";
// types // types
import { IIssueFilterOptions } from "@/types/issue"; import { TIssueQueryFilters } from "@/types/issue";
// components // components
import { FiltersDropdown } from "./helpers/dropdown"; import { FiltersDropdown } from "./helpers/dropdown";
import { FilterSelection } from "./selection"; import { FilterSelection } from "./selection";
@ -17,48 +20,44 @@ type IssueFiltersDropdownProps = {
}; };
export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((props) => { export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((props) => {
const { workspaceSlug, projectId } = props;
const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
// store hooks const { workspaceSlug, projectId } = props;
const { activeLayout } = useProject(); // hooks
const { issueFilters, updateIssueFilters } = useIssueFilter();
const { states, labels } = useIssue(); const { states, labels } = useIssue();
const { issueFilters, updateFilters } = useIssueFilter();
const activeLayout = issueFilters?.display_filters?.layout || undefined;
const updateRouteParams = useCallback( const updateRouteParams = useCallback(
(key: keyof IIssueFilterOptions, value: string[]) => { (key: keyof TIssueQueryFilters, value: string[]) => {
const state = key === "state" ? value : issueFilters?.filters?.state ?? []; const state = key === "state" ? value : issueFilters?.filters?.state ?? [];
const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? []; const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? [];
const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? []; const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? [];
let params: any = { board: activeLayout || "list" }; let params: any = { board: activeLayout || "list" };
if (priority.length > 0) params = { ...params, priorities: priority.join(",") }; if (priority.length > 0) params = { ...params, priority: priority.join(",") };
if (state.length > 0) params = { ...params, states: state.join(",") }; if (state.length > 0) params = { ...params, state: state.join(",") };
if (labels.length > 0) params = { ...params, labels: labels.join(",") }; if (labels.length > 0) params = { ...params, labels: labels.join(",") };
console.log("params", params); params = new URLSearchParams(params).toString();
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`);
router.push(`/${workspaceSlug}/${projectId}?${params}`);
}, },
[workspaceSlug, projectId, activeLayout, issueFilters, router] [workspaceSlug, projectId, activeLayout, issueFilters, router]
); );
const handleFilters = useCallback( const handleFilters = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => { (key: keyof TIssueQueryFilters, value: string) => {
if (!projectId) return; if (!projectId || !value) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) { const newValues = cloneDeep(issueFilters?.filters?.[key]) ?? [];
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 }); if (newValues.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
updateIssueFilters(projectId, "filters", key, newValues);
updateRouteParams(key, newValues); updateRouteParams(key, newValues);
}, },
[projectId, issueFilters, updateFilters, updateRouteParams] [projectId, issueFilters, updateIssueFilters, updateRouteParams]
); );
return ( return (
@ -67,7 +66,7 @@ export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((pro
<FilterSelection <FilterSelection
filters={issueFilters?.filters ?? {}} filters={issueFilters?.filters ?? {}}
handleFilters={handleFilters as any} handleFilters={handleFilters as any}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined} layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT?.[activeLayout]?.filters : []}
states={states ?? undefined} states={states ?? undefined}
labels={labels ?? undefined} labels={labels ?? undefined}
/> />

View File

@ -1,16 +1,17 @@
"use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Search, X } from "lucide-react"; import { Search, X } from "lucide-react";
// types // types
import { IIssueState, IIssueLabel, IIssueFilterOptions } from "@/types/issue"; import { IIssueState, IIssueLabel, IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
import { ILayoutDisplayFiltersOptions } from "@/types/issue-filters";
// components // components
import { FilterPriority, FilterState } from "./"; import { FilterPriority, FilterState } from "./";
type Props = { type Props = {
filters: IIssueFilterOptions; filters: IIssueFilterOptions;
handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void; handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; layoutDisplayFiltersOptions: TIssueFilterKeys[];
labels?: IIssueLabel[] | undefined; labels?: IIssueLabel[] | undefined;
states?: IIssueState[] | undefined; states?: IIssueState[] | undefined;
}; };
@ -20,7 +21,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter); const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions.includes(filter);
return ( return (
<div className="flex h-full w-full flex-col overflow-hidden"> <div className="flex h-full w-full flex-col overflow-hidden">

View File

@ -1,10 +1,11 @@
"use client";
import React, { useState } from "react"; import React, { useState } from "react";
// components
// ui
import { Loader, StateGroupIcon } from "@plane/ui"; import { Loader, StateGroupIcon } from "@plane/ui";
// components
import { FilterHeader, FilterOption } from "@/components/issues/filters/helpers";
// types // types
import { IIssueState } from "types/issue"; import { IIssueState } from "@/types/issue";
import { FilterHeader, FilterOption } from "./helpers";
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;

View File

@ -3,49 +3,48 @@
import { useEffect, FC } from "react"; import { useEffect, FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useParams, useSearchParams, usePathname } from "next/navigation"; import { useRouter, useSearchParams, usePathname } from "next/navigation";
import useSWR from "swr";
// ui // ui
import { Avatar, Button } from "@plane/ui"; import { Avatar, Button } from "@plane/ui";
// components // components
import { IssueFiltersDropdown } from "@/components/issues/filters"; import { IssueFiltersDropdown } from "@/components/issues/filters";
import { NavbarIssueBoardView } from "@/components/issues/navbar/issue-board-view";
import { NavbarTheme } from "@/components/issues/navbar/theme";
// hooks // hooks
import { useProject, useUser, useIssueFilter } from "@/hooks/store"; import { useProject, useUser, useIssueFilter, useIssueDetails } from "@/hooks/store";
// types // types
import { TIssueBoardKeys } from "@/types/issue"; import { TIssueLayout } from "@/types/issue";
// components
import { NavbarIssueBoardView } from "./issue-board-view";
import { NavbarTheme } from "./theme";
export type NavbarControlsProps = { export type NavbarControlsProps = {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
projectSettings: any;
}; };
export const NavbarControls: FC<NavbarControlsProps> = observer((props) => { export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
const { workspaceSlug, projectId, projectSettings } = props; // props
const { views } = projectSettings; const { workspaceSlug, projectId } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { board, labels, states, priorities, peekId } = useParams<any>();
const searchParams = useSearchParams();
const pathName = usePathname(); const pathName = usePathname();
// store const searchParams = useSearchParams();
const { updateFilters } = useIssueFilter(); // query params
const { settings, activeLayout, hydrate, setActiveLayout } = useProject(); const board = searchParams.get("board") || undefined;
hydrate(projectSettings); const labels = searchParams.get("labels") || undefined;
const state = searchParams.get("state") || undefined;
const { data: user, fetchCurrentUser } = useUser(); const priority = searchParams.get("priority") || undefined;
const peekId = searchParams.get("peekId") || undefined;
useSWR("CURRENT_USER", () => fetchCurrentUser(), { errorRetryCount: 2 }); // hooks
const { issueFilters, isIssueFiltersUpdated, initIssueFilters } = useIssueFilter();
console.log("user", user); const { settings } = useProject();
const { data: user } = useUser();
const { setPeekId } = useIssueDetails();
// derived values
const activeLayout = issueFilters?.display_filters?.layout || undefined;
useEffect(() => { useEffect(() => {
if (workspaceSlug && projectId && settings) { if (workspaceSlug && projectId && settings) {
const viewsAcceptable: string[] = []; const viewsAcceptable: string[] = [];
const currentBoard: TIssueBoardKeys | null = null; let currentBoard: TIssueLayout | null = null;
if (settings?.views?.list) viewsAcceptable.push("list"); if (settings?.views?.list) viewsAcceptable.push("list");
if (settings?.views?.kanban) viewsAcceptable.push("kanban"); if (settings?.views?.kanban) viewsAcceptable.push("kanban");
@ -53,59 +52,66 @@ export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
if (settings?.views?.gantt) viewsAcceptable.push("gantt"); if (settings?.views?.gantt) viewsAcceptable.push("gantt");
if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet"); if (settings?.views?.spreadsheet) viewsAcceptable.push("spreadsheet");
// if (board) { if (board) {
// if (viewsAcceptable.includes(board.toString())) { if (viewsAcceptable.includes(board.toString())) currentBoard = board.toString() as TIssueLayout;
// currentBoard = board.toString() as TIssueBoardKeys; else {
// } else { if (viewsAcceptable && viewsAcceptable.length > 0) currentBoard = viewsAcceptable[0] as TIssueLayout;
// if (viewsAcceptable && viewsAcceptable.length > 0) { }
// currentBoard = viewsAcceptable[0] as TIssueBoardKeys; } else {
// } if (viewsAcceptable && viewsAcceptable.length > 0) currentBoard = viewsAcceptable[0] as TIssueLayout;
// } }
// } else {
// if (viewsAcceptable && viewsAcceptable.length > 0) {
// currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
// }
// }
if (currentBoard) { if (currentBoard) {
if (activeLayout === null || activeLayout !== currentBoard) { if (activeLayout === undefined || activeLayout !== currentBoard) {
let params: any = { board: currentBoard }; let queryParams: any = { board: currentBoard };
if (peekId && peekId.length > 0) params = { ...params, peekId: peekId }; const params: any = { display_filters: { layout: currentBoard }, filters: {} };
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 (storeParams) updateFilters(projectId, storeParams); if (peekId && peekId.length > 0) {
setActiveLayout(currentBoard); queryParams = { ...queryParams, peekId: peekId };
router.push(`/${workspaceSlug}/${projectId}?${searchParams}`); 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, workspaceSlug,
projectId, projectId,
router, board,
updateFilters,
labels, labels,
states, state,
priorities, priority,
peekId, peekId,
settings, settings,
activeLayout, activeLayout,
setActiveLayout, router,
searchParams, initIssueFilters,
setPeekId,
isIssueFiltersUpdated,
]); ]);
return ( return (
<> <>
{/* issue views */} {/* issue views */}
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out"> <div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
<NavbarIssueBoardView layouts={views} /> <NavbarIssueBoardView workspaceSlug={workspaceSlug} projectId={projectId} />
</div> </div>
{/* issue filters */} {/* issue filters */}

View File

@ -1,43 +1,44 @@
"use client"; "use client";
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite";
import { Briefcase } from "lucide-react"; import { Briefcase } from "lucide-react";
// components // components
import { ProjectLogo } from "@/components/common"; import { ProjectLogo } from "@/components/common";
import { NavbarControls } from "./controls"; import { NavbarControls } from "@/components/issues/navbar/controls";
// hooks
import { useProject } from "@/hooks/store";
type IssueNavbarProps = { type IssueNavbarProps = {
projectSettings: any;
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
}; };
const IssueNavbar: FC<IssueNavbarProps> = (props) => { const IssueNavbar: FC<IssueNavbarProps> = observer((props) => {
const { projectSettings, workspaceSlug, projectId } = props; const { workspaceSlug, projectId } = props;
const { project_details } = projectSettings; // hooks
const { project } = useProject();
return ( return (
<div className="relative flex justify-between w-full gap-4 px-5"> <div className="relative flex justify-between w-full gap-4 px-5">
{/* project detail */} {/* project detail */}
<div className="flex flex-shrink-0 items-center gap-2"> <div className="flex flex-shrink-0 items-center gap-2">
{project_details ? ( {project ? (
<span className="h-7 w-7 flex-shrink-0 grid place-items-center"> <span className="h-7 w-7 flex-shrink-0 grid place-items-center">
<ProjectLogo logo={project_details.logo_props} className="text-lg" /> <ProjectLogo logo={project.logo_props} className="text-lg" />
</span> </span>
) : ( ) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase"> <span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" /> <Briefcase className="h-4 w-4" />
</span> </span>
)} )}
<div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium"> <div className="line-clamp-1 max-w-[300px] overflow-hidden text-lg font-medium">{project?.name || `...`}</div>
{project_details?.name || `...`}
</div>
</div> </div>
<div className="flex flex-shrink-0 items-center gap-2"> <div className="flex flex-shrink-0 items-center gap-2">
<NavbarControls workspaceSlug={workspaceSlug} projectId={projectId} projectSettings={projectSettings} /> <NavbarControls workspaceSlug={workspaceSlug} projectId={projectId} />
</div> </div>
</div> </div>
); );
}; });
export default IssueNavbar; export default IssueNavbar;

View File

@ -2,31 +2,53 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter, useSearchParams } from "next/navigation";
// constants // constants
import { issueViews } from "@/constants/data"; import { issueLayoutViews } from "@/constants/issue";
// hooks // hooks
import { useProject } from "@/hooks/store"; import { useIssueFilter } from "@/hooks/store";
// mobx // mobx
import { TIssueBoardKeys } from "@/types/issue"; import { TIssueLayout } from "@/types/issue";
type NavbarIssueBoardViewProps = { type NavbarIssueBoardViewProps = {
layouts: Record<TIssueBoardKeys, boolean>; workspaceSlug: string;
projectId: string;
}; };
export const NavbarIssueBoardView: FC<NavbarIssueBoardViewProps> = observer((props) => { export const NavbarIssueBoardView: FC<NavbarIssueBoardViewProps> = 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) => { const handleCurrentBoardView = (boardView: TIssueLayout) => {
setActiveLayout(boardView as TIssueBoardKeys); 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 ( return (
<> <>
{layouts && {issueLayoutViews &&
Object.keys(layouts).map((layoutKey: string) => { Object.keys(issueLayoutViews).map((key: string) => {
if (layouts[layoutKey as TIssueBoardKeys]) { const layoutKey = key as TIssueLayout;
if (layoutOptions[layoutKey]) {
return ( return (
<div <div
key={layoutKey} key={layoutKey}
@ -40,10 +62,10 @@ export const NavbarIssueBoardView: FC<NavbarIssueBoardViewProps> = observer((pro
> >
<span <span
className={`material-symbols-rounded text-[18px] ${ className={`material-symbols-rounded text-[18px] ${
issueViews[layoutKey]?.className ? issueViews[layoutKey]?.className : `` issueLayoutViews[layoutKey]?.className ? issueLayoutViews[layoutKey]?.className : ``
}`} }`}
> >
{issueViews[layoutKey]?.icon} {issueLayoutViews[layoutKey]?.icon}
</span> </span>
</div> </div>
); );

View File

@ -4,7 +4,7 @@ import { StateGroupIcon } from "@plane/ui";
// icons // icons
import { Icon } from "@/components/ui"; import { Icon } from "@/components/ui";
// helpers // helpers
import { issueGroupFilter, issuePriorityFilter } from "@/constants/data"; import { issueGroupFilter, issuePriorityFilter } from "@/constants/issue";
import { renderFullDate } from "@/helpers/date-time.helper"; import { renderFullDate } from "@/helpers/date-time.helper";
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper"; import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
// types // types

View File

@ -10,7 +10,7 @@ import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overv
import { useIssue, useIssueDetails } from "@/hooks/store"; import { useIssue, useIssueDetails } from "@/hooks/store";
export const IssuePeekOverview: React.FC = observer((props: any) => { 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 // states
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false); const [isSidePeekOpen, setIsSidePeekOpen] = useState(false);
const [isModalPeekOpen, setIsModalPeekOpen] = useState(false); const [isModalPeekOpen, setIsModalPeekOpen] = useState(false);
@ -33,7 +33,7 @@ export const IssuePeekOverview: React.FC = observer((props: any) => {
const params: any = { board: board }; const params: any = { board: board };
if (states && states.length > 0) params.states = states; 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; if (labels && labels.length > 0) params.labels = labels;
// TODO: fix this redirection // TODO: fix this redirection
// router.push( encodeURI(`/${workspaceSlug?.toString()}/${projectId}`, ) { pathname: `/${workspaceSlug?.toString()}/${projectId}`, query: { ...params } }); // router.push( encodeURI(`/${workspaceSlug?.toString()}/${projectId}`, ) { pathname: `/${workspaceSlug?.toString()}/${projectId}`, query: { ...params } });

View File

@ -3,7 +3,7 @@
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Image from "next/image"; import Image from "next/image";
import { useParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
// components // components
import { IssueCalendarView } from "@/components/issues/board-views/calendar"; 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 { IssueAppliedFilters } from "@/components/issues/filters/applied-filters/root";
import { IssuePeekOverview } from "@/components/issues/peek-overview"; import { IssuePeekOverview } from "@/components/issues/peek-overview";
// mobx store // mobx store
import { useIssue, useUser, useProject, useIssueDetails } from "@/hooks/store"; import { useIssue, useUser, useIssueDetails, useIssueFilter, useProject } from "@/hooks/store";
// assets // assets
import SomethingWentWrongImage from "public/something-went-wrong.svg"; import SomethingWentWrongImage from "public/something-went-wrong.svg";
type ProjectDetailsViewProps = { type ProjectDetailsViewProps = {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
peekId: string; peekId: string | undefined;
}; };
export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props) => { export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props) => {
const { workspaceSlug, projectId, peekId } = props;
// router // router
const params = useParams(); const searchParams = useSearchParams();
// store hooks // 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 { 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 issueDetailStore = useIssueDetails();
const { data: currentUser, fetchCurrentUser } = useUser(); 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(() => { useEffect(() => {
if (!currentUser) { if (!currentUser) {
fetchCurrentUser(); fetchCurrentUser();
@ -53,15 +64,18 @@ export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props)
} }
}, [peekId, issueDetailStore, projectId, workspaceSlug]); }, [peekId, issueDetailStore, projectId, workspaceSlug]);
// derived values
const activeLayout = issueFilters?.display_filters?.layout || undefined;
return ( return (
<div className="relative h-full w-full overflow-hidden"> <div className="relative h-full w-full overflow-hidden">
{workspaceSlug && <IssuePeekOverview />} {workspaceSlug && <IssuePeekOverview />}
{issueStore?.loader && !issueStore.issues ? ( {loader && !issues ? (
<div className="py-10 text-center text-sm text-custom-text-100">Loading...</div> <div className="py-10 text-center text-sm text-custom-text-100">Loading...</div>
) : ( ) : (
<> <>
{issueStore?.error ? ( {error ? (
<div className="grid h-full w-full place-items-center p-6"> <div className="grid h-full w-full place-items-center p-6">
<div className="text-center"> <div className="text-center">
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80"> <div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
@ -77,7 +91,7 @@ export const ProjectDetailsView: FC<ProjectDetailsViewProps> = observer((props)
activeLayout && ( activeLayout && (
<div className="relative flex h-full w-full flex-col overflow-hidden"> <div className="relative flex h-full w-full flex-col overflow-hidden">
{/* applied filters */} {/* applied filters */}
<IssueAppliedFilters /> <IssueAppliedFilters workspaceSlug={workspaceSlug} projectId={projectId} />
{activeLayout === "list" && ( {activeLayout === "list" && (
<div className="relative h-full w-full overflow-y-auto"> <div className="relative h-full w-full overflow-y-auto">

View File

@ -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 };
};

View File

@ -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: { // issue filters
[pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]> } = {
} = { list: {
issues: { filters: ["priority", "state", "labels"],
list: { },
filters: ["priority", "state", "labels"], kanban: {
display_properties: null, filters: ["priority", "state", "labels"],
display_filters: null, },
extra_options: null, calendar: {
}, filters: ["priority", "state", "labels"],
kanban: { },
filters: ["priority", "state", "labels"], spreadsheet: {
display_properties: null, filters: ["priority", "state", "labels"],
display_filters: null, },
extra_options: null, gantt: {
}, filters: ["priority", "state", "labels"],
}, },
}; };
export const issueLayoutViews: Partial<TIssueLayoutViews> = {
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;
};

View File

@ -20,8 +20,6 @@ export const AuthWrapper: FC<TAuthWrapper> = observer((props) => {
const { isLoading, data: currentUser, fetchCurrentUser } = useUser(); const { isLoading, data: currentUser, fetchCurrentUser } = useUser();
const { data: currentUserProfile } = useUserProfile(); const { data: currentUserProfile } = useUserProfile();
console;
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchCurrentUser(), { const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchCurrentUser(), {
revalidateOnFocus: false, revalidateOnFocus: false,
}); });

View File

@ -17,12 +17,12 @@
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@headlessui/react": "^1.7.13", "@headlessui/react": "^1.7.13",
"@mui/material": "^5.14.1", "@mui/material": "^5.14.1",
"@plane/constants": "*",
"@plane/document-editor": "*", "@plane/document-editor": "*",
"@plane/lite-text-editor": "*", "@plane/lite-text-editor": "*",
"@plane/rich-text-editor": "*", "@plane/rich-text-editor": "*",
"@plane/types": "*", "@plane/types": "*",
"@plane/ui": "*", "@plane/ui": "*",
"@plane/constants": "*",
"@sentry/nextjs": "^7.108.0", "@sentry/nextjs": "^7.108.0",
"axios": "^1.3.4", "axios": "^1.3.4",
"clsx": "^2.0.0", "clsx": "^2.0.0",
@ -34,6 +34,7 @@
"lucide-react": "^0.378.0", "lucide-react": "^0.378.0",
"mobx": "^6.10.0", "mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3", "mobx-react-lite": "^4.0.3",
"mobx-utils": "^6.0.8",
"next": "^14.2.3", "next": "^14.2.3",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",

View File

@ -29,7 +29,7 @@ export abstract class APIService {
} }
get(url: string, params = {}) { get(url: string, params = {}) {
return this.axiosInstance.get(url, { params }); return this.axiosInstance.get(url, params);
} }
post(url: string, data: any, config = {}) { post(url: string, data: any, config = {}) {

View File

@ -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 { action, makeObservable, observable, runInAction, computed } from "mobx";
import { computedFn } from "mobx-utils";
// constants // constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// store // store
import { RootStore } from "@/store/root.store"; import { RootStore } from "@/store/root.store";
// types // types
import { TIssueBoardKeys, IIssueFilterOptions, TIssueParams } from "@/types/issue"; import {
TIssueLayoutOptions,
interface IFiltersOptions { TIssueFilters,
filters: IIssueFilterOptions; TIssueQueryFilters,
} TIssueQueryFiltersParams,
TIssueFilterKeys,
} from "@/types/issue";
export interface IIssueFilterStore { export interface IIssueFilterStore {
// observables // observables
projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined; layoutOptions: TIssueLayoutOptions;
filters: { [projectId: string]: TIssueFilters } | undefined;
// computed // computed
issueFilters: IFiltersOptions | undefined; issueFilters: TIssueFilters | undefined;
appliedFilters: TIssueParams[] | undefined; appliedFilters: TIssueQueryFiltersParams | undefined;
// helpers isIssueFiltersUpdated: (filters: TIssueFilters) => boolean;
issueDisplayFilters: (projectId: string) => IFiltersOptions | undefined;
// actions // actions
updateFilters: (projectId: string, filters: IIssueFilterOptions) => Promise<IFiltersOptions>; updateLayoutOptions: (layout: TIssueLayoutOptions) => void;
initIssueFilters: (projectId: string, filters: TIssueFilters) => void;
updateIssueFilters: <K extends keyof TIssueFilters>(
projectId: string,
filterKind: K,
filterKey: keyof TIssueFilters[K],
filters: TIssueFilters[K][typeof filterKey]
) => Promise<void>;
} }
export class IssueFilterStore implements IIssueFilterStore { export class IssueFilterStore implements IIssueFilterStore {
// observables // observables
projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined = undefined; layoutOptions: TIssueLayoutOptions = {
// root store list: true,
rootStore; kanban: false,
calendar: false,
gantt: false,
spreadsheet: false,
};
filters: { [projectId: string]: TIssueFilters } | undefined = undefined;
constructor(_rootStore: RootStore) { constructor(private store: RootStore) {
makeObservable(this, { makeObservable(this, {
// observables // observables
projectIssueFilters: observable.ref, layoutOptions: observable,
filters: observable,
// computed // computed
issueFilters: computed, issueFilters: computed,
appliedFilters: computed, appliedFilters: computed,
// actions // actions
updateFilters: action, updateLayoutOptions: action,
initIssueFilters: action,
updateIssueFilters: action,
}); });
// root store
this.rootStore = _rootStore;
} }
// helper methods // helper methods
computedFilter = (filters: any, filteredParams: any) => { computedFilter = (filters: TIssueQueryFilters, filteredParams: TIssueFilterKeys[]) => {
const computedFilters: any = {}; const computedFilters: TIssueQueryFiltersParams = {};
Object.keys(filters).map((key) => { Object.keys(filters).map((key) => {
if (filters[key] != undefined && filteredParams.includes(key)) const currentFilterKey = key as TIssueFilterKeys;
computedFilters[key] =
typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); 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; return computedFilters;
}; };
// helpers // computed
issueDisplayFilters = (projectId: string) => { get issueFilters() {
const projectId = this.store.project.project?.id;
if (!projectId) return undefined; if (!projectId) return undefined;
return this.projectIssueFilters?.[projectId] || undefined;
};
handleIssueQueryParamsByLayout = (layout: TIssueBoardKeys | undefined, viewType: "issues"): TIssueParams[] | null => { const currentFilters = this.filters?.[projectId];
const queryParams: TIssueParams[] = []; 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 const currentLayout = currentIssueFilters?.display_filters?.layout;
layoutOptions.filters.forEach((option: any) => { if (!currentLayout) return undefined;
queryParams.push(option);
});
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 // actions
updateFilters = async (projectId: string, filters: IIssueFilterOptions) => { updateLayoutOptions = (options: TIssueLayoutOptions) => set(this, ["layoutOptions"], options);
initIssueFilters = async (projectId: string, initFilters: TIssueFilters) => {
try { try {
let issueFilters = { ...this.projectIssueFilters }; if (!projectId) return;
if (!issueFilters) issueFilters = {}; if (this.filters === undefined) runInAction(() => (this.filters = {}));
if (!issueFilters[projectId]) issueFilters[projectId] = { filters: {} }; if (this.filters && initFilters) set(this.filters, [projectId], initFilters);
const newFilters = { const workspaceSlug = this.store.project.workspace?.slug;
filters: { ...issueFilters[projectId].filters }, const currentAppliedFilters = this.appliedFilters;
};
newFilters.filters = { ...newFilters.filters, ...filters }; if (!workspaceSlug) return;
await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters);
issueFilters[projectId] = {
filters: newFilters.filters,
};
runInAction(() => {
this.projectIssueFilters = issueFilters;
});
return newFilters;
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
get issueFilters() { updateIssueFilters = async <K extends keyof TIssueFilters>(
const projectId = this.rootStore.project.project?.id; projectId: string,
if (!projectId) return undefined; 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); runInAction(() => {
if (!issueFilters) return undefined; 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() { if (!workspaceSlug) return;
const userFilters = this.issueFilters; await this.store.issue.fetchPublicIssues(workspaceSlug, projectId, currentAppliedFilters);
const layout = this.rootStore.project?.activeLayout; } catch (error) {
if (!userFilters || !layout) return undefined; throw error;
}
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;
}
} }

View File

@ -21,7 +21,7 @@ export interface IIssueStore {
// service // service
issueService: any; issueService: any;
// actions // actions
fetchPublicIssues: (workspace_slug: string, project_slug: string, params: any) => void; fetchPublicIssues: (workspace_slug: string, project_slug: string, params: any) => Promise<void>;
getCountOfIssuesByState: (state: string) => number; getCountOfIssuesByState: (state: string) => number;
getFilteredIssuesByState: (state: string) => IIssue[]; getFilteredIssuesByState: (state: string) => IIssue[];
} }

View File

@ -2,50 +2,40 @@
import { observable, action, makeObservable, runInAction } from "mobx"; import { observable, action, makeObservable, runInAction } from "mobx";
// service // service
import ProjectService from "@/services/project.service"; import ProjectService from "@/services/project.service";
// store types
import { RootStore } from "@/store/root.store";
// types // types
import { TIssueBoardKeys } from "@/types/issue";
import { IWorkspace, IProject, IProjectSettings } from "@/types/project"; import { IWorkspace, IProject, IProjectSettings } from "@/types/project";
export interface IProjectStore { export interface IProjectStore {
// observables
loader: boolean; loader: boolean;
error: any | null; error: any | null;
workspace: IWorkspace | null; workspace: IWorkspace | null;
project: IProject | null; project: IProject | null;
settings: IProjectSettings | null; settings: IProjectSettings | null;
activeLayout: TIssueBoardKeys;
layoutOptions: Record<TIssueBoardKeys, boolean>;
canReact: boolean; canReact: boolean;
canComment: boolean; canComment: boolean;
canVote: boolean; canVote: boolean;
// actions
fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise<void>; fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise<void>;
setActiveLayout: (value: TIssueBoardKeys) => void;
hydrate: (projectSettings: any) => void; hydrate: (projectSettings: any) => void;
} }
export class ProjectStore implements IProjectStore { export class ProjectStore implements IProjectStore {
// observables
loader: boolean = false; loader: boolean = false;
error: any | null = null; error: any | null = null;
// data
workspace: IWorkspace | null = null; workspace: IWorkspace | null = null;
project: IProject | null = null; project: IProject | null = null;
settings: IProjectSettings | null = null; settings: IProjectSettings | null = null;
activeLayout: TIssueBoardKeys = "list";
layoutOptions: Record<TIssueBoardKeys, boolean> = {
list: true,
kanban: true,
calendar: false,
gantt: false,
spreadsheet: false,
};
canReact: boolean = false; canReact: boolean = false;
canComment: boolean = false; canComment: boolean = false;
canVote: boolean = false; canVote: boolean = false;
// root store
rootStore;
// service // service
projectService; projectService;
constructor(_rootStore: any | null = null) { constructor(private store: RootStore) {
makeObservable(this, { makeObservable(this, {
// loaders and error observables // loaders and error observables
loader: observable, loader: observable,
@ -54,36 +44,29 @@ export class ProjectStore implements IProjectStore {
workspace: observable, workspace: observable,
project: observable, project: observable,
settings: observable, settings: observable,
layoutOptions: observable,
activeLayout: observable.ref,
canReact: observable.ref, canReact: observable.ref,
canComment: observable.ref, canComment: observable.ref,
canVote: observable.ref, canVote: observable.ref,
// actions // actions
fetchProjectSettings: action, fetchProjectSettings: action,
setActiveLayout: action,
hydrate: action, hydrate: action,
// computed // computed
}); });
this.rootStore = _rootStore; // services
this.projectService = new ProjectService(); this.projectService = new ProjectService();
} }
hydrate = (projectSettings: any) => { 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.workspace = workspace_detail;
this.project = project_details; this.project = project_details;
this.layoutOptions = views;
this.canComment = comments; this.canComment = comments;
this.canVote = votes; this.canVote = votes;
this.canReact = reactions; this.canReact = reactions;
}; };
setActiveLayout = (boardValue: TIssueBoardKeys) => {
this.activeLayout = boardValue;
};
fetchProjectSettings = async (workspace_slug: string, project_slug: string) => { fetchProjectSettings = async (workspace_slug: string, project_slug: string) => {
try { try {
this.loader = true; this.loader = true;
@ -94,12 +77,11 @@ export class ProjectStore implements IProjectStore {
if (response) { if (response) {
const currentProject: IProject = { ...response?.project_details }; const currentProject: IProject = { ...response?.project_details };
const currentWorkspace: IWorkspace = { ...response?.workspace_detail }; const currentWorkspace: IWorkspace = { ...response?.workspace_detail };
const currentViewOptions = { ...response?.views };
const currentDeploySettings = { ...response }; const currentDeploySettings = { ...response };
this.store.issueFilter.updateLayoutOptions(response?.views);
runInAction(() => { runInAction(() => {
this.project = currentProject; this.project = currentProject;
this.workspace = currentWorkspace; this.workspace = currentWorkspace;
this.layoutOptions = currentViewOptions;
this.settings = currentDeploySettings; this.settings = currentDeploySettings;
this.loader = false; this.loader = false;
}); });

View File

@ -1,6 +0,0 @@
export interface ILayoutDisplayFiltersOptions {
filters: (keyof IIssueFilterOptions)[];
display_properties: boolean | null;
display_filters: null;
extra_options: null;
}

View File

@ -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 { export type TIssueFilterPriority = "urgent" | "high" | "medium" | "low" | "none";
key: TIssueBoardKeys; export type TIssueFilterPriorityObject = {
key: TIssueFilterPriority;
title: string; 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; className: string;
icon: string; icon: string;
} };
export type TIssueGroupKey = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; export type TIssueFilterState = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
export type TIssueGroupTitle = "Backlog" | "Unstarted" | "Started" | "Completed" | "Cancelled"; export type TIssueFilterStateObject = {
key: TIssueFilterState;
export interface IIssueGroup { title: string;
key: TIssueGroupKey;
title: TIssueGroupTitle;
color: string; color: string;
className: 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<TFilters>;
export type TIssueQueryFiltersParams = Partial<Record<keyof TFilters, string>>;
export interface IIssue { export interface IIssue {
id: string; id: string;