forked from github/plane
chore: workspace profile issues, kanabn DND upgrade, implemented filters in plaen deploy (#2991)
This commit is contained in:
parent
8b2d78ef92
commit
8dee7e51ca
@ -13,7 +13,7 @@ import { IIssue } from "types/issue";
|
|||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export const IssueListBlock = observer(({ issue }: { issue: IIssue }) => {
|
export const IssueKanBanBlock = observer(({ issue }: { issue: IIssue }) => {
|
||||||
const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
const { project: projectStore, issueDetails: issueDetailStore }: RootStore = useMobxStore();
|
||||||
|
|
||||||
// router
|
// router
|
||||||
|
@ -10,7 +10,7 @@ import { StateGroupIcon } from "@plane/ui";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
|
export const IssueKanBanHeader = observer(({ state }: { state: IIssueState }) => {
|
||||||
const store: RootStore = useMobxStore();
|
const store: RootStore = useMobxStore();
|
||||||
|
|
||||||
const stateGroup = issueGroupFilter(state.group);
|
const stateGroup = issueGroupFilter(state.group);
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
// mobx react lite
|
// mobx react lite
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { IssueListHeader } from "components/issues/board-views/kanban/header";
|
import { IssueKanBanHeader } from "components/issues/board-views/kanban/header";
|
||||||
import { IssueListBlock } from "components/issues/board-views/kanban/block";
|
import { IssueKanBanBlock } from "components/issues/board-views/kanban/block";
|
||||||
// ui
|
// ui
|
||||||
import { Icon } from "components/ui";
|
import { Icon } from "components/ui";
|
||||||
// interfaces
|
// interfaces
|
||||||
@ -23,14 +23,14 @@ export const IssueKanbanView = observer(() => {
|
|||||||
store?.issue?.states.map((_state: IIssueState) => (
|
store?.issue?.states.map((_state: IIssueState) => (
|
||||||
<div key={_state.id} className="flex-shrink-0 relative w-[340px] h-full flex flex-col">
|
<div key={_state.id} className="flex-shrink-0 relative w-[340px] h-full flex flex-col">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<IssueListHeader state={_state} />
|
<IssueKanBanHeader state={_state} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full overflow-hidden overflow-y-auto hide-vertical-scrollbar">
|
<div className="w-full h-full overflow-hidden overflow-y-auto hide-vertical-scrollbar">
|
||||||
{store.issue.getFilteredIssuesByState(_state.id) &&
|
{store.issue.getFilteredIssuesByState(_state.id) &&
|
||||||
store.issue.getFilteredIssuesByState(_state.id).length > 0 ? (
|
store.issue.getFilteredIssuesByState(_state.id).length > 0 ? (
|
||||||
<div className="space-y-3 pb-2 px-2">
|
<div className="space-y-3 pb-2 px-2">
|
||||||
{store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
{store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => (
|
||||||
<IssueListBlock key={_issue.id} issue={_issue} />
|
<IssueKanBanBlock key={_issue.id} issue={_issue} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// components
|
|
||||||
import IssueStateFilter from "./state";
|
|
||||||
import IssueLabelFilter from "./label";
|
|
||||||
import IssuePriorityFilter from "./priority";
|
|
||||||
// mobx hook
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
|
|
||||||
const IssueFilter = observer(() => {
|
|
||||||
const store: RootStore = useMobxStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
|
||||||
|
|
||||||
const clearAllFilters = () => {
|
|
||||||
// router.replace(
|
|
||||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
|
||||||
// key: "all",
|
|
||||||
// removeAll: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
// if (store.issue.getIfFiltersIsEmpty()) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-shrink-0 min-h-[50px] h-auto py-1.5 border-b border-custom-border-200 relative flex items-center shadow-md bg-whiate select-none">
|
|
||||||
<div className="px-5 flex justify-start items-center flex-wrap gap-2 text-sm">
|
|
||||||
{/* state */}
|
|
||||||
{/* {store.issue.checkIfFilterExistsForKey("state") && <IssueStateFilter />} */}
|
|
||||||
{/* labels */}
|
|
||||||
{/* {store.issue.checkIfFilterExistsForKey("label") && <IssueLabelFilter />} */}
|
|
||||||
{/* priority */}
|
|
||||||
{/* {store.issue.checkIfFilterExistsForKey("priority") && <IssuePriorityFilter />} */}
|
|
||||||
{/* clear all filters */}
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 border border-custom-border-200 px-2 py-1 cursor-pointer text-xs rounded-full"
|
|
||||||
onClick={clearAllFilters}
|
|
||||||
>
|
|
||||||
<div>Clear all filters</div>
|
|
||||||
<div className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-sm">
|
|
||||||
<span className="material-symbols-rounded text-[12px]">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default IssueFilter;
|
|
@ -1,43 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// mobx hook
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// interfaces
|
|
||||||
import { IIssueLabel } from "types/issue";
|
|
||||||
|
|
||||||
export const RenderIssueLabel = observer(({ label }: { label: IIssueLabel }) => {
|
|
||||||
const store = useMobxStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
|
||||||
|
|
||||||
const removeLabelFromFilter = () => {
|
|
||||||
// router.replace(
|
|
||||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
|
||||||
// key: "label",
|
|
||||||
// value: label?.id,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 relative flex items-center flex-wrap gap-1 px-2 py-0.5 rounded-full select-none"
|
|
||||||
style={{ color: label?.color, backgroundColor: `${label?.color}10` }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-1.5 h-1.5 flex justify-center items-center overflow-hidden rounded-full"
|
|
||||||
style={{ backgroundColor: `${label?.color}` }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="font-medium whitespace-nowrap text-xs">{label?.name}</div>
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-full"
|
|
||||||
onClick={removeLabelFromFilter}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-rounded text-xs">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,51 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// components
|
|
||||||
import { RenderIssueLabel } from "./filter-label-block";
|
|
||||||
// interfaces
|
|
||||||
import { IIssueLabel } from "types/issue";
|
|
||||||
// mobx hook
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
|
|
||||||
const IssueLabelFilter = observer(() => {
|
|
||||||
const store: RootStore = useMobxStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
|
||||||
|
|
||||||
const clearLabelFilters = () => {
|
|
||||||
// router.replace(
|
|
||||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
|
||||||
// key: "label",
|
|
||||||
// removeAll: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2 border border-custom-border-300 px-2 py-1 rounded-full text-xs">
|
|
||||||
<div className="flex-shrink-0 text-custom-text-200">Labels</div>
|
|
||||||
<div className="relative flex flex-wrap items-center gap-1">
|
|
||||||
{/* {store?.issue?.labels &&
|
|
||||||
store?.issue?.labels.map(
|
|
||||||
(_label: IIssueLabel, _index: number) =>
|
|
||||||
store.issue.getUserSelectedFilter("label", _label.id) && (
|
|
||||||
<RenderIssueLabel key={_label.id} label={_label} />
|
|
||||||
)
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-sm"
|
|
||||||
onClick={clearLabelFilters}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-rounded text-[12px]">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default IssueLabelFilter;
|
|
@ -1,42 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// mobx hook
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// interfaces
|
|
||||||
import { IIssuePriorityFilters } from "types/issue";
|
|
||||||
|
|
||||||
export const RenderIssuePriority = observer(({ priority }: { priority: IIssuePriorityFilters }) => {
|
|
||||||
const store = useMobxStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
|
||||||
|
|
||||||
const removePriorityFromFilter = () => {
|
|
||||||
// router.replace(
|
|
||||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
|
||||||
// key: "priority",
|
|
||||||
// value: priority?.key,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`flex-shrink-0 relative flex items-center flex-wrap gap-1 px-2 py-0.5 text-xs rounded-full select-none ${
|
|
||||||
priority.className || ``
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 flex justify-center items-center overflow-hidden rounded-full">
|
|
||||||
<span className="material-symbols-rounded text-xs">{priority?.icon}</span>
|
|
||||||
</div>
|
|
||||||
<div className="whitespace-nowrap">{priority?.title}</div>
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-full"
|
|
||||||
onClick={removePriorityFromFilter}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-rounded text-xs">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,53 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// mobx hook
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// components
|
|
||||||
import { RenderIssuePriority } from "./filter-priority-block";
|
|
||||||
// interfaces
|
|
||||||
import { IIssuePriorityFilters } from "types/issue";
|
|
||||||
// constants
|
|
||||||
import { issuePriorityFilters } from "constants/data";
|
|
||||||
|
|
||||||
const IssuePriorityFilter = observer(() => {
|
|
||||||
const store = useMobxStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
|
||||||
|
|
||||||
const clearPriorityFilters = () => {
|
|
||||||
// router.replace(
|
|
||||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
|
||||||
// key: "priority",
|
|
||||||
// removeAll: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2 border border-custom-border-300 px-2 py-1 rounded-full text-xs">
|
|
||||||
<div className="flex-shrink-0 text-custom-text-200">Priority</div>
|
|
||||||
<div className="relative flex flex-wrap items-center gap-1">
|
|
||||||
{/* {issuePriorityFilters.map(
|
|
||||||
(_priority: IIssuePriorityFilters, _index: number) =>
|
|
||||||
store.issue.getUserSelectedFilter("priority", _priority.key) && (
|
|
||||||
<RenderIssuePriority key={_priority.key} priority={_priority} />
|
|
||||||
)
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-sm"
|
|
||||||
onClick={() => {
|
|
||||||
clearPriorityFilters();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-rounded text-[12px]">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default IssuePriorityFilter;
|
|
@ -1,34 +0,0 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// interfaces
|
|
||||||
import { IIssueState } from "types/issue";
|
|
||||||
// constants
|
|
||||||
import { issueGroupFilter } from "constants/data";
|
|
||||||
|
|
||||||
export const RenderIssueState = observer(({ state }: { state: IIssueState }) => {
|
|
||||||
const stateGroup = issueGroupFilter(state.group);
|
|
||||||
|
|
||||||
const removeStateFromFilter = () => {
|
|
||||||
// router.replace(
|
|
||||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
|
||||||
// key: "state",
|
|
||||||
// value: state?.id,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
if (stateGroup === null) return <></>;
|
|
||||||
return (
|
|
||||||
<div className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 ${stateGroup.className || ``}`}>
|
|
||||||
<div className="flex h-3 w-3 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
|
|
||||||
{/* <stateGroup.icon /> */}
|
|
||||||
</div>
|
|
||||||
<div className="whitespace-nowrap text-xs font-medium">{state?.name}</div>
|
|
||||||
<div
|
|
||||||
className="flex h-3 w-3 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-full"
|
|
||||||
onClick={removeStateFromFilter}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-rounded text-xs">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,51 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
// mobx react lite
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// components
|
|
||||||
import { RenderIssueState } from "./filter-state-block";
|
|
||||||
// interfaces
|
|
||||||
import { IIssueState } from "types/issue";
|
|
||||||
// mobx hook
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
|
|
||||||
const IssueStateFilter = observer(() => {
|
|
||||||
const store: RootStore = useMobxStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
|
||||||
|
|
||||||
const clearStateFilters = () => {
|
|
||||||
// router.replace(
|
|
||||||
// store.issue.getURLDefinition(workspace_slug, project_slug, {
|
|
||||||
// key: "state",
|
|
||||||
// removeAll: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2 border border-custom-border-300 px-2 py-1 rounded-full text-xs">
|
|
||||||
<div className="flex-shrink-0 text-custom-text-200">State</div>
|
|
||||||
<div className="relative flex flex-wrap items-center gap-1">
|
|
||||||
{/* {store?.issue?.states &&
|
|
||||||
store?.issue?.states.map(
|
|
||||||
(_state: IIssueState, _index: number) =>
|
|
||||||
store.issue.getUserSelectedFilter("state", _state.id) && (
|
|
||||||
<RenderIssueState key={_state.id} state={_state} />
|
|
||||||
)
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex-shrink-0 w-3 h-3 cursor-pointer flex justify-center items-center overflow-hidden rounded-sm"
|
|
||||||
onClick={clearStateFilters}
|
|
||||||
>
|
|
||||||
<span className="material-symbols-rounded text-[12px]">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default IssueStateFilter;
|
|
@ -0,0 +1,80 @@
|
|||||||
|
// components
|
||||||
|
import { AppliedLabelsFilters } from "./label";
|
||||||
|
import { AppliedPriorityFilters } from "./priority";
|
||||||
|
import { AppliedStateFilters } from "./state";
|
||||||
|
// icons
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { IIssueFilterOptions } from "store/issues/types";
|
||||||
|
import { IIssueLabel, IIssueState } from "types/issue";
|
||||||
|
// types
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: IIssueFilterOptions;
|
||||||
|
handleRemoveAllFilters: () => void;
|
||||||
|
handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void;
|
||||||
|
labels?: IIssueLabel[] | undefined;
|
||||||
|
states?: IIssueState[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
||||||
|
|
||||||
|
export const AppliedFiltersList: React.FC<Props> = (props) => {
|
||||||
|
const { appliedFilters, handleRemoveAllFilters, handleRemoveFilter, labels, states } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-stretch gap-2 flex-wrap bg-custom-background-100">
|
||||||
|
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||||
|
const filterKey = key as keyof IIssueFilterOptions;
|
||||||
|
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={filterKey}
|
||||||
|
className="capitalize py-1 px-2 border border-custom-border-200 rounded-md flex items-center gap-2 flex-wrap"
|
||||||
|
>
|
||||||
|
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
||||||
|
<div className="flex items-center gap-1 flex-wrap">
|
||||||
|
{filterKey === "priority" && (
|
||||||
|
<AppliedPriorityFilters handleRemove={(val) => handleRemoveFilter("priority", val)} values={value} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filterKey === "labels" && labels && (
|
||||||
|
<AppliedLabelsFilters
|
||||||
|
handleRemove={(val) => handleRemoveFilter("labels", val)}
|
||||||
|
labels={labels}
|
||||||
|
values={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filterKey === "state" && states && (
|
||||||
|
<AppliedStateFilters
|
||||||
|
handleRemove={(val) => handleRemoveFilter("state", val)}
|
||||||
|
states={states}
|
||||||
|
values={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||||
|
>
|
||||||
|
<X size={12} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRemoveAllFilters}
|
||||||
|
className="flex items-center gap-2 text-xs border border-custom-border-200 py-1 px-2 rounded-md text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
<X size={12} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
42
space/components/issues/filters/applied-filters/label.tsx
Normal file
42
space/components/issues/filters/applied-filters/label.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { X } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { IIssueLabel } from "types/issue";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleRemove: (val: string) => void;
|
||||||
|
labels: IIssueLabel[] | undefined;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppliedLabelsFilters: React.FC<Props> = (props) => {
|
||||||
|
const { handleRemove, labels, values } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{values.map((labelId) => {
|
||||||
|
const labelDetails = labels?.find((l) => l.id === labelId);
|
||||||
|
|
||||||
|
if (!labelDetails) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={labelId} className="text-xs flex items-center gap-1 bg-custom-background-80 p-1 rounded">
|
||||||
|
<span
|
||||||
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: labelDetails.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="normal-case">{labelDetails.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemove(labelId)}
|
||||||
|
>
|
||||||
|
<X size={10} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
31
space/components/issues/filters/applied-filters/priority.tsx
Normal file
31
space/components/issues/filters/applied-filters/priority.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { PriorityIcon } from "@plane/ui";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleRemove: (val: string) => void;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppliedPriorityFilters: React.FC<Props> = (props) => {
|
||||||
|
const { handleRemove, values } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{values &&
|
||||||
|
values.length > 0 &&
|
||||||
|
values.map((priority) => (
|
||||||
|
<div key={priority} className="text-xs flex items-center gap-1 bg-custom-background-80 p-1 rounded">
|
||||||
|
<PriorityIcon priority={priority as any} className={`h-3 w-3`} />
|
||||||
|
{priority}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemove(priority)}
|
||||||
|
>
|
||||||
|
<X size={10} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
90
space/components/issues/filters/applied-filters/root.tsx
Normal file
90
space/components/issues/filters/applied-filters/root.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { FC, useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { AppliedFiltersList } from "./filters-list";
|
||||||
|
// store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
import { RootStore } from "store/root";
|
||||||
|
import { IIssueFilterOptions } from "store/issues/types";
|
||||||
|
|
||||||
|
export const IssueAppliedFilters: FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as {
|
||||||
|
workspace_slug: string;
|
||||||
|
project_slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
issuesFilter: { issueFilters, updateFilters },
|
||||||
|
issue: { states, labels },
|
||||||
|
project: { activeBoard },
|
||||||
|
}: RootStore = useMobxStore();
|
||||||
|
|
||||||
|
const userFilters = issueFilters?.filters || {};
|
||||||
|
|
||||||
|
const appliedFilters: IIssueFilterOptions = {};
|
||||||
|
Object.entries(userFilters).forEach(([key, value]) => {
|
||||||
|
if (!value) return;
|
||||||
|
if (Array.isArray(value) && value.length === 0) return;
|
||||||
|
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateRouteParams = useCallback(
|
||||||
|
(key: keyof IIssueFilterOptions | null, value: string[] | null, clearFields: boolean = false) => {
|
||||||
|
const state = key === "state" ? value || [] : issueFilters?.filters?.state ?? [];
|
||||||
|
const priority = key === "priority" ? value || [] : issueFilters?.filters?.priority ?? [];
|
||||||
|
const labels = key === "labels" ? value || [] : issueFilters?.filters?.labels ?? [];
|
||||||
|
|
||||||
|
let params: any = { board: activeBoard || "list" };
|
||||||
|
if (!clearFields) {
|
||||||
|
if (priority.length > 0) params = { ...params, priorities: priority.join(",") };
|
||||||
|
if (state.length > 0) params = { ...params, states: state.join(",") };
|
||||||
|
if (labels.length > 0) params = { ...params, labels: labels.join(",") };
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
|
||||||
|
},
|
||||||
|
[workspaceSlug, projectId, activeBoard, issueFilters, router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
if (!value) {
|
||||||
|
updateFilters(projectId, { [key]: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||||
|
newValues = newValues.filter((val) => val !== value);
|
||||||
|
|
||||||
|
updateFilters(projectId, { [key]: newValues });
|
||||||
|
updateRouteParams(key, newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveAllFilters = () => {
|
||||||
|
if (!projectId) return;
|
||||||
|
|
||||||
|
const newFilters: IIssueFilterOptions = {};
|
||||||
|
Object.keys(userFilters).forEach((key) => {
|
||||||
|
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateFilters(projectId, { ...newFilters });
|
||||||
|
updateRouteParams(null, null, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-5 py-3 border-b border-custom-border-200">
|
||||||
|
<AppliedFiltersList
|
||||||
|
appliedFilters={appliedFilters || {}}
|
||||||
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
|
handleRemoveAllFilters={handleRemoveAllFilters}
|
||||||
|
labels={labels ?? []}
|
||||||
|
states={states ?? []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
39
space/components/issues/filters/applied-filters/state.tsx
Normal file
39
space/components/issues/filters/applied-filters/state.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { X } from "lucide-react";
|
||||||
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
|
// icons
|
||||||
|
import { IIssueState } from "types/issue";
|
||||||
|
// types
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleRemove: (val: string) => void;
|
||||||
|
states: IIssueState[];
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppliedStateFilters: React.FC<Props> = (props) => {
|
||||||
|
const { handleRemove, states, values } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{values.map((stateId) => {
|
||||||
|
const stateDetails = states?.find((s) => s.id === stateId);
|
||||||
|
|
||||||
|
if (!stateDetails) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={stateId} className="text-xs flex items-center gap-1 bg-custom-background-80 p-1 rounded">
|
||||||
|
<StateGroupIcon color={stateDetails.color} stateGroup={stateDetails.group} height="12px" width="12px" />
|
||||||
|
{stateDetails.name}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||||
|
onClick={() => handleRemove(stateId)}
|
||||||
|
>
|
||||||
|
<X size={10} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
72
space/components/issues/filters/helpers/dropdown.tsx
Normal file
72
space/components/issues/filters/helpers/dropdown.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React, { Fragment, useState } from "react";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
|
import { Placement } from "@popperjs/core";
|
||||||
|
// ui
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
// icons
|
||||||
|
import { ChevronUp } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
placement?: Placement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersDropdown: React.FC<Props> = (props) => {
|
||||||
|
const { children, title = "Dropdown", placement } = props;
|
||||||
|
|
||||||
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: placement ?? "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover as="div">
|
||||||
|
{({ open }) => {
|
||||||
|
if (open) {
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popover.Button as={React.Fragment}>
|
||||||
|
<Button
|
||||||
|
ref={setReferenceElement}
|
||||||
|
variant="neutral-primary"
|
||||||
|
size="sm"
|
||||||
|
appendIcon={
|
||||||
|
<ChevronUp className={`transition-all ${open ? "" : "rotate-180"}`} size={14} strokeWidth={2} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Popover.Button>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 translate-y-1"
|
||||||
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
leave="transition ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
|
<Popover.Panel>
|
||||||
|
<div
|
||||||
|
className="z-10 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded overflow-hidden"
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.popper}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
<div className="w-[18.75rem] max-h-[37.5rem] flex flex-col overflow-hidden">{children}</div>
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
22
space/components/issues/filters/helpers/filter-header.tsx
Normal file
22
space/components/issues/filters/helpers/filter-header.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
// lucide icons
|
||||||
|
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
|
||||||
|
interface IFilterHeader {
|
||||||
|
title: string;
|
||||||
|
isPreviewEnabled: boolean;
|
||||||
|
handleIsPreviewEnabled: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => (
|
||||||
|
<div className="flex items-center justify-between gap-2 bg-custom-background-100 sticky top-0">
|
||||||
|
<div className="text-custom-text-300 text-xs font-medium flex-grow truncate">{title}</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex-shrink-0 w-5 h-5 grid place-items-center rounded hover:bg-custom-background-80"
|
||||||
|
onClick={handleIsPreviewEnabled}
|
||||||
|
>
|
||||||
|
{isPreviewEnabled ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
35
space/components/issues/filters/helpers/filter-option.tsx
Normal file
35
space/components/issues/filters/helpers/filter-option.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
// lucide icons
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
isChecked: boolean;
|
||||||
|
title: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
multiple?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterOption: React.FC<Props> = (props) => {
|
||||||
|
const { icon, isChecked, multiple = true, onClick, title } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 rounded hover:bg-custom-background-80 w-full p-1.5"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 w-3 h-3 grid place-items-center bg-custom-background-90 border ${
|
||||||
|
isChecked ? "bg-custom-primary-100 border-custom-primary-100 text-white" : "border-custom-border-300"
|
||||||
|
} ${multiple ? "rounded-sm" : "rounded-full"}`}
|
||||||
|
>
|
||||||
|
{isChecked && <Check size={10} strokeWidth={3} />}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 truncate">
|
||||||
|
{icon && <div className="flex-shrink-0 grid place-items-center w-5">{icon}</div>}
|
||||||
|
<div className="flex-grow truncate text-custom-text-200 text-xs">{title}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
3
space/components/issues/filters/helpers/index.ts
Normal file
3
space/components/issues/filters/helpers/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./dropdown";
|
||||||
|
export * from "./filter-header";
|
||||||
|
export * from "./filter-option";
|
11
space/components/issues/filters/index.ts
Normal file
11
space/components/issues/filters/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// filters
|
||||||
|
export * from "./root";
|
||||||
|
export * from "./selection";
|
||||||
|
|
||||||
|
// properties
|
||||||
|
export * from "./state";
|
||||||
|
export * from "./priority";
|
||||||
|
export * from "./labels";
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
export * from "./helpers";
|
83
space/components/issues/filters/labels.tsx
Normal file
83
space/components/issues/filters/labels.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "./helpers";
|
||||||
|
// ui
|
||||||
|
import { Loader } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { IIssueLabel } from "types/issue";
|
||||||
|
|
||||||
|
const LabelIcons = ({ color }: { color: string }) => (
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (val: string) => void;
|
||||||
|
labels: IIssueLabel[] | undefined;
|
||||||
|
searchQuery: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterLabels: React.FC<Props> = (props) => {
|
||||||
|
const { appliedFilters, handleUpdate, labels, searchQuery } = props;
|
||||||
|
|
||||||
|
const [itemsToRender, setItemsToRender] = useState(5);
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
const handleViewToggle = () => {
|
||||||
|
if (!filteredOptions) return;
|
||||||
|
|
||||||
|
if (itemsToRender === filteredOptions.length) setItemsToRender(5);
|
||||||
|
else setItemsToRender(filteredOptions.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterHeader
|
||||||
|
title={`Label${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{filteredOptions ? (
|
||||||
|
filteredOptions.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{filteredOptions.slice(0, itemsToRender).map((label) => (
|
||||||
|
<FilterOption
|
||||||
|
key={label?.id}
|
||||||
|
isChecked={appliedFilters?.includes(label?.id) ? true : false}
|
||||||
|
onClick={() => handleUpdate(label?.id)}
|
||||||
|
icon={<LabelIcons color={label.color} />}
|
||||||
|
title={label.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredOptions.length > 5 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-custom-primary-100 text-xs font-medium ml-8"
|
||||||
|
onClick={handleViewToggle}
|
||||||
|
>
|
||||||
|
{itemsToRender === filteredOptions.length ? "View less" : "View all"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-custom-text-400 italic">No matches found</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-2">
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
51
space/components/issues/filters/priority.tsx
Normal file
51
space/components/issues/filters/priority.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// ui
|
||||||
|
import { PriorityIcon } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "./helpers";
|
||||||
|
// constants
|
||||||
|
import { issuePriorityFilters } from "constants/data";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (val: string) => void;
|
||||||
|
searchQuery: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterPriority: React.FC<Props> = observer((props) => {
|
||||||
|
const { appliedFilters, handleUpdate, searchQuery } = props;
|
||||||
|
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
const filteredOptions = issuePriorityFilters.filter((p) => p.key.includes(searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterHeader
|
||||||
|
title={`Priority${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{filteredOptions.length > 0 ? (
|
||||||
|
filteredOptions.map((priority) => (
|
||||||
|
<FilterOption
|
||||||
|
key={priority.key}
|
||||||
|
isChecked={appliedFilters?.includes(priority.key) ? true : false}
|
||||||
|
onClick={() => handleUpdate(priority.key)}
|
||||||
|
icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />}
|
||||||
|
title={priority.title}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-custom-text-400 italic">No matches found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
77
space/components/issues/filters/root.tsx
Normal file
77
space/components/issues/filters/root.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { FC, useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { FiltersDropdown } from "./helpers/dropdown";
|
||||||
|
import { FilterSelection } from "./selection";
|
||||||
|
// types
|
||||||
|
import { IIssueFilterOptions } from "store/issues/types";
|
||||||
|
// helpers
|
||||||
|
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "store/issues/helpers";
|
||||||
|
// store
|
||||||
|
import { RootStore } from "store/root";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
|
export const IssueFiltersDropdown: FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspace_slug: workspaceSlug, project_slug: projectId } = router.query as {
|
||||||
|
workspace_slug: string;
|
||||||
|
project_slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
project: { activeBoard },
|
||||||
|
issue: { states, labels },
|
||||||
|
issuesFilter: { issueFilters, updateFilters },
|
||||||
|
}: RootStore = useMobxStore();
|
||||||
|
|
||||||
|
const updateRouteParams = useCallback(
|
||||||
|
(key: keyof IIssueFilterOptions, value: string[]) => {
|
||||||
|
const state = key === "state" ? value : issueFilters?.filters?.state ?? [];
|
||||||
|
const priority = key === "priority" ? value : issueFilters?.filters?.priority ?? [];
|
||||||
|
const labels = key === "labels" ? value : issueFilters?.filters?.labels ?? [];
|
||||||
|
|
||||||
|
let params: any = { board: activeBoard || "list" };
|
||||||
|
if (priority.length > 0) params = { ...params, priorities: priority.join(",") };
|
||||||
|
if (state.length > 0) params = { ...params, states: state.join(",") };
|
||||||
|
if (labels.length > 0) params = { ...params, labels: labels.join(",") };
|
||||||
|
|
||||||
|
router.push({ pathname: `/${workspaceSlug}/${projectId}`, query: { ...params } }, undefined, { shallow: true });
|
||||||
|
},
|
||||||
|
[workspaceSlug, projectId, activeBoard, issueFilters, router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilters = useCallback(
|
||||||
|
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((val) => {
|
||||||
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilters(projectId, { [key]: newValues });
|
||||||
|
updateRouteParams(key, newValues);
|
||||||
|
},
|
||||||
|
[projectId, issueFilters, updateFilters, updateRouteParams]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col z-50">
|
||||||
|
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||||
|
<FilterSelection
|
||||||
|
filters={issueFilters?.filters ?? {}}
|
||||||
|
handleFilters={handleFilters}
|
||||||
|
layoutDisplayFiltersOptions={activeBoard ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeBoard] : undefined}
|
||||||
|
states={states ?? undefined}
|
||||||
|
labels={labels ?? undefined}
|
||||||
|
/>
|
||||||
|
</FiltersDropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
86
space/components/issues/filters/selection.tsx
Normal file
86
space/components/issues/filters/selection.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Search, X } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { FilterLabels, FilterPriority, FilterState } from "./";
|
||||||
|
// types
|
||||||
|
|
||||||
|
// filter helpers
|
||||||
|
import { ILayoutDisplayFiltersOptions } from "store/issues/helpers";
|
||||||
|
import { IIssueFilterOptions } from "store/issues/types";
|
||||||
|
import { IIssueState, IIssueLabel } from "types/issue";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filters: IIssueFilterOptions;
|
||||||
|
handleFilters: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||||
|
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
|
||||||
|
labels?: IIssueLabel[] | undefined;
|
||||||
|
states?: IIssueState[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||||
|
const { filters, handleFilters, layoutDisplayFiltersOptions, labels, states } = props;
|
||||||
|
|
||||||
|
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col overflow-hidden">
|
||||||
|
<div className="p-2.5 pb-0 bg-custom-background-100">
|
||||||
|
<div className="bg-custom-background-90 border-[0.5px] border-custom-border-200 text-xs rounded flex items-center gap-1.5 px-1.5 py-1">
|
||||||
|
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="bg-custom-background-90 placeholder:text-custom-text-400 w-full outline-none"
|
||||||
|
placeholder="Search"
|
||||||
|
value={filtersSearchQuery}
|
||||||
|
onChange={(e) => setFiltersSearchQuery(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{filtersSearchQuery !== "" && (
|
||||||
|
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
|
||||||
|
<X className="text-custom-text-300" size={12} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full divide-y divide-custom-border-200 px-2.5 overflow-y-auto">
|
||||||
|
{/* priority */}
|
||||||
|
{isFilterEnabled("priority") && (
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterPriority
|
||||||
|
appliedFilters={filters.priority ?? null}
|
||||||
|
handleUpdate={(val) => handleFilters("priority", val)}
|
||||||
|
searchQuery={filtersSearchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* state */}
|
||||||
|
{isFilterEnabled("state") && (
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterState
|
||||||
|
appliedFilters={filters.state ?? null}
|
||||||
|
handleUpdate={(val) => handleFilters("state", val)}
|
||||||
|
searchQuery={filtersSearchQuery}
|
||||||
|
states={states}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* labels */}
|
||||||
|
{isFilterEnabled("labels") && (
|
||||||
|
<div className="py-2">
|
||||||
|
<FilterLabels
|
||||||
|
appliedFilters={filters.labels ?? null}
|
||||||
|
handleUpdate={(val) => handleFilters("labels", val)}
|
||||||
|
labels={labels}
|
||||||
|
searchQuery={filtersSearchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
78
space/components/issues/filters/state.tsx
Normal file
78
space/components/issues/filters/state.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
// components
|
||||||
|
import { FilterHeader, FilterOption } from "./helpers";
|
||||||
|
// ui
|
||||||
|
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { IIssueState } from "types/issue";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appliedFilters: string[] | null;
|
||||||
|
handleUpdate: (val: string) => void;
|
||||||
|
searchQuery: string;
|
||||||
|
states: IIssueState[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterState: React.FC<Props> = (props) => {
|
||||||
|
const { appliedFilters, handleUpdate, searchQuery, states } = props;
|
||||||
|
|
||||||
|
const [itemsToRender, setItemsToRender] = useState(5);
|
||||||
|
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||||
|
|
||||||
|
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||||
|
|
||||||
|
const filteredOptions = states?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
const handleViewToggle = () => {
|
||||||
|
if (!filteredOptions) return;
|
||||||
|
|
||||||
|
if (itemsToRender === filteredOptions.length) setItemsToRender(5);
|
||||||
|
else setItemsToRender(filteredOptions.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterHeader
|
||||||
|
title={`State${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
|
||||||
|
isPreviewEnabled={previewEnabled}
|
||||||
|
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||||
|
/>
|
||||||
|
{previewEnabled && (
|
||||||
|
<div>
|
||||||
|
{filteredOptions ? (
|
||||||
|
filteredOptions.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{filteredOptions.slice(0, itemsToRender).map((state) => (
|
||||||
|
<FilterOption
|
||||||
|
key={state.id}
|
||||||
|
isChecked={appliedFilters?.includes(state.id) ? true : false}
|
||||||
|
onClick={() => handleUpdate(state.id)}
|
||||||
|
icon={<StateGroupIcon stateGroup={state.group} color={state.color} />}
|
||||||
|
title={state.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredOptions.length > 5 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-custom-primary-100 text-xs font-medium ml-8"
|
||||||
|
onClick={handleViewToggle}
|
||||||
|
>
|
||||||
|
{itemsToRender === filteredOptions.length ? "View less" : "View all"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-custom-text-400 italic">No matches found</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Loader className="space-y-2">
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
<Loader.Item height="20px" />
|
||||||
|
</Loader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -9,6 +9,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
// import { NavbarSearch } from "./search";
|
// import { NavbarSearch } from "./search";
|
||||||
import { NavbarIssueBoardView } from "./issue-board-view";
|
import { NavbarIssueBoardView } from "./issue-board-view";
|
||||||
import { NavbarTheme } from "./theme";
|
import { NavbarTheme } from "./theme";
|
||||||
|
import { IssueFiltersDropdown } from "components/issues/filters";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, Button } from "@plane/ui";
|
import { Avatar, Button } from "@plane/ui";
|
||||||
import { Briefcase } from "lucide-react";
|
import { Briefcase } from "lucide-react";
|
||||||
@ -16,6 +17,7 @@ import { Briefcase } from "lucide-react";
|
|||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// store
|
// store
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
import { TIssueBoardKeys } from "types/issue";
|
||||||
|
|
||||||
const renderEmoji = (emoji: string | { name: string; color: string }) => {
|
const renderEmoji = (emoji: string | { name: string; color: string }) => {
|
||||||
if (!emoji) return;
|
if (!emoji) return;
|
||||||
@ -30,10 +32,21 @@ const renderEmoji = (emoji: string | { name: string; color: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const IssueNavbar = observer(() => {
|
const IssueNavbar = observer(() => {
|
||||||
const { project: projectStore, user: userStore }: RootStore = useMobxStore();
|
const {
|
||||||
|
project: projectStore,
|
||||||
|
user: userStore,
|
||||||
|
issuesFilter: { updateFilters },
|
||||||
|
}: RootStore = useMobxStore();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspace_slug, project_slug, board } = router.query;
|
const { workspace_slug, project_slug, board, states, priorities, labels } = router.query as {
|
||||||
|
workspace_slug: string;
|
||||||
|
project_slug: string;
|
||||||
|
board: string;
|
||||||
|
states: string;
|
||||||
|
priorities: string;
|
||||||
|
labels: string;
|
||||||
|
};
|
||||||
|
|
||||||
const user = userStore?.currentUser;
|
const user = userStore?.currentUser;
|
||||||
|
|
||||||
@ -46,7 +59,7 @@ const IssueNavbar = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspace_slug && project_slug && projectStore?.deploySettings) {
|
if (workspace_slug && project_slug && projectStore?.deploySettings) {
|
||||||
const viewsAcceptable: string[] = [];
|
const viewsAcceptable: string[] = [];
|
||||||
let currentBoard: string | null = null;
|
let currentBoard: TIssueBoardKeys | null = null;
|
||||||
|
|
||||||
if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("list");
|
if (projectStore?.deploySettings?.views?.list) viewsAcceptable.push("list");
|
||||||
if (projectStore?.deploySettings?.views?.kanban) viewsAcceptable.push("kanban");
|
if (projectStore?.deploySettings?.views?.kanban) viewsAcceptable.push("kanban");
|
||||||
@ -56,31 +69,41 @@ const IssueNavbar = observer(() => {
|
|||||||
|
|
||||||
if (board) {
|
if (board) {
|
||||||
if (viewsAcceptable.includes(board.toString())) {
|
if (viewsAcceptable.includes(board.toString())) {
|
||||||
currentBoard = board.toString();
|
currentBoard = board.toString() as TIssueBoardKeys;
|
||||||
} else {
|
} else {
|
||||||
if (viewsAcceptable && viewsAcceptable.length > 0) {
|
if (viewsAcceptable && viewsAcceptable.length > 0) {
|
||||||
currentBoard = viewsAcceptable[0];
|
currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (viewsAcceptable && viewsAcceptable.length > 0) {
|
if (viewsAcceptable && viewsAcceptable.length > 0) {
|
||||||
currentBoard = viewsAcceptable[0];
|
currentBoard = viewsAcceptable[0] as TIssueBoardKeys;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentBoard) {
|
if (currentBoard) {
|
||||||
if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) {
|
if (projectStore?.activeBoard === null || projectStore?.activeBoard !== currentBoard) {
|
||||||
|
let params: any = { board: currentBoard };
|
||||||
|
if (priorities && priorities.length > 0) params = { ...params, priorities: priorities };
|
||||||
|
if (states && states.length > 0) params = { ...params, states: states };
|
||||||
|
if (labels && labels.length > 0) params = { ...params, labels: labels };
|
||||||
|
|
||||||
|
let storeParams: any = {};
|
||||||
|
if (priorities && priorities.length > 0) storeParams = { ...storeParams, priority: priorities.split(",") };
|
||||||
|
if (states && states.length > 0) storeParams = { ...storeParams, state: states.split(",") };
|
||||||
|
if (labels && labels.length > 0) storeParams = { ...storeParams, labels: labels.split(",") };
|
||||||
|
|
||||||
|
if (storeParams) updateFilters(project_slug, storeParams);
|
||||||
|
|
||||||
projectStore.setActiveBoard(currentBoard);
|
projectStore.setActiveBoard(currentBoard);
|
||||||
router.push({
|
router.push({
|
||||||
pathname: `/${workspace_slug}/${project_slug}`,
|
pathname: `/${workspace_slug}/${project_slug}`,
|
||||||
query: {
|
query: { ...params },
|
||||||
board: currentBoard,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [board, workspace_slug, project_slug, router, projectStore, projectStore?.deploySettings]);
|
}, [board, workspace_slug, project_slug, router, projectStore, projectStore?.deploySettings, updateFilters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full items-center gap-4 px-5">
|
<div className="relative flex w-full items-center gap-4 px-5">
|
||||||
@ -120,6 +143,11 @@ const IssueNavbar = observer(() => {
|
|||||||
<NavbarIssueBoardView />
|
<NavbarIssueBoardView />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* issue filters */}
|
||||||
|
<div className="relative flex flex-shrink-0 items-center gap-1 transition-all delay-150 ease-in-out">
|
||||||
|
<IssueFiltersDropdown />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* theming */}
|
{/* theming */}
|
||||||
<div className="relative flex-shrink-0">
|
<div className="relative flex-shrink-0">
|
||||||
<NavbarTheme />
|
<NavbarTheme />
|
||||||
|
@ -5,6 +5,7 @@ import { issueViews } from "constants/data";
|
|||||||
// mobx
|
// mobx
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
|
import { TIssueBoardKeys } from "types/issue";
|
||||||
|
|
||||||
export const NavbarIssueBoardView = observer(() => {
|
export const NavbarIssueBoardView = observer(() => {
|
||||||
const {
|
const {
|
||||||
@ -15,7 +16,7 @@ export const NavbarIssueBoardView = observer(() => {
|
|||||||
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
const { workspace_slug, project_slug } = router.query as { workspace_slug: string; project_slug: string };
|
||||||
|
|
||||||
const handleCurrentBoardView = (boardView: string) => {
|
const handleCurrentBoardView = (boardView: string) => {
|
||||||
setActiveBoard(boardView);
|
setActiveBoard(boardView as TIssueBoardKeys);
|
||||||
router.push(`/${workspace_slug}/${project_slug}?board=${boardView}`);
|
router.push(`/${workspace_slug}/${project_slug}?board=${boardView}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
import { RootStore } from "store/root";
|
|
||||||
// components
|
|
||||||
import { Dropdown } from "components/ui/dropdown";
|
|
||||||
// constants
|
|
||||||
import { issueGroupFilter } from "constants/data";
|
|
||||||
|
|
||||||
const PRIORITIES = ["urgent", "high", "medium", "low"];
|
|
||||||
|
|
||||||
export const NavbarIssueFilter = observer(() => {
|
|
||||||
const store: RootStore = useMobxStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const pathName = router.asPath;
|
|
||||||
|
|
||||||
const handleOnSelect = (key: "states" | "labels" | "priorities", value: string) => {
|
|
||||||
// if (key === "states") {
|
|
||||||
// store.issue.userSelectedStates = store.issue.userSelectedStates.includes(value)
|
|
||||||
// ? store.issue.userSelectedStates.filter((s) => s !== value)
|
|
||||||
// : [...store.issue.userSelectedStates, value];
|
|
||||||
// } else if (key === "labels") {
|
|
||||||
// store.issue.userSelectedLabels = store.issue.userSelectedLabels.includes(value)
|
|
||||||
// ? store.issue.userSelectedLabels.filter((l) => l !== value)
|
|
||||||
// : [...store.issue.userSelectedLabels, value];
|
|
||||||
// } else if (key === "priorities") {
|
|
||||||
// store.issue.userSelectedPriorities = store.issue.userSelectedPriorities.includes(value)
|
|
||||||
// ? store.issue.userSelectedPriorities.filter((p) => p !== value)
|
|
||||||
// : [...store.issue.userSelectedPriorities, value];
|
|
||||||
// }
|
|
||||||
// const paramsCommaSeparated = `${`board=${store.issue.currentIssueBoardView || "list"}`}${
|
|
||||||
// store.issue.userSelectedPriorities.length > 0 ? `&priorities=${store.issue.userSelectedPriorities.join(",")}` : ""
|
|
||||||
// }${store.issue.userSelectedStates.length > 0 ? `&states=${store.issue.userSelectedStates.join(",")}` : ""}${
|
|
||||||
// store.issue.userSelectedLabels.length > 0 ? `&labels=${store.issue.userSelectedLabels.join(",")}` : ""
|
|
||||||
// }`;
|
|
||||||
// router.replace(`${pathName}?${paramsCommaSeparated}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
button={
|
|
||||||
<>
|
|
||||||
<span>Filters</span>
|
|
||||||
<ChevronDown className="h-3 w-3" aria-hidden="true" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
display: "Priority",
|
|
||||||
children: PRIORITIES.map((priority) => ({
|
|
||||||
display: (
|
|
||||||
<span className="capitalize flex items-center gap-x-2">
|
|
||||||
<span className="material-symbols-rounded text-[14px]">
|
|
||||||
{priority === "urgent"
|
|
||||||
? "error"
|
|
||||||
: priority === "high"
|
|
||||||
? "signal_cellular_alt"
|
|
||||||
: priority === "medium"
|
|
||||||
? "signal_cellular_alt_2_bar"
|
|
||||||
: "signal_cellular_alt_1_bar"}
|
|
||||||
</span>
|
|
||||||
{priority}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
onClick: () => handleOnSelect("priorities", priority),
|
|
||||||
isSelected: store.issue.filteredPriorities.includes(priority),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: "State",
|
|
||||||
children: (store.issue.states || []).map((state) => {
|
|
||||||
const stateGroup = issueGroupFilter(state.group);
|
|
||||||
|
|
||||||
return {
|
|
||||||
display: (
|
|
||||||
<span className="capitalize flex items-center gap-x-2">
|
|
||||||
{/* {stateGroup && <stateGroup.icon />} */}
|
|
||||||
{state.name}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
onClick: () => handleOnSelect("states", state.id),
|
|
||||||
isSelected: store.issue.filteredStates.includes(state.id),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: "Labels",
|
|
||||||
children: [...(store.issue.labels || [])].map((label) => ({
|
|
||||||
display: (
|
|
||||||
<span className="capitalize flex items-center gap-x-2">
|
|
||||||
<span
|
|
||||||
className="w-3 h-3 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: label.color || "#000",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{label.name}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
onClick: () => handleOnSelect("labels", label.id),
|
|
||||||
isSelected: store.issue.filteredLabels.includes(label.id),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -9,6 +9,7 @@ import { IssueCalendarView } from "components/issues/board-views/calendar";
|
|||||||
import { IssueSpreadsheetView } from "components/issues/board-views/spreadsheet";
|
import { IssueSpreadsheetView } from "components/issues/board-views/spreadsheet";
|
||||||
import { IssueGanttView } from "components/issues/board-views/gantt";
|
import { IssueGanttView } from "components/issues/board-views/gantt";
|
||||||
import { IssuePeekOverview } from "components/issues/peek-overview";
|
import { IssuePeekOverview } from "components/issues/peek-overview";
|
||||||
|
import { IssueAppliedFilters } from "components/issues/filters/applied-filters/root";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { RootStore } from "store/root";
|
import { RootStore } from "store/root";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
@ -71,7 +72,10 @@ export const ProjectDetailsView = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
projectStore?.activeBoard && (
|
projectStore?.activeBoard && (
|
||||||
<>
|
<div className="relative w-full h-full overflow-hidden flex flex-col">
|
||||||
|
{/* applied filters */}
|
||||||
|
<IssueAppliedFilters />
|
||||||
|
|
||||||
{projectStore?.activeBoard === "list" && (
|
{projectStore?.activeBoard === "list" && (
|
||||||
<div className="relative h-full w-full overflow-y-auto">
|
<div className="relative h-full w-full overflow-y-auto">
|
||||||
<IssueListView />
|
<IssueListView />
|
||||||
@ -85,7 +89,7 @@ export const ProjectDetailsView = observer(() => {
|
|||||||
{projectStore?.activeBoard === "calendar" && <IssueCalendarView />}
|
{projectStore?.activeBoard === "calendar" && <IssueCalendarView />}
|
||||||
{projectStore?.activeBoard === "spreadsheet" && <IssueSpreadsheetView />}
|
{projectStore?.activeBoard === "spreadsheet" && <IssueSpreadsheetView />}
|
||||||
{projectStore?.activeBoard === "gantt" && <IssueGanttView />}
|
{projectStore?.activeBoard === "gantt" && <IssueGanttView />}
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
// mobx
|
// mobx
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { observable, action, computed, makeObservable, runInAction, reaction } from "mobx";
|
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
||||||
// services
|
// services
|
||||||
import IssueService from "services/issue.service";
|
import IssueService from "services/issue.service";
|
||||||
// store
|
// store
|
||||||
|
29
space/store/issues/base-issue-filter.store.ts
Normal file
29
space/store/issues/base-issue-filter.store.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// types
|
||||||
|
import { RootStore } from "store/root";
|
||||||
|
|
||||||
|
export interface IIssueFilterBaseStore {
|
||||||
|
// helper methods
|
||||||
|
computedFilter(filters: any, filteredParams: any): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IssueFilterBaseStore implements IIssueFilterBaseStore {
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
// root store
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper methods
|
||||||
|
computedFilter = (filters: any, filteredParams: any) => {
|
||||||
|
const computedFilters: any = {};
|
||||||
|
Object.keys(filters).map((key) => {
|
||||||
|
if (filters[key] != undefined && filteredParams.includes(key))
|
||||||
|
computedFilters[key] =
|
||||||
|
typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(",");
|
||||||
|
});
|
||||||
|
|
||||||
|
return computedFilters;
|
||||||
|
};
|
||||||
|
}
|
52
space/store/issues/helpers.ts
Normal file
52
space/store/issues/helpers.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { TIssueBoardKeys } from "types/issue";
|
||||||
|
import { IIssueFilterOptions, TIssueParams } from "./types";
|
||||||
|
|
||||||
|
export const isNil = (value: any) => {
|
||||||
|
if (value === undefined || value === null) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ILayoutDisplayFiltersOptions {
|
||||||
|
filters: (keyof IIssueFilterOptions)[];
|
||||||
|
display_properties: boolean | null;
|
||||||
|
display_filters: null;
|
||||||
|
extra_options: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
||||||
|
[pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions };
|
||||||
|
} = {
|
||||||
|
issues: {
|
||||||
|
list: {
|
||||||
|
filters: ["priority", "state", "labels"],
|
||||||
|
display_properties: null,
|
||||||
|
display_filters: null,
|
||||||
|
extra_options: null,
|
||||||
|
},
|
||||||
|
kanban: {
|
||||||
|
filters: ["priority", "state", "labels"],
|
||||||
|
display_properties: null,
|
||||||
|
display_filters: null,
|
||||||
|
extra_options: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleIssueQueryParamsByLayout = (
|
||||||
|
layout: TIssueBoardKeys | undefined,
|
||||||
|
viewType: "issues"
|
||||||
|
): TIssueParams[] | null => {
|
||||||
|
const queryParams: TIssueParams[] = [];
|
||||||
|
|
||||||
|
if (!layout) return null;
|
||||||
|
|
||||||
|
const layoutOptions = ISSUE_DISPLAY_FILTERS_BY_LAYOUT[viewType][layout];
|
||||||
|
|
||||||
|
// add filters query params
|
||||||
|
layoutOptions.filters.forEach((option) => {
|
||||||
|
queryParams.push(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
return queryParams;
|
||||||
|
};
|
106
space/store/issues/issue-filters.store.ts
Normal file
106
space/store/issues/issue-filters.store.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { action, makeObservable, observable, runInAction, computed } from "mobx";
|
||||||
|
// types
|
||||||
|
import { RootStore } from "store/root";
|
||||||
|
import { IIssueFilterOptions, TIssueParams } from "./types";
|
||||||
|
import { handleIssueQueryParamsByLayout } from "./helpers";
|
||||||
|
import { IssueFilterBaseStore } from "./base-issue-filter.store";
|
||||||
|
|
||||||
|
interface IFiltersOptions {
|
||||||
|
filters: IIssueFilterOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIssuesFilterStore {
|
||||||
|
// observables
|
||||||
|
projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined;
|
||||||
|
// computed
|
||||||
|
issueFilters: IFiltersOptions | undefined;
|
||||||
|
appliedFilters: TIssueParams[] | undefined;
|
||||||
|
// helpers
|
||||||
|
issueDisplayFilters: (projectId: string) => IFiltersOptions | undefined;
|
||||||
|
// actions
|
||||||
|
updateFilters: (projectId: string, filters: IIssueFilterOptions) => Promise<IFiltersOptions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IssuesFilterStore extends IssueFilterBaseStore implements IIssuesFilterStore {
|
||||||
|
// observables
|
||||||
|
projectIssueFilters: { [projectId: string]: IFiltersOptions } | undefined = undefined;
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
super(_rootStore);
|
||||||
|
|
||||||
|
makeObservable(this, {
|
||||||
|
// observables
|
||||||
|
projectIssueFilters: observable.ref,
|
||||||
|
// computed
|
||||||
|
issueFilters: computed,
|
||||||
|
appliedFilters: computed,
|
||||||
|
// actions
|
||||||
|
updateFilters: action,
|
||||||
|
});
|
||||||
|
// root store
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
issueDisplayFilters = (projectId: string) => {
|
||||||
|
if (!projectId) return undefined;
|
||||||
|
return this.projectIssueFilters?.[projectId] || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// actions
|
||||||
|
|
||||||
|
updateFilters = async (projectId: string, filters: IIssueFilterOptions) => {
|
||||||
|
try {
|
||||||
|
let _projectIssueFilters = { ...this.projectIssueFilters };
|
||||||
|
if (!_projectIssueFilters) _projectIssueFilters = {};
|
||||||
|
if (!_projectIssueFilters[projectId]) _projectIssueFilters[projectId] = { filters: {} };
|
||||||
|
|
||||||
|
const _filters = {
|
||||||
|
filters: { ..._projectIssueFilters[projectId].filters },
|
||||||
|
};
|
||||||
|
|
||||||
|
_filters.filters = { ..._filters.filters, ...filters };
|
||||||
|
|
||||||
|
_projectIssueFilters[projectId] = {
|
||||||
|
filters: _filters.filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.projectIssueFilters = _projectIssueFilters;
|
||||||
|
});
|
||||||
|
|
||||||
|
return _filters;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get issueFilters() {
|
||||||
|
const projectId = this.rootStore.project.project?.id;
|
||||||
|
if (!projectId) return undefined;
|
||||||
|
|
||||||
|
const issueFilters = this.issueDisplayFilters(projectId);
|
||||||
|
if (!issueFilters) return undefined;
|
||||||
|
|
||||||
|
return issueFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
get appliedFilters() {
|
||||||
|
const userFilters = this.issueFilters;
|
||||||
|
const layout = this.rootStore.project?.activeBoard;
|
||||||
|
if (!userFilters || !layout) return undefined;
|
||||||
|
|
||||||
|
let filteredRouteParams: any = {
|
||||||
|
priority: userFilters?.filters?.priority || undefined,
|
||||||
|
state: userFilters?.filters?.state || undefined,
|
||||||
|
labels: userFilters?.filters?.labels || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredParams = handleIssueQueryParamsByLayout(layout, "issues");
|
||||||
|
if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams);
|
||||||
|
|
||||||
|
return filteredRouteParams;
|
||||||
|
}
|
||||||
|
}
|
36
space/store/issues/types.ts
Normal file
36
space/store/issues/types.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { IIssue } from "types/issue";
|
||||||
|
|
||||||
|
export type TIssueGroupByOptions = "state" | "priority" | "labels" | null;
|
||||||
|
|
||||||
|
export type TIssueParams = "priority" | "state" | "labels";
|
||||||
|
|
||||||
|
export interface IIssueFilterOptions {
|
||||||
|
state?: string[] | null;
|
||||||
|
labels?: string[] | null;
|
||||||
|
priority?: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// issues
|
||||||
|
export interface IGroupedIssues {
|
||||||
|
[group_id: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISubGroupedIssues {
|
||||||
|
[sub_grouped_id: string]: {
|
||||||
|
[group_id: string]: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TUnGroupedIssues = string[];
|
||||||
|
|
||||||
|
export interface IIssueResponse {
|
||||||
|
[issue_id: string]: IIssue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TLoader = "init-loader" | "mutation" | undefined;
|
||||||
|
|
||||||
|
export interface ViewFlags {
|
||||||
|
enableQuickAdd: boolean;
|
||||||
|
enableIssueCreation: boolean;
|
||||||
|
enableInlineEditing: boolean;
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
import { observable, action, makeObservable, runInAction } from "mobx";
|
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||||
// service
|
// service
|
||||||
import ProjectService from "services/project.service";
|
import ProjectService from "services/project.service";
|
||||||
|
import { TIssueBoardKeys } from "types/issue";
|
||||||
// types
|
// types
|
||||||
import { IWorkspace, IProject, IProjectSettings } from "types/project";
|
import { IWorkspace, IProject, IProjectSettings } from "types/project";
|
||||||
|
|
||||||
@ -12,9 +13,9 @@ export interface IProjectStore {
|
|||||||
project: IProject | null;
|
project: IProject | null;
|
||||||
deploySettings: IProjectSettings | null;
|
deploySettings: IProjectSettings | null;
|
||||||
viewOptions: any;
|
viewOptions: any;
|
||||||
activeBoard: string | null;
|
activeBoard: TIssueBoardKeys | null;
|
||||||
fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise<void>;
|
fetchProjectSettings: (workspace_slug: string, project_slug: string) => Promise<void>;
|
||||||
setActiveBoard: (value: string) => void;
|
setActiveBoard: (value: TIssueBoardKeys) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProjectStore implements IProjectStore {
|
class ProjectStore implements IProjectStore {
|
||||||
@ -25,7 +26,7 @@ class ProjectStore implements IProjectStore {
|
|||||||
project: IProject | null = null;
|
project: IProject | null = null;
|
||||||
deploySettings: IProjectSettings | null = null;
|
deploySettings: IProjectSettings | null = null;
|
||||||
viewOptions: any = null;
|
viewOptions: any = null;
|
||||||
activeBoard: string | null = null;
|
activeBoard: TIssueBoardKeys | null = null;
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
// service
|
// service
|
||||||
@ -80,7 +81,7 @@ class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setActiveBoard = (boardValue: string) => {
|
setActiveBoard = (boardValue: TIssueBoardKeys) => {
|
||||||
this.activeBoard = boardValue;
|
this.activeBoard = boardValue;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import IssueStore, { IIssueStore } from "./issue";
|
|||||||
import ProjectStore, { IProjectStore } from "./project";
|
import ProjectStore, { IProjectStore } from "./project";
|
||||||
import IssueDetailStore, { IIssueDetailStore } from "./issue_details";
|
import IssueDetailStore, { IIssueDetailStore } from "./issue_details";
|
||||||
import { IMentionsStore, MentionsStore } from "./mentions.store";
|
import { IMentionsStore, MentionsStore } from "./mentions.store";
|
||||||
|
import { IIssuesFilterStore, IssuesFilterStore } from "./issues/issue-filters.store";
|
||||||
|
|
||||||
enableStaticRendering(typeof window === "undefined");
|
enableStaticRendering(typeof window === "undefined");
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ export class RootStore {
|
|||||||
issueDetails: IIssueDetailStore;
|
issueDetails: IIssueDetailStore;
|
||||||
project: IProjectStore;
|
project: IProjectStore;
|
||||||
mentionsStore: IMentionsStore;
|
mentionsStore: IMentionsStore;
|
||||||
|
issuesFilter: IIssuesFilterStore;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.user = new UserStore(this);
|
this.user = new UserStore(this);
|
||||||
@ -22,5 +24,6 @@ export class RootStore {
|
|||||||
this.project = new ProjectStore(this);
|
this.project = new ProjectStore(this);
|
||||||
this.issueDetails = new IssueDetailStore(this);
|
this.issueDetails = new IssueDetailStore(this);
|
||||||
this.mentionsStore = new MentionsStore(this);
|
this.mentionsStore = new MentionsStore(this);
|
||||||
|
this.issuesFilter = new IssuesFilterStore(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import { memo } from "react";
|
||||||
import { Draggable } from "@hello-pangea/dnd";
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
// components
|
// components
|
||||||
import { KanBanProperties } from "./properties";
|
import { KanBanProperties } from "./properties";
|
||||||
// ui
|
// ui
|
||||||
@ -21,7 +23,7 @@ interface IssueBlockProps {
|
|||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
export const KanBanIssueMemoBlock: React.FC<IssueBlockProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
sub_group_id,
|
sub_group_id,
|
||||||
columnId,
|
columnId,
|
||||||
@ -63,30 +65,36 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
|||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
onClick={handleIssuePeekOverview}
|
|
||||||
>
|
>
|
||||||
{issue.tempId !== undefined && (
|
{issue.tempId !== undefined && (
|
||||||
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
<div className="absolute top-0 left-0 w-full h-full animate-pulse bg-custom-background-100/20 z-[99999]" />
|
||||||
)}
|
)}
|
||||||
<div className="absolute top-3 right-3 hidden group-hover/kanban-block:block">
|
|
||||||
{quickActions(
|
|
||||||
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
|
||||||
!columnId && columnId === "null" ? null : columnId,
|
|
||||||
issue
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className={`text-sm rounded py-2 px-3 shadow-custom-shadow-2xs space-y-2 border-[0.5px] border-custom-border-200 transition-all bg-custom-background-100 ${
|
className={`text-sm rounded py-2 px-3 shadow-custom-shadow-2xs space-y-2 border-[0.5px] border-custom-border-200 transition-all bg-custom-background-100 ${
|
||||||
isDragDisabled ? "" : "hover:cursor-grab"
|
isDragDisabled ? "" : "hover:cursor-grab"
|
||||||
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
|
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
|
||||||
>
|
>
|
||||||
{displayProperties && displayProperties?.key && (
|
{displayProperties && displayProperties?.key && (
|
||||||
|
<div className="relative">
|
||||||
<div className="text-xs line-clamp-1 text-custom-text-300">
|
<div className="text-xs line-clamp-1 text-custom-text-300">
|
||||||
{issue.project_detail.identifier}-{issue.sequence_id}
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">
|
||||||
|
{quickActions(
|
||||||
|
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
|
||||||
|
!columnId && columnId === "null" ? null : columnId,
|
||||||
|
issue
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
|
<div
|
||||||
|
className="line-clamp-2 text-sm font-medium text-custom-text-100"
|
||||||
|
onClick={handleIssuePeekOverview}
|
||||||
|
>
|
||||||
|
{issue.name}
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div>
|
<div>
|
||||||
<KanBanProperties
|
<KanBanProperties
|
||||||
@ -106,3 +114,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateMemo = (prevProps: IssueBlockProps, nextProps: IssueBlockProps) => {
|
||||||
|
if (prevProps.issue != nextProps.issue) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KanbanIssueBlock = memo(KanBanIssueMemoBlock, validateMemo);
|
||||||
|
@ -240,7 +240,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
if (handleSubmit) {
|
if (handleSubmit) {
|
||||||
await handleSubmit(res);
|
await handleSubmit(res);
|
||||||
} else {
|
} else {
|
||||||
currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId);
|
if (viewId) currentIssueStore.fetchIssues(workspaceSlug, dataIdToUpdate, "mutation", viewId);
|
||||||
|
|
||||||
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res, payload.cycle);
|
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res, payload.cycle);
|
||||||
if (payload.module && payload.module !== "") await addIssueToModule(res, payload.module);
|
if (payload.module && payload.module !== "") await addIssueToModule(res, payload.module);
|
||||||
|
@ -16,6 +16,8 @@ interface IProfileIssuesPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
||||||
|
const { type } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, userId } = router.query as {
|
const { workspaceSlug, userId } = router.query as {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -28,11 +30,11 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
|
|||||||
}: RootStore = useMobxStore();
|
}: RootStore = useMobxStore();
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${props.type}` : null,
|
workspaceSlug && userId ? `CURRENT_WORKSPACE_PROFILE_ISSUES_${workspaceSlug}_${userId}_${type}` : null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && userId) {
|
if (workspaceSlug && userId) {
|
||||||
await fetchFilters(workspaceSlug);
|
await fetchFilters(workspaceSlug);
|
||||||
await fetchIssues(workspaceSlug, userId, getIssues ? "mutation" : "init-loader", props.type);
|
await fetchIssues(workspaceSlug, userId, getIssues ? "mutation" : "init-loader", type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
import { ProfileAuthWrapper } from "layouts/user-profile-layout";
|
||||||
@ -9,7 +8,7 @@ import { UserProfileHeader } from "components/headers";
|
|||||||
import { NextPageWithLayout } from "types/app";
|
import { NextPageWithLayout } from "types/app";
|
||||||
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
import { ProfileIssuesPage } from "components/profile/profile-issues";
|
||||||
|
|
||||||
const ProfileAssignedIssuesPage: NextPageWithLayout = observer(() => <ProfileIssuesPage type="assigned" />);
|
const ProfileAssignedIssuesPage: NextPageWithLayout = () => <ProfileIssuesPage type="assigned" />;
|
||||||
|
|
||||||
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
ProfileAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
|
@ -118,8 +118,6 @@ export class KanBanHelpers implements IKanBanHelpers {
|
|||||||
|
|
||||||
const [removed] = sourceIssues.splice(source.index, 1);
|
const [removed] = sourceIssues.splice(source.index, 1);
|
||||||
|
|
||||||
console.log("removed", removed);
|
|
||||||
|
|
||||||
if (removed) {
|
if (removed) {
|
||||||
if (viewId) store?.removeIssue(workspaceSlug, projectId, removed, viewId);
|
if (viewId) store?.removeIssue(workspaceSlug, projectId, removed, viewId);
|
||||||
else store?.removeIssue(workspaceSlug, projectId, removed);
|
else store?.removeIssue(workspaceSlug, projectId, removed);
|
||||||
|
@ -28,7 +28,6 @@ export interface IProfileIssuesStore {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
loadType: TLoader,
|
loadType: TLoader,
|
||||||
_?: string,
|
|
||||||
type?: "assigned" | "created" | "subscribed"
|
type?: "assigned" | "created" | "subscribed"
|
||||||
) => Promise<IIssueResponse>;
|
) => Promise<IIssueResponse>;
|
||||||
createIssue: (workspaceSlug: string, userId: string, data: Partial<IIssue>) => Promise<IIssue | undefined>;
|
createIssue: (workspaceSlug: string, userId: string, data: Partial<IIssue>) => Promise<IIssue | undefined>;
|
||||||
@ -151,7 +150,6 @@ export class ProfileIssuesStore extends IssueBaseStore implements IProfileIssues
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
loadType: TLoader = "init-loader",
|
loadType: TLoader = "init-loader",
|
||||||
_?: string,
|
|
||||||
type?: "assigned" | "created" | "subscribed"
|
type?: "assigned" | "created" | "subscribed"
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
Loading…
Reference in New Issue
Block a user