mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
refactor: filter components (#2359)
* fix: calendar layout dividers * refactor: filter selection components * fix: dropdown closing after selection * refactor: filters components
This commit is contained in:
parent
41fd9ce6e8
commit
7be038ac5a
@ -6,7 +6,7 @@ import useSWR from "swr";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { AppliedFiltersList, CalendarLayout, GanttLayout, KanBanLayout, SpreadsheetLayout } from "components/issues";
|
||||
import { AppliedFiltersRoot, CalendarLayout, GanttLayout, KanBanLayout, SpreadsheetLayout } from "components/issues";
|
||||
|
||||
export const AllViews: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
@ -35,7 +35,7 @@ export const AllViews: React.FC = observer(() => {
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full flex flex-col overflow-auto">
|
||||
<AppliedFiltersList />
|
||||
<AppliedFiltersRoot />
|
||||
{activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
|
@ -38,7 +38,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
|
||||
<CalendarWeekHeader />
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{calendarLayout === "month" ? (
|
||||
<div className="h-full w-full grid grid-cols-1">
|
||||
<div className="h-full w-full grid grid-cols-1 divide-y-[0.5px] divide-custom-border-200">
|
||||
{allWeeksOfActiveMonth &&
|
||||
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
|
||||
<CalendarWeekDays key={weekIndex} week={week} issues={issues} />
|
||||
|
@ -4,9 +4,10 @@ import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// icons
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
// constants
|
||||
import { MONTHS_LIST } from "constants/calendar";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
export const CalendarMonthsDropdown: React.FC = observer(() => {
|
||||
const { calendar: calendarStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
@ -46,73 +47,63 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<Popover.Button className="outline-none text-xl font-semibold" disabled={calendarLayout === "week"}>
|
||||
{calendarLayout === "month"
|
||||
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}`
|
||||
: getWeekLayoutHeader()}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={React.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="absolute left-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded w-56 p-3 divide-y divide-custom-border-200">
|
||||
<div className="flex items-center justify-between gap-2 pb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
const previousYear = new Date(activeMonthDate.getFullYear() - 1, activeMonthDate.getMonth(), 1);
|
||||
handleDateChange(previousYear);
|
||||
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
</button>
|
||||
<span className="text-xs">{activeMonthDate.getFullYear()}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
const nextYear = new Date(activeMonthDate.getFullYear() + 1, activeMonthDate.getMonth(), 1);
|
||||
handleDateChange(nextYear);
|
||||
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 items-stretch justify-items-stretch pt-3">
|
||||
{Object.values(MONTHS_LIST).map((month, index) => (
|
||||
<button
|
||||
key={month.shortTitle}
|
||||
type="button"
|
||||
className="text-xs hover:bg-custom-background-80 rounded py-0.5"
|
||||
onClick={() => {
|
||||
const newDate = new Date(activeMonthDate.getFullYear(), index, 1);
|
||||
handleDateChange(newDate);
|
||||
|
||||
close();
|
||||
}}
|
||||
>
|
||||
{month.shortTitle}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
<Popover.Button className="outline-none text-xl font-semibold" disabled={calendarLayout === "week"}>
|
||||
{calendarLayout === "month"
|
||||
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}`
|
||||
: getWeekLayoutHeader()}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={React.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="absolute left-0 z-10 mt-1 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded w-56 p-3 divide-y divide-custom-border-200">
|
||||
<div className="flex items-center justify-between gap-2 pb-3">
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
const previousYear = new Date(activeMonthDate.getFullYear() - 1, activeMonthDate.getMonth(), 1);
|
||||
handleDateChange(previousYear);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
</button>
|
||||
<span className="text-xs">{activeMonthDate.getFullYear()}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
const nextYear = new Date(activeMonthDate.getFullYear() + 1, activeMonthDate.getMonth(), 1);
|
||||
handleDateChange(nextYear);
|
||||
}}
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 items-stretch justify-items-stretch pt-3">
|
||||
{Object.values(MONTHS_LIST).map((month, index) => (
|
||||
<button
|
||||
key={month.shortTitle}
|
||||
type="button"
|
||||
className="text-xs hover:bg-custom-background-80 rounded py-0.5"
|
||||
onClick={() => {
|
||||
const newDate = new Date(activeMonthDate.getFullYear(), index, 1);
|
||||
handleDateChange(newDate);
|
||||
}}
|
||||
>
|
||||
{month.shortTitle}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
@ -86,7 +86,7 @@ export const CalendarHeader: React.FC = observer(() => {
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className="px-2.5 py-1 text-xs bg-custom-background-80 rounded font-medium"
|
||||
className="px-2.5 py-1 text-xs bg-custom-background-80 rounded font-medium text-custom-text-200 hover:text-custom-text-100"
|
||||
onClick={handleToday}
|
||||
>
|
||||
Today
|
||||
|
@ -27,9 +27,9 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid divide-x-[0.5px] divide-y-[0.5px] divide-custom-border-200 ${
|
||||
showWeekends ? "grid-cols-7" : "grid-cols-5"
|
||||
} ${calendarLayout === "month" ? "" : "h-full"}`}
|
||||
className={`grid divide-x-[0.5px] divide-custom-border-200 ${showWeekends ? "grid-cols-7" : "grid-cols-5"} ${
|
||||
calendarLayout === "month" ? "" : "h-full"
|
||||
}`}
|
||||
>
|
||||
{Object.values(week).map((date: ICalendarDate) => {
|
||||
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import {
|
||||
AppliedDateFilters,
|
||||
@ -17,65 +14,19 @@ import { X } from "lucide-react";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
import { IIssueFilterOptions, IIssueLabels, IStateResponse, IUserLite } from "types";
|
||||
|
||||
export const AppliedFiltersList: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
type Props = {
|
||||
appliedFilters: IIssueFilterOptions;
|
||||
handleClearAllFilters: () => void;
|
||||
handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void;
|
||||
labels: IIssueLabels[] | undefined;
|
||||
members: IUserLite[] | undefined;
|
||||
states: IStateResponse | undefined;
|
||||
};
|
||||
|
||||
const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore();
|
||||
|
||||
const userFilters = issueFilterStore.userFilters;
|
||||
|
||||
// filters whose value not null or empty array
|
||||
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 handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!value) {
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = issueFilterStore.userFilters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: newValues,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: { ...newFilters },
|
||||
});
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, states } = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-stretch gap-2 flex-wrap bg-custom-background-100 p-4">
|
||||
@ -91,7 +42,7 @@ export const AppliedFiltersList: React.FC = observer(() => {
|
||||
{(filterKey === "assignees" || filterKey === "created_by" || filterKey === "subscriber") && (
|
||||
<AppliedMembersFilters
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
members={members}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
@ -101,7 +52,7 @@ export const AppliedFiltersList: React.FC = observer(() => {
|
||||
{filterKey === "labels" && (
|
||||
<AppliedLabelsFilters
|
||||
handleRemove={(val) => handleRemoveFilter("labels", val)}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||
labels={labels}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
@ -111,7 +62,7 @@ export const AppliedFiltersList: React.FC = observer(() => {
|
||||
{filterKey === "state" && (
|
||||
<AppliedStateFilters
|
||||
handleRemove={(val) => handleRemoveFilter("state", val)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||
states={states}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
|
@ -3,5 +3,6 @@ export * from "./filters-list";
|
||||
export * from "./label";
|
||||
export * from "./members";
|
||||
export * from "./priority";
|
||||
export * from "./root";
|
||||
export * from "./state";
|
||||
export * from "./state-group";
|
||||
|
@ -0,0 +1,79 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "types";
|
||||
|
||||
export const AppliedFiltersRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore();
|
||||
|
||||
const userFilters = issueFilterStore.userFilters;
|
||||
|
||||
// filters whose value not null or empty array
|
||||
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 handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
// remove all values of the key if value is null
|
||||
if (!value) {
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = issueFilterStore.userFilters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: newValues,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: { ...newFilters },
|
||||
});
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||
/>
|
||||
);
|
||||
});
|
@ -1,34 +1,30 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
// ui
|
||||
import { Avatar, Loader } from "components/ui";
|
||||
// types
|
||||
import { IUserLite } from "types";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
itemsToRender: number;
|
||||
projectId: string;
|
||||
members: IUserLite[] | undefined;
|
||||
searchQuery: string;
|
||||
viewButtons: React.ReactNode;
|
||||
};
|
||||
|
||||
export const FilterAssignees: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props;
|
||||
export const FilterAssignees: React.FC<Props> = (props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, members, searchQuery, viewButtons } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
const store = useMobxStore();
|
||||
const { project: projectStore } = store;
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
|
||||
const filteredOptions = projectStore.members?.[projectId?.toString() ?? ""]?.filter((member) =>
|
||||
member.member.display_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const filteredOptions = members?.filter((member) =>
|
||||
member.display_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
@ -45,11 +41,11 @@ export const FilterAssignees: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
{filteredOptions.slice(0, itemsToRender).map((member) => (
|
||||
<FilterOption
|
||||
key={`assignees-${member?.member?.id}`}
|
||||
isChecked={appliedFilters?.includes(member.member?.id) ? true : false}
|
||||
onClick={() => handleUpdate(member.member?.id)}
|
||||
icon={<Avatar user={member.member} height="18px" width="18px" />}
|
||||
title={member.member?.display_name}
|
||||
key={`assignees-${member.id}`}
|
||||
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
||||
onClick={() => handleUpdate(member.id)}
|
||||
icon={<Avatar user={member} height="18px" width="18px" />}
|
||||
title={member.display_name}
|
||||
/>
|
||||
))}
|
||||
{viewButtons}
|
||||
@ -68,4 +64,4 @@ export const FilterAssignees: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -1,34 +1,30 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
// ui
|
||||
import { Avatar, Loader } from "components/ui";
|
||||
// types
|
||||
import { IUserLite } from "types";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
itemsToRender: number;
|
||||
projectId: string;
|
||||
members: IUserLite[] | undefined;
|
||||
searchQuery: string;
|
||||
viewButtons: React.ReactNode;
|
||||
};
|
||||
|
||||
export const FilterCreatedBy: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props;
|
||||
export const FilterCreatedBy: React.FC<Props> = (props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, members, searchQuery, viewButtons } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
const store = useMobxStore();
|
||||
const { project: projectStore } = store;
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
|
||||
const filteredOptions = projectStore.members?.[projectId?.toString() ?? ""]?.filter((member) =>
|
||||
member.member.display_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const filteredOptions = members?.filter((member) =>
|
||||
member.display_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
@ -45,11 +41,11 @@ export const FilterCreatedBy: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
{filteredOptions.slice(0, itemsToRender).map((member) => (
|
||||
<FilterOption
|
||||
key={`created-by-${member.member?.id}`}
|
||||
isChecked={appliedFilters?.includes(member.member?.id) ? true : false}
|
||||
onClick={() => handleUpdate(member.member?.id)}
|
||||
icon={<Avatar user={member.member} height="18px" width="18px" />}
|
||||
title={member.member?.display_name}
|
||||
key={`created-by-${member.id}`}
|
||||
isChecked={appliedFilters?.includes(member.id) ? true : false}
|
||||
onClick={() => handleUpdate(member.id)}
|
||||
icon={<Avatar user={member} height="18px" width="18px" />}
|
||||
title={member.display_name}
|
||||
/>
|
||||
))}
|
||||
{viewButtons}
|
||||
@ -68,4 +64,4 @@ export const FilterCreatedBy: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -31,6 +31,29 @@ type Props = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
type ViewButtonProps = {
|
||||
handleLess: () => void;
|
||||
handleMore: () => void;
|
||||
isViewLessVisible: boolean;
|
||||
isViewMoreVisible: boolean;
|
||||
};
|
||||
|
||||
const ViewButtons = ({ handleLess, handleMore, isViewLessVisible, isViewMoreVisible }: ViewButtonProps) => (
|
||||
<div className="flex items-center gap-2 ml-7 mt-1">
|
||||
{/* TODO: handle view more and less in a better way */}
|
||||
{isViewMoreVisible && (
|
||||
<button className="text-custom-primary-100 text-xs font-medium" onClick={handleMore}>
|
||||
View more
|
||||
</button>
|
||||
)}
|
||||
{isViewLessVisible && (
|
||||
<button className="text-custom-primary-100 text-xs font-medium" onClick={handleLess}>
|
||||
View less
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, projectId } = props;
|
||||
|
||||
@ -157,25 +180,12 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
itemsToRender={filtersToRender.priority?.currentLength ?? 0}
|
||||
searchQuery={filtersSearchQuery}
|
||||
viewButtons={
|
||||
<div className="flex items-center gap-2 ml-7 mt-1">
|
||||
{/* TODO: handle view more and less in a better way */}
|
||||
{isViewMoreVisible("priority") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium"
|
||||
onClick={() => handleViewMore("priority")}
|
||||
>
|
||||
View more
|
||||
</button>
|
||||
)}
|
||||
{isViewLessVisible("priority") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium"
|
||||
onClick={() => handleViewLess("priority")}
|
||||
>
|
||||
View less
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ViewButtons
|
||||
isViewLessVisible={isViewLessVisible("priority")}
|
||||
isViewMoreVisible={isViewMoreVisible("priority")}
|
||||
handleLess={() => handleViewLess("priority")}
|
||||
handleMore={() => handleViewMore("priority")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -189,25 +199,15 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
handleUpdate={(val) => handleFiltersUpdate("state_group", val)}
|
||||
itemsToRender={filtersToRender.state_group?.currentLength ?? 0}
|
||||
searchQuery={filtersSearchQuery}
|
||||
viewButtons={
|
||||
<ViewButtons
|
||||
isViewLessVisible={isViewLessVisible("state_group")}
|
||||
isViewMoreVisible={isViewMoreVisible("state_group")}
|
||||
handleLess={() => handleViewLess("state_group")}
|
||||
handleMore={() => handleViewMore("state_group")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center gap-2 ml-7 mt-1">
|
||||
{isViewMoreVisible("state_group") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium ml-7"
|
||||
onClick={() => handleViewMore("state_group")}
|
||||
>
|
||||
View more
|
||||
</button>
|
||||
)}
|
||||
{isViewLessVisible("state_group") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium"
|
||||
onClick={() => handleViewLess("state_group")}
|
||||
>
|
||||
View less
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -219,23 +219,16 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
handleUpdate={(val) => handleFiltersUpdate("state", val)}
|
||||
itemsToRender={filtersToRender.state?.currentLength ?? 0}
|
||||
searchQuery={filtersSearchQuery}
|
||||
projectId={projectId}
|
||||
states={projectStore.states?.[projectId]}
|
||||
viewButtons={
|
||||
<ViewButtons
|
||||
isViewLessVisible={isViewLessVisible("state")}
|
||||
isViewMoreVisible={isViewMoreVisible("state")}
|
||||
handleLess={() => handleViewLess("state")}
|
||||
handleMore={() => handleViewMore("state")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center gap-2 ml-7 mt-1">
|
||||
{isViewMoreVisible("state") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium ml-7"
|
||||
onClick={() => handleViewMore("state")}
|
||||
>
|
||||
View more
|
||||
</button>
|
||||
)}
|
||||
{isViewLessVisible("state") && (
|
||||
<button className="text-custom-primary-100 text-xs font-medium" onClick={() => handleViewLess("state")}>
|
||||
View less
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -246,27 +239,15 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
appliedFilters={filters.assignees ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("assignees", val)}
|
||||
itemsToRender={filtersToRender.assignees?.currentLength ?? 0}
|
||||
projectId={projectId}
|
||||
members={projectStore.members?.[projectId]?.map((m) => m.member) ?? undefined}
|
||||
searchQuery={filtersSearchQuery}
|
||||
viewButtons={
|
||||
<div className="flex items-center gap-2 ml-7 mt-1">
|
||||
{isViewMoreVisible("assignees") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium ml-7"
|
||||
onClick={() => handleViewMore("assignees")}
|
||||
>
|
||||
View more
|
||||
</button>
|
||||
)}
|
||||
{isViewLessVisible("assignees") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium"
|
||||
onClick={() => handleViewLess("assignees")}
|
||||
>
|
||||
View less
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ViewButtons
|
||||
isViewLessVisible={isViewLessVisible("assignees")}
|
||||
isViewMoreVisible={isViewMoreVisible("assignees")}
|
||||
handleLess={() => handleViewLess("assignees")}
|
||||
handleMore={() => handleViewMore("assignees")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -279,27 +260,15 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
appliedFilters={filters.created_by ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("created_by", val)}
|
||||
itemsToRender={filtersToRender.created_by?.currentLength ?? 0}
|
||||
projectId={projectId}
|
||||
members={projectStore.members?.[projectId]?.map((m) => m.member) ?? undefined}
|
||||
searchQuery={filtersSearchQuery}
|
||||
viewButtons={
|
||||
<div className="flex items-center gap-2 ml-7 mt-1">
|
||||
{isViewMoreVisible("created_by") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium ml-7"
|
||||
onClick={() => handleViewMore("created_by")}
|
||||
>
|
||||
View more
|
||||
</button>
|
||||
)}
|
||||
{isViewLessVisible("created_by") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium"
|
||||
onClick={() => handleViewLess("created_by")}
|
||||
>
|
||||
View less
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ViewButtons
|
||||
isViewLessVisible={isViewLessVisible("created_by")}
|
||||
isViewMoreVisible={isViewMoreVisible("created_by")}
|
||||
handleLess={() => handleViewLess("created_by")}
|
||||
handleMore={() => handleViewMore("created_by")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -312,27 +281,15 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
appliedFilters={filters.labels ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("labels", val)}
|
||||
itemsToRender={filtersToRender.labels?.currentLength ?? 0}
|
||||
projectId={projectId}
|
||||
labels={projectStore.labels?.[projectId] ?? undefined}
|
||||
searchQuery={filtersSearchQuery}
|
||||
viewButtons={
|
||||
<div className="flex items-center gap-2 ml-7 mt-1">
|
||||
{isViewMoreVisible("labels") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium"
|
||||
onClick={() => handleViewMore("labels")}
|
||||
>
|
||||
View more
|
||||
</button>
|
||||
)}
|
||||
{isViewLessVisible("labels") && (
|
||||
<button
|
||||
className="text-custom-primary-100 text-xs font-medium"
|
||||
onClick={() => handleViewLess("labels")}
|
||||
>
|
||||
View less
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ViewButtons
|
||||
isViewLessVisible={isViewLessVisible("labels")}
|
||||
isViewMoreVisible={isViewMoreVisible("labels")}
|
||||
handleLess={() => handleViewLess("labels")}
|
||||
handleMore={() => handleViewMore("labels")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,12 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// types
|
||||
import { IIssueLabels } from "types";
|
||||
|
||||
const LabelIcons = ({ color }: { color: string }) => (
|
||||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
|
||||
@ -16,24 +15,19 @@ type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
itemsToRender: number;
|
||||
projectId: string;
|
||||
labels: IIssueLabels[] | undefined;
|
||||
searchQuery: string;
|
||||
viewButtons: React.ReactNode;
|
||||
};
|
||||
|
||||
export const FilterLabels: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props;
|
||||
export const FilterLabels: React.FC<Props> = (props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, labels, searchQuery, viewButtons } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
const store = useMobxStore();
|
||||
const { project: projectStore } = store;
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
|
||||
const filteredOptions = projectStore.labels?.[projectId?.toString() ?? ""]?.filter((label) =>
|
||||
label.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -72,4 +66,4 @@ export const FilterLabels: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -49,15 +49,10 @@ export const FilterStartDate: React.FC<Props> = observer((props) => {
|
||||
isChecked={appliedFilters?.includes(option.value) ? true : false}
|
||||
onClick={() => handleUpdate(option.value)}
|
||||
title={option.name}
|
||||
multiple={false}
|
||||
multiple
|
||||
/>
|
||||
))}
|
||||
<FilterOption
|
||||
isChecked={false}
|
||||
onClick={() => setIsDateFilterModalOpen(true)}
|
||||
title="Custom"
|
||||
multiple={false}
|
||||
/>
|
||||
<FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-custom-text-400 italic">No matches found</p>
|
||||
|
@ -13,10 +13,11 @@ type Props = {
|
||||
handleUpdate: (val: string) => void;
|
||||
itemsToRender: number;
|
||||
searchQuery: string;
|
||||
viewButtons: React.ReactNode;
|
||||
};
|
||||
|
||||
export const FilterStateGroup: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, searchQuery } = props;
|
||||
const { appliedFilters, handleUpdate, itemsToRender, searchQuery, viewButtons } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
@ -34,9 +35,8 @@ export const FilterStateGroup: React.FC<Props> = observer((props) => {
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{filteredOptions.length > 0 ? (
|
||||
filteredOptions
|
||||
.slice(0, itemsToRender)
|
||||
.map((stateGroup) => (
|
||||
<>
|
||||
{filteredOptions.slice(0, itemsToRender).map((stateGroup) => (
|
||||
<FilterOption
|
||||
key={stateGroup.key}
|
||||
isChecked={appliedFilters?.includes(stateGroup.key) ? true : false}
|
||||
@ -44,7 +44,9 @@ export const FilterStateGroup: React.FC<Props> = observer((props) => {
|
||||
icon={<StateGroupIcon stateGroup={stateGroup.key} />}
|
||||
title={stateGroup.title}
|
||||
/>
|
||||
))
|
||||
))}
|
||||
{viewButtons}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-custom-text-400 italic">No matches found</p>
|
||||
)}
|
||||
|
@ -1,8 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "components/issues";
|
||||
// ui
|
||||
@ -11,25 +8,24 @@ import { Loader } from "components/ui";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { IStateResponse } from "types";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
handleUpdate: (val: string) => void;
|
||||
itemsToRender: number;
|
||||
projectId: string;
|
||||
searchQuery: string;
|
||||
states: IStateResponse | undefined;
|
||||
viewButtons: React.ReactNode;
|
||||
};
|
||||
|
||||
export const FilterState: React.FC<Props> = observer((props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery } = props;
|
||||
export const FilterState: React.FC<Props> = (props) => {
|
||||
const { appliedFilters, handleUpdate, itemsToRender, searchQuery, states, viewButtons } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = useState(true);
|
||||
|
||||
const store = useMobxStore();
|
||||
const { project: projectStore } = store;
|
||||
|
||||
const statesByGroups = projectStore.states?.[projectId?.toString() ?? ""];
|
||||
const statesList = getStatesList(statesByGroups);
|
||||
const statesList = getStatesList(states);
|
||||
|
||||
const appliedFiltersCount = appliedFilters?.length ?? 0;
|
||||
|
||||
@ -56,6 +52,7 @@ export const FilterState: React.FC<Props> = observer((props) => {
|
||||
title={state.name}
|
||||
/>
|
||||
))}
|
||||
{viewButtons}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-custom-text-400 italic">No matches found</p>
|
||||
@ -71,4 +68,4 @@ export const FilterState: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -49,15 +49,10 @@ export const FilterTargetDate: React.FC<Props> = observer((props) => {
|
||||
isChecked={appliedFilters?.includes(option.value) ? true : false}
|
||||
onClick={() => handleUpdate(option.value)}
|
||||
title={option.name}
|
||||
multiple={false}
|
||||
multiple
|
||||
/>
|
||||
))}
|
||||
<FilterOption
|
||||
isChecked={false}
|
||||
onClick={() => setIsDateFilterModalOpen(true)}
|
||||
title="Custom"
|
||||
multiple={false}
|
||||
/>
|
||||
<FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-custom-text-400 italic">No matches found</p>
|
||||
|
Loading…
Reference in New Issue
Block a user