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 { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { Properties, TIssueViewOptions } from "types";
import { Properties, TIssueLayouts } from "types";
// constants
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",
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",
Icon: FormatListBulletedOutlined,

View File

@ -65,28 +65,21 @@ export const AllBoards: React.FC<Props> = ({
const { displayFilters, groupedIssues } = viewProps;
console.log("viewProps", viewProps);
return (
<>
<IssuePeekOverview
handleMutation={() =>
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
}
handleMutation={() => (isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues())}
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions}
/>
{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) => {
const currentState =
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
return null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0) return null;
return (
<SingleBoard
@ -115,15 +108,13 @@ export const AllBoards: React.FC<Props> = ({
<div className="space-y-3">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
if (groupedIssues[singleGroup].length === 0)
return (
<div
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">
{currentState && (

View File

@ -490,6 +490,7 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
labels: null,
priority: null,
state: null,
state_group: null,
start_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;
console.log("properties", properties);
return (
<>
<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 { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader } from "../helpers/filter-header";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
// types
import { IIssueDisplayProperties } from "types";
// constants
import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue";
export const FilterDisplayProperties = observer(() => {
const store: RootStore = useMobxStore();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const [previewEnabled, setPreviewEnabled] = React.useState(true);
const handleDisplayProperties = (key: string, value: boolean) => {
// issueFilterStore.handleUserFilter("display_properties", key, !value);
const handleDisplayProperties = (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
};
return (
<div>
<FilterHeader
title={"Display Properties"}
title="Display Properties"
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!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) => (
<div
key={displayProperty?.key}
className={`cursor-pointer rounded-sm transition-all text-xs border p-0.5 px-1.5 ${
issueFilterStore?.userDisplayProperties?.[displayProperty?.key]
? `bg-custom-primary-200 border-custom-primary-200 text-white`
: `hover:bg-custom-border-100 border-custom-border-100`
<button
key={displayProperty.key}
type="button"
className={`rounded transition-all text-xs border px-2 py-0.5 ${
issueFilterStore?.userDisplayProperties?.[displayProperty.key]
? "bg-custom-primary-100 border-custom-primary-100 text-white"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() => {
handleDisplayProperties(
displayProperty?.key,
issueFilterStore?.userDisplayProperties?.[displayProperty?.key]
);
}}
onClick={() =>
handleDisplayProperties({
[displayProperty.key]: !issueFilterStore?.userDisplayProperties?.[displayProperty.key],
})
}
>
{displayProperty?.title}
</div>
{displayProperty.title}
</button>
))}
</div>
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,19 @@
import React from "react";
// lucide icons
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
// components
import { FilterHeader } from "../helpers/filter-header";
import { FilterOption } from "../helpers/filter-option";
// mobx react lite
import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
// mobx store
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 = ({
priority,
size = 14,
strokeWidth = 2,
size = 12,
strokeWidth = 1.5,
}: {
priority: string;
size?: number;
@ -21,84 +21,78 @@ const PriorityIcons = ({
}) => {
if (priority === "urgent")
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} />
</div>
);
if (priority === "high")
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} />
</div>
);
if (priority === "medium")
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} />
</div>
);
if (priority === "low")
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} />
</div>
);
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} />
</div>
);
};
export const FilterPriority = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
type Props = { workspaceSlug: string; projectId: string };
const [previewEnabled, setPreviewEnabled] = React.useState(true);
export const FilterPriority: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = props;
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null
? issueFilterStore?.userFilters?.filters?.[key].includes(value)
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
: [...issueFilterStore?.userFilters?.filters?.[key], value]
: [value];
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const handleUpdatePriority = (value: string) => {
const newValues = issueFilterStore.userFilters?.priority ?? [];
if (issueFilterStore.userFilters?.priority?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
priority: newValues,
},
});
};
return (
<div>
<>
<FilterHeader
title={`Priority (${issueFilterStore?.issueRenderFilters?.priority.length || 0})`}
title={`Priority (${issueFilterStore.userFilters?.priority?.length ?? 0})`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div className="space-y-[2px] pt-1">
{issueFilterStore?.issueRenderFilters?.priority &&
issueFilterStore?.issueRenderFilters?.priority.length > 0 &&
issueFilterStore?.issueRenderFilters?.priority.map((_priority) => (
<FilterOption
key={_priority?.key}
isChecked={
issueFilterStore?.userFilters?.filters?.priority != null &&
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 className="space-y-1 pt-1">
{ISSUE_PRIORITIES.map((priority) => (
<FilterOption
key={priority.key}
isChecked={issueFilterStore.userFilters?.priority?.includes(priority.key) ? true : false}
onClick={() => handleUpdatePriority(priority.key)}
icon={<PriorityIcons priority={priority.key} />}
title={priority.title}
/>
))}
</div>
)}
</div>
</>
);
});

View File

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

View File

@ -1,108 +1,59 @@
import React 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";
import React, { useState } from "react";
export const StateGroupIcons = ({
stateGroup,
width = "14px",
height = "14px",
color = null,
}: {
stateGroup: string;
width?: string | undefined;
height?: string | undefined;
color?: string | null;
}) => {
if (stateGroup === "cancelled")
return (
<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 <></>;
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { FilterHeader, FilterOption } from "components/issue-layouts";
// icons
import { StateGroupIcon } from "components/icons";
// constants
import { ISSUE_STATE_GROUPS } from "constants/issue";
type Props = {
workspaceSlug: string;
projectId: string;
};
export const FilterStateGroup = observer(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore } = store;
export const FilterStateGroup: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = props;
const [previewEnabled, setPreviewEnabled] = React.useState(true);
const [previewEnabled, setPreviewEnabled] = useState(true);
const handleFilter = (key: string, value: string) => {
let _value =
issueFilterStore?.userFilters?.filters?.[key] != null
? issueFilterStore?.userFilters?.filters?.[key].includes(value)
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
: [...issueFilterStore?.userFilters?.filters?.[key], value]
: [value];
_value = _value && _value.length > 0 ? _value : null;
issueFilterStore.handleUserFilter("filters", key, _value);
const store = useMobxStore();
const { issueFilter: issueFilterStore } = store;
const handleUpdateStateGroup = (value: string) => {
const newValues = issueFilterStore.userFilters?.state_group ?? [];
if (issueFilterStore.userFilters?.state_group?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
state_group: newValues,
},
});
};
return (
<div>
<FilterHeader
title={`State Group (${issueFilterStore?.issueRenderFilters?.state_group?.length})`}
title={`State Group (${issueFilterStore.userFilters?.state_group?.length ?? 0})`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div className="space-y-[2px] pt-1">
{issueFilterStore?.issueRenderFilters?.state_group &&
issueFilterStore?.issueRenderFilters?.state_group.length > 0 &&
issueFilterStore?.issueRenderFilters?.state_group.map((_stateGroup) => (
<FilterOption
key={_stateGroup?.key}
isChecked={
issueFilterStore?.userFilters?.filters?.state_group != null &&
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 className="space-y-1 pt-1">
{ISSUE_STATE_GROUPS.map((stateGroup) => (
<FilterOption
key={stateGroup.key}
isChecked={issueFilterStore.userFilters?.state_group?.includes(stateGroup.key) ? true : false}
onClick={() => handleUpdateStateGroup(stateGroup.key)}
icon={<StateGroupIcon stateGroup={stateGroup.key} />}
title={stateGroup.title}
/>
))}
</div>
)}
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -2,27 +2,34 @@ import React from "react";
// lucide icons
import { Check } from "lucide-react";
interface IFilterOption {
isChecked: boolean;
type Props = {
icon?: React.ReactNode;
title: string;
multiple?: boolean;
isChecked: boolean;
title: React.ReactNode;
onClick?: () => void;
}
multiple?: boolean;
};
export const FilterOption = ({ isChecked, icon, title, multiple = true, onClick }: IFilterOption) => (
<div
className="flex items-center gap-3 cursor-pointer rounded p-[6px] py-[5px] transition-all hover:bg-custom-border-100"
onClick={onClick}
>
<div
className={`flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center border border-custom-border-300 bg-custom-background-90 ${
isChecked ? `bg-custom-primary-300 text-white` : ``
} ${multiple ? `rounded-sm` : `rounded-full`}`}
export const FilterOption: React.FC<Props> = (props) => {
const { icon, isChecked, multiple = true, onClick, title } = props;
return (
<button
type="button"
className="flex items-center gap-2 rounded p-1.5 hover:bg-custom-background-80 w-full"
onClick={onClick}
>
{isChecked && <Check size={10} strokeWidth={2} />}
</div>
{icon}
<div className="hyphens-auto line-clamp-1 text-custom-text-200 text-xs w-full">{title}</div>
</div>
);
<div
className={`flex-shrink-0 w-3 h-3 grid place-items-center bg-custom-background-90 border ${
isChecked ? "bg-custom-primary-100 border-custom-primary-100 text-white" : "border-custom-border-300"
} ${multiple ? "rounded-sm" : "rounded-full"}`}
>
{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";
// 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(() => {
const store: RootStore = useMobxStore();
const { issueFilters: issueFilterStore, issueView: issueStore } = store;
// types
import { TIssueLayouts } from "types";
// constants
import { ISSUE_LAYOUTS } from "constants/issue";
const layoutSelectionFilters: { key: TIssueLayouts; title: string; icon: any }[] = [
{
key: "list",
title: "List",
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,
},
];
type Props = {
layouts: TIssueLayouts[];
onChange: (layout: TIssueLayouts) => void;
selectedLayout: TIssueLayouts;
};
const handleLayoutSectionVisibility = (layout_key: string) =>
issueFilterStore?.issueView &&
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("----");
export const LayoutSelection: React.FC<Props> = (props) => {
const { layouts, onChange, selectedLayout } = props;
return (
<div className="relative flex items-center p-1 rounded gap-1 bg-custom-background-80">
{layoutSelectionFilters.map(
(_layout) =>
handleLayoutSectionVisibility(_layout?.key) && (
<div
key={_layout?.key}
className={`w-[28px] h-[22px] rounded flex justify-center items-center cursor-pointer transition-all hover:bg-custom-background-100 overflow-hidden group ${
issueFilterStore?.issueLayout == _layout?.key ? `bg-custom-background-100 shadow shadow-gray-200` : ``
}}`}
onClick={() => handleLayoutSelection(_layout?.key)}
>
<_layout.icon
size={14}
strokeWidth={2}
className={`${
issueFilterStore?.issueLayout == _layout?.key
? `text-custom-text-100`
: `text-custom-text-100 group-hover:text-custom-text-200`
}`}
/>
</div>
)
)}
<div className="flex items-center gap-1 p-1 rounded bg-custom-background-80">
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
<button
key={layout.key}
type="button"
className={`w-7 h-[22px] rounded grid place-items-center transition-all hover:bg-custom-background-100 overflow-hidden group ${
selectedLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
onClick={() => onChange(layout.key)}
>
<layout.icon
size={14}
strokeWidth={2}
className={`${selectedLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"}`}
/>
</button>
))}
</div>
);
});
};

View File

@ -2,7 +2,7 @@ import React from "react";
// components
import { LayoutSelection } from "./layout-selection";
import { IssueDropdown } from "./helpers/dropdown";
import { FilterSelection } from "./filters";
import { FilterSelection } from "./filters/filter-selection";
import { DisplayFiltersSelection } from "./display-filters";
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 { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { Properties, TIssueViewOptions } from "types";
import { Properties, TIssueLayouts } from "types";
// constants
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",
Icon: FormatListBulletedOutlined,
@ -37,8 +37,9 @@ export const MyIssuesViewOptions: React.FC = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } =
useMyIssuesFilters(workspaceSlug?.toString());
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } = useMyIssuesFilters(
workspaceSlug?.toString()
);
const { isEstimateActive } = useEstimateOption();
@ -48,9 +49,7 @@ export const MyIssuesViewOptions: React.FC = () => {
{issueViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
position="bottom"
>
<button
@ -88,9 +87,7 @@ export const MyIssuesViewOptions: React.FC = () => {
if (valueExists)
setFilters({
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
});
else
setFilters({
@ -106,9 +103,7 @@ export const MyIssuesViewOptions: React.FC = () => {
<>
<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 ${
open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
}`}
>
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">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
displayFilters?.group_by === "project"
? "Project"
: GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters?.group_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters?.layout === "kanban" && option.key === null)
return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
displayFilters?.group_by === "project"
? "Project"
: GROUP_BY_OPTIONS.find((option) => option.key === displayFilters?.group_by)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters?.layout === "kanban" && option.key === null) return null;
if (option.key === "state" || option.key === "created_by" || option.key === "assignees")
return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters?.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (
displayFilters?.group_by === "priority" &&
option.key === "priority"
)
return null;
if (option.key === "sort_order") return null;
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === displayFilters?.order_by)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (displayFilters?.group_by === "priority" && option.key === "priority") return null;
if (option.key === "sort_order") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</>
)}
</div>
</>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters?.type
)?.name ?? "Select"
FILTER_ISSUE_OPTIONS.find((option) => option.key === displayFilters?.type)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
@ -230,24 +211,23 @@ export const MyIssuesViewOptions: React.FC = () => {
</div>
</div>
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters?.show_empty_groups,
})
}
/>
</div>
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters?.show_empty_groups,
})
}
/>
</div>
</>
)}
</div>
</>
)}
</div>
<div className="space-y-2 py-3">
@ -258,16 +238,11 @@ export const MyIssuesViewOptions: React.FC = () => {
if (
displayFilters?.layout === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
(key === "attachment_count" || key === "link" || key === "sub_issue_count")
)
return null;
if (
displayFilters?.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
if (displayFilters?.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
return null;
return (

View File

@ -18,12 +18,12 @@ import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-materia
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { Properties, TIssueViewOptions } from "types";
import { Properties, TIssueLayouts } from "types";
// constants
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
import useProjects from "hooks/use-projects";
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
{
type: "list",
Icon: FormatListBulletedOutlined,
@ -40,14 +40,10 @@ export const ProfileIssuesViewOptions: React.FC = () => {
const { projects } = useProjects();
const {
displayFilters,
setDisplayFilters,
filters,
displayProperties,
setProperties,
setFilters,
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
const { displayFilters, setDisplayFilters, filters, displayProperties, setProperties, setFilters } = useProfileIssues(
workspaceSlug?.toString(),
userId?.toString()
);
const { isEstimateActive } = useEstimateOption();
@ -80,9 +76,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
{issueViewOptions.map((option) => (
<Tooltip
key={option.type}
tooltipContent={
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
}
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
position="bottom"
>
<button
@ -120,9 +114,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
if (valueExists)
setFilters({
[option.key]: ((filters[key] ?? []) as any[])?.filter(
(val) => val !== option.value
),
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
});
else
setFilters({
@ -138,9 +130,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
<>
<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 ${
open
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200"
open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
}`}
>
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">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
displayFilters?.group_by === "project"
? "Project"
: GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters?.group_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters?.layout === "kanban" && option.key === null)
return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
displayFilters?.group_by === "project"
? "Project"
: GROUP_BY_OPTIONS.find((option) => option.key === displayFilters?.group_by)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters?.layout === "kanban" && option.key === null) return null;
if (option.key === "state" || option.key === "created_by" || option.key === "assignees")
return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters?.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (
displayFilters?.group_by === "priority" &&
option.key === "priority"
)
return null;
if (option.key === "sort_order") return null;
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === displayFilters?.order_by)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (displayFilters?.group_by === "priority" && option.key === "priority") return null;
if (option.key === "sort_order") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</>
)}
</div>
</>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters?.type
)?.name ?? "Select"
FILTER_ISSUE_OPTIONS.find((option) => option.key === displayFilters?.type)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
@ -262,24 +238,23 @@ export const ProfileIssuesViewOptions: React.FC = () => {
</div>
</div>
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters?.show_empty_groups,
})
}
/>
</div>
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters?.show_empty_groups,
})
}
/>
</div>
</>
)}
</div>
</>
)}
</div>
<div className="space-y-2 py-3">
@ -290,16 +265,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
if (
displayFilters?.layout === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
(key === "attachment_count" || key === "link" || key === "sub_issue_count")
)
return null;
if (
displayFilters?.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
if (displayFilters?.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
return null;
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: "high", title: "High" },
{ key: "medium", title: "Medium" },
@ -6,7 +23,10 @@ export const ISSUE_PRIORITIES = [
{ key: "none", title: "None" },
];
export const ISSUE_STATE_GROUPS = [
export const ISSUE_STATE_GROUPS: {
key: TStateGroups;
title: string;
}[] = [
{ key: "backlog", title: "Backlog" },
{ key: "unstarted", title: "Unstarted" },
{ key: "started", title: "Started" },
@ -30,7 +50,10 @@ export const ISSUE_DUE_DATE_OPTIONS = [
{ 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_detail.group", title: "State Groups" },
{ key: "priority", title: "Priority" },
@ -38,24 +61,34 @@ export const ISSUE_GROUP_BY_OPTIONS = [
{ key: "labels", title: "Labels" },
{ key: "assignees", title: "Assignees" },
{ 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: "created_at", title: "Last Created" },
{ key: "updated_at", title: "Last Updated" },
{ key: "-created_at", title: "Last Created" },
{ key: "-updated_at", title: "Last Updated" },
{ key: "start_date", title: "Start Date" },
{ key: "priority", title: "Priority" },
];
export const ISSUE_FILTER_OPTIONS = [
{ key: "all", title: "All" },
export const ISSUE_FILTER_OPTIONS: {
key: TIssueTypeFilters;
title: string;
}[] = [
{ key: null, title: "All" },
{ key: "active", title: "Active Issues" },
{ key: "backlog", title: "Backlog 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: "start_date", title: "Start Date" },
{ key: "due_date", title: "Due Date" },
@ -69,19 +102,26 @@ export const ISSUE_DISPLAY_PROPERTIES = [
{ 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: "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: "start_target_date", title: "Start target Date" }, // gantt always be true
];
export const ISSUE_LAYOUTS = [
{ key: "list", title: "List View" },
{ key: "kanban", title: "Kanban View" },
{ key: "calendar", title: "Calendar View" },
{ key: "spreadsheet", title: "Spreadsheet View" },
{ key: "gantt_chart", title: "Gantt Chart View" },
export const ISSUE_LAYOUTS: {
key: TIssueLayouts;
title: string;
icon: any;
}[] = [
{ 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 = [

View File

@ -1,7 +1,7 @@
import { orderArrayBy } from "helpers/array.helper";
import { renderDateFormat } from "helpers/date-time.helper";
// types
import { IIssue, TIssueGroupByOptions, TIssueOrderByOptions } from "types";
import { IIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "types";
type THandleIssuesMutation = (
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 => {
if (_layout === "list")
return [
@ -197,3 +179,117 @@ export const handleIssueParamsDateFormat = (key: string, start_date: any | null,
if (key === "custom" && start_date && target_date)
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 Link from "next/link";
import { useRouter } from "next/router";
import useSWR from "swr";
// mobx
import { useMobxStore } from "lib/mobx/store-provider";
// services
import projectService from "services/project.service";
import inboxService from "services/inbox.service";
@ -15,8 +16,9 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
// helper
import { truncateText } from "helpers/string.helper";
// components
import { IssuesFilterView, IssuesView } from "components/core";
import { IssuesView } from "components/core";
import { AnalyticsProjectModal } from "components/analytics";
import { ProjectIssuesHeader } from "components/headers";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
@ -25,7 +27,14 @@ import { PlusIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage } from "next";
// 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 [analyticsModal, setAnalyticsModal] = useState(false);
@ -33,17 +42,43 @@ const ProjectIssues: NextPage = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore();
const { data: projectDetails } = useSWR(
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.getProject(workspaceSlug as string, projectId as string)
: null
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
);
const { data: inboxList } = useSWR(
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
? () => 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
);
@ -53,49 +88,47 @@ const ProjectIssues: NextPage = () => {
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
<BreadcrumbItem
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`}
/>
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
</Breadcrumbs>
}
right={
<div className="flex items-center gap-2">
<IssuesFilterView />
<SecondaryButton
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"
outline
>
Analytics
</SecondaryButton>
{projectDetails && projectDetails.inbox_view && (
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
<a>
<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"
outline
>
<span>Inbox</span>
{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">
{inboxList?.[0]?.pending_issue_count}
</span>
)}
</SecondaryButton>
</a>
</Link>
)}
<PrimaryButton
className="flex items-center gap-2"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</PrimaryButton>
</div>
<ProjectIssuesHeader />
// <div className="flex items-center gap-2">
// <SecondaryButton
// 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"
// outline
// >
// Analytics
// </SecondaryButton>
// {projectDetails && projectDetails.inbox_view && (
// <Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
// <a>
// <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"
// outline
// >
// <span>Inbox</span>
// {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">
// {inboxList?.[0]?.pending_issue_count}
// </span>
// )}
// </SecondaryButton>
// </a>
// </Link>
// )}
// <PrimaryButton
// className="flex items-center gap-2"
// onClick={() => {
// const e = new KeyboardEvent("keydown", { key: "c" });
// document.dispatchEvent(e);
// }}
// >
// <PlusIcon className="h-4 w-4" />
// Add Issue
// </PrimaryButton>
// </div>
}
bg="secondary"
>

View File

@ -1,43 +1,74 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// types
import { RootStore } from "./root";
// services
import { ProjectService } from "services/project.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 {
loader: boolean;
error: any | null;
userDisplayProperties: any;
userDisplayFilters: any;
userFilters: any;
defaultDisplayFilters: any;
defaultFilters: any;
userDisplayProperties: IIssueDisplayProperties;
userDisplayFilters: IIssueDisplayFilterOptions;
userFilters: IIssueFilterOptions;
defaultDisplayFilters: IIssueDisplayFilterOptions;
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 {
loader: boolean = false;
error: any | null = null;
// observables
userDisplayProperties: any = {};
userDisplayFilters: any = {};
userFilters: any = {};
defaultDisplayFilters: any = {};
defaultFilters: any = {};
defaultDisplayProperties: any = {
userDisplayFilters: IIssueDisplayFilterOptions = {};
userFilters: IIssueFilterOptions = {};
defaultDisplayFilters: IIssueDisplayFilterOptions = {};
defaultFilters: IIssueFilterOptions = {};
defaultDisplayProperties: IIssueDisplayProperties = {
assignee: true,
due_date: true,
key: true,
labels: true,
priority: true,
start_date: true,
due_date: true,
labels: true,
key: true,
priority: true,
state: true,
sub_issue_count: true,
link: true,
attachment_count: true,
estimate: true,
created_on: true,
updated_on: true,
};
// root store
rootStore;
// services
projectService;
issueService;
@ -45,12 +76,21 @@ class IssueFilterStore implements IIssueFilterStore {
makeObservable(this, {
loader: observable.ref,
error: observable.ref,
// observables
defaultDisplayFilters: observable.ref,
defaultFilters: observable.ref,
userDisplayProperties: observable.ref,
userDisplayFilters: observable.ref,
userFilters: observable.ref,
fetchUserFilters: action,
// actions
fetchUserProjectFilters: action,
updateUserFilters: action,
updateDisplayProperties: action,
// computed
appliedFilters: computed,
});
this.rootStore = _rootStore;
@ -59,27 +99,90 @@ class IssueFilterStore implements IIssueFilterStore {
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 {
const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId);
const issueProperties = await this.issueService.getIssueProperties(workspaceSlug, projectId);
console.log("memberResponse", memberResponse);
console.log("issueProperties", issueProperties);
runInAction(() => {
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;
// default props from api
this.defaultFilters = memberResponse.default_props.filters;
this.defaultDisplayFilters = memberResponse.default_props.display_filters;
this.defaultDisplayFilters = memberResponse.default_props.display_filters ?? {};
});
} catch (error) {
runInAction(() => {
this.error = 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;

View File

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

View File

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

View File

@ -1,15 +1,4 @@
import type {
IIssueFilterOptions,
IUserLite,
IWorkspace,
IWorkspaceLite,
IUserMemberLite,
TIssueGroupByOptions,
TIssueOrderByOptions,
TIssueViewOptions,
TStateGroups,
IProjectViewProps,
} from ".";
import type { IUserLite, IWorkspace, IWorkspaceLite, IUserMemberLite, TStateGroups, IProjectViewProps } from ".";
export interface IProject {
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 =
| "state"
@ -28,6 +28,25 @@ export type TIssueOrderByOptions =
| "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 {
assignees?: string[] | null;
created_by?: string[] | null;
@ -43,12 +62,27 @@ export interface IIssueFilterOptions {
export interface IIssueDisplayFilterOptions {
calendar_date_range?: string;
group_by?: TIssueGroupByOptions;
layout?: TIssueViewOptions;
layout?: TIssueLayouts;
order_by?: TIssueOrderByOptions;
show_empty_groups?: boolean;
start_target_date?: 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 {

View File

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