Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-477] feat: enhanced project issue filtering by cycles and modules #3830

Merged
merged 2 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/types/src/view-props.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export type TIssueParams =
| "created_by"
| "subscriber"
| "labels"
| "cycle"
| "module"
| "start_date"
| "target_date"
| "project"
Expand All @@ -79,6 +81,8 @@ export interface IIssueFilterOptions {
labels?: string[] | null;
priority?: string[] | null;
project?: string[] | null;
cycle?: string[] | null;
module?: string[] | null;
start_date?: string[] | null;
state?: string[] | null;
state_group?: string[] | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
// hooks
import { useCycle } from "hooks/store";
// ui
import { CycleGroupIcon } from "@plane/ui";
// types
import { TCycleGroups } from "@plane/types";

type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};

export const AppliedCycleFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values, editable } = props;
// store hooks
const { getCycleById } = useCycle();

return (
<>
{values.map((cycleId) => {
const cycleDetails = getCycleById(cycleId) ?? null;

if (!cycleDetails) return null;

const cycleStatus = (cycleDetails?.status ? cycleDetails?.status.toLocaleLowerCase() : "draft") as TCycleGroups;

return (
<div key={cycleId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3 w-3 flex-shrink-0" />
<span className="normal-case">{cycleDetails.name}</span>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(cycleId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}
</>
);
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
import { useRouter } from "next/router";
// hooks
import { useUser } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
// components
import {
AppliedCycleFilters,
AppliedDateFilters,
AppliedLabelsFilters,
AppliedMembersFilters,
AppliedModuleFilters,
AppliedPriorityFilters,
AppliedProjectFilters,
AppliedStateFilters,
Expand Down Expand Up @@ -34,6 +37,9 @@ const dateFilters = ["start_date", "target_date"];
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states, alwaysAllowEditing } = props;
// store hooks
const {
router: { moduleId, cycleId },
} = useApplication();
const {
membership: { currentProjectRole },
} = useUser();
Expand Down Expand Up @@ -104,6 +110,20 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
values={value}
/>
)}
{filterKey === "cycle" && !cycleId && (
<AppliedCycleFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("cycle", val)}
values={value}
/>
)}
{filterKey === "module" && !moduleId && (
<AppliedModuleFilters
editable={isEditingAllowed}
handleRemove={(val) => handleRemoveFilter("module", val)}
values={value}
/>
)}
{isEditingAllowed && (
<button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ export * from "./label";
export * from "./members";
export * from "./priority";
export * from "./project";
export * from "./module";
export * from "./cycle";
export * from "./state";
export * from "./state-group";
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { observer } from "mobx-react-lite";
import { X } from "lucide-react";
// hooks
import { useModule } from "hooks/store";
// ui
import { DiceIcon } from "@plane/ui";

type Props = {
handleRemove: (val: string) => void;
values: string[];
editable: boolean | undefined;
};

export const AppliedModuleFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values, editable } = props;
// store hooks
const { getModuleById } = useModule();

return (
<>
{values.map((moduleId) => {
const moduleDetails = getModuleById(moduleId) ?? null;

if (!moduleDetails) return null;

return (
<div key={moduleId} className="flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
<DiceIcon className="h-3 w-3 flex-shrink-0" />
<span className="normal-case">{moduleDetails.name}</span>
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(moduleId)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}
</>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import sortBy from "lodash/sortBy";
// components
import { FilterHeader, FilterOption } from "components/issues";
import { useApplication, useCycle } from "hooks/store";
// ui
import { Loader, CycleGroupIcon } from "@plane/ui";
// types
import { TCycleGroups } from "@plane/types";

type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
searchQuery: string;
};

export const FilterCycle: React.FC<Props> = observer((props) => {
const { appliedFilters, handleUpdate, searchQuery } = props;

// hooks
const {
router: { projectId },
} = useApplication();
const { getCycleById, getProjectCycleIds } = useCycle();

// states
const [itemsToRender, setItemsToRender] = useState(5);
const [previewEnabled, setPreviewEnabled] = useState(true);

const cycleIds = projectId ? getProjectCycleIds(projectId) : undefined;
const cycles = cycleIds?.map((projectId) => getCycleById(projectId)!) ?? null;
const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = sortBy(
cycles?.filter((cycle) => cycle.name.toLowerCase().includes(searchQuery.toLowerCase())),
(cycle) => cycle.name.toLowerCase()
);

const handleViewToggle = () => {
if (!filteredOptions) return;

if (itemsToRender === filteredOptions.length) setItemsToRender(5);
else setItemsToRender(filteredOptions.length);
};

const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups;

return (
<>
<FilterHeader
title={`Cycle ${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={previewEnabled}
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
/>
{previewEnabled && (
<div>
{filteredOptions ? (
filteredOptions.length > 0 ? (
<>
{filteredOptions.slice(0, itemsToRender).map((cycle) => (
<FilterOption
key={cycle.id}
isChecked={appliedFilters?.includes(cycle.id) ? true : false}
onClick={() => handleUpdate(cycle.id)}
icon={
<CycleGroupIcon cycleGroup={cycleStatus(cycle?.status)} className="h-3.5 w-3.5 flex-shrink-0" />
}
title={cycle.name}
activePulse={cycleStatus(cycle?.status) === "current" ? true : false}
/>
))}
{filteredOptions.length > 5 && (
<button
type="button"
className="ml-8 text-xs font-medium text-custom-primary-100"
onClick={handleViewToggle}
>
{itemsToRender === filteredOptions.length ? "View less" : "View all"}
</button>
)}
</>
) : (
<p className="text-xs italic text-custom-text-400">No matches found</p>
)
) : (
<Loader className="space-y-2">
<Loader.Item height="20px" />
<Loader.Item height="20px" />
<Loader.Item height="20px" />
</Loader>
)}
</div>
)}
</>
);
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { Search, X } from "lucide-react";
// hooks
import { useApplication } from "hooks/store";
// components
import {
FilterAssignees,
Expand All @@ -13,6 +15,8 @@ import {
FilterState,
FilterStateGroup,
FilterTargetDate,
FilterCycle,
FilterModule,
} from "components/issues";
// types
import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types";
Expand All @@ -30,6 +34,10 @@ type Props = {

export const FilterSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props;
// hooks
const {
router: { moduleId, cycleId },
} = useApplication();
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");

Expand Down Expand Up @@ -102,6 +110,28 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
</div>
)}

{/* cycle */}
{isFilterEnabled("cycle") && !cycleId && (
<div className="py-2">
<FilterCycle
appliedFilters={filters.cycle ?? null}
handleUpdate={(val) => handleFiltersUpdate("cycle", val)}
searchQuery={filtersSearchQuery}
/>
</div>
)}

{/* module */}
{isFilterEnabled("module") && !moduleId && (
<div className="py-2">
<FilterModule
appliedFilters={filters.module ?? null}
handleUpdate={(val) => handleFiltersUpdate("module", val)}
searchQuery={filtersSearchQuery}
/>
</div>
)}

{/* assignees */}
{isFilterEnabled("mentions") && (
<div className="py-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export * from "./project";
export * from "./start-date";
export * from "./state-group";
export * from "./state";
export * from "./cycle";
export * from "./module";
export * from "./target-date";
Loading
Loading