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 { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { Properties, TIssueViewOptions } from "types";
|
import { Properties, TIssueLayouts } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue";
|
import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
|
||||||
{
|
{
|
||||||
type: "list",
|
type: "list",
|
||||||
Icon: FormatListBulletedOutlined,
|
Icon: FormatListBulletedOutlined,
|
||||||
@ -52,7 +52,7 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
|
const issueViewForDraftIssues: { type: TIssueLayouts; Icon: any }[] = [
|
||||||
{
|
{
|
||||||
type: "list",
|
type: "list",
|
||||||
Icon: FormatListBulletedOutlined,
|
Icon: FormatListBulletedOutlined,
|
||||||
|
@ -65,28 +65,21 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { displayFilters, groupedIssues } = viewProps;
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
console.log("viewProps", viewProps);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IssuePeekOverview
|
<IssuePeekOverview
|
||||||
handleMutation={() =>
|
handleMutation={() => (isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues())}
|
||||||
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
|
|
||||||
}
|
|
||||||
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
|
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
readOnly={disableUserActions}
|
readOnly={disableUserActions}
|
||||||
/>
|
/>
|
||||||
{groupedIssues ? (
|
{groupedIssues ? (
|
||||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8 bg-custom-background-90">
|
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-5 bg-custom-background-90">
|
||||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
displayFilters?.group_by === "state"
|
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
? states?.find((s) => s.id === singleGroup)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
|
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleBoard
|
<SingleBoard
|
||||||
@ -115,15 +108,13 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
displayFilters?.group_by === "state"
|
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
? states?.find((s) => s.id === singleGroup)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (groupedIssues[singleGroup].length === 0)
|
if (groupedIssues[singleGroup].length === 0)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
|
className="flex items-center justify-between gap-2 rounded bg-custom-background-100 p-2 shadow-custom-shadow-2xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{currentState && (
|
{currentState && (
|
||||||
|
@ -490,6 +490,7 @@ export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserAc
|
|||||||
labels: null,
|
labels: null,
|
||||||
priority: null,
|
priority: null,
|
||||||
state: null,
|
state: null,
|
||||||
|
state_group: null,
|
||||||
start_date: null,
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
})
|
})
|
||||||
|
@ -255,8 +255,6 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
||||||
|
|
||||||
console.log("properties", properties);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
|
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 React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
import { FilterHeader } from "../helpers/filter-header";
|
||||||
// mobx react lite
|
// types
|
||||||
import { observer } from "mobx-react-lite";
|
import { IIssueDisplayProperties } from "types";
|
||||||
// mobx store
|
// constants
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue";
|
import { ISSUE_DISPLAY_PROPERTIES } from "constants/issue";
|
||||||
|
|
||||||
export const FilterDisplayProperties = observer(() => {
|
export const FilterDisplayProperties = observer(() => {
|
||||||
const store: RootStore = useMobxStore();
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const store = useMobxStore();
|
||||||
const { issueFilter: issueFilterStore } = store;
|
const { issueFilter: issueFilterStore } = store;
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||||
|
|
||||||
const handleDisplayProperties = (key: string, value: boolean) => {
|
const handleDisplayProperties = (property: Partial<IIssueDisplayProperties>) => {
|
||||||
// issueFilterStore.handleUserFilter("display_properties", key, !value);
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={"Display Properties"}
|
title="Display Properties"
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1 px-1 flex items-center whitespace-nowrap gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap mx-1 mt-1">
|
||||||
{ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => (
|
{ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => (
|
||||||
<div
|
<button
|
||||||
key={displayProperty?.key}
|
key={displayProperty.key}
|
||||||
className={`cursor-pointer rounded-sm transition-all text-xs border p-0.5 px-1.5 ${
|
type="button"
|
||||||
issueFilterStore?.userDisplayProperties?.[displayProperty?.key]
|
className={`rounded transition-all text-xs border px-2 py-0.5 ${
|
||||||
? `bg-custom-primary-200 border-custom-primary-200 text-white`
|
issueFilterStore?.userDisplayProperties?.[displayProperty.key]
|
||||||
: `hover:bg-custom-border-100 border-custom-border-100`
|
? "bg-custom-primary-100 border-custom-primary-100 text-white"
|
||||||
|
: "border-custom-border-200 hover:bg-custom-background-80"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
handleDisplayProperties(
|
handleDisplayProperties({
|
||||||
displayProperty?.key,
|
[displayProperty.key]: !issueFilterStore?.userDisplayProperties?.[displayProperty.key],
|
||||||
issueFilterStore?.userDisplayProperties?.[displayProperty?.key]
|
})
|
||||||
);
|
}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{displayProperty?.title}
|
{displayProperty.title}
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,53 +1,47 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
// components
|
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
// mobx
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
|
||||||
import { ISSUE_EXTRA_PROPERTIES } from "constants/issue";
|
// components
|
||||||
// default data
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
// import { issueFilterVisibilityData } from "helpers/issue.helper";
|
// helpers
|
||||||
|
import { issueFilterVisibilityData } from "helpers/issue.helper";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_EXTRA_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
export const FilterExtraOptions = observer(() => {
|
export const FilterExtraOptions = observer(() => {
|
||||||
const store: RootStore = useMobxStore();
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
|
const store = useMobxStore();
|
||||||
const { issueFilter: issueFilterStore } = store;
|
const { issueFilter: issueFilterStore } = store;
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const isExtraOptionEnabled = (option: string) =>
|
||||||
|
issueFilterVisibilityData.issues.extra_options[
|
||||||
const handleExtraOptions = (key: string, value: boolean) => {
|
issueFilterStore.userDisplayFilters.layout ?? "list"
|
||||||
// issueFilterStore.handleUserFilter("display_filters", key, !value);
|
].values.includes(option);
|
||||||
};
|
|
||||||
|
|
||||||
const handleExtraOptionsSectionVisibility = (key: string) => {
|
|
||||||
// issueFilterStore?.issueView &&
|
|
||||||
// issueFilterStore?.issueLayout &&
|
|
||||||
// issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"]?.extra_options?.[
|
|
||||||
// issueFilterStore?.issueLayout
|
|
||||||
// ].values?.includes(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={"Extra Options"}
|
title="Extra Options"
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-[2px] pt-1">
|
||||||
{ISSUE_EXTRA_PROPERTIES.map((_extraProperties) => (
|
{ISSUE_EXTRA_OPTIONS.map((option) => {
|
||||||
<FilterOption
|
if (!isExtraOptionEnabled(option.key)) return null;
|
||||||
key={_extraProperties?.key}
|
|
||||||
isChecked={issueFilterStore?.userDisplayFilters?.[_extraProperties?.key] ? true : false}
|
return (
|
||||||
onClick={() =>
|
<FilterOption
|
||||||
handleExtraOptions(_extraProperties?.key, issueFilterStore?.userDisplayFilters?.[_extraProperties?.key])
|
key={option.key}
|
||||||
}
|
isChecked={issueFilterStore?.userDisplayFilters?.[option.key] ? true : false}
|
||||||
title={_extraProperties.title}
|
title={option.title}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,39 +1,52 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
import { FilterHeader } from "../helpers/filter-header";
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
import { FilterOption } from "../helpers/filter-option";
|
||||||
// mobx react lite
|
// types
|
||||||
import { observer } from "mobx-react-lite";
|
import { TIssueGroupByOptions } from "types";
|
||||||
// mobx store
|
// constants
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue";
|
import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
export const FilterGroupBy = observer(() => {
|
export const FilterGroupBy = observer(() => {
|
||||||
const store: RootStore = useMobxStore();
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const store = useMobxStore();
|
||||||
const { issueFilter: issueFilterStore } = store;
|
const { issueFilter: issueFilterStore } = store;
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||||
|
|
||||||
const handleGroupBy = (key: string, value: string) => {
|
const handleGroupBy = (value: TIssueGroupByOptions) => {
|
||||||
// issueFilterStore.handleUserFilter("display_filters", key, value);
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
group_by: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={"Group By"}
|
title="Group By"
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-1 pt-1">
|
||||||
{ISSUE_GROUP_BY_OPTIONS.map((_groupBy) => (
|
{ISSUE_GROUP_BY_OPTIONS.map((groupBy) => (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={_groupBy?.key}
|
key={groupBy?.key}
|
||||||
isChecked={issueFilterStore?.userDisplayFilters?.group_by === _groupBy?.key ? true : false}
|
isChecked={issueFilterStore?.userDisplayFilters?.group_by === groupBy?.key ? true : false}
|
||||||
onClick={() => handleGroupBy("group_by", _groupBy?.key)}
|
onClick={() => handleGroupBy(groupBy.key)}
|
||||||
title={_groupBy.title}
|
title={groupBy.title}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
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 React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
import { FilterHeader } from "../helpers/filter-header";
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
import { FilterOption } from "../helpers/filter-option";
|
||||||
// mobx store
|
// types
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { TIssueTypeFilters } from "types";
|
||||||
import { RootStore } from "store/root";
|
// constants
|
||||||
import { ISSUE_FILTER_OPTIONS } from "constants/issue";
|
import { ISSUE_FILTER_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
export const FilterIssueType = observer(() => {
|
export const FilterIssueType = observer(() => {
|
||||||
const store: RootStore = useMobxStore();
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const store = useMobxStore();
|
||||||
const { issueFilter: issueFilterStore } = store;
|
const { issueFilter: issueFilterStore } = store;
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||||
|
|
||||||
const handleIssueType = (key: string, value: string) => {
|
const handleIssueType = (value: TIssueTypeFilters) => {
|
||||||
// issueFilterStore.handleUserFilter("display_filters", key, value);
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
type: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={"Issue Type"}
|
title="Issue Type"
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-1 pt-1">
|
||||||
{ISSUE_FILTER_OPTIONS.map((_issueType) => (
|
{ISSUE_FILTER_OPTIONS.map((issueType) => (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={_issueType?.key}
|
key={issueType?.key}
|
||||||
isChecked={issueFilterStore?.userDisplayFilters?.type === _issueType?.key ? true : false}
|
isChecked={issueFilterStore?.userDisplayFilters?.type === issueType?.key ? true : false}
|
||||||
onClick={() => handleIssueType("type", _issueType?.key)}
|
onClick={() => handleIssueType(issueType?.key)}
|
||||||
title={_issueType.title}
|
title={issueType.title}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -1,22 +1,35 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
import { FilterHeader } from "../helpers/filter-header";
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
import { FilterOption } from "../helpers/filter-option";
|
||||||
// mobx react lite
|
// types
|
||||||
import { observer } from "mobx-react-lite";
|
import { TIssueOrderByOptions } from "types";
|
||||||
// mobx store
|
// constants
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue";
|
import { ISSUE_ORDER_BY_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
export const FilterOrderBy = observer(() => {
|
export const FilterOrderBy = observer(() => {
|
||||||
const store: RootStore = useMobxStore();
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const store = useMobxStore();
|
||||||
const { issueFilter: issueFilterStore } = store;
|
const { issueFilter: issueFilterStore } = store;
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||||
|
|
||||||
const handleOrderBy = (key: string, value: string) => {
|
const handleOrderBy = (value: TIssueOrderByOptions) => {
|
||||||
// issueFilterStore.handleUserFilter("display_filters", key, value);
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
order_by: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,12 +41,12 @@ export const FilterOrderBy = observer(() => {
|
|||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-[2px] pt-1">
|
||||||
{ISSUE_ORDER_BY_OPTIONS.map((_orderBy) => (
|
{ISSUE_ORDER_BY_OPTIONS.map((orderBy) => (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={_orderBy?.key}
|
key={orderBy?.key}
|
||||||
isChecked={issueFilterStore?.userDisplayFilters?.order_by === _orderBy?.key ? true : false}
|
isChecked={issueFilterStore?.userDisplayFilters?.order_by === orderBy?.key ? true : false}
|
||||||
onClick={() => handleOrderBy("order_by", _orderBy?.key)}
|
onClick={() => handleOrderBy(orderBy.key)}
|
||||||
title={_orderBy.title}
|
title={orderBy.title}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -1,67 +1,62 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
// components
|
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
// mobx
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
|
// ui
|
||||||
|
import { Avatar } from "components/ui";
|
||||||
|
|
||||||
export const MemberIcons = ({ display_name, avatar }: { display_name: string; avatar: string | null }) => (
|
type Props = {
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
workspaceSlug: string;
|
||||||
{avatar ? (
|
projectId: string;
|
||||||
<img src={avatar} alt={display_name || ""} className="" />
|
};
|
||||||
) : (
|
|
||||||
<div className="text-[12px] w-full h-full flex justify-center items-center capitalize font-medium bg-gray-700 text-white">
|
|
||||||
{(display_name ?? "U")[0]}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const FilterAssignees = observer(() => {
|
export const FilterAssignees: React.FC<Props> = observer((props) => {
|
||||||
const store: RootStore = useMobxStore();
|
const { workspaceSlug, projectId } = props;
|
||||||
const { issueFilter: issueFilterStore } = store;
|
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
const handleFilter = (key: string, value: string) => {
|
const store = useMobxStore();
|
||||||
// let _value =
|
const { issueFilter: issueFilterStore, project: projectStore } = store;
|
||||||
// issueFilterStore?.userFilters?.filters?.[key] != null
|
|
||||||
// ? issueFilterStore?.userFilters?.filters?.[key].includes(value)
|
const handleUpdateAssignees = (value: string) => {
|
||||||
// ? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
|
const newValues = issueFilterStore.userFilters?.assignees ?? [];
|
||||||
// : [...issueFilterStore?.userFilters?.filters?.[key], value]
|
|
||||||
// : [value];
|
if (issueFilterStore.userFilters?.assignees?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
// _value = _value && _value.length > 0 ? _value : null;
|
else newValues.push(value);
|
||||||
// issueFilterStore.handleUserFilter("filters", key, _value);
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
assignees: newValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={`Assignees (${issueFilterStore?.projectMembers?.length || 0})`}
|
title={`Assignees (${issueFilterStore?.userFilters.assignees?.length ?? 0})`}
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-[2px] pt-1">
|
||||||
{issueFilterStore?.projectMembers &&
|
{projectStore.members?.[projectId?.toString() ?? ""]?.map((member) => (
|
||||||
issueFilterStore?.projectMembers.length > 0 &&
|
<FilterOption
|
||||||
issueFilterStore?.projectMembers.map((_member) => (
|
key={`assignees-${member?.member?.id}`}
|
||||||
<FilterOption
|
isChecked={
|
||||||
key={`assignees-${_member?.member?.id}`}
|
issueFilterStore?.userFilters?.assignees != null &&
|
||||||
isChecked={
|
issueFilterStore?.userFilters?.assignees.includes(member.member?.id)
|
||||||
issueFilterStore?.userFilters?.filters?.assignees != null &&
|
? true
|
||||||
issueFilterStore?.userFilters?.filters?.assignees.includes(_member?.member?.id)
|
: false
|
||||||
? true
|
}
|
||||||
: false
|
onClick={() => handleUpdateAssignees(member.member?.id)}
|
||||||
}
|
icon={<Avatar user={member.member} height="18px" width="18px" />}
|
||||||
onClick={() => handleFilter("assignees", _member?.member?.id)}
|
title={member.member?.display_name}
|
||||||
icon={<MemberIcons display_name={_member?.member.display_name} avatar={_member?.member.avatar} />}
|
/>
|
||||||
title={`${_member?.member?.display_name} (${_member?.member?.first_name} ${_member?.member?.last_name})`}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,56 +1,57 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
// components
|
|
||||||
import { MemberIcons } from "./assignees";
|
// mobx
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
|
// ui
|
||||||
|
import { Avatar } from "components/ui";
|
||||||
|
|
||||||
export const FilterCreatedBy = observer(() => {
|
type Props = {
|
||||||
const store: RootStore = useMobxStore();
|
workspaceSlug: string;
|
||||||
const { issueFilters: issueFilterStore } = store;
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
export const FilterCreatedBy: React.FC<Props> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId } = props;
|
||||||
|
|
||||||
const handleFilter = (key: string, value: string) => {
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
let _value =
|
|
||||||
issueFilterStore?.userFilters?.filters?.[key] != null
|
const store = useMobxStore();
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].includes(value)
|
const { issueFilter: issueFilterStore, project: projectStore } = store;
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
|
|
||||||
: [...issueFilterStore?.userFilters?.filters?.[key], value]
|
const handleUpdateCreatedBy = (value: string) => {
|
||||||
: [value];
|
const newValues = issueFilterStore.userFilters?.created_by ?? [];
|
||||||
_value = _value && _value.length > 0 ? _value : null;
|
|
||||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
if (issueFilterStore.userFilters?.created_by?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
created_by: newValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={`Created By (${issueFilterStore?.projectMembers?.length || 0})`}
|
title={`Created By (${issueFilterStore?.userFilters.created_by?.length ?? 0})`}
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-1 pt-1">
|
||||||
{issueFilterStore?.projectMembers &&
|
{projectStore.members?.[projectId?.toString() ?? ""]?.map((member) => (
|
||||||
issueFilterStore?.projectMembers.length > 0 &&
|
<FilterOption
|
||||||
issueFilterStore?.projectMembers.map((_member) => (
|
key={`created-by-${member.member?.id}`}
|
||||||
<FilterOption
|
isChecked={issueFilterStore?.userFilters?.created_by?.includes(member.member?.id) ? true : false}
|
||||||
key={`create-by-${_member?.member?.id}`}
|
onClick={() => handleUpdateCreatedBy(member.member?.id)}
|
||||||
isChecked={
|
icon={<Avatar user={member.member} height="18px" width="18px" />}
|
||||||
issueFilterStore?.userFilters?.filters?.created_by != null &&
|
title={member.member?.display_name}
|
||||||
issueFilterStore?.userFilters?.filters?.created_by.includes(_member?.member?.id)
|
/>
|
||||||
? true
|
))}
|
||||||
: false
|
|
||||||
}
|
|
||||||
onClick={() => handleFilter("created_by", _member?.member?.id)}
|
|
||||||
icon={<MemberIcons display_name={_member?.member.display_name} avatar={_member?.member.avatar} />}
|
|
||||||
title={`${_member?.member?.display_name} (${_member?.member?.first_name} ${_member?.member?.last_name})`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
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
|
// components
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
// mobx react lite
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
|
||||||
|
|
||||||
const LabelIcons = ({ color }: { color: string }) => (
|
const LabelIcons = ({ color }: { color: string }) => (
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
|
||||||
<div className={`w-[12px] h-[12px] rounded-full`} style={{ backgroundColor: color }} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FilterLabels = observer(() => {
|
type Props = {
|
||||||
const store: RootStore = useMobxStore();
|
workspaceSlug: string;
|
||||||
const { issueFilters: issueFilterStore } = store;
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
export const FilterLabels: React.FC<Props> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId } = props;
|
||||||
|
|
||||||
const handleFilter = (key: string, value: string) => {
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
let _value =
|
|
||||||
issueFilterStore?.userFilters?.filters?.[key] != null
|
const store = useMobxStore();
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].includes(value)
|
const { issueFilter: issueFilterStore, project: projectStore } = store;
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
|
|
||||||
: [...issueFilterStore?.userFilters?.filters?.[key], value]
|
const handleUpdateLabels = (value: string) => {
|
||||||
: [value];
|
const newValues = issueFilterStore.userFilters?.labels ?? [];
|
||||||
_value = _value && _value.length > 0 ? _value : null;
|
|
||||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
if (issueFilterStore.userFilters?.labels?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
labels: newValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLabels =
|
|
||||||
issueFilterStore.issueView && issueFilterStore.issueView === "my_issues"
|
|
||||||
? issueFilterStore?.workspaceLabels
|
|
||||||
: issueFilterStore?.projectLabels;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={`Labels (${(handleLabels && handleLabels?.length) || 0})`}
|
title={`Labels (${issueFilterStore.userFilters?.labels?.length ?? 0})`}
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-[2px] pt-1">
|
||||||
{handleLabels &&
|
{projectStore.labels?.[projectId?.toString() ?? ""]?.map((label) => (
|
||||||
handleLabels.length > 0 &&
|
<FilterOption
|
||||||
handleLabels.map((_label) => (
|
key={label?.id}
|
||||||
<FilterOption
|
isChecked={issueFilterStore?.userFilters?.labels?.includes(label?.id) ? true : false}
|
||||||
key={_label?.id}
|
onClick={() => handleUpdateLabels(label?.id)}
|
||||||
isChecked={
|
icon={<LabelIcons color={label.color} />}
|
||||||
issueFilterStore?.userFilters?.filters?.labels != null &&
|
title={label.name}
|
||||||
issueFilterStore?.userFilters?.filters?.labels.includes(_label?.id)
|
/>
|
||||||
? true
|
))}
|
||||||
: false
|
|
||||||
}
|
|
||||||
onClick={() => handleFilter("labels", _label?.id)}
|
|
||||||
icon={<LabelIcons color={_label.color} />}
|
|
||||||
title={_label.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
// lucide icons
|
|
||||||
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
|
// mobx
|
||||||
// components
|
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
|
// icons
|
||||||
|
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
const PriorityIcons = ({
|
const PriorityIcons = ({
|
||||||
priority,
|
priority,
|
||||||
size = 14,
|
size = 12,
|
||||||
strokeWidth = 2,
|
strokeWidth = 1.5,
|
||||||
}: {
|
}: {
|
||||||
priority: string;
|
priority: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
@ -21,84 +21,78 @@ const PriorityIcons = ({
|
|||||||
}) => {
|
}) => {
|
||||||
if (priority === "urgent")
|
if (priority === "urgent")
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-red-500 bg-red-500 text-white flex justify-center items-center">
|
<div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-red-500 bg-red-500 text-white flex justify-center items-center">
|
||||||
<AlertCircle size={size} strokeWidth={strokeWidth} />
|
<AlertCircle size={size} strokeWidth={strokeWidth} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (priority === "high")
|
if (priority === "high")
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-custom-border-300 text-red-500 flex justify-center items-center pl-1">
|
<div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-custom-border-200 text-red-500 flex justify-center items-center pl-1">
|
||||||
<SignalHigh size={size} strokeWidth={strokeWidth} />
|
<SignalHigh size={size} strokeWidth={strokeWidth} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (priority === "medium")
|
if (priority === "medium")
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-custom-border-300 text-orange-500 flex justify-center items-center pl-1">
|
<div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-custom-border-200 text-orange-500 flex justify-center items-center pl-1">
|
||||||
<SignalMedium size={size} strokeWidth={strokeWidth} />
|
<SignalMedium size={size} strokeWidth={strokeWidth} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (priority === "low")
|
if (priority === "low")
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-custom-border-300 text-green-500 flex justify-center items-center pl-1">
|
<div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-custom-border-200 text-green-500 flex justify-center items-center pl-1">
|
||||||
<SignalLow size={size} strokeWidth={strokeWidth} />
|
<SignalLow size={size} strokeWidth={strokeWidth} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] border border-custom-border-300 text-gray-500 flex justify-center items-center">
|
<div className="flex-shrink-0 rounded-sm overflow-hidden w-5 h-5 border border-custom-border-200 text-custom-text-400 flex justify-center items-center">
|
||||||
<Ban size={size} strokeWidth={strokeWidth} />
|
<Ban size={size} strokeWidth={strokeWidth} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FilterPriority = observer(() => {
|
type Props = { workspaceSlug: string; projectId: string };
|
||||||
const store: RootStore = useMobxStore();
|
|
||||||
const { issueFilters: issueFilterStore } = store;
|
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
export const FilterPriority: React.FC<Props> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId } = props;
|
||||||
|
|
||||||
const handleFilter = (key: string, value: string) => {
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
let _value =
|
|
||||||
issueFilterStore?.userFilters?.filters?.[key] != null
|
const store = useMobxStore();
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].includes(value)
|
const { issueFilter: issueFilterStore } = store;
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
|
|
||||||
: [...issueFilterStore?.userFilters?.filters?.[key], value]
|
const handleUpdatePriority = (value: string) => {
|
||||||
: [value];
|
const newValues = issueFilterStore.userFilters?.priority ?? [];
|
||||||
_value = _value && _value.length > 0 ? _value : null;
|
|
||||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
if (issueFilterStore.userFilters?.priority?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
priority: newValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={`Priority (${issueFilterStore?.issueRenderFilters?.priority.length || 0})`}
|
title={`Priority (${issueFilterStore.userFilters?.priority?.length ?? 0})`}
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-1 pt-1">
|
||||||
{issueFilterStore?.issueRenderFilters?.priority &&
|
{ISSUE_PRIORITIES.map((priority) => (
|
||||||
issueFilterStore?.issueRenderFilters?.priority.length > 0 &&
|
<FilterOption
|
||||||
issueFilterStore?.issueRenderFilters?.priority.map((_priority) => (
|
key={priority.key}
|
||||||
<FilterOption
|
isChecked={issueFilterStore.userFilters?.priority?.includes(priority.key) ? true : false}
|
||||||
key={_priority?.key}
|
onClick={() => handleUpdatePriority(priority.key)}
|
||||||
isChecked={
|
icon={<PriorityIcons priority={priority.key} />}
|
||||||
issueFilterStore?.userFilters?.filters?.priority != null &&
|
title={priority.title}
|
||||||
issueFilterStore?.userFilters?.filters?.priority.includes(_priority?.key)
|
/>
|
||||||
? true
|
))}
|
||||||
: false
|
|
||||||
}
|
|
||||||
onClick={() => handleFilter("priority", _priority?.key)}
|
|
||||||
icon={<PriorityIcons priority={_priority.key} />}
|
|
||||||
title={_priority.title}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<div className="pl-[32px] flex items-center gap-2 py-[6px] text-xs text-custom-primary-100">
|
|
||||||
<div>View less</div>
|
|
||||||
<div>View more</div>
|
|
||||||
{/* TODO: <div>View all</div> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,45 +1,32 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// components
|
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
// mobx
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
|
|
||||||
export const FilterStartDate = observer(() => {
|
export const FilterStartDate = observer(() => {
|
||||||
const store: RootStore = useMobxStore();
|
const store = useMobxStore();
|
||||||
const { issueFilters: issueFilterStore } = store;
|
const { issueFilter: issueFilterStore } = store;
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||||
|
|
||||||
const handleFilter = (key: string, value: string) => {
|
|
||||||
const _value = [value];
|
|
||||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={`Start Date (${issueFilterStore?.issueRenderFilters?.start_date?.length})`}
|
title={`Start Date (${issueFilterStore?.userFilters?.start_date?.length})`}
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-1 pt-1">
|
||||||
{issueFilterStore?.issueRenderFilters?.start_date &&
|
{issueFilterStore?.userFilters?.start_date &&
|
||||||
issueFilterStore?.issueRenderFilters?.start_date.length > 0 &&
|
issueFilterStore?.userFilters?.start_date.length > 0 &&
|
||||||
issueFilterStore?.issueRenderFilters?.start_date.map((_startDate) => (
|
issueFilterStore?.userFilters?.start_date.map((_startDate) => (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={_startDate?.key}
|
key={_startDate?.key}
|
||||||
isChecked={
|
isChecked={issueFilterStore?.userFilters?.start_date?.includes(_startDate?.key) ? true : false}
|
||||||
issueFilterStore?.userFilters?.filters?.start_date != null &&
|
|
||||||
issueFilterStore?.userFilters?.filters?.start_date.includes(_startDate?.key)
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
onClick={() => handleFilter("start_date", _startDate?.key)}
|
|
||||||
title={_startDate.title}
|
title={_startDate.title}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
/>
|
/>
|
||||||
|
@ -1,108 +1,59 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
|
||||||
StateGroupBacklogIcon,
|
|
||||||
StateGroupCancelledIcon,
|
|
||||||
StateGroupCompletedIcon,
|
|
||||||
StateGroupStartedIcon,
|
|
||||||
StateGroupUnstartedIcon,
|
|
||||||
} from "components/icons";
|
|
||||||
// components
|
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
// constants
|
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
|
||||||
|
|
||||||
export const StateGroupIcons = ({
|
// mobx
|
||||||
stateGroup,
|
import { observer } from "mobx-react-lite";
|
||||||
width = "14px",
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
height = "14px",
|
// components
|
||||||
color = null,
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
}: {
|
// icons
|
||||||
stateGroup: string;
|
import { StateGroupIcon } from "components/icons";
|
||||||
width?: string | undefined;
|
// constants
|
||||||
height?: string | undefined;
|
import { ISSUE_STATE_GROUPS } from "constants/issue";
|
||||||
color?: string | null;
|
|
||||||
}) => {
|
type Props = {
|
||||||
if (stateGroup === "cancelled")
|
workspaceSlug: string;
|
||||||
return (
|
projectId: string;
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
|
||||||
<StateGroupCancelledIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
if (stateGroup === "completed")
|
|
||||||
return (
|
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
|
||||||
<StateGroupCompletedIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
if (stateGroup === "started")
|
|
||||||
return (
|
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
|
||||||
<StateGroupStartedIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
if (stateGroup === "unstarted")
|
|
||||||
return (
|
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
|
||||||
<StateGroupUnstartedIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
if (stateGroup === "backlog")
|
|
||||||
return (
|
|
||||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
|
||||||
<StateGroupBacklogIcon width={width} height={height} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return <></>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FilterStateGroup = observer(() => {
|
export const FilterStateGroup: React.FC<Props> = observer((props) => {
|
||||||
const store: RootStore = useMobxStore();
|
const { workspaceSlug, projectId } = props;
|
||||||
const { issueFilters: issueFilterStore } = store;
|
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
const handleFilter = (key: string, value: string) => {
|
const store = useMobxStore();
|
||||||
let _value =
|
const { issueFilter: issueFilterStore } = store;
|
||||||
issueFilterStore?.userFilters?.filters?.[key] != null
|
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].includes(value)
|
const handleUpdateStateGroup = (value: string) => {
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
|
const newValues = issueFilterStore.userFilters?.state_group ?? [];
|
||||||
: [...issueFilterStore?.userFilters?.filters?.[key], value]
|
|
||||||
: [value];
|
if (issueFilterStore.userFilters?.state_group?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
_value = _value && _value.length > 0 ? _value : null;
|
else newValues.push(value);
|
||||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
state_group: newValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={`State Group (${issueFilterStore?.issueRenderFilters?.state_group?.length})`}
|
title={`State Group (${issueFilterStore.userFilters?.state_group?.length ?? 0})`}
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-1 pt-1">
|
||||||
{issueFilterStore?.issueRenderFilters?.state_group &&
|
{ISSUE_STATE_GROUPS.map((stateGroup) => (
|
||||||
issueFilterStore?.issueRenderFilters?.state_group.length > 0 &&
|
<FilterOption
|
||||||
issueFilterStore?.issueRenderFilters?.state_group.map((_stateGroup) => (
|
key={stateGroup.key}
|
||||||
<FilterOption
|
isChecked={issueFilterStore.userFilters?.state_group?.includes(stateGroup.key) ? true : false}
|
||||||
key={_stateGroup?.key}
|
onClick={() => handleUpdateStateGroup(stateGroup.key)}
|
||||||
isChecked={
|
icon={<StateGroupIcon stateGroup={stateGroup.key} />}
|
||||||
issueFilterStore?.userFilters?.filters?.state_group != null &&
|
title={stateGroup.title}
|
||||||
issueFilterStore?.userFilters?.filters?.state_group.includes(_stateGroup?.key)
|
/>
|
||||||
? true
|
))}
|
||||||
: false
|
|
||||||
}
|
|
||||||
onClick={() => handleFilter("state_group", _stateGroup?.key)}
|
|
||||||
icon={<StateGroupIcons stateGroup={_stateGroup.key} />}
|
|
||||||
title={_stateGroup.title}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,68 +1,62 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// components
|
|
||||||
import { StateGroupIcons } from "./state-group";
|
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
// store default data
|
|
||||||
import { stateGroups } from "store/helpers/issue-data";
|
|
||||||
|
|
||||||
export const FilterState = observer(() => {
|
// mobx
|
||||||
const store: RootStore = useMobxStore();
|
import { observer } from "mobx-react-lite";
|
||||||
const { issueFilters: issueFilterStore } = store;
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
|
// icons
|
||||||
|
import { StateGroupIcon } from "components/icons";
|
||||||
|
// helpers
|
||||||
|
import { getStatesList } from "helpers/state.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterState: React.FC<Props> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId } = props;
|
||||||
|
|
||||||
|
const store = useMobxStore();
|
||||||
|
const { issueFilter: issueFilterStore, project: projectStore } = store;
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||||
|
|
||||||
const handleFilter = (key: string, value: string) => {
|
const statesByGroups = projectStore.states?.[projectId?.toString() ?? ""];
|
||||||
let _value =
|
const statesList = getStatesList(statesByGroups);
|
||||||
issueFilterStore?.userFilters?.filters?.[key] != null
|
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].includes(value)
|
const handleUpdateState = (value: string) => {
|
||||||
? issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value)
|
const newValues = issueFilterStore.userFilters?.state ?? [];
|
||||||
: [...issueFilterStore?.userFilters?.filters?.[key], value]
|
|
||||||
: [value];
|
if (issueFilterStore.userFilters?.state?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
_value = _value && _value.length > 0 ? _value : null;
|
else newValues.push(value);
|
||||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
|
||||||
|
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
state: newValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const countAllState = stateGroups
|
|
||||||
.map((_stateGroup) => issueFilterStore?.projectStates?.[_stateGroup?.key].length || 0)
|
|
||||||
.reduce((sum: number, currentValue: number) => sum + currentValue, 0);
|
|
||||||
|
|
||||||
console.log("countAllState", countAllState);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={`State (${countAllState})`}
|
title={`State (${issueFilterStore.userFilters?.state?.length ?? 0})`}
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-1 pt-1">
|
||||||
{stateGroups.map(
|
{statesList?.map((state) => (
|
||||||
(_stateGroup) =>
|
<FilterOption
|
||||||
issueFilterStore?.projectStates &&
|
key={state.id}
|
||||||
issueFilterStore?.projectStates[_stateGroup?.key] &&
|
isChecked={issueFilterStore?.userFilters?.state?.includes(state?.id) ? true : false}
|
||||||
issueFilterStore?.projectStates[_stateGroup?.key].length > 0 &&
|
onClick={() => handleUpdateState(state?.id)}
|
||||||
issueFilterStore?.projectStates[_stateGroup?.key].map((_state: any) => (
|
icon={<StateGroupIcon stateGroup={state?.group} color={state?.color} />}
|
||||||
<FilterOption
|
title={state?.name}
|
||||||
key={_state?.id}
|
/>
|
||||||
isChecked={
|
))}
|
||||||
issueFilterStore?.userFilters?.filters?.state != null &&
|
|
||||||
issueFilterStore?.userFilters?.filters?.state.includes(_state?.id)
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
onClick={() => handleFilter("state", _state?.id)}
|
|
||||||
icon={<StateGroupIcons stateGroup={_stateGroup?.key} color={_state?.color} />}
|
|
||||||
title={_state?.name}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,46 +1,32 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
// components
|
// mobx
|
||||||
import { FilterHeader } from "../helpers/filter-header";
|
|
||||||
import { FilterOption } from "../helpers/filter-option";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "components/issue-layouts";
|
||||||
|
|
||||||
export const FilterTargetDate = observer(() => {
|
export const FilterTargetDate = observer(() => {
|
||||||
const store: RootStore = useMobxStore();
|
const store = useMobxStore();
|
||||||
const { issueFilters: issueFilterStore } = store;
|
const { issueFilter: issueFilterStore } = store;
|
||||||
|
|
||||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||||
|
|
||||||
const handleFilter = (key: string, value: string) => {
|
|
||||||
const _value = [value];
|
|
||||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FilterHeader
|
<FilterHeader
|
||||||
title={`Target Date (${issueFilterStore?.issueRenderFilters?.due_date.length})`}
|
title={`Target Date (${issueFilterStore?.userFilters?.target_date?.length})`}
|
||||||
isPreviewEnabled={previewEnabled}
|
isPreviewEnabled={previewEnabled}
|
||||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
/>
|
/>
|
||||||
{previewEnabled && (
|
{previewEnabled && (
|
||||||
<div className="space-y-[2px] pt-1">
|
<div className="space-y-[2px] pt-1">
|
||||||
{issueFilterStore?.issueRenderFilters?.due_date &&
|
{issueFilterStore?.userFilters?.target_date &&
|
||||||
issueFilterStore?.issueRenderFilters?.due_date.length > 0 &&
|
issueFilterStore?.userFilters?.target_date.length > 0 &&
|
||||||
issueFilterStore?.issueRenderFilters?.due_date.map((_targetDate) => (
|
issueFilterStore?.userFilters?.target_date.map((_targetDate) => (
|
||||||
<FilterOption
|
<FilterOption
|
||||||
key={_targetDate?.key}
|
key={_targetDate?.key}
|
||||||
isChecked={
|
isChecked={issueFilterStore?.userFilters?.target_date?.includes(_targetDate?.key) ? true : false}
|
||||||
issueFilterStore?.userFilters?.filters?.target_date != null &&
|
|
||||||
issueFilterStore?.userFilters?.filters?.target_date.includes(_targetDate?.key)
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
onClick={() => handleFilter("target_date", _targetDate?.key)}
|
|
||||||
title={_targetDate.title}
|
title={_targetDate.title}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
/>
|
/>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// lucide icons
|
// icons
|
||||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
import { ChevronUp } from "lucide-react";
|
||||||
|
|
||||||
interface IIssueDropdown {
|
interface IIssueDropdown {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -17,11 +18,13 @@ export const IssueDropdown = ({ children, title = "Dropdown" }: IIssueDropdown)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`outline-none border border-custom-border-200 text-xs rounded flex items-center gap-2 p-2 py-1.5 hover:bg-custom-background-100`}
|
className={`outline-none border border-custom-border-200 text-xs rounded flex items-center gap-2 px-2 py-1.5 hover:bg-custom-background-80 ${
|
||||||
|
open ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="font-medium">{title}</div>
|
<div className="font-medium">{title}</div>
|
||||||
<div className="w-[14px] h-[14px] flex justify-center items-center">
|
<div className={`w-3.5 h-3.5 flex items-center justify-center transition-all ${open ? "" : "rotate-180"}`}>
|
||||||
{open ? <ChevronUp width={14} strokeWidth={2} /> : <ChevronDown width={14} strokeWidth={2} />}
|
<ChevronUp width={14} strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Transition
|
<Transition
|
||||||
@ -33,10 +36,8 @@ export const IssueDropdown = ({ children, title = "Dropdown" }: IIssueDropdown)
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-1 w-[300px] h-[600px]">
|
<Popover.Panel className="absolute right-0 z-10 mt-1 w-[18.75rem] h-auto max-h-[37.5rem] bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded overflow-y-auto">
|
||||||
<div className="w-full h-full overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 shadow-xl">
|
{children}
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</>
|
</>
|
||||||
|
@ -9,13 +9,14 @@ interface IFilterHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => (
|
export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => (
|
||||||
<div className="flex items-center justify-between gap-2 p-[6px] pb-1 bg-custom-background-100 sticky top-0">
|
<div className="flex items-center justify-between gap-2 p-1.5 pb-1 bg-custom-background-100 sticky top-0">
|
||||||
<div className="text-gray-500 text-xs text-custom-text-300 font-medium">{title}</div>
|
<div className="text-custom-text-300 text-xs font-medium flex-grow truncate">{title}</div>
|
||||||
<div
|
<button
|
||||||
className="flex-shrink-0 w-[20px] h-[20px] flex justify-center items-center rounded transition-all hover:bg-custom-background-80 cursor-pointer"
|
type="button"
|
||||||
|
className="flex-shrink-0 w-5 h-5 grid place-items-center rounded hover:bg-custom-background-80"
|
||||||
onClick={handleIsPreviewEnabled}
|
onClick={handleIsPreviewEnabled}
|
||||||
>
|
>
|
||||||
{isPreviewEnabled ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
{isPreviewEnabled ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,27 +2,34 @@ import React from "react";
|
|||||||
// lucide icons
|
// lucide icons
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
interface IFilterOption {
|
type Props = {
|
||||||
isChecked: boolean;
|
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
title: string;
|
isChecked: boolean;
|
||||||
multiple?: boolean;
|
title: React.ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
multiple?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const FilterOption = ({ isChecked, icon, title, multiple = true, onClick }: IFilterOption) => (
|
export const FilterOption: React.FC<Props> = (props) => {
|
||||||
<div
|
const { icon, isChecked, multiple = true, onClick, title } = props;
|
||||||
className="flex items-center gap-3 cursor-pointer rounded p-[6px] py-[5px] transition-all hover:bg-custom-border-100"
|
|
||||||
onClick={onClick}
|
return (
|
||||||
>
|
<button
|
||||||
<div
|
type="button"
|
||||||
className={`flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center border border-custom-border-300 bg-custom-background-90 ${
|
className="flex items-center gap-2 rounded p-1.5 hover:bg-custom-background-80 w-full"
|
||||||
isChecked ? `bg-custom-primary-300 text-white` : ``
|
onClick={onClick}
|
||||||
} ${multiple ? `rounded-sm` : `rounded-full`}`}
|
|
||||||
>
|
>
|
||||||
{isChecked && <Check size={10} strokeWidth={2} />}
|
<div
|
||||||
</div>
|
className={`flex-shrink-0 w-3 h-3 grid place-items-center bg-custom-background-90 border ${
|
||||||
{icon}
|
isChecked ? "bg-custom-primary-100 border-custom-primary-100 text-white" : "border-custom-border-300"
|
||||||
<div className="hyphens-auto line-clamp-1 text-custom-text-200 text-xs w-full">{title}</div>
|
} ${multiple ? "rounded-sm" : "rounded-full"}`}
|
||||||
</div>
|
>
|
||||||
);
|
{isChecked && <Check size={10} strokeWidth={3} />}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 truncate">
|
||||||
|
<div className="flex-shrink-0 grid place-items-center">{icon}</div>
|
||||||
|
<div className="flex-grow truncate text-custom-text-200 text-xs">{title}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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";
|
import React from "react";
|
||||||
// lucide icons
|
|
||||||
import { Columns, Grid3x3, Calendar, GanttChart, List } from "lucide-react";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// mobx store
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// types and default data
|
|
||||||
import { TIssueLayouts } from "store/issue_filters.legacy";
|
|
||||||
import { issueFilterVisibilityData } from "store/helpers/issue-data";
|
|
||||||
|
|
||||||
export const LayoutSelection = observer(() => {
|
// types
|
||||||
const store: RootStore = useMobxStore();
|
import { TIssueLayouts } from "types";
|
||||||
const { issueFilters: issueFilterStore, issueView: issueStore } = store;
|
// constants
|
||||||
|
import { ISSUE_LAYOUTS } from "constants/issue";
|
||||||
|
|
||||||
const layoutSelectionFilters: { key: TIssueLayouts; title: string; icon: any }[] = [
|
type Props = {
|
||||||
{
|
layouts: TIssueLayouts[];
|
||||||
key: "list",
|
onChange: (layout: TIssueLayouts) => void;
|
||||||
title: "List",
|
selectedLayout: TIssueLayouts;
|
||||||
icon: List,
|
};
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "kanban",
|
|
||||||
title: "Kanban",
|
|
||||||
icon: Grid3x3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "calendar",
|
|
||||||
title: "Calendar",
|
|
||||||
icon: Calendar,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "spreadsheet",
|
|
||||||
title: "Spreadsheet",
|
|
||||||
icon: Columns,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "gantt_chart",
|
|
||||||
title: "Gantt",
|
|
||||||
icon: GanttChart,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleLayoutSectionVisibility = (layout_key: string) =>
|
export const LayoutSelection: React.FC<Props> = (props) => {
|
||||||
issueFilterStore?.issueView &&
|
const { layouts, onChange, selectedLayout } = props;
|
||||||
issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"].layout.includes(
|
|
||||||
layout_key
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleLayoutSelection = (_layoutKey: string) => {
|
|
||||||
issueFilterStore.handleUserFilter("display_filters", "layout", _layoutKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
// console.log("----");
|
|
||||||
// console.log("my_user_id", issueFilterStore.myUserId);
|
|
||||||
// console.log("workspace_id", issueFilterStore.workspaceId);
|
|
||||||
// console.log("project_id", issueFilterStore.projectId);
|
|
||||||
// console.log("module_id", issueFilterStore.moduleId);
|
|
||||||
// console.log("cycle_id", issueFilterStore.cycleId);
|
|
||||||
// console.log("view_id", issueFilterStore.viewId);
|
|
||||||
|
|
||||||
// console.log("issue_view", issueFilterStore.issueView);
|
|
||||||
// console.log("issue_layout", issueFilterStore.issueLayout);
|
|
||||||
|
|
||||||
// console.log("user_filters", issueFilterStore.userFilters);
|
|
||||||
// console.log("issues", issueStore.issues);
|
|
||||||
// console.log("issues", issueStore.getIssues);
|
|
||||||
// console.log("----");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center p-1 rounded gap-1 bg-custom-background-80">
|
<div className="flex items-center gap-1 p-1 rounded bg-custom-background-80">
|
||||||
{layoutSelectionFilters.map(
|
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
|
||||||
(_layout) =>
|
<button
|
||||||
handleLayoutSectionVisibility(_layout?.key) && (
|
key={layout.key}
|
||||||
<div
|
type="button"
|
||||||
key={_layout?.key}
|
className={`w-7 h-[22px] rounded grid place-items-center transition-all hover:bg-custom-background-100 overflow-hidden group ${
|
||||||
className={`w-[28px] h-[22px] rounded flex justify-center items-center cursor-pointer transition-all hover:bg-custom-background-100 overflow-hidden group ${
|
selectedLayout == layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
|
||||||
issueFilterStore?.issueLayout == _layout?.key ? `bg-custom-background-100 shadow shadow-gray-200` : ``
|
}`}
|
||||||
}}`}
|
onClick={() => onChange(layout.key)}
|
||||||
onClick={() => handleLayoutSelection(_layout?.key)}
|
>
|
||||||
>
|
<layout.icon
|
||||||
<_layout.icon
|
size={14}
|
||||||
size={14}
|
strokeWidth={2}
|
||||||
strokeWidth={2}
|
className={`${selectedLayout == layout.key ? "text-custom-text-100" : "text-custom-text-200"}`}
|
||||||
className={`${
|
/>
|
||||||
issueFilterStore?.issueLayout == _layout?.key
|
</button>
|
||||||
? `text-custom-text-100`
|
))}
|
||||||
: `text-custom-text-100 group-hover:text-custom-text-200`
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
// components
|
// components
|
||||||
import { LayoutSelection } from "./layout-selection";
|
import { LayoutSelection } from "./layout-selection";
|
||||||
import { IssueDropdown } from "./helpers/dropdown";
|
import { IssueDropdown } from "./helpers/dropdown";
|
||||||
import { FilterSelection } from "./filters";
|
import { FilterSelection } from "./filters/filter-selection";
|
||||||
import { DisplayFiltersSelection } from "./display-filters";
|
import { DisplayFiltersSelection } from "./display-filters";
|
||||||
|
|
||||||
import { FilterPreview } from "./filters-preview";
|
import { FilterPreview } from "./filters-preview";
|
||||||
|
@ -18,11 +18,11 @@ import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-materia
|
|||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { Properties, TIssueViewOptions } from "types";
|
import { Properties, TIssueLayouts } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||||
|
|
||||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
|
||||||
{
|
{
|
||||||
type: "list",
|
type: "list",
|
||||||
Icon: FormatListBulletedOutlined,
|
Icon: FormatListBulletedOutlined,
|
||||||
@ -37,8 +37,9 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } =
|
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } = useMyIssuesFilters(
|
||||||
useMyIssuesFilters(workspaceSlug?.toString());
|
workspaceSlug?.toString()
|
||||||
|
);
|
||||||
|
|
||||||
const { isEstimateActive } = useEstimateOption();
|
const { isEstimateActive } = useEstimateOption();
|
||||||
|
|
||||||
@ -48,9 +49,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
{issueViewOptions.map((option) => (
|
{issueViewOptions.map((option) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={option.type}
|
key={option.type}
|
||||||
tooltipContent={
|
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
|
||||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
|
||||||
}
|
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -88,9 +87,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
|
|
||||||
if (valueExists)
|
if (valueExists)
|
||||||
setFilters({
|
setFilters({
|
||||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
|
||||||
(val) => val !== option.value
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
setFilters({
|
setFilters({
|
||||||
@ -106,9 +103,7 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||||
open
|
open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
|
||||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
|
||||||
: "text-custom-sidebar-text-200"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Display
|
Display
|
||||||
@ -127,89 +122,75 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2 divide-custom-border-200">
|
<div className="relative divide-y-2 divide-custom-border-200">
|
||||||
<div className="space-y-4 pb-3 text-xs">
|
<div className="space-y-4 pb-3 text-xs">
|
||||||
{displayFilters?.layout !== "calendar" &&
|
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
|
||||||
displayFilters?.layout !== "spreadsheet" && (
|
<>
|
||||||
<>
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<h4 className="text-custom-text-200">Group by</h4>
|
||||||
<h4 className="text-custom-text-200">Group by</h4>
|
<div className="w-28">
|
||||||
<div className="w-28">
|
<CustomMenu
|
||||||
<CustomMenu
|
label={
|
||||||
label={
|
displayFilters?.group_by === "project"
|
||||||
displayFilters?.group_by === "project"
|
? "Project"
|
||||||
? "Project"
|
: GROUP_BY_OPTIONS.find((option) => option.key === displayFilters?.group_by)?.name ??
|
||||||
: GROUP_BY_OPTIONS.find(
|
"Select"
|
||||||
(option) => option.key === displayFilters?.group_by
|
}
|
||||||
)?.name ?? "Select"
|
className="!w-full"
|
||||||
}
|
buttonClassName="w-full"
|
||||||
className="!w-full"
|
>
|
||||||
buttonClassName="w-full"
|
{GROUP_BY_OPTIONS.map((option) => {
|
||||||
>
|
if (displayFilters?.layout === "kanban" && option.key === null) return null;
|
||||||
{GROUP_BY_OPTIONS.map((option) => {
|
if (option.key === "state" || option.key === "created_by" || option.key === "assignees")
|
||||||
if (displayFilters?.layout === "kanban" && option.key === null)
|
return null;
|
||||||
return null;
|
|
||||||
if (
|
|
||||||
option.key === "state" ||
|
|
||||||
option.key === "created_by" ||
|
|
||||||
option.key === "assignees"
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.name}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
</div>
|
||||||
<h4 className="text-custom-text-200">Order by</h4>
|
<div className="flex items-center justify-between">
|
||||||
<div className="w-28">
|
<h4 className="text-custom-text-200">Order by</h4>
|
||||||
<CustomMenu
|
<div className="w-28">
|
||||||
label={
|
<CustomMenu
|
||||||
ORDER_BY_OPTIONS.find(
|
label={
|
||||||
(option) => option.key === displayFilters?.order_by
|
ORDER_BY_OPTIONS.find((option) => option.key === displayFilters?.order_by)?.name ??
|
||||||
)?.name ?? "Select"
|
"Select"
|
||||||
}
|
}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
>
|
>
|
||||||
{ORDER_BY_OPTIONS.map((option) => {
|
{ORDER_BY_OPTIONS.map((option) => {
|
||||||
if (
|
if (displayFilters?.group_by === "priority" && option.key === "priority") return null;
|
||||||
displayFilters?.group_by === "priority" &&
|
if (option.key === "sort_order") return null;
|
||||||
option.key === "priority"
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
if (option.key === "sort_order") return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisplayFilters({ order_by: option.key });
|
setDisplayFilters({ order_by: option.key });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.name}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Issue type</h4>
|
<h4 className="text-custom-text-200">Issue type</h4>
|
||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
FILTER_ISSUE_OPTIONS.find(
|
FILTER_ISSUE_OPTIONS.find((option) => option.key === displayFilters?.type)?.name ?? "Select"
|
||||||
(option) => option.key === displayFilters?.type
|
|
||||||
)?.name ?? "Select"
|
|
||||||
}
|
}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
@ -230,24 +211,23 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{displayFilters?.layout !== "calendar" &&
|
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
|
||||||
displayFilters?.layout !== "spreadsheet" && (
|
<>
|
||||||
<>
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
<div className="w-28">
|
||||||
<div className="w-28">
|
<ToggleSwitch
|
||||||
<ToggleSwitch
|
value={displayFilters?.show_empty_groups ?? true}
|
||||||
value={displayFilters?.show_empty_groups ?? true}
|
onChange={() =>
|
||||||
onChange={() =>
|
setDisplayFilters({
|
||||||
setDisplayFilters({
|
show_empty_groups: !displayFilters?.show_empty_groups,
|
||||||
show_empty_groups: !displayFilters?.show_empty_groups,
|
})
|
||||||
})
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 py-3">
|
<div className="space-y-2 py-3">
|
||||||
@ -258,16 +238,11 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
displayFilters?.layout === "spreadsheet" &&
|
displayFilters?.layout === "spreadsheet" &&
|
||||||
(key === "attachment_count" ||
|
(key === "attachment_count" || key === "link" || key === "sub_issue_count")
|
||||||
key === "link" ||
|
|
||||||
key === "sub_issue_count")
|
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (
|
if (displayFilters?.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
|
||||||
displayFilters?.layout !== "spreadsheet" &&
|
|
||||||
(key === "created_on" || key === "updated_on")
|
|
||||||
)
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -18,12 +18,12 @@ import { FormatListBulletedOutlined, GridViewOutlined } from "@mui/icons-materia
|
|||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||||
// types
|
// types
|
||||||
import { Properties, TIssueViewOptions } from "types";
|
import { Properties, TIssueLayouts } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
|
|
||||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
|
||||||
{
|
{
|
||||||
type: "list",
|
type: "list",
|
||||||
Icon: FormatListBulletedOutlined,
|
Icon: FormatListBulletedOutlined,
|
||||||
@ -40,14 +40,10 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
|
|
||||||
const { projects } = useProjects();
|
const { projects } = useProjects();
|
||||||
|
|
||||||
const {
|
const { displayFilters, setDisplayFilters, filters, displayProperties, setProperties, setFilters } = useProfileIssues(
|
||||||
displayFilters,
|
workspaceSlug?.toString(),
|
||||||
setDisplayFilters,
|
userId?.toString()
|
||||||
filters,
|
);
|
||||||
displayProperties,
|
|
||||||
setProperties,
|
|
||||||
setFilters,
|
|
||||||
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
|
|
||||||
|
|
||||||
const { isEstimateActive } = useEstimateOption();
|
const { isEstimateActive } = useEstimateOption();
|
||||||
|
|
||||||
@ -80,9 +76,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
{issueViewOptions.map((option) => (
|
{issueViewOptions.map((option) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={option.type}
|
key={option.type}
|
||||||
tooltipContent={
|
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
|
||||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
|
||||||
}
|
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -120,9 +114,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
|
|
||||||
if (valueExists)
|
if (valueExists)
|
||||||
setFilters({
|
setFilters({
|
||||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
|
||||||
(val) => val !== option.value
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
setFilters({
|
setFilters({
|
||||||
@ -138,9 +130,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Popover.Button
|
<Popover.Button
|
||||||
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 bg-transparent px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||||
open
|
open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
|
||||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
|
||||||
: "text-custom-sidebar-text-200"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Display
|
Display
|
||||||
@ -159,89 +149,75 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2 divide-custom-border-200">
|
<div className="relative divide-y-2 divide-custom-border-200">
|
||||||
<div className="space-y-4 pb-3 text-xs">
|
<div className="space-y-4 pb-3 text-xs">
|
||||||
{displayFilters?.layout !== "calendar" &&
|
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
|
||||||
displayFilters?.layout !== "spreadsheet" && (
|
<>
|
||||||
<>
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<h4 className="text-custom-text-200">Group by</h4>
|
||||||
<h4 className="text-custom-text-200">Group by</h4>
|
<div className="w-28">
|
||||||
<div className="w-28">
|
<CustomMenu
|
||||||
<CustomMenu
|
label={
|
||||||
label={
|
displayFilters?.group_by === "project"
|
||||||
displayFilters?.group_by === "project"
|
? "Project"
|
||||||
? "Project"
|
: GROUP_BY_OPTIONS.find((option) => option.key === displayFilters?.group_by)?.name ??
|
||||||
: GROUP_BY_OPTIONS.find(
|
"Select"
|
||||||
(option) => option.key === displayFilters?.group_by
|
}
|
||||||
)?.name ?? "Select"
|
className="!w-full"
|
||||||
}
|
buttonClassName="w-full"
|
||||||
className="!w-full"
|
>
|
||||||
buttonClassName="w-full"
|
{GROUP_BY_OPTIONS.map((option) => {
|
||||||
>
|
if (displayFilters?.layout === "kanban" && option.key === null) return null;
|
||||||
{GROUP_BY_OPTIONS.map((option) => {
|
if (option.key === "state" || option.key === "created_by" || option.key === "assignees")
|
||||||
if (displayFilters?.layout === "kanban" && option.key === null)
|
return null;
|
||||||
return null;
|
|
||||||
if (
|
|
||||||
option.key === "state" ||
|
|
||||||
option.key === "created_by" ||
|
|
||||||
option.key === "assignees"
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.name}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
</div>
|
||||||
<h4 className="text-custom-text-200">Order by</h4>
|
<div className="flex items-center justify-between">
|
||||||
<div className="w-28">
|
<h4 className="text-custom-text-200">Order by</h4>
|
||||||
<CustomMenu
|
<div className="w-28">
|
||||||
label={
|
<CustomMenu
|
||||||
ORDER_BY_OPTIONS.find(
|
label={
|
||||||
(option) => option.key === displayFilters?.order_by
|
ORDER_BY_OPTIONS.find((option) => option.key === displayFilters?.order_by)?.name ??
|
||||||
)?.name ?? "Select"
|
"Select"
|
||||||
}
|
}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
>
|
>
|
||||||
{ORDER_BY_OPTIONS.map((option) => {
|
{ORDER_BY_OPTIONS.map((option) => {
|
||||||
if (
|
if (displayFilters?.group_by === "priority" && option.key === "priority") return null;
|
||||||
displayFilters?.group_by === "priority" &&
|
if (option.key === "sort_order") return null;
|
||||||
option.key === "priority"
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
if (option.key === "sort_order") return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDisplayFilters({ order_by: option.key });
|
setDisplayFilters({ order_by: option.key });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{option.name}
|
{option.name}
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-custom-text-200">Issue type</h4>
|
<h4 className="text-custom-text-200">Issue type</h4>
|
||||||
<div className="w-28">
|
<div className="w-28">
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
FILTER_ISSUE_OPTIONS.find(
|
FILTER_ISSUE_OPTIONS.find((option) => option.key === displayFilters?.type)?.name ?? "Select"
|
||||||
(option) => option.key === displayFilters?.type
|
|
||||||
)?.name ?? "Select"
|
|
||||||
}
|
}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
buttonClassName="w-full"
|
buttonClassName="w-full"
|
||||||
@ -262,24 +238,23 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{displayFilters?.layout !== "calendar" &&
|
{displayFilters?.layout !== "calendar" && displayFilters?.layout !== "spreadsheet" && (
|
||||||
displayFilters?.layout !== "spreadsheet" && (
|
<>
|
||||||
<>
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
<div className="w-28">
|
||||||
<div className="w-28">
|
<ToggleSwitch
|
||||||
<ToggleSwitch
|
value={displayFilters?.show_empty_groups ?? true}
|
||||||
value={displayFilters?.show_empty_groups ?? true}
|
onChange={() =>
|
||||||
onChange={() =>
|
setDisplayFilters({
|
||||||
setDisplayFilters({
|
show_empty_groups: !displayFilters?.show_empty_groups,
|
||||||
show_empty_groups: !displayFilters?.show_empty_groups,
|
})
|
||||||
})
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 py-3">
|
<div className="space-y-2 py-3">
|
||||||
@ -290,16 +265,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
displayFilters?.layout === "spreadsheet" &&
|
displayFilters?.layout === "spreadsheet" &&
|
||||||
(key === "attachment_count" ||
|
(key === "attachment_count" || key === "link" || key === "sub_issue_count")
|
||||||
key === "link" ||
|
|
||||||
key === "sub_issue_count")
|
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (
|
if (displayFilters?.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
|
||||||
displayFilters?.layout !== "spreadsheet" &&
|
|
||||||
(key === "created_on" || key === "updated_on")
|
|
||||||
)
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,21 @@
|
|||||||
export const ISSUE_PRIORITIES = [
|
// icons
|
||||||
|
import { Calendar, GanttChart, Kanban, List, Sheet } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueDisplayProperties,
|
||||||
|
TIssueGroupByOptions,
|
||||||
|
TIssueLayouts,
|
||||||
|
TIssueOrderByOptions,
|
||||||
|
TIssuePriorities,
|
||||||
|
TIssueTypeFilters,
|
||||||
|
TStateGroups,
|
||||||
|
} from "types";
|
||||||
|
|
||||||
|
export const ISSUE_PRIORITIES: {
|
||||||
|
key: TIssuePriorities;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
{ key: "urgent", title: "Urgent" },
|
{ key: "urgent", title: "Urgent" },
|
||||||
{ key: "high", title: "High" },
|
{ key: "high", title: "High" },
|
||||||
{ key: "medium", title: "Medium" },
|
{ key: "medium", title: "Medium" },
|
||||||
@ -6,7 +23,10 @@ export const ISSUE_PRIORITIES = [
|
|||||||
{ key: "none", title: "None" },
|
{ key: "none", title: "None" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ISSUE_STATE_GROUPS = [
|
export const ISSUE_STATE_GROUPS: {
|
||||||
|
key: TStateGroups;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
{ key: "backlog", title: "Backlog" },
|
{ key: "backlog", title: "Backlog" },
|
||||||
{ key: "unstarted", title: "Unstarted" },
|
{ key: "unstarted", title: "Unstarted" },
|
||||||
{ key: "started", title: "Started" },
|
{ key: "started", title: "Started" },
|
||||||
@ -30,7 +50,10 @@ export const ISSUE_DUE_DATE_OPTIONS = [
|
|||||||
{ key: "custom", title: "Custom" },
|
{ key: "custom", title: "Custom" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ISSUE_GROUP_BY_OPTIONS = [
|
export const ISSUE_GROUP_BY_OPTIONS: {
|
||||||
|
key: TIssueGroupByOptions;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
{ key: "state", title: "States" },
|
{ key: "state", title: "States" },
|
||||||
{ key: "state_detail.group", title: "State Groups" },
|
{ key: "state_detail.group", title: "State Groups" },
|
||||||
{ key: "priority", title: "Priority" },
|
{ key: "priority", title: "Priority" },
|
||||||
@ -38,24 +61,34 @@ export const ISSUE_GROUP_BY_OPTIONS = [
|
|||||||
{ key: "labels", title: "Labels" },
|
{ key: "labels", title: "Labels" },
|
||||||
{ key: "assignees", title: "Assignees" },
|
{ key: "assignees", title: "Assignees" },
|
||||||
{ key: "created_by", title: "Created By" },
|
{ key: "created_by", title: "Created By" },
|
||||||
|
{ key: null, title: "None" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ISSUE_ORDER_BY_OPTIONS = [
|
export const ISSUE_ORDER_BY_OPTIONS: {
|
||||||
|
key: TIssueOrderByOptions;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
{ key: "sort_order", title: "Manual" },
|
{ key: "sort_order", title: "Manual" },
|
||||||
{ key: "created_at", title: "Last Created" },
|
{ key: "-created_at", title: "Last Created" },
|
||||||
{ key: "updated_at", title: "Last Updated" },
|
{ key: "-updated_at", title: "Last Updated" },
|
||||||
{ key: "start_date", title: "Start Date" },
|
{ key: "start_date", title: "Start Date" },
|
||||||
{ key: "priority", title: "Priority" },
|
{ key: "priority", title: "Priority" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ISSUE_FILTER_OPTIONS = [
|
export const ISSUE_FILTER_OPTIONS: {
|
||||||
{ key: "all", title: "All" },
|
key: TIssueTypeFilters;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
|
{ key: null, title: "All" },
|
||||||
{ key: "active", title: "Active Issues" },
|
{ key: "active", title: "Active Issues" },
|
||||||
{ key: "backlog", title: "Backlog Issues" },
|
{ key: "backlog", title: "Backlog Issues" },
|
||||||
// { key: "draft", title: "Draft Issues" },
|
// { key: "draft", title: "Draft Issues" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ISSUE_DISPLAY_PROPERTIES = [
|
export const ISSUE_DISPLAY_PROPERTIES: {
|
||||||
|
key: keyof IIssueDisplayProperties;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
{ key: "assignee", title: "Assignee" },
|
{ key: "assignee", title: "Assignee" },
|
||||||
{ key: "start_date", title: "Start Date" },
|
{ key: "start_date", title: "Start Date" },
|
||||||
{ key: "due_date", title: "Due Date" },
|
{ key: "due_date", title: "Due Date" },
|
||||||
@ -69,19 +102,26 @@ export const ISSUE_DISPLAY_PROPERTIES = [
|
|||||||
{ key: "estimate", title: "Estimate" },
|
{ key: "estimate", title: "Estimate" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ISSUE_EXTRA_PROPERTIES = [
|
export const ISSUE_EXTRA_OPTIONS: {
|
||||||
|
key: keyof IIssueDisplayFilterOptions;
|
||||||
|
title: string;
|
||||||
|
}[] = [
|
||||||
{ key: "sub_issue", title: "Show sub-issues" }, // in spreadsheet its always false
|
{ key: "sub_issue", title: "Show sub-issues" }, // in spreadsheet its always false
|
||||||
{ key: "show_empty_groups", title: "Show empty states" }, // filter on front-end
|
{ key: "show_empty_groups", title: "Show empty states" }, // filter on front-end
|
||||||
{ key: "calendar_date_range", title: "Calendar Date Range" }, // calendar date range yyyy-mm-dd;before range yyyy-mm-dd;after
|
{ key: "calendar_date_range", title: "Calendar Date Range" }, // calendar date range yyyy-mm-dd;before range yyyy-mm-dd;after
|
||||||
{ key: "start_target_date", title: "Start target Date" }, // gantt always be true
|
{ key: "start_target_date", title: "Start target Date" }, // gantt always be true
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ISSUE_LAYOUTS = [
|
export const ISSUE_LAYOUTS: {
|
||||||
{ key: "list", title: "List View" },
|
key: TIssueLayouts;
|
||||||
{ key: "kanban", title: "Kanban View" },
|
title: string;
|
||||||
{ key: "calendar", title: "Calendar View" },
|
icon: any;
|
||||||
{ key: "spreadsheet", title: "Spreadsheet View" },
|
}[] = [
|
||||||
{ key: "gantt_chart", title: "Gantt Chart View" },
|
{ key: "list", title: "List View", icon: List },
|
||||||
|
{ key: "kanban", title: "Kanban View", icon: Kanban },
|
||||||
|
{ key: "calendar", title: "Calendar View", icon: Calendar },
|
||||||
|
{ key: "spreadsheet", title: "Spreadsheet View", icon: Sheet },
|
||||||
|
{ key: "gantt_chart", title: "Gantt Chart View", icon: GanttChart },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ISSUE_LIST_FILTERS = [
|
export const ISSUE_LIST_FILTERS = [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { orderArrayBy } from "helpers/array.helper";
|
import { orderArrayBy } from "helpers/array.helper";
|
||||||
import { renderDateFormat } from "helpers/date-time.helper";
|
import { renderDateFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, TIssueGroupByOptions, TIssueOrderByOptions } from "types";
|
import { IIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams } from "types";
|
||||||
|
|
||||||
type THandleIssuesMutation = (
|
type THandleIssuesMutation = (
|
||||||
formData: Partial<IIssue>,
|
formData: Partial<IIssue>,
|
||||||
@ -79,24 +79,6 @@ export const handleIssuesMutation: THandleIssuesMutation = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
|
||||||
export type TIssueParams =
|
|
||||||
| "priority"
|
|
||||||
| "state_group"
|
|
||||||
| "state"
|
|
||||||
| "assignees"
|
|
||||||
| "created_by"
|
|
||||||
| "labels"
|
|
||||||
| "start_date"
|
|
||||||
| "target_date"
|
|
||||||
| "group_by"
|
|
||||||
| "order_by"
|
|
||||||
| "type"
|
|
||||||
| "sub_issue"
|
|
||||||
| "show_empty_groups"
|
|
||||||
| "calendar_date_range"
|
|
||||||
| "start_target_date";
|
|
||||||
|
|
||||||
export const handleIssueQueryParamsByLayout = (_layout: TIssueLayouts | undefined): TIssueParams[] | null => {
|
export const handleIssueQueryParamsByLayout = (_layout: TIssueLayouts | undefined): TIssueParams[] | null => {
|
||||||
if (_layout === "list")
|
if (_layout === "list")
|
||||||
return [
|
return [
|
||||||
@ -197,3 +179,117 @@ export const handleIssueParamsDateFormat = (key: string, start_date: any | null,
|
|||||||
if (key === "custom" && start_date && target_date)
|
if (key === "custom" && start_date && target_date)
|
||||||
return `${renderDateFormat(start_date)};after,${renderDateFormat(target_date)};before`;
|
return `${renderDateFormat(start_date)};after,${renderDateFormat(target_date)};before`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const issueFilterVisibilityData: {
|
||||||
|
[key: string]: {
|
||||||
|
layout: TIssueLayouts[];
|
||||||
|
filters: {
|
||||||
|
[key in TIssueLayouts]: string[];
|
||||||
|
};
|
||||||
|
display_properties: {
|
||||||
|
[key in TIssueLayouts]: boolean;
|
||||||
|
};
|
||||||
|
display_filters: {
|
||||||
|
[key in TIssueLayouts]: string[];
|
||||||
|
};
|
||||||
|
extra_options: {
|
||||||
|
[key in TIssueLayouts]: {
|
||||||
|
access: boolean;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
my_issues: {
|
||||||
|
layout: ["list", "kanban"],
|
||||||
|
filters: {
|
||||||
|
list: ["priority", "state_group", "labels", "start_date", "due_date"],
|
||||||
|
kanban: ["priority", "state_group", "labels", "start_date", "due_date"],
|
||||||
|
calendar: [],
|
||||||
|
spreadsheet: [],
|
||||||
|
gantt_chart: [],
|
||||||
|
},
|
||||||
|
display_properties: {
|
||||||
|
list: true,
|
||||||
|
kanban: true,
|
||||||
|
calendar: true,
|
||||||
|
spreadsheet: true,
|
||||||
|
gantt_chart: false,
|
||||||
|
},
|
||||||
|
display_filters: {
|
||||||
|
list: ["group_by", "order_by", "issue_type"],
|
||||||
|
kanban: ["group_by", "order_by", "issue_type"],
|
||||||
|
calendar: ["issue_type"],
|
||||||
|
spreadsheet: ["issue_type"],
|
||||||
|
gantt_chart: ["order_by", "issue_type"],
|
||||||
|
},
|
||||||
|
extra_options: {
|
||||||
|
list: {
|
||||||
|
access: true,
|
||||||
|
values: ["show_empty_groups", "sub_issue"],
|
||||||
|
},
|
||||||
|
kanban: {
|
||||||
|
access: true,
|
||||||
|
values: ["show_empty_groups", "sub_issue"],
|
||||||
|
},
|
||||||
|
calendar: {
|
||||||
|
access: false,
|
||||||
|
values: [],
|
||||||
|
},
|
||||||
|
spreadsheet: {
|
||||||
|
access: false,
|
||||||
|
values: [],
|
||||||
|
},
|
||||||
|
gantt_chart: {
|
||||||
|
access: false,
|
||||||
|
values: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
issues: {
|
||||||
|
layout: ["list", "kanban", "calendar", "spreadsheet", "gantt_chart"],
|
||||||
|
filters: {
|
||||||
|
list: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
|
||||||
|
kanban: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
|
||||||
|
calendar: ["priority", "state", "assignees", "created_by", "labels"],
|
||||||
|
spreadsheet: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
|
||||||
|
gantt_chart: ["priority", "state", "assignees", "created_by", "labels", "start_date", "due_date"],
|
||||||
|
},
|
||||||
|
display_properties: {
|
||||||
|
list: true,
|
||||||
|
kanban: true,
|
||||||
|
calendar: true,
|
||||||
|
spreadsheet: true,
|
||||||
|
gantt_chart: false,
|
||||||
|
},
|
||||||
|
display_filters: {
|
||||||
|
list: ["group_by", "order_by", "issue_type"],
|
||||||
|
kanban: ["group_by", "order_by", "issue_type"],
|
||||||
|
calendar: ["issue_type"],
|
||||||
|
spreadsheet: ["issue_type"],
|
||||||
|
gantt_chart: ["order_by", "issue_type"],
|
||||||
|
},
|
||||||
|
extra_options: {
|
||||||
|
list: {
|
||||||
|
access: true,
|
||||||
|
values: ["show_empty_groups", "sub_issue"],
|
||||||
|
},
|
||||||
|
kanban: {
|
||||||
|
access: true,
|
||||||
|
values: ["show_empty_groups", "sub_issue"],
|
||||||
|
},
|
||||||
|
calendar: {
|
||||||
|
access: false,
|
||||||
|
values: [],
|
||||||
|
},
|
||||||
|
spreadsheet: {
|
||||||
|
access: false,
|
||||||
|
values: [],
|
||||||
|
},
|
||||||
|
gantt_chart: {
|
||||||
|
access: true,
|
||||||
|
values: ["sub_issue"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// mobx
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import inboxService from "services/inbox.service";
|
import inboxService from "services/inbox.service";
|
||||||
@ -15,8 +16,9 @@ import { IssueViewContextProvider } from "contexts/issue-view.context";
|
|||||||
// helper
|
// helper
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
// components
|
// components
|
||||||
import { IssuesFilterView, IssuesView } from "components/core";
|
import { IssuesView } from "components/core";
|
||||||
import { AnalyticsProjectModal } from "components/analytics";
|
import { AnalyticsProjectModal } from "components/analytics";
|
||||||
|
import { ProjectIssuesHeader } from "components/headers";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
@ -25,7 +27,14 @@ import { PlusIcon } from "@heroicons/react/24/outline";
|
|||||||
// types
|
// types
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_DETAILS, INBOX_LIST } from "constants/fetch-keys";
|
import {
|
||||||
|
PROJECT_DETAILS,
|
||||||
|
INBOX_LIST,
|
||||||
|
STATES_LIST,
|
||||||
|
PROJECT_ISSUE_LABELS,
|
||||||
|
PROJECT_MEMBERS,
|
||||||
|
USER_PROJECT_VIEW,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
const ProjectIssues: NextPage = () => {
|
const ProjectIssues: NextPage = () => {
|
||||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||||
@ -33,17 +42,43 @@ const ProjectIssues: NextPage = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore();
|
||||||
|
|
||||||
const { data: projectDetails } = useSWR(
|
const { data: projectDetails } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
|
||||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: inboxList } = useSWR(
|
const { data: inboxList } = useSWR(
|
||||||
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
|
? () => issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId ? STATES_LIST(projectId.toString()) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
useSWR(
|
||||||
|
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId.toString()) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -53,49 +88,47 @@ const ProjectIssues: NextPage = () => {
|
|||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`} />
|
||||||
title={`${truncateText(projectDetails?.name ?? "Project", 32)} Issues`}
|
|
||||||
/>
|
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<div className="flex items-center gap-2">
|
<ProjectIssuesHeader />
|
||||||
<IssuesFilterView />
|
// <div className="flex items-center gap-2">
|
||||||
<SecondaryButton
|
// <SecondaryButton
|
||||||
onClick={() => setAnalyticsModal(true)}
|
// onClick={() => setAnalyticsModal(true)}
|
||||||
className="!py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
|
// className="!py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
|
||||||
outline
|
// outline
|
||||||
>
|
// >
|
||||||
Analytics
|
// Analytics
|
||||||
</SecondaryButton>
|
// </SecondaryButton>
|
||||||
{projectDetails && projectDetails.inbox_view && (
|
// {projectDetails && projectDetails.inbox_view && (
|
||||||
<Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
|
// <Link href={`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`}>
|
||||||
<a>
|
// <a>
|
||||||
<SecondaryButton
|
// <SecondaryButton
|
||||||
className="relative !py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
|
// className="relative !py-1.5 rounded-md font-normal text-custom-sidebar-text-200 border-custom-border-200 hover:text-custom-text-100 hover:bg-custom-sidebar-background-90"
|
||||||
outline
|
// outline
|
||||||
>
|
// >
|
||||||
<span>Inbox</span>
|
// <span>Inbox</span>
|
||||||
{inboxList && inboxList?.[0]?.pending_issue_count !== 0 && (
|
// {inboxList && inboxList?.[0]?.pending_issue_count !== 0 && (
|
||||||
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
|
// <span className="absolute -top-1 -right-1 h-4 w-4 rounded-full text-custom-text-100 bg-custom-sidebar-background-80 border border-custom-sidebar-border-200">
|
||||||
{inboxList?.[0]?.pending_issue_count}
|
// {inboxList?.[0]?.pending_issue_count}
|
||||||
</span>
|
// </span>
|
||||||
)}
|
// )}
|
||||||
</SecondaryButton>
|
// </SecondaryButton>
|
||||||
</a>
|
// </a>
|
||||||
</Link>
|
// </Link>
|
||||||
)}
|
// )}
|
||||||
<PrimaryButton
|
// <PrimaryButton
|
||||||
className="flex items-center gap-2"
|
// className="flex items-center gap-2"
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
// const e = new KeyboardEvent("keydown", { key: "c" });
|
||||||
document.dispatchEvent(e);
|
// document.dispatchEvent(e);
|
||||||
}}
|
// }}
|
||||||
>
|
// >
|
||||||
<PlusIcon className="h-4 w-4" />
|
// <PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
// Add Issue
|
||||||
</PrimaryButton>
|
// </PrimaryButton>
|
||||||
</div>
|
// </div>
|
||||||
}
|
}
|
||||||
bg="secondary"
|
bg="secondary"
|
||||||
>
|
>
|
||||||
|
@ -1,43 +1,74 @@
|
|||||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||||
// types
|
// services
|
||||||
import { RootStore } from "./root";
|
|
||||||
import { ProjectService } from "services/project.service";
|
import { ProjectService } from "services/project.service";
|
||||||
import { IssueService } from "services/issue.service";
|
import { IssueService } from "services/issue.service";
|
||||||
|
// types
|
||||||
|
import { RootStore } from "./root";
|
||||||
|
import {
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueDisplayProperties,
|
||||||
|
IIssueFilterOptions,
|
||||||
|
IProjectViewProps,
|
||||||
|
TIssueParams,
|
||||||
|
} from "types";
|
||||||
|
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
|
||||||
|
|
||||||
export interface IIssueFilterStore {
|
export interface IIssueFilterStore {
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
error: any | null;
|
error: any | null;
|
||||||
userDisplayProperties: any;
|
userDisplayProperties: IIssueDisplayProperties;
|
||||||
userDisplayFilters: any;
|
userDisplayFilters: IIssueDisplayFilterOptions;
|
||||||
userFilters: any;
|
userFilters: IIssueFilterOptions;
|
||||||
defaultDisplayFilters: any;
|
defaultDisplayFilters: IIssueDisplayFilterOptions;
|
||||||
defaultFilters: any;
|
defaultFilters: IIssueFilterOptions;
|
||||||
|
|
||||||
fetchUserFilters: (workspaceSlug: string, projectSlug: string) => void;
|
// action
|
||||||
|
fetchUserProjectFilters: (workspaceSlug: string, projectSlug: string) => Promise<void>;
|
||||||
|
updateUserFilters: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectSlug: string,
|
||||||
|
filterToUpdate: Partial<IProjectViewProps>
|
||||||
|
) => Promise<void>;
|
||||||
|
updateDisplayProperties: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectSlug: string,
|
||||||
|
properties: Partial<IIssueDisplayProperties>
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
// computed
|
||||||
|
appliedFilters: TIssueParams[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class IssueFilterStore implements IIssueFilterStore {
|
class IssueFilterStore implements IIssueFilterStore {
|
||||||
loader: boolean = false;
|
loader: boolean = false;
|
||||||
error: any | null = null;
|
error: any | null = null;
|
||||||
|
|
||||||
// observables
|
// observables
|
||||||
userDisplayProperties: any = {};
|
userDisplayProperties: any = {};
|
||||||
userDisplayFilters: any = {};
|
userDisplayFilters: IIssueDisplayFilterOptions = {};
|
||||||
userFilters: any = {};
|
userFilters: IIssueFilterOptions = {};
|
||||||
defaultDisplayFilters: any = {};
|
defaultDisplayFilters: IIssueDisplayFilterOptions = {};
|
||||||
defaultFilters: any = {};
|
defaultFilters: IIssueFilterOptions = {};
|
||||||
defaultDisplayProperties: any = {
|
defaultDisplayProperties: IIssueDisplayProperties = {
|
||||||
assignee: true,
|
assignee: true,
|
||||||
due_date: true,
|
|
||||||
key: true,
|
|
||||||
labels: true,
|
|
||||||
priority: true,
|
|
||||||
start_date: true,
|
start_date: true,
|
||||||
|
due_date: true,
|
||||||
|
labels: true,
|
||||||
|
key: true,
|
||||||
|
priority: true,
|
||||||
state: true,
|
state: true,
|
||||||
sub_issue_count: true,
|
sub_issue_count: true,
|
||||||
|
link: true,
|
||||||
|
attachment_count: true,
|
||||||
|
estimate: true,
|
||||||
|
created_on: true,
|
||||||
|
updated_on: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
|
|
||||||
|
// services
|
||||||
projectService;
|
projectService;
|
||||||
issueService;
|
issueService;
|
||||||
|
|
||||||
@ -45,12 +76,21 @@ class IssueFilterStore implements IIssueFilterStore {
|
|||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
loader: observable.ref,
|
loader: observable.ref,
|
||||||
error: observable.ref,
|
error: observable.ref,
|
||||||
|
|
||||||
|
// observables
|
||||||
defaultDisplayFilters: observable.ref,
|
defaultDisplayFilters: observable.ref,
|
||||||
defaultFilters: observable.ref,
|
defaultFilters: observable.ref,
|
||||||
userDisplayProperties: observable.ref,
|
userDisplayProperties: observable.ref,
|
||||||
userDisplayFilters: observable.ref,
|
userDisplayFilters: observable.ref,
|
||||||
userFilters: observable.ref,
|
userFilters: observable.ref,
|
||||||
fetchUserFilters: action,
|
|
||||||
|
// actions
|
||||||
|
fetchUserProjectFilters: action,
|
||||||
|
updateUserFilters: action,
|
||||||
|
updateDisplayProperties: action,
|
||||||
|
|
||||||
|
// computed
|
||||||
|
appliedFilters: computed,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
@ -59,27 +99,90 @@ class IssueFilterStore implements IIssueFilterStore {
|
|||||||
this.issueService = new IssueService();
|
this.issueService = new IssueService();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchUserFilters = async (workspaceSlug: string, projectId: string) => {
|
get appliedFilters(): TIssueParams[] | null {
|
||||||
|
return handleIssueQueryParamsByLayout(this.userDisplayFilters.layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUserProjectFilters = async (workspaceSlug: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId);
|
const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId);
|
||||||
const issueProperties = await this.issueService.getIssueProperties(workspaceSlug, projectId);
|
const issueProperties = await this.issueService.getIssueProperties(workspaceSlug, projectId);
|
||||||
|
|
||||||
console.log("memberResponse", memberResponse);
|
|
||||||
|
|
||||||
console.log("issueProperties", issueProperties);
|
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.userFilters = memberResponse?.view_props?.filters;
|
this.userFilters = memberResponse?.view_props?.filters;
|
||||||
this.userDisplayFilters = memberResponse?.view_props?.display_filters;
|
this.userDisplayFilters = memberResponse?.view_props?.display_filters ?? {};
|
||||||
this.userDisplayProperties = issueProperties?.properties || this.defaultDisplayProperties;
|
this.userDisplayProperties = issueProperties?.properties || this.defaultDisplayProperties;
|
||||||
// default props from api
|
// default props from api
|
||||||
this.defaultFilters = memberResponse.default_props.filters;
|
this.defaultFilters = memberResponse.default_props.filters;
|
||||||
this.defaultDisplayFilters = memberResponse.default_props.display_filters;
|
this.defaultDisplayFilters = memberResponse.default_props.display_filters ?? {};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
console.log("Failed to fetch user filters in issue filter store", error);
|
console.log("Failed to fetch user filters in issue filter store", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateUserFilters = async (workspaceSlug: string, projectId: string, filterToUpdate: Partial<IProjectViewProps>) => {
|
||||||
|
const newViewProps = {
|
||||||
|
display_filters: {
|
||||||
|
...this.userDisplayFilters,
|
||||||
|
...filterToUpdate.display_filters,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
...this.userFilters,
|
||||||
|
...filterToUpdate.filters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.userFilters = newViewProps.filters;
|
||||||
|
this.userDisplayFilters = newViewProps.display_filters;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.projectService.setProjectView(workspaceSlug, projectId, {
|
||||||
|
view_props: newViewProps,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Failed to update user filters in issue filter store", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDisplayProperties = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
properties: Partial<IIssueDisplayProperties>
|
||||||
|
) => {
|
||||||
|
const newProperties = {
|
||||||
|
...this.userDisplayProperties,
|
||||||
|
...properties,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.userDisplayProperties = newProperties;
|
||||||
|
});
|
||||||
|
|
||||||
|
// await this.issueService.patchIssueProperties(workspaceSlug, projectId, newProperties);
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Failed to update user filters in issue filter store", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IssueFilterStore;
|
export default IssueFilterStore;
|
||||||
|
@ -264,15 +264,15 @@ class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchProjectLabels = async (workspaceSlug: string, projectSlug: string) => {
|
fetchProjectLabels = async (workspaceSlug: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const labelResponse = await this.issueService.getIssueLabels(workspaceSlug, projectSlug);
|
const labelResponse = await this.issueService.getIssueLabels(workspaceSlug, projectId);
|
||||||
const _labels = {
|
const _labels = {
|
||||||
...this.labels,
|
...this.labels,
|
||||||
[projectSlug]: labelResponse,
|
[projectId]: labelResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
6
web/types/issues.d.ts
vendored
6
web/types/issues.d.ts
vendored
@ -2,19 +2,13 @@ import { KeyedMutator } from "swr";
|
|||||||
import type {
|
import type {
|
||||||
IState,
|
IState,
|
||||||
IUser,
|
IUser,
|
||||||
IProject,
|
|
||||||
ICycle,
|
ICycle,
|
||||||
IModule,
|
IModule,
|
||||||
IUserLite,
|
IUserLite,
|
||||||
IProjectLite,
|
IProjectLite,
|
||||||
IWorkspaceLite,
|
IWorkspaceLite,
|
||||||
IStateLite,
|
IStateLite,
|
||||||
TStateGroups,
|
|
||||||
Properties,
|
Properties,
|
||||||
IIssueFilterOptions,
|
|
||||||
TIssueGroupByOptions,
|
|
||||||
TIssueViewOptions,
|
|
||||||
TIssueOrderByOptions,
|
|
||||||
IIssueDisplayFilterOptions,
|
IIssueDisplayFilterOptions,
|
||||||
} from "types";
|
} from "types";
|
||||||
|
|
||||||
|
13
web/types/projects.d.ts
vendored
13
web/types/projects.d.ts
vendored
@ -1,15 +1,4 @@
|
|||||||
import type {
|
import type { IUserLite, IWorkspace, IWorkspaceLite, IUserMemberLite, TStateGroups, IProjectViewProps } from ".";
|
||||||
IIssueFilterOptions,
|
|
||||||
IUserLite,
|
|
||||||
IWorkspace,
|
|
||||||
IWorkspaceLite,
|
|
||||||
IUserMemberLite,
|
|
||||||
TIssueGroupByOptions,
|
|
||||||
TIssueOrderByOptions,
|
|
||||||
TIssueViewOptions,
|
|
||||||
TStateGroups,
|
|
||||||
IProjectViewProps,
|
|
||||||
} from ".";
|
|
||||||
|
|
||||||
export interface IProject {
|
export interface IProject {
|
||||||
archive_in: number;
|
archive_in: number;
|
||||||
|
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 =
|
export type TIssueGroupByOptions =
|
||||||
| "state"
|
| "state"
|
||||||
@ -28,6 +28,25 @@ export type TIssueOrderByOptions =
|
|||||||
| "start_date"
|
| "start_date"
|
||||||
| "-start_date";
|
| "-start_date";
|
||||||
|
|
||||||
|
export type TIssueTypeFilters = "active" | "backlog" | null;
|
||||||
|
|
||||||
|
export type TIssueParams =
|
||||||
|
| "priority"
|
||||||
|
| "state_group"
|
||||||
|
| "state"
|
||||||
|
| "assignees"
|
||||||
|
| "created_by"
|
||||||
|
| "labels"
|
||||||
|
| "start_date"
|
||||||
|
| "target_date"
|
||||||
|
| "group_by"
|
||||||
|
| "order_by"
|
||||||
|
| "type"
|
||||||
|
| "sub_issue"
|
||||||
|
| "show_empty_groups"
|
||||||
|
| "calendar_date_range"
|
||||||
|
| "start_target_date";
|
||||||
|
|
||||||
export interface IIssueFilterOptions {
|
export interface IIssueFilterOptions {
|
||||||
assignees?: string[] | null;
|
assignees?: string[] | null;
|
||||||
created_by?: string[] | null;
|
created_by?: string[] | null;
|
||||||
@ -43,12 +62,27 @@ export interface IIssueFilterOptions {
|
|||||||
export interface IIssueDisplayFilterOptions {
|
export interface IIssueDisplayFilterOptions {
|
||||||
calendar_date_range?: string;
|
calendar_date_range?: string;
|
||||||
group_by?: TIssueGroupByOptions;
|
group_by?: TIssueGroupByOptions;
|
||||||
layout?: TIssueViewOptions;
|
layout?: TIssueLayouts;
|
||||||
order_by?: TIssueOrderByOptions;
|
order_by?: TIssueOrderByOptions;
|
||||||
show_empty_groups?: boolean;
|
show_empty_groups?: boolean;
|
||||||
start_target_date?: boolean;
|
start_target_date?: boolean;
|
||||||
sub_issue?: boolean;
|
sub_issue?: boolean;
|
||||||
type?: "active" | "backlog" | null;
|
type?: TIssueTypeFilters;
|
||||||
|
}
|
||||||
|
export interface IIssueDisplayProperties {
|
||||||
|
assignee: boolean;
|
||||||
|
start_date: boolean;
|
||||||
|
due_date: boolean;
|
||||||
|
labels: boolean;
|
||||||
|
key: boolean;
|
||||||
|
priority: boolean;
|
||||||
|
state: boolean;
|
||||||
|
sub_issue_count: boolean;
|
||||||
|
link: boolean;
|
||||||
|
attachment_count: boolean;
|
||||||
|
estimate: boolean;
|
||||||
|
created_on: boolean;
|
||||||
|
updated_on: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectViewProps {
|
export interface IProjectViewProps {
|
||||||
|
11
web/types/workspace.d.ts
vendored
11
web/types/workspace.d.ts
vendored
@ -1,13 +1,4 @@
|
|||||||
import type {
|
import type { IProjectMember, IUser, IUserMemberLite, IWorkspaceViewProps } from "types";
|
||||||
IIssueFilterOptions,
|
|
||||||
IProjectMember,
|
|
||||||
IUser,
|
|
||||||
IUserMemberLite,
|
|
||||||
IWorkspaceViewProps,
|
|
||||||
TIssueGroupByOptions,
|
|
||||||
TIssueOrderByOptions,
|
|
||||||
TIssueViewOptions,
|
|
||||||
} from "types";
|
|
||||||
|
|
||||||
export interface IWorkspace {
|
export interface IWorkspace {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user