mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
0ebe36bdb3
commit
27f78dd283
@ -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,
|
||||
|
@ -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 && (
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -255,8 +255,6 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
||||
|
||||
console.log("properties", properties);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu
|
||||
|
1
web/components/headers/index.ts
Normal file
1
web/components/headers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./project-issues";
|
42
web/components/headers/project-issues.tsx
Normal file
42
web/components/headers/project-issues.tsx
Normal 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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
6
web/components/issue-layouts/display-filters/index.ts
Normal file
6
web/components/issue-layouts/display-filters/index.ts
Normal 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";
|
@ -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>
|
||||
);
|
||||
});
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
71
web/components/issue-layouts/filters/filter-selection.tsx
Normal file
71
web/components/issue-layouts/filters/filter-selection.tsx
Normal 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>
|
||||
);
|
||||
};
|
9
web/components/issue-layouts/filters/index.ts
Normal file
9
web/components/issue-layouts/filters/index.ts
Normal 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";
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
3
web/components/issue-layouts/helpers/index.ts
Normal file
3
web/components/issue-layouts/helpers/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./dropdown";
|
||||
export * from "./filter-header";
|
||||
export * from "./filter-option";
|
4
web/components/issue-layouts/index.ts
Normal file
4
web/components/issue-layouts/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./display-filters";
|
||||
export * from "./filters";
|
||||
export * from "./helpers";
|
||||
export * from "./layout-selection";
|
@ -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>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -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";
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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 = [
|
||||
|
@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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;
|
||||
|
@ -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(() => {
|
||||
|
6
web/types/issues.d.ts
vendored
6
web/types/issues.d.ts
vendored
@ -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";
|
||||
|
||||
|
13
web/types/projects.d.ts
vendored
13
web/types/projects.d.ts
vendored
@ -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;
|
||||
|
40
web/types/view-props.d.ts
vendored
40
web/types/view-props.d.ts
vendored
@ -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 {
|
||||
|
11
web/types/workspace.d.ts
vendored
11
web/types/workspace.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user