feat: project issues topbar (#2256)

* chore: project issues topbar

* style: theming and minor UI fixes

* refactor: file structure

* chore: layout wise authorization added

* style: filter dropdowns

* chore: add fetch keys
This commit is contained in:
Aaryan Khandelwal 2023-09-25 13:24:23 +05:30 committed by GitHub
parent 0ebe36bdb3
commit 27f78dd283
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1299 additions and 1161 deletions

View File

@ -25,11 +25,11 @@ import {
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types // types
import { Properties, TIssueViewOptions } from "types"; import { Properties, TIssueLayouts } from "types";
// constants // constants
import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue"; import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue";
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
{ {
type: "list", type: "list",
Icon: FormatListBulletedOutlined, Icon: FormatListBulletedOutlined,
@ -52,7 +52,7 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
}, },
]; ];
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [ const issueViewForDraftIssues: { type: TIssueLayouts; Icon: any }[] = [
{ {
type: "list", type: "list",
Icon: FormatListBulletedOutlined, Icon: FormatListBulletedOutlined,

View File

@ -65,28 +65,21 @@ export const AllBoards: React.FC<Props> = ({
const { displayFilters, groupedIssues } = viewProps; const { displayFilters, groupedIssues } = viewProps;
console.log("viewProps", viewProps);
return ( return (
<> <>
<IssuePeekOverview <IssuePeekOverview
handleMutation={() => handleMutation={() => (isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues())}
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
}
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""} projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions} readOnly={disableUserActions}
/> />
{groupedIssues ? ( {groupedIssues ? (
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8 bg-custom-background-90"> <div className="horizontal-scroll-enable flex h-full gap-x-4 p-5 bg-custom-background-90">
{Object.keys(groupedIssues).map((singleGroup, index) => { {Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState = const currentState =
displayFilters?.group_by === "state" displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
? states?.find((s) => s.id === singleGroup)
: null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0) if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0) return null;
return null;
return ( return (
<SingleBoard <SingleBoard
@ -115,15 +108,13 @@ export const AllBoards: React.FC<Props> = ({
<div className="space-y-3"> <div className="space-y-3">
{Object.keys(groupedIssues).map((singleGroup, index) => { {Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState = const currentState =
displayFilters?.group_by === "state" displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
? states?.find((s) => s.id === singleGroup)
: null;
if (groupedIssues[singleGroup].length === 0) if (groupedIssues[singleGroup].length === 0)
return ( return (
<div <div
key={index} key={index}
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow" className="flex items-center justify-between gap-2 rounded bg-custom-background-100 p-2 shadow-custom-shadow-2xs"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{currentState && ( {currentState && (

View File

@ -490,6 +490,7 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
labels: null, labels: null,
priority: null, priority: null,
state: null, state: null,
state_group: null,
start_date: null, start_date: null,
target_date: null, target_date: null,
}) })

View File

@ -255,8 +255,6 @@ export const SingleListIssue: React.FC<Props> = ({
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues; const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
console.log("properties", properties);
return ( return (
<> <>
<ContextMenu <ContextMenu

View File

@ -0,0 +1 @@
export * from "./project-issues";

View File

@ -0,0 +1,42 @@
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DisplayFiltersSelection, FilterSelection, IssueDropdown, LayoutSelection } from "components/issue-layouts";
// types
import { TIssueLayouts } from "types";
export const ProjectIssuesHeader = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { issueFilter: issueFilterStore } = useMobxStore();
const handleLayoutChange = (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
},
});
};
return (
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["calendar", "gantt_chart", "kanban", "list", "spreadsheet"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={issueFilterStore.userDisplayFilters.layout ?? "list"}
/>
<IssueDropdown title="Filters">
<FilterSelection workspaceSlug={workspaceSlug?.toString() ?? ""} projectId={projectId?.toString() ?? ""} />
</IssueDropdown>
<IssueDropdown title="View">
<DisplayFiltersSelection />
</IssueDropdown>
</div>
);
});

View File

@ -0,0 +1,67 @@
import React from "react";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import {
FilterDisplayProperties,
FilterExtraOptions,
FilterGroupBy,
FilterIssueType,
FilterOrderBy,
} from "components/issue-layouts";
// helpers
import { issueFilterVisibilityData } from "helpers/issue.helper";
export const DisplayFiltersSelection = observer(() => {
const { issueFilter: issueFilterStore } = useMobxStore();
const isDisplayFilterEnabled = (displayFilter: string) =>
issueFilterVisibilityData.issues.display_filters[issueFilterStore.userDisplayFilters.layout ?? "list"].includes(
displayFilter
);
return (
<div className="w-full h-full overflow-hidden select-none relative flex flex-col divide-y divide-custom-border-200 px-0.5">
{/* <div className="flex-shrink-0 p-2 text-sm">Search container</div> */}
<div className="w-full h-full overflow-hidden overflow-y-auto relative pb-2 divide-y divide-custom-border-200">
{/* display properties */}
{issueFilterVisibilityData.issues.display_properties[issueFilterStore.userDisplayFilters.layout ?? "list"] && (
<div className="pb-2 px-2">
<FilterDisplayProperties />
</div>
)}
{/* group by */}
{isDisplayFilterEnabled("group_by") && (
<div className="py-1 px-2">
<FilterGroupBy />
</div>
)}
{/* order by */}
{isDisplayFilterEnabled("order_by") && (
<div className="py-1 px-2">
<FilterOrderBy />
</div>
)}
{/* issue type */}
{isDisplayFilterEnabled("issue_type") && (
<div className="py-1 px-2">
<FilterIssueType />
</div>
)}
{/* Options */}
{issueFilterVisibilityData.issues.extra_options[issueFilterStore.userDisplayFilters.layout ?? "list"]
.access && (
<div className="pt-1 px-2">
<FilterExtraOptions />
</div>
)}
</div>
</div>
);
});

View File

@ -1,49 +1,58 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { FilterHeader } from "../helpers/filter-header"; import { FilterHeader } from "../helpers/filter-header";
// mobx react lite // types
import { observer } from "mobx-react-lite"; import { IIssueDisplayProperties } from "types";
// mobx store // constants
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue"; import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue";
export const FilterDisplayProperties = observer(() => { export const FilterDisplayProperties = observer(() => {
const store: RootStore = useMobxStore(); const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store; const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleDisplayProperties = (key: string, value: boolean) => { const handleDisplayProperties = (property: Partial<IIssueDisplayProperties>) => {
// issueFilterStore.handleUserFilter("display_properties", key, !value); if (!workspaceSlug || !projectId) return;
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
}; };
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={"Display Properties"} title="Display Properties"
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1 px-1 flex items-center whitespace-nowrap gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap mx-1 mt-1">
{ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => ( {ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => (
<div <button
key={displayProperty?.key} key={displayProperty.key}
className={`cursor-pointer rounded-sm transition-all text-xs border p-0.5 px-1.5 ${ type="button"
issueFilterStore?.userDisplayProperties?.[displayProperty?.key] className={`rounded transition-all text-xs border px-2 py-0.5 ${
? `bg-custom-primary-200 border-custom-primary-200 text-white` issueFilterStore?.userDisplayProperties?.[displayProperty.key]
: `hover:bg-custom-border-100 border-custom-border-100` ? "bg-custom-primary-100 border-custom-primary-100 text-white"
: "border-custom-border-200 hover:bg-custom-background-80"
}`} }`}
onClick={() => { onClick={() =>
handleDisplayProperties( handleDisplayProperties({
displayProperty?.key, [displayProperty.key]: !issueFilterStore?.userDisplayProperties?.[displayProperty.key],
issueFilterStore?.userDisplayProperties?.[displayProperty?.key] })
); }
}}
> >
{displayProperty?.title} {displayProperty.title}
</div> </button>
))} ))}
</div> </div>
)} )}

View File

@ -1,53 +1,47 @@
import React from "react"; import React, { useState } from "react";
// components
import { FilterHeader } from "../helpers/filter-header"; // mobx
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
import { ISSUE_EXTRA_PROPERTIES } from "constants/issue"; // components
// default data import { FilterHeader, FilterOption } from "components/issue-layouts";
// import { issueFilterVisibilityData } from "helpers/issue.helper"; // helpers
import { issueFilterVisibilityData } from "helpers/issue.helper";
// constants
import { ISSUE_EXTRA_OPTIONS } from "constants/issue";
export const FilterExtraOptions = observer(() => { export const FilterExtraOptions = observer(() => {
const store: RootStore = useMobxStore(); const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store; const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const isExtraOptionEnabled = (option: string) =>
issueFilterVisibilityData.issues.extra_options[
const handleExtraOptions = (key: string, value: boolean) => { issueFilterStore.userDisplayFilters.layout ?? "list"
// issueFilterStore.handleUserFilter("display_filters", key, !value); ].values.includes(option);
};
const handleExtraOptionsSectionVisibility = (key: string) => {
// issueFilterStore?.issueView &&
// issueFilterStore?.issueLayout &&
// issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"]?.extra_options?.[
// issueFilterStore?.issueLayout
// ].values?.includes(key);
};
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={"Extra Options"} title="Extra Options"
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-[2px] pt-1">
{ISSUE_EXTRA_PROPERTIES.map((_extraProperties) => ( {ISSUE_EXTRA_OPTIONS.map((option) => {
<FilterOption if (!isExtraOptionEnabled(option.key)) return null;
key={_extraProperties?.key}
isChecked={issueFilterStore?.userDisplayFilters?.[_extraProperties?.key] ? true : false} return (
onClick={() => <FilterOption
handleExtraOptions(_extraProperties?.key, issueFilterStore?.userDisplayFilters?.[_extraProperties?.key]) key={option.key}
} isChecked={issueFilterStore?.userDisplayFilters?.[option.key] ? true : false}
title={_extraProperties.title} title={option.title}
/> />
))} );
})}
</div> </div>
)} )}
</div> </div>

View File

@ -1,39 +1,52 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { FilterHeader } from "../helpers/filter-header"; import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option"; import { FilterOption } from "../helpers/filter-option";
// mobx react lite // types
import { observer } from "mobx-react-lite"; import { TIssueGroupByOptions } from "types";
// mobx store // constants
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue";
export const FilterGroupBy = observer(() => { export const FilterGroupBy = observer(() => {
const store: RootStore = useMobxStore(); const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store; const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleGroupBy = (key: string, value: string) => { const handleGroupBy = (value: TIssueGroupByOptions) => {
// issueFilterStore.handleUserFilter("display_filters", key, value); if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
group_by: value,
},
});
}; };
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={"Group By"} title="Group By"
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-1 pt-1">
{ISSUE_GROUP_BY_OPTIONS.map((_groupBy) => ( {ISSUE_GROUP_BY_OPTIONS.map((groupBy) => (
<FilterOption <FilterOption
key={_groupBy?.key} key={groupBy?.key}
isChecked={issueFilterStore?.userDisplayFilters?.group_by === _groupBy?.key ? true : false} isChecked={issueFilterStore?.userDisplayFilters?.group_by === groupBy?.key ? true : false}
onClick={() => handleGroupBy("group_by", _groupBy?.key)} onClick={() => handleGroupBy(groupBy.key)}
title={_groupBy.title} title={groupBy.title}
multiple={false} multiple={false}
/> />
))} ))}

View File

@ -0,0 +1,6 @@
export * from "./display-filters-selection";
export * from "./display-properties";
export * from "./extra-options";
export * from "./group-by";
export * from "./issue-type";
export * from "./order-by";

View File

@ -1,82 +0,0 @@
import React from "react";
// components
import { FilterDisplayProperties } from "./display-properties";
import { FilterGroupBy } from "./group-by";
import { FilterOrderBy } from "./order-by";
import { FilterIssueType } from "./issue-type";
import { FilterExtraOptions } from "./extra-options";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// default data
// import { issueFilterVisibilityData } from "store/helpers/issue-data";
export const DisplayFiltersSelection = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilter: issueFilterStore } = store;
// const handleDisplayPropertiesSectionVisibility =
// issueFilterStore?.issueView &&
// issueFilterStore?.issueLayout &&
// issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"]
// ?.display_properties?.[issueFilterStore?.issueLayout];
const handleDisplayFilterSectionVisibility = (section_key: string) => {
// issueFilterStore?.issueView &&
// issueFilterStore?.issueLayout &&
// issueFilterVisibilityData[
// issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"
// ]?.display_filters?.[issueFilterStore?.issueLayout].includes(section_key);
};
// const handleExtraOptionsSectionVisibility =
// issueFilterStore?.issueView &&
// issueFilterStore?.issueLayout &&
// issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"]?.extra_options?.[
// issueFilterStore?.issueLayout
// ].access;
return (
<div className="w-full h-full overflow-hidden select-none relative flex flex-col divide-y divide-custom-border-200">
<div className="flex-shrink-0 p-2 text-sm">Search container</div>
<div className="w-full h-full overflow-hidden overflow-y-auto relative pb-2 divide-y divide-custom-border-200">
{/* display properties */}
{/* {handleDisplayPropertiesSectionVisibility && (
<div className="pb-2 px-2">
<FilterDisplayProperties />
</div>
)} */}
{/* group by */}
{/* {handleDisplayFilterSectionVisibility("group_by") && (
<div className="py-1 px-2">
<FilterGroupBy />
</div>
)} */}
{/* order by */}
{/* {handleDisplayFilterSectionVisibility("order_by") && (
<div className="py-1 px-2">
<FilterOrderBy />
</div>
)} */}
{/* issue type */}
{/* {handleDisplayFilterSectionVisibility("issue_type") && (
<div className="py-1 px-2">
<FilterIssueType />
</div>
)} */}
{/* Options */}
{/* {handleExtraOptionsSectionVisibility && (
<div className="pt-1 px-2">
<FilterExtraOptions />
</div>
)} */}
</div>
</div>
);
});

View File

@ -1,38 +1,52 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { FilterHeader } from "../helpers/filter-header"; import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option"; import { FilterOption } from "../helpers/filter-option";
// mobx store // types
import { useMobxStore } from "lib/mobx/store-provider"; import { TIssueTypeFilters } from "types";
import { RootStore } from "store/root"; // constants
import { ISSUE_FILTER_OPTIONS } from "constants/issue"; import { ISSUE_FILTER_OPTIONS } from "constants/issue";
export const FilterIssueType = observer(() => { export const FilterIssueType = observer(() => {
const store: RootStore = useMobxStore(); const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store; const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleIssueType = (key: string, value: string) => { const handleIssueType = (value: TIssueTypeFilters) => {
// issueFilterStore.handleUserFilter("display_filters", key, value); if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
type: value,
},
});
}; };
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={"Issue Type"} title="Issue Type"
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-1 pt-1">
{ISSUE_FILTER_OPTIONS.map((_issueType) => ( {ISSUE_FILTER_OPTIONS.map((issueType) => (
<FilterOption <FilterOption
key={_issueType?.key} key={issueType?.key}
isChecked={issueFilterStore?.userDisplayFilters?.type === _issueType?.key ? true : false} isChecked={issueFilterStore?.userDisplayFilters?.type === issueType?.key ? true : false}
onClick={() => handleIssueType("type", _issueType?.key)} onClick={() => handleIssueType(issueType?.key)}
title={_issueType.title} title={issueType.title}
multiple={false} multiple={false}
/> />
))} ))}

View File

@ -1,22 +1,35 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { FilterHeader } from "../helpers/filter-header"; import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option"; import { FilterOption } from "../helpers/filter-option";
// mobx react lite // types
import { observer } from "mobx-react-lite"; import { TIssueOrderByOptions } from "types";
// mobx store // constants
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue"; import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue";
export const FilterOrderBy = observer(() => { export const FilterOrderBy = observer(() => {
const store: RootStore = useMobxStore(); const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store; const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleOrderBy = (key: string, value: string) => { const handleOrderBy = (value: TIssueOrderByOptions) => {
// issueFilterStore.handleUserFilter("display_filters", key, value); if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
order_by: value,
},
});
}; };
return ( return (
@ -28,12 +41,12 @@ export const FilterOrderBy = observer(() => {
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-[2px] pt-1">
{ISSUE_ORDER_BY_OPTIONS.map((_orderBy) => ( {ISSUE_ORDER_BY_OPTIONS.map((orderBy) => (
<FilterOption <FilterOption
key={_orderBy?.key} key={orderBy?.key}
isChecked={issueFilterStore?.userDisplayFilters?.order_by === _orderBy?.key ? true : false} isChecked={issueFilterStore?.userDisplayFilters?.order_by === orderBy?.key ? true : false}
onClick={() => handleOrderBy("order_by", _orderBy?.key)} onClick={() => handleOrderBy(orderBy.key)}
title={_orderBy.title} title={orderBy.title}
multiple={false} multiple={false}
/> />
))} ))}

View File

@ -1,67 +1,62 @@
import React from "react"; import React, { useState } from "react";
// components
import { FilterHeader } from "../helpers/filter-header"; // mobx
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; // components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// ui
import { Avatar } from "components/ui";
export const MemberIcons = ({ display_name, avatar }: { display_name: string; avatar: string | null }) => ( type Props = {
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center"> workspaceSlug: string;
{avatar ? ( projectId: string;
<img src={avatar} alt={display_name || ""} className="" /> };
) : (
<div className="text-[12px] w-full h-full flex justify-center items-center capitalize font-medium bg-gray-700 text-white">
{(display_name ?? "U")[0]}
</div>
)}
</div>
);
export const FilterAssignees = observer(() => { export const FilterAssignees: React.FC<Props> = observer((props) => {
const store: RootStore = useMobxStore(); const { workspaceSlug, projectId } = props;
const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const handleFilter = (key: string, value: string) => { const store = useMobxStore();
// let _value = const { issueFilter: issueFilterStore, project: projectStore } = store;
// issueFilterStore?.userFilters?.filters?.[key] != null
// ? issueFilterStore?.userFilters?.filters?.[key].includes(value) const handleUpdateAssignees = (value: string) => {
// ? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value) const newValues = issueFilterStore.userFilters?.assignees ?? [];
// : [...issueFilterStore?.userFilters?.filters?.[key], value]
// : [value]; if (issueFilterStore.userFilters?.assignees?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
// _value = _value && _value.length > 0 ? _value : null; else newValues.push(value);
// issueFilterStore.handleUserFilter("filters", key, _value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
assignees: newValues,
},
});
}; };
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={`Assignees (${issueFilterStore?.projectMembers?.length || 0})`} title={`Assignees (${issueFilterStore?.userFilters.assignees?.length ?? 0})`}
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-[2px] pt-1">
{issueFilterStore?.projectMembers && {projectStore.members?.[projectId?.toString() ?? ""]?.map((member) => (
issueFilterStore?.projectMembers.length > 0 && <FilterOption
issueFilterStore?.projectMembers.map((_member) => ( key={`assignees-${member?.member?.id}`}
<FilterOption isChecked={
key={`assignees-${_member?.member?.id}`} issueFilterStore?.userFilters?.assignees != null &&
isChecked={ issueFilterStore?.userFilters?.assignees.includes(member.member?.id)
issueFilterStore?.userFilters?.filters?.assignees != null && ? true
issueFilterStore?.userFilters?.filters?.assignees.includes(_member?.member?.id) : false
? true }
: false onClick={() => handleUpdateAssignees(member.member?.id)}
} icon={<Avatar user={member.member} height="18px" width="18px" />}
onClick={() => handleFilter("assignees", _member?.member?.id)} title={member.member?.display_name}
icon={<MemberIcons display_name={_member?.member.display_name} avatar={_member?.member.avatar} />} />
title={`${_member?.member?.display_name} (${_member?.member?.first_name} ${_member?.member?.last_name})`} ))}
/>
))}
</div> </div>
)} )}
</div> </div>

View File

@ -1,56 +1,57 @@
import React from "react"; import React, { useState } from "react";
// components
import { MemberIcons } from "./assignees"; // mobx
import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; // components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// ui
import { Avatar } from "components/ui";
export const FilterCreatedBy = observer(() => { type Props = {
const store: RootStore = useMobxStore(); workspaceSlug: string;
const { issueFilters: issueFilterStore } = store; projectId: string;
};
const [previewEnabled, setPreviewEnabled] = React.useState(true); export const FilterCreatedBy: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = props;
const handleFilter = (key: string, value: string) => { const [previewEnabled, setPreviewEnabled] = useState(true);
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null const store = useMobxStore();
? issueFilterStore?.userFilters?.filters?.[key].includes(value) const { issueFilter: issueFilterStore, project: projectStore } = store;
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
: [...issueFilterStore?.userFilters?.filters?.[key], value] const handleUpdateCreatedBy = (value: string) => {
: [value]; const newValues = issueFilterStore.userFilters?.created_by ?? [];
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value); if (issueFilterStore.userFilters?.created_by?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
created_by: newValues,
},
});
}; };
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={`Created By (${issueFilterStore?.projectMembers?.length || 0})`} title={`Created By (${issueFilterStore?.userFilters.created_by?.length ?? 0})`}
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-1 pt-1">
{issueFilterStore?.projectMembers && {projectStore.members?.[projectId?.toString() ?? ""]?.map((member) => (
issueFilterStore?.projectMembers.length > 0 && <FilterOption
issueFilterStore?.projectMembers.map((_member) => ( key={`created-by-${member.member?.id}`}
<FilterOption isChecked={issueFilterStore?.userFilters?.created_by?.includes(member.member?.id) ? true : false}
key={`create-by-${_member?.member?.id}`} onClick={() => handleUpdateCreatedBy(member.member?.id)}
isChecked={ icon={<Avatar user={member.member} height="18px" width="18px" />}
issueFilterStore?.userFilters?.filters?.created_by != null && title={member.member?.display_name}
issueFilterStore?.userFilters?.filters?.created_by.includes(_member?.member?.id) />
? true ))}
: false
}
onClick={() => handleFilter("created_by", _member?.member?.id)}
icon={<MemberIcons display_name={_member?.member.display_name} avatar={_member?.member.avatar} />}
title={`${_member?.member?.display_name} (${_member?.member?.first_name} ${_member?.member?.last_name})`}
/>
))}
</div> </div>
)} )}
</div> </div>

View File

@ -0,0 +1,71 @@
import React from "react";
// components
import {
FilterAssignees,
FilterCreatedBy,
FilterLabels,
FilterPriority,
FilterState,
FilterStateGroup,
} from "components/issue-layouts";
type Props = {
workspaceSlug: string;
projectId: string;
};
export const FilterSelection: React.FC<Props> = (props) => {
const { workspaceSlug, projectId } = props;
return (
<div className="w-full h-full overflow-hidden select-none relative flex flex-col divide-y divide-custom-border-200 px-0.5">
{/* <div className="flex-shrink-0 p-2 text-sm">Search container</div> */}
<div className="w-full h-full overflow-hidden overflow-y-auto relative pb-2 divide-y divide-custom-border-200">
{/* priority */}
<div className="pb-1 px-2">
<FilterPriority workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
{/* state group */}
<div className="py-1 px-2">
<FilterStateGroup workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
{/* state */}
<div className="py-1 px-2">
<FilterState workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
{/* assignees */}
<div className="py-1 px-2">
<FilterAssignees workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
{/* created_by */}
<div className="py-1 px-2">
<FilterCreatedBy workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
{/* labels */}
<div className="py-1 px-2">
<FilterLabels workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
{/* start_date */}
{/* {handleFilterSectionVisibility("start_date") && (
<div className="py-1 px-2">
<FilterStartDate />
</div>
)} */}
{/* due_date */}
{/* {handleFilterSectionVisibility("due_date") && (
<div className="pt-1 px-2">
<FilterTargetDate />
</div>
)} */}
</div>
</div>
);
};

View File

@ -0,0 +1,9 @@
export * from "./assignees";
export * from "./created-by";
export * from "./filter-selection";
export * from "./labels";
export * from "./priority";
export * from "./start-date";
export * from "./state-group";
export * from "./state";
export * from "./target-date";

View File

@ -1,92 +0,0 @@
import React from "react";
// components
import { FilterPriority } from "./priority";
import { FilterState } from "./state";
import { FilterStateGroup } from "./state-group";
import { FilterAssignees } from "./assignees";
import { FilterCreatedBy } from "./created-by";
import { FilterLabels } from "./labels";
import { FilterStartDate } from "./start-date";
import { FilterTargetDate } from "./target-date";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// default data
import { issueFilterVisibilityData } from "store/helpers/issue-data";
export const FilterSelection = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const handleFilterSectionVisibility = (section_key: string) =>
issueFilterStore?.issueView &&
issueFilterStore?.issueLayout &&
issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"]?.filters?.[
issueFilterStore?.issueLayout
]?.includes(section_key);
return (
<div className="w-full h-full overflow-hidden select-none relative flex flex-col divide-y divide-custom-border-200">
<div className="flex-shrink-0 p-2 text-sm ">Search container</div>
<div className="w-full h-full overflow-hidden overflow-y-auto relative pb-2 divide-y divide-custom-border-200">
{/* priority */}
{handleFilterSectionVisibility("priority") && (
<div className="pb-1 px-2">
<FilterPriority />
</div>
)}
{/* state group */}
{handleFilterSectionVisibility("state_group") && (
<div className="py-1 px-2">
<FilterStateGroup />
</div>
)}
{/* state */}
{handleFilterSectionVisibility("state") && (
<div className="py-1 px-2">
<FilterState />
</div>
)}
{/* assignees */}
{handleFilterSectionVisibility("assignees") && (
<div className="py-1 px-2">
<FilterAssignees />
</div>
)}
{/* created_by */}
{handleFilterSectionVisibility("created_by") && (
<div className="py-1 px-2">
<FilterCreatedBy />
</div>
)}
{/* labels */}
{handleFilterSectionVisibility("labels") && (
<div className="py-1 px-2">
<FilterLabels />
</div>
)}
{/* start_date */}
{handleFilterSectionVisibility("start_date") && (
<div className="py-1 px-2">
<FilterStartDate />
</div>
)}
{/* due_date */}
{handleFilterSectionVisibility("due_date") && (
<div className="pt-1 px-2">
<FilterTargetDate />
</div>
)}
</div>
</div>
);
});

View File

@ -1,66 +1,60 @@
import React from "react"; import React, { useState } from "react";
// components // components
import { FilterHeader } from "../helpers/filter-header"; import { FilterHeader, FilterOption } from "components/issue-layouts";
import { FilterOption } from "../helpers/filter-option";
// mobx react lite // mobx react lite
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
const LabelIcons = ({ color }: { color: string }) => ( const LabelIcons = ({ color }: { color: string }) => (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center"> <span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
<div className={`w-[12px] h-[12px] rounded-full`} style={{ backgroundColor: color }} />
</div>
); );
export const FilterLabels = observer(() => { type Props = {
const store: RootStore = useMobxStore(); workspaceSlug: string;
const { issueFilters: issueFilterStore } = store; projectId: string;
};
const [previewEnabled, setPreviewEnabled] = React.useState(true); export const FilterLabels: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = props;
const handleFilter = (key: string, value: string) => { const [previewEnabled, setPreviewEnabled] = useState(true);
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null const store = useMobxStore();
? issueFilterStore?.userFilters?.filters?.[key].includes(value) const { issueFilter: issueFilterStore, project: projectStore } = store;
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
: [...issueFilterStore?.userFilters?.filters?.[key], value] const handleUpdateLabels = (value: string) => {
: [value]; const newValues = issueFilterStore.userFilters?.labels ?? [];
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value); if (issueFilterStore.userFilters?.labels?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
labels: newValues,
},
});
}; };
const handleLabels =
issueFilterStore.issueView && issueFilterStore.issueView === "my_issues"
? issueFilterStore?.workspaceLabels
: issueFilterStore?.projectLabels;
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={`Labels (${(handleLabels && handleLabels?.length) || 0})`} title={`Labels (${issueFilterStore.userFilters?.labels?.length ?? 0})`}
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-[2px] pt-1">
{handleLabels && {projectStore.labels?.[projectId?.toString() ?? ""]?.map((label) => (
handleLabels.length > 0 && <FilterOption
handleLabels.map((_label) => ( key={label?.id}
<FilterOption isChecked={issueFilterStore?.userFilters?.labels?.includes(label?.id) ? true : false}
key={_label?.id} onClick={() => handleUpdateLabels(label?.id)}
isChecked={ icon={<LabelIcons color={label.color} />}
issueFilterStore?.userFilters?.filters?.labels != null && title={label.name}
issueFilterStore?.userFilters?.filters?.labels.includes(_label?.id) />
? true ))}
: false
}
onClick={() => handleFilter("labels", _label?.id)}
icon={<LabelIcons color={_label.color} />}
title={_label.name}
/>
))}
</div> </div>
)} )}
</div> </div>

View File

@ -1,19 +1,19 @@
import React from "react"; import React, { useState } from "react";
// lucide icons
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react"; // mobx
// components
import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; // components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// icons
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
const PriorityIcons = ({ const PriorityIcons = ({
priority, priority,
size = 14, size = 12,
strokeWidth = 2, strokeWidth = 1.5,
}: { }: {
priority: string; priority: string;
size?: number; size?: number;
@ -21,84 +21,78 @@ const PriorityIcons = ({
}) => { }) => {
if (priority === "urgent") if (priority === "urgent")
return ( return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-red-500 bg-red-500 text-white flex justify-center items-center"> <div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-red-500 bg-red-500 text-white flex justify-center items-center">
<AlertCircle size={size} strokeWidth={strokeWidth} /> <AlertCircle size={size} strokeWidth={strokeWidth} />
</div> </div>
); );
if (priority === "high") if (priority === "high")
return ( return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-custom-border-300 text-red-500 flex justify-center items-center pl-1"> <div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-custom-border-200 text-red-500 flex justify-center items-center pl-1">
<SignalHigh size={size} strokeWidth={strokeWidth} /> <SignalHigh size={size} strokeWidth={strokeWidth} />
</div> </div>
); );
if (priority === "medium") if (priority === "medium")
return ( return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-custom-border-300 text-orange-500 flex justify-center items-center pl-1"> <div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-custom-border-200 text-orange-500 flex justify-center items-center pl-1">
<SignalMedium size={size} strokeWidth={strokeWidth} /> <SignalMedium size={size} strokeWidth={strokeWidth} />
</div> </div>
); );
if (priority === "low") if (priority === "low")
return ( return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-custom-border-300 text-green-500 flex justify-center items-center pl-1"> <div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-custom-border-200 text-green-500 flex justify-center items-center pl-1">
<SignalLow size={size} strokeWidth={strokeWidth} /> <SignalLow size={size} strokeWidth={strokeWidth} />
</div> </div>
); );
return ( return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-custom-border-300 text-gray-500 flex justify-center items-center"> <div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-custom-border-200 text-custom-text-400 flex justify-center items-center">
<Ban size={size} strokeWidth={strokeWidth} /> <Ban size={size} strokeWidth={strokeWidth} />
</div> </div>
); );
}; };
export const FilterPriority = observer(() => { type Props = { workspaceSlug: string; projectId: string };
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); export const FilterPriority: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = props;
const handleFilter = (key: string, value: string) => { const [previewEnabled, setPreviewEnabled] = useState(true);
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null const store = useMobxStore();
? issueFilterStore?.userFilters?.filters?.[key].includes(value) const { issueFilter: issueFilterStore } = store;
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
: [...issueFilterStore?.userFilters?.filters?.[key], value] const handleUpdatePriority = (value: string) => {
: [value]; const newValues = issueFilterStore.userFilters?.priority ?? [];
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value); if (issueFilterStore.userFilters?.priority?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
priority: newValues,
},
});
}; };
return ( return (
<div> <>
<FilterHeader <FilterHeader
title={`Priority (${issueFilterStore?.issueRenderFilters?.priority.length || 0})`} title={`Priority (${issueFilterStore.userFilters?.priority?.length ?? 0})`}
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-1 pt-1">
{issueFilterStore?.issueRenderFilters?.priority && {ISSUE_PRIORITIES.map((priority) => (
issueFilterStore?.issueRenderFilters?.priority.length > 0 && <FilterOption
issueFilterStore?.issueRenderFilters?.priority.map((_priority) => ( key={priority.key}
<FilterOption isChecked={issueFilterStore.userFilters?.priority?.includes(priority.key) ? true : false}
key={_priority?.key} onClick={() => handleUpdatePriority(priority.key)}
isChecked={ icon={<PriorityIcons priority={priority.key} />}
issueFilterStore?.userFilters?.filters?.priority != null && title={priority.title}
issueFilterStore?.userFilters?.filters?.priority.includes(_priority?.key) />
? true ))}
: false
}
onClick={() => handleFilter("priority", _priority?.key)}
icon={<PriorityIcons priority={_priority.key} />}
title={_priority.title}
/>
))}
<div className="pl-[32px] flex items-center gap-2 py-[6px] text-xs text-custom-primary-100">
<div>View less</div>
<div>View more</div>
{/* TODO: <div>View all</div> */}
</div>
</div> </div>
)} )}
</div> </>
); );
}); });

View File

@ -1,45 +1,32 @@
import React from "react"; import React from "react";
// components
import { FilterHeader } from "../helpers/filter-header"; // mobx
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; // components
import { FilterHeader, FilterOption } from "components/issue-layouts";
export const FilterStartDate = observer(() => { export const FilterStartDate = observer(() => {
const store: RootStore = useMobxStore(); const store = useMobxStore();
const { issueFilters: issueFilterStore } = store; const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleFilter = (key: string, value: string) => {
const _value = [value];
issueFilterStore.handleUserFilter("filters", key, _value);
};
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={`Start Date (${issueFilterStore?.issueRenderFilters?.start_date?.length})`} title={`Start Date (${issueFilterStore?.userFilters?.start_date?.length})`}
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-1 pt-1">
{issueFilterStore?.issueRenderFilters?.start_date && {issueFilterStore?.userFilters?.start_date &&
issueFilterStore?.issueRenderFilters?.start_date.length > 0 && issueFilterStore?.userFilters?.start_date.length > 0 &&
issueFilterStore?.issueRenderFilters?.start_date.map((_startDate) => ( issueFilterStore?.userFilters?.start_date.map((_startDate) => (
<FilterOption <FilterOption
key={_startDate?.key} key={_startDate?.key}
isChecked={ isChecked={issueFilterStore?.userFilters?.start_date?.includes(_startDate?.key) ? true : false}
issueFilterStore?.userFilters?.filters?.start_date != null &&
issueFilterStore?.userFilters?.filters?.start_date.includes(_startDate?.key)
? true
: false
}
onClick={() => handleFilter("start_date", _startDate?.key)}
title={_startDate.title} title={_startDate.title}
multiple={false} multiple={false}
/> />

View File

@ -1,108 +1,59 @@
import React from "react"; import React, { useState } from "react";
import {
StateGroupBacklogIcon,
StateGroupCancelledIcon,
StateGroupCompletedIcon,
StateGroupStartedIcon,
StateGroupUnstartedIcon,
} from "components/icons";
// components
import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
export const StateGroupIcons = ({ // mobx
stateGroup, import { observer } from "mobx-react-lite";
width = "14px", import { useMobxStore } from "lib/mobx/store-provider";
height = "14px", // components
color = null, import { FilterHeader, FilterOption } from "components/issue-layouts";
}: { // icons
stateGroup: string; import { StateGroupIcon } from "components/icons";
width?: string | undefined; // constants
height?: string | undefined; import { ISSUE_STATE_GROUPS } from "constants/issue";
color?: string | null;
}) => { type Props = {
if (stateGroup === "cancelled") workspaceSlug: string;
return ( projectId: string;
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
<StateGroupCancelledIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
</div>
);
if (stateGroup === "completed")
return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
<StateGroupCompletedIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
</div>
);
if (stateGroup === "started")
return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
<StateGroupStartedIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
</div>
);
if (stateGroup === "unstarted")
return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
<StateGroupUnstartedIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
</div>
);
if (stateGroup === "backlog")
return (
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
<StateGroupBacklogIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
</div>
);
return <></>;
}; };
export const FilterStateGroup = observer(() => { export const FilterStateGroup: React.FC<Props> = observer((props) => {
const store: RootStore = useMobxStore(); const { workspaceSlug, projectId } = props;
const { issueFilters: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const handleFilter = (key: string, value: string) => { const store = useMobxStore();
let _value = const { issueFilter: issueFilterStore } = store;
issueFilterStore?.userFilters?.filters?.[key] != null
? issueFilterStore?.userFilters?.filters?.[key].includes(value) const handleUpdateStateGroup = (value: string) => {
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value) const newValues = issueFilterStore.userFilters?.state_group ?? [];
: [...issueFilterStore?.userFilters?.filters?.[key], value]
: [value]; if (issueFilterStore.userFilters?.state_group?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
_value = _value && _value.length > 0 ? _value : null; else newValues.push(value);
issueFilterStore.handleUserFilter("filters", key, _value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
state_group: newValues,
},
});
}; };
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={`State Group (${issueFilterStore?.issueRenderFilters?.state_group?.length})`} title={`State Group (${issueFilterStore.userFilters?.state_group?.length ?? 0})`}
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-1 pt-1">
{issueFilterStore?.issueRenderFilters?.state_group && {ISSUE_STATE_GROUPS.map((stateGroup) => (
issueFilterStore?.issueRenderFilters?.state_group.length > 0 && <FilterOption
issueFilterStore?.issueRenderFilters?.state_group.map((_stateGroup) => ( key={stateGroup.key}
<FilterOption isChecked={issueFilterStore.userFilters?.state_group?.includes(stateGroup.key) ? true : false}
key={_stateGroup?.key} onClick={() => handleUpdateStateGroup(stateGroup.key)}
isChecked={ icon={<StateGroupIcon stateGroup={stateGroup.key} />}
issueFilterStore?.userFilters?.filters?.state_group != null && title={stateGroup.title}
issueFilterStore?.userFilters?.filters?.state_group.includes(_stateGroup?.key) />
? true ))}
: false
}
onClick={() => handleFilter("state_group", _stateGroup?.key)}
icon={<StateGroupIcons stateGroup={_stateGroup.key} />}
title={_stateGroup.title}
/>
))}
</div> </div>
)} )}
</div> </div>

View File

@ -1,68 +1,62 @@
import React from "react"; import React from "react";
// components
import { StateGroupIcons } from "./state-group";
import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// store default data
import { stateGroups } from "store/helpers/issue-data";
export const FilterState = observer(() => { // mobx
const store: RootStore = useMobxStore(); import { observer } from "mobx-react-lite";
const { issueFilters: issueFilterStore } = store; import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// icons
import { StateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
type Props = {
workspaceSlug: string;
projectId: string;
};
export const FilterState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = props;
const store = useMobxStore();
const { issueFilter: issueFilterStore, project: projectStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleFilter = (key: string, value: string) => { const statesByGroups = projectStore.states?.[projectId?.toString() ?? ""];
let _value = const statesList = getStatesList(statesByGroups);
issueFilterStore?.userFilters?.filters?.[key] != null
? issueFilterStore?.userFilters?.filters?.[key].includes(value) const handleUpdateState = (value: string) => {
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value) const newValues = issueFilterStore.userFilters?.state ?? [];
: [...issueFilterStore?.userFilters?.filters?.[key], value]
: [value]; if (issueFilterStore.userFilters?.state?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
_value = _value && _value.length > 0 ? _value : null; else newValues.push(value);
issueFilterStore.handleUserFilter("filters", key, _value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
state: newValues,
},
});
}; };
const countAllState = stateGroups
.map((_stateGroup) => issueFilterStore?.projectStates?.[_stateGroup?.key].length || 0)
.reduce((sum: number, currentValue: number) => sum + currentValue, 0);
console.log("countAllState", countAllState);
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={`State (${countAllState})`} title={`State (${issueFilterStore.userFilters?.state?.length ?? 0})`}
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-1 pt-1">
{stateGroups.map( {statesList?.map((state) => (
(_stateGroup) => <FilterOption
issueFilterStore?.projectStates && key={state.id}
issueFilterStore?.projectStates[_stateGroup?.key] && isChecked={issueFilterStore?.userFilters?.state?.includes(state?.id) ? true : false}
issueFilterStore?.projectStates[_stateGroup?.key].length > 0 && onClick={() => handleUpdateState(state?.id)}
issueFilterStore?.projectStates[_stateGroup?.key].map((_state: any) => ( icon={<StateGroupIcon stateGroup={state?.group} color={state?.color} />}
<FilterOption title={state?.name}
key={_state?.id} />
isChecked={ ))}
issueFilterStore?.userFilters?.filters?.state != null &&
issueFilterStore?.userFilters?.filters?.state.includes(_state?.id)
? true
: false
}
onClick={() => handleFilter("state", _state?.id)}
icon={<StateGroupIcons stateGroup={_stateGroup?.key} color={_state?.color} />}
title={_state?.name}
/>
))
)}
</div> </div>
)} )}
</div> </div>

View File

@ -1,46 +1,32 @@
import React from "react"; import React from "react";
// components // mobx
import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root"; // components
import { FilterHeader, FilterOption } from "components/issue-layouts";
export const FilterTargetDate = observer(() => { export const FilterTargetDate = observer(() => {
const store: RootStore = useMobxStore(); const store = useMobxStore();
const { issueFilters: issueFilterStore } = store; const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true); const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleFilter = (key: string, value: string) => {
const _value = [value];
issueFilterStore.handleUserFilter("filters", key, _value);
};
return ( return (
<div> <div>
<FilterHeader <FilterHeader
title={`Target Date (${issueFilterStore?.issueRenderFilters?.due_date.length})`} title={`Target Date (${issueFilterStore?.userFilters?.target_date?.length})`}
isPreviewEnabled={previewEnabled} isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/> />
{previewEnabled && ( {previewEnabled && (
<div className="space-y-[2px] pt-1"> <div className="space-y-[2px] pt-1">
{issueFilterStore?.issueRenderFilters?.due_date && {issueFilterStore?.userFilters?.target_date &&
issueFilterStore?.issueRenderFilters?.due_date.length > 0 && issueFilterStore?.userFilters?.target_date.length > 0 &&
issueFilterStore?.issueRenderFilters?.due_date.map((_targetDate) => ( issueFilterStore?.userFilters?.target_date.map((_targetDate) => (
<FilterOption <FilterOption
key={_targetDate?.key} key={_targetDate?.key}
isChecked={ isChecked={issueFilterStore?.userFilters?.target_date?.includes(_targetDate?.key) ? true : false}
issueFilterStore?.userFilters?.filters?.target_date != null &&
issueFilterStore?.userFilters?.filters?.target_date.includes(_targetDate?.key)
? true
: false
}
onClick={() => handleFilter("target_date", _targetDate?.key)}
title={_targetDate.title} title={_targetDate.title}
multiple={false} multiple={false}
/> />

View File

@ -1,8 +1,9 @@
import { Fragment } from "react"; import { Fragment } from "react";
// headless ui // headless ui
import { Popover, Transition } from "@headlessui/react"; import { Popover, Transition } from "@headlessui/react";
// lucide icons // icons
import { ChevronDown, ChevronUp } from "lucide-react"; import { ChevronUp } from "lucide-react";
interface IIssueDropdown { interface IIssueDropdown {
children: React.ReactNode; children: React.ReactNode;
@ -17,11 +18,13 @@ export const IssueDropdown = ({ children, title = "Dropdown" }: IIssueDropdown)
return ( return (
<> <>
<Popover.Button <Popover.Button
className={`outline-none border border-custom-border-200 text-xs rounded flex items-center gap-2 p-2 py-1.5 hover:bg-custom-background-100`} className={`outline-none border border-custom-border-200 text-xs rounded flex items-center gap-2 px-2 py-1.5 hover:bg-custom-background-80 ${
open ? "text-custom-text-100" : "text-custom-text-200"
}`}
> >
<div className="font-medium">{title}</div> <div className="font-medium">{title}</div>
<div className="w-[14px] h-[14px] flex justify-center items-center"> <div className={`w-3.5 h-3.5 flex items-center justify-center transition-all ${open ? "" : "rotate-180"}`}>
{open ? <ChevronUp width={14} strokeWidth={2} /> : <ChevronDown width={14} strokeWidth={2} />} <ChevronUp width={14} strokeWidth={2} />
</div> </div>
</Popover.Button> </Popover.Button>
<Transition <Transition
@ -33,10 +36,8 @@ export const IssueDropdown = ({ children, title = "Dropdown" }: IIssueDropdown)
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute right-0 z-10 mt-1 w-[300px] h-[600px]"> <Popover.Panel className="absolute right-0 z-10 mt-1 w-[18.75rem] h-auto max-h-[37.5rem] bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded overflow-y-auto">
<div className="w-full h-full overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 shadow-xl"> {children}
{children}
</div>
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
</> </>

View File

@ -9,13 +9,14 @@ interface IFilterHeader {
} }
export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => ( export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => (
<div className="flex items-center justify-between gap-2 p-[6px] pb-1 bg-custom-background-100 sticky top-0"> <div className="flex items-center justify-between gap-2 p-1.5 pb-1 bg-custom-background-100 sticky top-0">
<div className="text-gray-500 text-xs text-custom-text-300 font-medium">{title}</div> <div className="text-custom-text-300 text-xs font-medium flex-grow truncate">{title}</div>
<div <button
className="flex-shrink-0 w-[20px] h-[20px] flex justify-center items-center rounded transition-all hover:bg-custom-background-80 cursor-pointer" type="button"
className="flex-shrink-0 w-5 h-5 grid place-items-center rounded hover:bg-custom-background-80"
onClick={handleIsPreviewEnabled} onClick={handleIsPreviewEnabled}
> >
{isPreviewEnabled ? <ChevronUp size={14} /> : <ChevronDown size={14} />} {isPreviewEnabled ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</div> </button>
</div> </div>
); );

View File

@ -2,27 +2,34 @@ import React from "react";
// lucide icons // lucide icons
import { Check } from "lucide-react"; import { Check } from "lucide-react";
interface IFilterOption { type Props = {
isChecked: boolean;
icon?: React.ReactNode; icon?: React.ReactNode;
title: string; isChecked: boolean;
multiple?: boolean; title: React.ReactNode;
onClick?: () => void; onClick?: () => void;
} multiple?: boolean;
};
export const FilterOption = ({ isChecked, icon, title, multiple = true, onClick }: IFilterOption) => ( export const FilterOption: React.FC<Props> = (props) => {
<div const { icon, isChecked, multiple = true, onClick, title } = props;
className="flex items-center gap-3 cursor-pointer rounded p-[6px] py-[5px] transition-all hover:bg-custom-border-100"
onClick={onClick} return (
> <button
<div type="button"
className={`flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center border border-custom-border-300 bg-custom-background-90 ${ className="flex items-center gap-2 rounded p-1.5 hover:bg-custom-background-80 w-full"
isChecked ? `bg-custom-primary-300 text-white` : `` onClick={onClick}
} ${multiple ? `rounded-sm` : `rounded-full`}`}
> >
{isChecked && <Check size={10} strokeWidth={2} />} <div
</div> className={`flex-shrink-0 w-3 h-3 grid place-items-center bg-custom-background-90 border ${
{icon} isChecked ? "bg-custom-primary-100 border-custom-primary-100 text-white" : "border-custom-border-300"
<div className="hyphens-auto line-clamp-1 text-custom-text-200 text-xs w-full">{title}</div> } ${multiple ? "rounded-sm" : "rounded-full"}`}
</div> >
); {isChecked && <Check size={10} strokeWidth={3} />}
</div>
<div className="flex items-center gap-2 truncate">
<div className="flex-shrink-0 grid place-items-center">{icon}</div>
<div className="flex-grow truncate text-custom-text-200 text-xs">{title}</div>
</div>
</button>
);
};

View File

@ -0,0 +1,3 @@
export * from "./dropdown";
export * from "./filter-header";
export * from "./filter-option";

View File

@ -0,0 +1,4 @@
export * from "./display-filters";
export * from "./filters";
export * from "./helpers";
export * from "./layout-selection";

View File

@ -1,97 +1,37 @@
import React from "react"; import React from "react";
// lucide icons
import { Columns, Grid3x3, Calendar, GanttChart, List } from "lucide-react";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { RootStore } from "store/root";
import { useMobxStore } from "lib/mobx/store-provider";
// types and default data
import { TIssueLayouts } from "store/issue_filters.legacy";
import { issueFilterVisibilityData } from "store/helpers/issue-data";
export const LayoutSelection = observer(() => { // types
const store: RootStore = useMobxStore(); import { TIssueLayouts } from "types";
const { issueFilters: issueFilterStore, issueView: issueStore } = store; // constants
import { ISSUE_LAYOUTS } from "constants/issue";
const layoutSelectionFilters: { key: TIssueLayouts; title: string; icon: any }[] = [ type Props = {
{ layouts: TIssueLayouts[];
key: "list", onChange: (layout: TIssueLayouts) => void;
title: "List", selectedLayout: TIssueLayouts;
icon: List, };
},
{
key: "kanban",
title: "Kanban",
icon: Grid3x3,
},
{
key: "calendar",
title: "Calendar",
icon: Calendar,
},
{
key: "spreadsheet",
title: "Spreadsheet",
icon: Columns,
},
{
key: "gantt_chart",
title: "Gantt",
icon: GanttChart,
},
];
const handleLayoutSectionVisibility = (layout_key: string) => export const LayoutSelection: React.FC<Props> = (props) => {
issueFilterStore?.issueView && const { layouts, onChange, selectedLayout } = props;
issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"].layout.includes(
layout_key
);
const handleLayoutSelection = (_layoutKey: string) => {
issueFilterStore.handleUserFilter("display_filters", "layout", _layoutKey);
};
// console.log("----");
// console.log("my_user_id", issueFilterStore.myUserId);
// console.log("workspace_id", issueFilterStore.workspaceId);
// console.log("project_id", issueFilterStore.projectId);
// console.log("module_id", issueFilterStore.moduleId);
// console.log("cycle_id", issueFilterStore.cycleId);
// console.log("view_id", issueFilterStore.viewId);
// console.log("issue_view", issueFilterStore.issueView);
// console.log("issue_layout", issueFilterStore.issueLayout);
// console.log("user_filters", issueFilterStore.userFilters);
// console.log("issues", issueStore.issues);
// console.log("issues", issueStore.getIssues);
// console.log("----");
return ( return (
<div className="relative flex items-center p-1 rounded gap-1 bg-custom-background-80"> <div className="flex items-center gap-1 p-1 rounded bg-custom-background-80">
{layoutSelectionFilters.map( {ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
(_layout) => <button
handleLayoutSectionVisibility(_layout?.key) && ( key={layout.key}
<div type="button"
key={_layout?.key} className={`w-7 h-[22px] rounded grid place-items-center transition-all hover:bg-custom-background-100 overflow-hidden group ${
className={`w-[28px] h-[22px] rounded flex justify-center items-center cursor-pointer transition-all hover:bg-custom-background-100 overflow-hidden group ${ selectedLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
issueFilterStore?.issueLayout == _layout?.key ? `bg-custom-background-100 shadow shadow-gray-200` : `` }`}
}}`} onClick={() => onChange(layout.key)}
onClick={() => handleLayoutSelection(_layout?.key)} >
> <layout.icon
<_layout.icon size={14}
size={14} strokeWidth={2}
strokeWidth={2} className={`${selectedLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"}`}
className={`${ />
issueFilterStore?.issueLayout == _layout?.key </button>
? `text-custom-text-100` ))}
: `text-custom-text-100 group-hover:text-custom-text-200`
}`}
/>
</div>
)
)}
</div> </div>
); );
}); };

View File

@ -2,7 +2,7 @@ import React from "react";
// components // components
import { LayoutSelection } from "./layout-selection"; import { LayoutSelection } from "./layout-selection";
import { IssueDropdown } from "./helpers/dropdown"; import { IssueDropdown } from "./helpers/dropdown";
import { FilterSelection } from "./filters"; import { FilterSelection } from "./filters/filter-selection";
import { DisplayFiltersSelection } from "./display-filters"; import { DisplayFiltersSelection } from "./display-filters";
import { FilterPreview } from "./filters-preview"; import { FilterPreview } from "./filters-preview";

View File

@ -18,11 +18,11 @@ import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-materia
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types // types
import { Properties, TIssueViewOptions } from "types"; import { Properties, TIssueLayouts } from "types";
// constants // constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
{ {
type: "list", type: "list",
Icon: FormatListBulletedOutlined, Icon: FormatListBulletedOutlined,
@ -37,8 +37,9 @@ export const MyIssuesViewOptions: React.FC = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } = const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } = useMyIssuesFilters(
useMyIssuesFilters(workspaceSlug?.toString()); workspaceSlug?.toString()
);
const { isEstimateActive } = useEstimateOption(); const { isEstimateActive } = useEstimateOption();
@ -48,9 +49,7 @@ export const MyIssuesViewOptions: React.FC = () => {
{issueViewOptions.map((option) => ( {issueViewOptions.map((option) => (
<Tooltip <Tooltip
key={option.type} key={option.type}
tooltipContent={ tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom" position="bottom"
> >
<button <button
@ -88,9 +87,7 @@ export const MyIssuesViewOptions: React.FC = () => {
if (valueExists) if (valueExists)
setFilters({ setFilters({
[option.key]: ((filters[key] ?? []) as any[])?.filter( [option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
(val) => val !== option.value
),
}); });
else else
setFilters({ setFilters({
@ -106,9 +103,7 @@ export const MyIssuesViewOptions: React.FC = () => {
<> <>
<Popover.Button <Popover.Button
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${ className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
open open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
}`} }`}
> >
Display Display
@ -127,89 +122,75 @@ export const MyIssuesViewOptions: React.FC = () => {
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg"> <Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200"> <div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs"> <div className="space-y-4 pb-3 text-xs">
{displayFilters?.layout !== "calendar" && {displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
displayFilters?.layout !== "spreadsheet" && ( <>
<> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <h4 className="text-custom-text-200">Group by</h4>
<h4 className="text-custom-text-200">Group by</h4> <div className="w-28">
<div className="w-28"> <CustomMenu
<CustomMenu label={
label={ displayFilters?.group_by === "project"
displayFilters?.group_by === "project" ? "Project"
? "Project" : GROUP_BY_OPTIONS.find((option) => option.key === displayFilters?.group_by)?.name ??
: GROUP_BY_OPTIONS.find( "Select"
(option) => option.key === displayFilters?.group_by }
)?.name ?? "Select" className="!w-full"
} buttonClassName="w-full"
className="!w-full" >
buttonClassName="w-full" {GROUP_BY_OPTIONS.map((option) => {
> if (displayFilters?.layout === "kanban" && option.key === null) return null;
{GROUP_BY_OPTIONS.map((option) => { if (option.key === "state" || option.key === "created_by" || option.key === "assignees")
if (displayFilters?.layout === "kanban" && option.key === null) return null;
return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
return ( return (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={option.key} key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })} onClick={() => setDisplayFilters({ group_by: option.key })}
> >
{option.name} {option.name}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
); );
})} })}
</CustomMenu> </CustomMenu>
</div>
</div> </div>
<div className="flex items-center justify-between"> </div>
<h4 className="text-custom-text-200">Order by</h4> <div className="flex items-center justify-between">
<div className="w-28"> <h4 className="text-custom-text-200">Order by</h4>
<CustomMenu <div className="w-28">
label={ <CustomMenu
ORDER_BY_OPTIONS.find( label={
(option) => option.key === displayFilters?.order_by ORDER_BY_OPTIONS.find((option) => option.key === displayFilters?.order_by)?.name ??
)?.name ?? "Select" "Select"
} }
className="!w-full" className="!w-full"
buttonClassName="w-full" buttonClassName="w-full"
> >
{ORDER_BY_OPTIONS.map((option) => { {ORDER_BY_OPTIONS.map((option) => {
if ( if (displayFilters?.group_by === "priority" && option.key === "priority") return null;
displayFilters?.group_by === "priority" && if (option.key === "sort_order") return null;
option.key === "priority"
)
return null;
if (option.key === "sort_order") return null;
return ( return (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={option.key} key={option.key}
onClick={() => { onClick={() => {
setDisplayFilters({ order_by: option.key }); setDisplayFilters({ order_by: option.key });
}} }}
> >
{option.name} {option.name}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
); );
})} })}
</CustomMenu> </CustomMenu>
</div>
</div> </div>
</> </div>
)} </>
)}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4> <h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28"> <div className="w-28">
<CustomMenu <CustomMenu
label={ label={
FILTER_ISSUE_OPTIONS.find( FILTER_ISSUE_OPTIONS.find((option) => option.key === displayFilters?.type)?.name ?? "Select"
(option) => option.key === displayFilters?.type
)?.name ?? "Select"
} }
className="!w-full" className="!w-full"
buttonClassName="w-full" buttonClassName="w-full"
@ -230,24 +211,23 @@ export const MyIssuesViewOptions: React.FC = () => {
</div> </div>
</div> </div>
{displayFilters?.layout !== "calendar" && {displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
displayFilters?.layout !== "spreadsheet" && ( <>
<> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <h4 className="text-custom-text-200">Show empty states</h4>
<h4 className="text-custom-text-200">Show empty states</h4> <div className="w-28">
<div className="w-28"> <ToggleSwitch
<ToggleSwitch value={displayFilters?.show_empty_groups ?? true}
value={displayFilters?.show_empty_groups ?? true} onChange={() =>
onChange={() => setDisplayFilters({
setDisplayFilters({ show_empty_groups: !displayFilters?.show_empty_groups,
show_empty_groups: !displayFilters?.show_empty_groups, })
}) }
} />
/>
</div>
</div> </div>
</> </div>
)} </>
)}
</div> </div>
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
@ -258,16 +238,11 @@ export const MyIssuesViewOptions: React.FC = () => {
if ( if (
displayFilters?.layout === "spreadsheet" && displayFilters?.layout === "spreadsheet" &&
(key === "attachment_count" || (key === "attachment_count" || key === "link" || key === "sub_issue_count")
key === "link" ||
key === "sub_issue_count")
) )
return null; return null;
if ( if (displayFilters?.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
displayFilters?.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null; return null;
return ( return (

View File

@ -18,12 +18,12 @@ import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-materia
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper"; import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types // types
import { Properties, TIssueViewOptions } from "types"; import { Properties, TIssueLayouts } from "types";
// constants // constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue"; import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [ const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
{ {
type: "list", type: "list",
Icon: FormatListBulletedOutlined, Icon: FormatListBulletedOutlined,
@ -40,14 +40,10 @@ export const ProfileIssuesViewOptions: React.FC = () => {
const { projects } = useProjects(); const { projects } = useProjects();
const { const { displayFilters, setDisplayFilters, filters, displayProperties, setProperties, setFilters } = useProfileIssues(
displayFilters, workspaceSlug?.toString(),
setDisplayFilters, userId?.toString()
filters, );
displayProperties,
setProperties,
setFilters,
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
const { isEstimateActive } = useEstimateOption(); const { isEstimateActive } = useEstimateOption();
@ -80,9 +76,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
{issueViewOptions.map((option) => ( {issueViewOptions.map((option) => (
<Tooltip <Tooltip
key={option.type} key={option.type}
tooltipContent={ tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
position="bottom" position="bottom"
> >
<button <button
@ -120,9 +114,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
if (valueExists) if (valueExists)
setFilters({ setFilters({
[option.key]: ((filters[key] ?? []) as any[])?.filter( [option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
(val) => val !== option.value
),
}); });
else else
setFilters({ setFilters({
@ -138,9 +130,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
<> <>
<Popover.Button <Popover.Button
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${ className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
open open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
}`} }`}
> >
Display Display
@ -159,89 +149,75 @@ export const ProfileIssuesViewOptions: React.FC = () => {
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg"> <Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200"> <div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs"> <div className="space-y-4 pb-3 text-xs">
{displayFilters?.layout !== "calendar" && {displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
displayFilters?.layout !== "spreadsheet" && ( <>
<> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <h4 className="text-custom-text-200">Group by</h4>
<h4 className="text-custom-text-200">Group by</h4> <div className="w-28">
<div className="w-28"> <CustomMenu
<CustomMenu label={
label={ displayFilters?.group_by === "project"
displayFilters?.group_by === "project" ? "Project"
? "Project" : GROUP_BY_OPTIONS.find((option) => option.key === displayFilters?.group_by)?.name ??
: GROUP_BY_OPTIONS.find( "Select"
(option) => option.key === displayFilters?.group_by }
)?.name ?? "Select" className="!w-full"
} buttonClassName="w-full"
className="!w-full" >
buttonClassName="w-full" {GROUP_BY_OPTIONS.map((option) => {
> if (displayFilters?.layout === "kanban" && option.key === null) return null;
{GROUP_BY_OPTIONS.map((option) => { if (option.key === "state" || option.key === "created_by" || option.key === "assignees")
if (displayFilters?.layout === "kanban" && option.key === null) return null;
return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
return ( return (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={option.key} key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })} onClick={() => setDisplayFilters({ group_by: option.key })}
> >
{option.name} {option.name}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
); );
})} })}
</CustomMenu> </CustomMenu>
</div>
</div> </div>
<div className="flex items-center justify-between"> </div>
<h4 className="text-custom-text-200">Order by</h4> <div className="flex items-center justify-between">
<div className="w-28"> <h4 className="text-custom-text-200">Order by</h4>
<CustomMenu <div className="w-28">
label={ <CustomMenu
ORDER_BY_OPTIONS.find( label={
(option) => option.key === displayFilters?.order_by ORDER_BY_OPTIONS.find((option) => option.key === displayFilters?.order_by)?.name ??
)?.name ?? "Select" "Select"
} }
className="!w-full" className="!w-full"
buttonClassName="w-full" buttonClassName="w-full"
> >
{ORDER_BY_OPTIONS.map((option) => { {ORDER_BY_OPTIONS.map((option) => {
if ( if (displayFilters?.group_by === "priority" && option.key === "priority") return null;
displayFilters?.group_by === "priority" && if (option.key === "sort_order") return null;
option.key === "priority"
)
return null;
if (option.key === "sort_order") return null;
return ( return (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={option.key} key={option.key}
onClick={() => { onClick={() => {
setDisplayFilters({ order_by: option.key }); setDisplayFilters({ order_by: option.key });
}} }}
> >
{option.name} {option.name}
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
); );
})} })}
</CustomMenu> </CustomMenu>
</div>
</div> </div>
</> </div>
)} </>
)}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4> <h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28"> <div className="w-28">
<CustomMenu <CustomMenu
label={ label={
FILTER_ISSUE_OPTIONS.find( FILTER_ISSUE_OPTIONS.find((option) => option.key === displayFilters?.type)?.name ?? "Select"
(option) => option.key === displayFilters?.type
)?.name ?? "Select"
} }
className="!w-full" className="!w-full"
buttonClassName="w-full" buttonClassName="w-full"
@ -262,24 +238,23 @@ export const ProfileIssuesViewOptions: React.FC = () => {
</div> </div>
</div> </div>
{displayFilters?.layout !== "calendar" && {displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
displayFilters?.layout !== "spreadsheet" && ( <>
<> <div className="flex items-center justify-between">
<div className="flex items-center justify-between"> <h4 className="text-custom-text-200">Show empty states</h4>
<h4 className="text-custom-text-200">Show empty states</h4> <div className="w-28">
<div className="w-28"> <ToggleSwitch
<ToggleSwitch value={displayFilters?.show_empty_groups ?? true}
value={displayFilters?.show_empty_groups ?? true} onChange={() =>
onChange={() => setDisplayFilters({
setDisplayFilters({ show_empty_groups: !displayFilters?.show_empty_groups,
show_empty_groups: !displayFilters?.show_empty_groups, })
}) }
} />
/>
</div>
</div> </div>
</> </div>
)} </>
)}
</div> </div>
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
@ -290,16 +265,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
if ( if (
displayFilters?.layout === "spreadsheet" && displayFilters?.layout === "spreadsheet" &&
(key === "attachment_count" || (key === "attachment_count" || key === "link" || key === "sub_issue_count")
key === "link" ||
key === "sub_issue_count")
) )
return null; return null;
if ( if (displayFilters?.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
displayFilters?.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null; return null;
return ( return (

View File

@ -1,4 +1,21 @@
export const ISSUE_PRIORITIES = [ // icons
import { Calendar, GanttChart, Kanban, List, Sheet } from "lucide-react";
// types
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
TIssueGroupByOptions,
TIssueLayouts,
TIssueOrderByOptions,
TIssuePriorities,
TIssueTypeFilters,
TStateGroups,
} from "types";
export const ISSUE_PRIORITIES: {
key: TIssuePriorities;
title: string;
}[] = [
{ key: "urgent", title: "Urgent" }, { key: "urgent", title: "Urgent" },
{ key: "high", title: "High" }, { key: "high", title: "High" },
{ key: "medium", title: "Medium" }, { key: "medium", title: "Medium" },
@ -6,7 +23,10 @@ export const ISSUE_PRIORITIES = [
{ key: "none", title: "None" }, { key: "none", title: "None" },
]; ];
export const ISSUE_STATE_GROUPS = [ export const ISSUE_STATE_GROUPS: {
key: TStateGroups;
title: string;
}[] = [
{ key: "backlog", title: "Backlog" }, { key: "backlog", title: "Backlog" },
{ key: "unstarted", title: "Unstarted" }, { key: "unstarted", title: "Unstarted" },
{ key: "started", title: "Started" }, { key: "started", title: "Started" },
@ -30,7 +50,10 @@ export const ISSUE_DUE_DATE_OPTIONS = [
{ key: "custom", title: "Custom" }, { key: "custom", title: "Custom" },
]; ];
export const ISSUE_GROUP_BY_OPTIONS = [ export const ISSUE_GROUP_BY_OPTIONS: {
key: TIssueGroupByOptions;
title: string;
}[] = [
{ key: "state", title: "States" }, { key: "state", title: "States" },
{ key: "state_detail.group", title: "State Groups" }, { key: "state_detail.group", title: "State Groups" },
{ key: "priority", title: "Priority" }, { key: "priority", title: "Priority" },
@ -38,24 +61,34 @@ export const ISSUE_GROUP_BY_OPTIONS = [
{ key: "labels", title: "Labels" }, { key: "labels", title: "Labels" },
{ key: "assignees", title: "Assignees" }, { key: "assignees", title: "Assignees" },
{ key: "created_by", title: "Created By" }, { key: "created_by", title: "Created By" },
{ key: null, title: "None" },
]; ];
export const ISSUE_ORDER_BY_OPTIONS = [ export const ISSUE_ORDER_BY_OPTIONS: {
key: TIssueOrderByOptions;
title: string;
}[] = [
{ key: "sort_order", title: "Manual" }, { key: "sort_order", title: "Manual" },
{ key: "created_at", title: "Last Created" }, { key: "-created_at", title: "Last Created" },
{ key: "updated_at", title: "Last Updated" }, { key: "-updated_at", title: "Last Updated" },
{ key: "start_date", title: "Start Date" }, { key: "start_date", title: "Start Date" },
{ key: "priority", title: "Priority" }, { key: "priority", title: "Priority" },
]; ];
export const ISSUE_FILTER_OPTIONS = [ export const ISSUE_FILTER_OPTIONS: {
{ key: "all", title: "All" }, key: TIssueTypeFilters;
title: string;
}[] = [
{ key: null, title: "All" },
{ key: "active", title: "Active Issues" }, { key: "active", title: "Active Issues" },
{ key: "backlog", title: "Backlog Issues" }, { key: "backlog", title: "Backlog Issues" },
// { key: "draft", title: "Draft Issues" }, // { key: "draft", title: "Draft Issues" },
]; ];
export const ISSUE_DISPLAY_PROPERTIES = [ export const ISSUE_DISPLAY_PROPERTIES: {
key: keyof IIssueDisplayProperties;
title: string;
}[] = [
{ key: "assignee", title: "Assignee" }, { key: "assignee", title: "Assignee" },
{ key: "start_date", title: "Start Date" }, { key: "start_date", title: "Start Date" },
{ key: "due_date", title: "Due Date" }, { key: "due_date", title: "Due Date" },
@ -69,19 +102,26 @@ export const ISSUE_DISPLAY_PROPERTIES = [
{ key: "estimate", title: "Estimate" }, { key: "estimate", title: "Estimate" },
]; ];
export const ISSUE_EXTRA_PROPERTIES = [ export const ISSUE_EXTRA_OPTIONS: {
key: keyof IIssueDisplayFilterOptions;
title: string;
}[] = [
{ key: "sub_issue", title: "Show sub-issues" }, // in spreadsheet its always false { key: "sub_issue", title: "Show sub-issues" }, // in spreadsheet its always false
{ key: "show_empty_groups", title: "Show empty states" }, // filter on front-end { key: "show_empty_groups", title: "Show empty states" }, // filter on front-end
{ key: "calendar_date_range", title: "Calendar Date Range" }, // calendar date range yyyy-mm-dd;before range yyyy-mm-dd;after { key: "calendar_date_range", title: "Calendar Date Range" }, // calendar date range yyyy-mm-dd;before range yyyy-mm-dd;after
{ key: "start_target_date", title: "Start target Date" }, // gantt always be true { key: "start_target_date", title: "Start target Date" }, // gantt always be true
]; ];
export const ISSUE_LAYOUTS = [ export const ISSUE_LAYOUTS: {
{ key: "list", title: "List View" }, key: TIssueLayouts;
{ key: "kanban", title: "Kanban View" }, title: string;
{ key: "calendar", title: "Calendar View" }, icon: any;
{ key: "spreadsheet", title: "Spreadsheet View" }, }[] = [
{ key: "gantt_chart", title: "Gantt Chart View" }, { key: "list", title: "List View", icon: List },
{ key: "kanban", title: "Kanban View", icon: Kanban },
{ key: "calendar", title: "Calendar View", icon: Calendar },
{ key: "spreadsheet", title: "Spreadsheet View", icon: Sheet },
{ key: "gantt_chart", title: "Gantt Chart View", icon: GanttChart },
]; ];
export const ISSUE_LIST_FILTERS = [ export const ISSUE_LIST_FILTERS = [

View File

@ -1,7 +1,7 @@
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
import { renderDateFormat } from "helpers/date-time.helper"; import { renderDateFormat } from "helpers/date-time.helper";
// types // types
import { IIssue, TIssueGroupByOptions, TIssueOrderByOptions } from "types"; import { IIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "types";
type THandleIssuesMutation = ( type THandleIssuesMutation = (
formData: Partial<IIssue>, formData: Partial<IIssue>,
@ -79,24 +79,6 @@ export const handleIssuesMutation: THandleIssuesMutation = (
} }
}; };
export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
export type TIssueParams =
| "priority"
| "state_group"
| "state"
| "assignees"
| "created_by"
| "labels"
| "start_date"
| "target_date"
| "group_by"
| "order_by"
| "type"
| "sub_issue"
| "show_empty_groups"
| "calendar_date_range"
| "start_target_date";
export const handleIssueQueryParamsByLayout = (_layout: TIssueLayouts | undefined): TIssueParams[] | null => { export const handleIssueQueryParamsByLayout = (_layout: TIssueLayouts | undefined): TIssueParams[] | null => {
if (_layout === "list") if (_layout === "list")
return [ return [
@ -197,3 +179,117 @@ export const handleIssueParamsDateFormat = (key: string, start_date: any | null,
if (key === "custom" && start_date && target_date) if (key === "custom" && start_date && target_date)
return `${renderDateFormat(start_date)};after,${renderDateFormat(target_date)};before`; return `${renderDateFormat(start_date)};after,${renderDateFormat(target_date)};before`;
}; };
export const issueFilterVisibilityData: {
[key: string]: {
layout: TIssueLayouts[];
filters: {
[key in TIssueLayouts]: string[];
};
display_properties: {
[key in TIssueLayouts]: boolean;
};
display_filters: {
[key in TIssueLayouts]: string[];
};
extra_options: {
[key in TIssueLayouts]: {
access: boolean;
values: string[];
};
};
};
} = {
my_issues: {
layout: ["list", "kanban"],
filters: {
list: ["priority", "state_group", "labels", "start_date", "due_date"],
kanban: ["priority", "state_group", "labels", "start_date", "due_date"],
calendar: [],
spreadsheet: [],
gantt_chart: [],
},
display_properties: {
list: true,
kanban: true,
calendar: true,
spreadsheet: true,
gantt_chart: false,
},
display_filters: {
list: ["group_by", "order_by", "issue_type"],
kanban: ["group_by", "order_by", "issue_type"],
calendar: ["issue_type"],
spreadsheet: ["issue_type"],
gantt_chart: ["order_by", "issue_type"],
},
extra_options: {
list: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
kanban: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
calendar: {
access: false,
values: [],
},
spreadsheet: {
access: false,
values: [],
},
gantt_chart: {
access: false,
values: [],
},
},
},
issues: {
layout: ["list", "kanban", "calendar", "spreadsheet", "gantt_chart"],
filters: {
list: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
kanban: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
calendar: ["priority", "state", "assignees", "created_by", "labels"],
spreadsheet: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
gantt_chart: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
},
display_properties: {
list: true,
kanban: true,
calendar: true,
spreadsheet: true,
gantt_chart: false,
},
display_filters: {
list: ["group_by", "order_by", "issue_type"],
kanban: ["group_by", "order_by", "issue_type"],
calendar: ["issue_type"],
spreadsheet: ["issue_type"],
gantt_chart: ["order_by", "issue_type"],
},
extra_options: {
list: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
kanban: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
calendar: {
access: false,
values: [],
},
spreadsheet: {
access: false,
values: [],
},
gantt_chart: {
access: true,
values: ["sub_issue"],
},
},
},
};

View File

@ -1,10 +1,11 @@
import { useState } from "react"; import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
import inboxService from "services/inbox.service"; import inboxService from "services/inbox.service";
@ -15,8 +16,9 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
// helper // helper
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// components // components
import { IssuesFilterView, IssuesView } from "components/core"; import { IssuesView } from "components/core";
import { AnalyticsProjectModal } from "components/analytics"; import { AnalyticsProjectModal } from "components/analytics";
import { ProjectIssuesHeader } from "components/headers";
// ui // ui
import { PrimaryButton, SecondaryButton } from "components/ui"; import { PrimaryButton, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -25,7 +27,14 @@ import { PlusIcon } from "@heroicons/react/24/outline";
// types // types
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { PROJECT_DETAILS, INBOX_LIST } from "constants/fetch-keys"; import {
PROJECT_DETAILS,
INBOX_LIST,
STATES_LIST,
PROJECT_ISSUE_LABELS,
PROJECT_MEMBERS,
USER_PROJECT_VIEW,
} from "constants/fetch-keys";
const ProjectIssues: NextPage = () => { const ProjectIssues: NextPage = () => {
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
@ -33,17 +42,43 @@ const ProjectIssues: NextPage = () => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore();
const { data: projectDetails } = useSWR( const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
); );
const { data: inboxList } = useSWR( const { data: inboxList } = useSWR(
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null, workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
workspaceSlug && projectId ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) : null
);
useSWR(
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) ? () => issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString())
: null
);
useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString())
: null
);
useSWR(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString())
: null
);
useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString()) : null,
workspaceSlug && projectId
? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString())
: null : null
); );
@ -53,49 +88,47 @@ const ProjectIssues: NextPage = () => {
breadcrumbs={ breadcrumbs={
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} /> <BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem <BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`}
/>
</Breadcrumbs> </Breadcrumbs>
} }
right={ right={
<div className="flex items-center gap-2"> <ProjectIssuesHeader />
<IssuesFilterView /> // <div className="flex items-center gap-2">
<SecondaryButton // <SecondaryButton
onClick={() => setAnalyticsModal(true)} // onClick={() => setAnalyticsModal(true)}
className="!py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90" // className="!py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
outline // outline
> // >
Analytics // Analytics
</SecondaryButton> // </SecondaryButton>
{projectDetails && projectDetails.inbox_view && ( // {projectDetails && projectDetails.inbox_view && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}> // <Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
<a> // <a>
<SecondaryButton // <SecondaryButton
className="relative !py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90" // className="relative !py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
outline // outline
> // >
<span>Inbox</span> // <span>Inbox</span>
{inboxList && inboxList?.[0]?.pending_issue_count !== 0 && ( // {inboxList && inboxList?.[0]?.pending_issue_count !== 0 && (
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200"> // <span className="absolute -top-1 -right-1 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
{inboxList?.[0]?.pending_issue_count} // {inboxList?.[0]?.pending_issue_count}
</span> // </span>
)} // )}
</SecondaryButton> // </SecondaryButton>
</a> // </a>
</Link> // </Link>
)} // )}
<PrimaryButton // <PrimaryButton
className="flex items-center gap-2" // className="flex items-center gap-2"
onClick={() => { // onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" }); // const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e); // document.dispatchEvent(e);
}} // }}
> // >
<PlusIcon className="h-4 w-4" /> // <PlusIcon className="h-4 w-4" />
Add Issue // Add Issue
</PrimaryButton> // </PrimaryButton>
</div> // </div>
} }
bg="secondary" bg="secondary"
> >

View File

@ -1,43 +1,74 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx"; import { observable, action, computed, makeObservable, runInAction } from "mobx";
// types // services
import { RootStore } from "./root";
import { ProjectService } from "services/project.service"; import { ProjectService } from "services/project.service";
import { IssueService } from "services/issue.service"; import { IssueService } from "services/issue.service";
// types
import { RootStore } from "./root";
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
IProjectViewProps,
TIssueParams,
} from "types";
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
export interface IIssueFilterStore { export interface IIssueFilterStore {
loader: boolean; loader: boolean;
error: any | null; error: any | null;
userDisplayProperties: any; userDisplayProperties: IIssueDisplayProperties;
userDisplayFilters: any; userDisplayFilters: IIssueDisplayFilterOptions;
userFilters: any; userFilters: IIssueFilterOptions;
defaultDisplayFilters: any; defaultDisplayFilters: IIssueDisplayFilterOptions;
defaultFilters: any; defaultFilters: IIssueFilterOptions;
fetchUserFilters: (workspaceSlug: string, projectSlug: string) => void; // action
fetchUserProjectFilters: (workspaceSlug: string, projectSlug: string) => Promise<void>;
updateUserFilters: (
workspaceSlug: string,
projectSlug: string,
filterToUpdate: Partial<IProjectViewProps>
) => Promise<void>;
updateDisplayProperties: (
workspaceSlug: string,
projectSlug: string,
properties: Partial<IIssueDisplayProperties>
) => Promise<void>;
// computed
appliedFilters: TIssueParams[] | null;
} }
class IssueFilterStore implements IIssueFilterStore { class IssueFilterStore implements IIssueFilterStore {
loader: boolean = false; loader: boolean = false;
error: any | null = null; error: any | null = null;
// observables // observables
userDisplayProperties: any = {}; userDisplayProperties: any = {};
userDisplayFilters: any = {}; userDisplayFilters: IIssueDisplayFilterOptions = {};
userFilters: any = {}; userFilters: IIssueFilterOptions = {};
defaultDisplayFilters: any = {}; defaultDisplayFilters: IIssueDisplayFilterOptions = {};
defaultFilters: any = {}; defaultFilters: IIssueFilterOptions = {};
defaultDisplayProperties: any = { defaultDisplayProperties: IIssueDisplayProperties = {
assignee: true, assignee: true,
due_date: true,
key: true,
labels: true,
priority: true,
start_date: true, start_date: true,
due_date: true,
labels: true,
key: true,
priority: true,
state: true, state: true,
sub_issue_count: true, sub_issue_count: true,
link: true,
attachment_count: true,
estimate: true,
created_on: true,
updated_on: true,
}; };
// root store // root store
rootStore; rootStore;
// services
projectService; projectService;
issueService; issueService;
@ -45,12 +76,21 @@ class IssueFilterStore implements IIssueFilterStore {
makeObservable(this, { makeObservable(this, {
loader: observable.ref, loader: observable.ref,
error: observable.ref, error: observable.ref,
// observables
defaultDisplayFilters: observable.ref, defaultDisplayFilters: observable.ref,
defaultFilters: observable.ref, defaultFilters: observable.ref,
userDisplayProperties: observable.ref, userDisplayProperties: observable.ref,
userDisplayFilters: observable.ref, userDisplayFilters: observable.ref,
userFilters: observable.ref, userFilters: observable.ref,
fetchUserFilters: action,
// actions
fetchUserProjectFilters: action,
updateUserFilters: action,
updateDisplayProperties: action,
// computed
appliedFilters: computed,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
@ -59,27 +99,90 @@ class IssueFilterStore implements IIssueFilterStore {
this.issueService = new IssueService(); this.issueService = new IssueService();
} }
fetchUserFilters = async (workspaceSlug: string, projectId: string) => { get appliedFilters(): TIssueParams[] | null {
return handleIssueQueryParamsByLayout(this.userDisplayFilters.layout);
}
fetchUserProjectFilters = async (workspaceSlug: string, projectId: string) => {
try { try {
const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId); const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId);
const issueProperties = await this.issueService.getIssueProperties(workspaceSlug, projectId); const issueProperties = await this.issueService.getIssueProperties(workspaceSlug, projectId);
console.log("memberResponse", memberResponse);
console.log("issueProperties", issueProperties);
runInAction(() => { runInAction(() => {
this.userFilters = memberResponse?.view_props?.filters; this.userFilters = memberResponse?.view_props?.filters;
this.userDisplayFilters = memberResponse?.view_props?.display_filters; this.userDisplayFilters = memberResponse?.view_props?.display_filters ?? {};
this.userDisplayProperties = issueProperties?.properties || this.defaultDisplayProperties; this.userDisplayProperties = issueProperties?.properties || this.defaultDisplayProperties;
// default props from api // default props from api
this.defaultFilters = memberResponse.default_props.filters; this.defaultFilters = memberResponse.default_props.filters;
this.defaultDisplayFilters = memberResponse.default_props.display_filters; this.defaultDisplayFilters = memberResponse.default_props.display_filters ?? {};
}); });
} catch (error) { } catch (error) {
runInAction(() => {
this.error = error;
});
console.log("Failed to fetch user filters in issue filter store", error); console.log("Failed to fetch user filters in issue filter store", error);
} }
}; };
updateUserFilters = async (workspaceSlug: string, projectId: string, filterToUpdate: Partial<IProjectViewProps>) => {
const newViewProps = {
display_filters: {
...this.userDisplayFilters,
...filterToUpdate.display_filters,
},
filters: {
...this.userFilters,
...filterToUpdate.filters,
},
};
try {
runInAction(() => {
this.userFilters = newViewProps.filters;
this.userDisplayFilters = newViewProps.display_filters;
});
await this.projectService.setProjectView(workspaceSlug, projectId, {
view_props: newViewProps,
});
} catch (error) {
this.fetchUserProjectFilters(workspaceSlug, projectId);
runInAction(() => {
this.error = error;
});
console.log("Failed to update user filters in issue filter store", error);
}
};
updateDisplayProperties = async (
workspaceSlug: string,
projectId: string,
properties: Partial<IIssueDisplayProperties>
) => {
const newProperties = {
...this.userDisplayProperties,
...properties,
};
try {
runInAction(() => {
this.userDisplayProperties = newProperties;
});
// await this.issueService.patchIssueProperties(workspaceSlug, projectId, newProperties);
} catch (error) {
this.fetchUserProjectFilters(workspaceSlug, projectId);
runInAction(() => {
this.error = error;
});
console.log("Failed to update user filters in issue filter store", error);
}
};
} }
export default IssueFilterStore; export default IssueFilterStore;

View File

@ -264,15 +264,15 @@ class ProjectStore implements IProjectStore {
} }
}; };
fetchProjectLabels = async (workspaceSlug: string, projectSlug: string) => { fetchProjectLabels = async (workspaceSlug: string, projectId: string) => {
try { try {
this.loader = true; this.loader = true;
this.error = null; this.error = null;
const labelResponse = await this.issueService.getIssueLabels(workspaceSlug, projectSlug); const labelResponse = await this.issueService.getIssueLabels(workspaceSlug, projectId);
const _labels = { const _labels = {
...this.labels, ...this.labels,
[projectSlug]: labelResponse, [projectId]: labelResponse,
}; };
runInAction(() => { runInAction(() => {

View File

@ -2,19 +2,13 @@ import { KeyedMutator } from "swr";
import type { import type {
IState, IState,
IUser, IUser,
IProject,
ICycle, ICycle,
IModule, IModule,
IUserLite, IUserLite,
IProjectLite, IProjectLite,
IWorkspaceLite, IWorkspaceLite,
IStateLite, IStateLite,
TStateGroups,
Properties, Properties,
IIssueFilterOptions,
TIssueGroupByOptions,
TIssueViewOptions,
TIssueOrderByOptions,
IIssueDisplayFilterOptions, IIssueDisplayFilterOptions,
} from "types"; } from "types";

View File

@ -1,15 +1,4 @@
import type { import type { IUserLite, IWorkspace, IWorkspaceLite, IUserMemberLite, TStateGroups, IProjectViewProps } from ".";
IIssueFilterOptions,
IUserLite,
IWorkspace,
IWorkspaceLite,
IUserMemberLite,
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueViewOptions,
TStateGroups,
IProjectViewProps,
} from ".";
export interface IProject { export interface IProject {
archive_in: number; archive_in: number;

View File

@ -1,4 +1,4 @@
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart"; export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
export type TIssueGroupByOptions = export type TIssueGroupByOptions =
| "state" | "state"
@ -28,6 +28,25 @@ export type TIssueOrderByOptions =
| "start_date" | "start_date"
| "-start_date"; | "-start_date";
export type TIssueTypeFilters = "active" | "backlog" | null;
export type TIssueParams =
| "priority"
| "state_group"
| "state"
| "assignees"
| "created_by"
| "labels"
| "start_date"
| "target_date"
| "group_by"
| "order_by"
| "type"
| "sub_issue"
| "show_empty_groups"
| "calendar_date_range"
| "start_target_date";
export interface IIssueFilterOptions { export interface IIssueFilterOptions {
assignees?: string[] | null; assignees?: string[] | null;
created_by?: string[] | null; created_by?: string[] | null;
@ -43,12 +62,27 @@ export interface IIssueFilterOptions {
export interface IIssueDisplayFilterOptions { export interface IIssueDisplayFilterOptions {
calendar_date_range?: string; calendar_date_range?: string;
group_by?: TIssueGroupByOptions; group_by?: TIssueGroupByOptions;
layout?: TIssueViewOptions; layout?: TIssueLayouts;
order_by?: TIssueOrderByOptions; order_by?: TIssueOrderByOptions;
show_empty_groups?: boolean; show_empty_groups?: boolean;
start_target_date?: boolean; start_target_date?: boolean;
sub_issue?: boolean; sub_issue?: boolean;
type?: "active" | "backlog" | null; type?: TIssueTypeFilters;
}
export interface IIssueDisplayProperties {
assignee: boolean;
start_date: boolean;
due_date: boolean;
labels: boolean;
key: boolean;
priority: boolean;
state: boolean;
sub_issue_count: boolean;
link: boolean;
attachment_count: boolean;
estimate: boolean;
created_on: boolean;
updated_on: boolean;
} }
export interface IProjectViewProps { export interface IProjectViewProps {

View File

@ -1,13 +1,4 @@
import type { import type { IProjectMember, IUser, IUserMemberLite, IWorkspaceViewProps } from "types";
IIssueFilterOptions,
IProjectMember,
IUser,
IUserMemberLite,
IWorkspaceViewProps,
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueViewOptions,
} from "types";
export interface IWorkspace { export interface IWorkspace {
readonly id: string; readonly id: string;