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:
Aaryan Khandelwal 2023-10-04 12:04:55 +05:30 committed by GitHub
parent 41fd9ce6e8
commit 7be038ac5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 280 additions and 326 deletions

View File

@ -6,7 +6,7 @@ import useSWR from "swr";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // 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(() => { export const AllViews: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
@ -35,7 +35,7 @@ export const AllViews: React.FC = observer(() => {
return ( return (
<div className="relative w-full h-full flex flex-col overflow-auto"> <div className="relative w-full h-full flex flex-col overflow-auto">
<AppliedFiltersList /> <AppliedFiltersRoot />
{activeLayout === "kanban" ? ( {activeLayout === "kanban" ? (
<KanBanLayout /> <KanBanLayout />
) : activeLayout === "calendar" ? ( ) : activeLayout === "calendar" ? (

View File

@ -38,7 +38,7 @@ export const CalendarChart: React.FC<Props> = observer((props) => {
<CalendarWeekHeader /> <CalendarWeekHeader />
<div className="h-full w-full overflow-y-auto"> <div className="h-full w-full overflow-y-auto">
{calendarLayout === "month" ? ( {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 && {allWeeksOfActiveMonth &&
Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => (
<CalendarWeekDays key={weekIndex} week={week} issues={issues} /> <CalendarWeekDays key={weekIndex} week={week} issues={issues} />

View File

@ -4,9 +4,10 @@ import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// icons
import { ChevronLeft, ChevronRight } from "lucide-react";
// constants // constants
import { MONTHS_LIST } from "constants/calendar"; import { MONTHS_LIST } from "constants/calendar";
import { ChevronLeft, ChevronRight } from "lucide-react";
export const CalendarMonthsDropdown: React.FC = observer(() => { export const CalendarMonthsDropdown: React.FC = observer(() => {
const { calendar: calendarStore, issueFilter: issueFilterStore } = useMobxStore(); const { calendar: calendarStore, issueFilter: issueFilterStore } = useMobxStore();
@ -46,73 +47,63 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
return ( return (
<Popover className="relative"> <Popover className="relative">
{({ close }) => ( <Popover.Button className="outline-none text-xl font-semibold" disabled={calendarLayout === "week"}>
<> {calendarLayout === "month"
<Popover.Button className="outline-none text-xl font-semibold" disabled={calendarLayout === "week"}> ? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}`
{calendarLayout === "month" : getWeekLayoutHeader()}
? `${MONTHS_LIST[activeMonthDate.getMonth() + 1].title} ${activeMonthDate.getFullYear()}` </Popover.Button>
: getWeekLayoutHeader()} <Transition
</Popover.Button> as={React.Fragment}
<Transition enter="transition ease-out duration-200"
as={React.Fragment} enterFrom="opacity-0 translate-y-1"
enter="transition ease-out duration-200" enterTo="opacity-100 translate-y-0"
enterFrom="opacity-0 translate-y-1" leave="transition ease-in duration-150"
enterTo="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leave="transition ease-in duration-150" leaveTo="opacity-0 translate-y-1"
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">
<Popover.Panel> <div className="flex items-center justify-between gap-2 pb-3">
<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"> <button
<div className="flex items-center justify-between gap-2 pb-3"> type="button"
<button className="grid place-items-center"
type="button" onClick={() => {
className="grid place-items-center" const previousYear = new Date(activeMonthDate.getFullYear() - 1, activeMonthDate.getMonth(), 1);
onClick={() => { handleDateChange(previousYear);
const previousYear = new Date(activeMonthDate.getFullYear() - 1, activeMonthDate.getMonth(), 1); }}
handleDateChange(previousYear); >
<ChevronLeft size={14} />
close(); </button>
}} <span className="text-xs">{activeMonthDate.getFullYear()}</span>
> <button
<ChevronLeft size={14} /> type="button"
</button> className="grid place-items-center"
<span className="text-xs">{activeMonthDate.getFullYear()}</span> onClick={() => {
<button const nextYear = new Date(activeMonthDate.getFullYear() + 1, activeMonthDate.getMonth(), 1);
type="button" handleDateChange(nextYear);
className="grid place-items-center" }}
onClick={() => { >
const nextYear = new Date(activeMonthDate.getFullYear() + 1, activeMonthDate.getMonth(), 1); <ChevronRight size={14} />
handleDateChange(nextYear); </button>
</div>
close(); <div className="grid grid-cols-4 gap-4 items-stretch justify-items-stretch pt-3">
}} {Object.values(MONTHS_LIST).map((month, index) => (
> <button
<ChevronRight size={14} /> key={month.shortTitle}
</button> type="button"
</div> className="text-xs hover:bg-custom-background-80 rounded py-0.5"
<div className="grid grid-cols-4 gap-4 items-stretch justify-items-stretch pt-3"> onClick={() => {
{Object.values(MONTHS_LIST).map((month, index) => ( const newDate = new Date(activeMonthDate.getFullYear(), index, 1);
<button handleDateChange(newDate);
key={month.shortTitle} }}
type="button" >
className="text-xs hover:bg-custom-background-80 rounded py-0.5" {month.shortTitle}
onClick={() => { </button>
const newDate = new Date(activeMonthDate.getFullYear(), index, 1); ))}
handleDateChange(newDate); </div>
</div>
close(); </Popover.Panel>
}} </Transition>
>
{month.shortTitle}
</button>
))}
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover> </Popover>
); );
}); });

View File

@ -86,7 +86,7 @@ export const CalendarHeader: React.FC = observer(() => {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<button <button
type="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} onClick={handleToday}
> >
Today Today

View File

@ -27,9 +27,9 @@ export const CalendarWeekDays: React.FC<Props> = observer((props) => {
return ( return (
<div <div
className={`grid divide-x-[0.5px] divide-y-[0.5px] divide-custom-border-200 ${ className={`grid divide-x-[0.5px] divide-custom-border-200 ${showWeekends ? "grid-cols-7" : "grid-cols-5"} ${
showWeekends ? "grid-cols-7" : "grid-cols-5" calendarLayout === "month" ? "" : "h-full"
} ${calendarLayout === "month" ? "" : "h-full"}`} }`}
> >
{Object.values(week).map((date: ICalendarDate) => { {Object.values(week).map((date: ICalendarDate) => {
if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null; if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null;

View File

@ -1,8 +1,5 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { import {
AppliedDateFilters, AppliedDateFilters,
@ -17,65 +14,19 @@ import { X } from "lucide-react";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types // types
import { IIssueFilterOptions } from "types"; import { IIssueFilterOptions, IIssueLabels, IStateResponse, IUserLite } from "types";
export const AppliedFiltersList: React.FC = observer(() => { type Props = {
const router = useRouter(); appliedFilters: IIssueFilterOptions;
const { workspaceSlug, projectId } = router.query; 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(); export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, members, states } = props;
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 ( return (
<div className="flex items-stretch gap-2 flex-wrap bg-custom-background-100 p-4"> <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") && ( {(filterKey === "assignees" || filterKey === "created_by" || filterKey === "subscriber") && (
<AppliedMembersFilters <AppliedMembersFilters
handleRemove={(val) => handleRemoveFilter(filterKey, val)} handleRemove={(val) => handleRemoveFilter(filterKey, val)}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)} members={members}
values={value} values={value}
/> />
)} )}
@ -101,7 +52,7 @@ export const AppliedFiltersList: React.FC = observer(() => {
{filterKey === "labels" && ( {filterKey === "labels" && (
<AppliedLabelsFilters <AppliedLabelsFilters
handleRemove={(val) => handleRemoveFilter("labels", val)} handleRemove={(val) => handleRemoveFilter("labels", val)}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} labels={labels}
values={value} values={value}
/> />
)} )}
@ -111,7 +62,7 @@ export const AppliedFiltersList: React.FC = observer(() => {
{filterKey === "state" && ( {filterKey === "state" && (
<AppliedStateFilters <AppliedStateFilters
handleRemove={(val) => handleRemoveFilter("state", val)} handleRemove={(val) => handleRemoveFilter("state", val)}
states={projectStore.states?.[projectId?.toString() ?? ""]} states={states}
values={value} values={value}
/> />
)} )}

View File

@ -3,5 +3,6 @@ export * from "./filters-list";
export * from "./label"; export * from "./label";
export * from "./members"; export * from "./members";
export * from "./priority"; export * from "./priority";
export * from "./root";
export * from "./state"; export * from "./state";
export * from "./state-group"; export * from "./state-group";

View File

@ -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() ?? ""]}
/>
);
});

View File

@ -1,34 +1,30 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// ui // ui
import { Avatar, Loader } from "components/ui"; import { Avatar, Loader } from "components/ui";
// types
import { IUserLite } from "types";
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
handleUpdate: (val: string) => void; handleUpdate: (val: string) => void;
itemsToRender: number; itemsToRender: number;
projectId: string; members: IUserLite[] | undefined;
searchQuery: string; searchQuery: string;
viewButtons: React.ReactNode; viewButtons: React.ReactNode;
}; };
export const FilterAssignees: React.FC<Props> = observer((props) => { export const FilterAssignees: React.FC<Props> = (props) => {
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props; const { appliedFilters, handleUpdate, itemsToRender, members, searchQuery, viewButtons } = props;
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { project: projectStore } = store;
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = projectStore.members?.[projectId?.toString() ?? ""]?.filter((member) => const filteredOptions = members?.filter((member) =>
member.member.display_name.toLowerCase().includes(searchQuery.toLowerCase()) member.display_name.toLowerCase().includes(searchQuery.toLowerCase())
); );
return ( return (
@ -45,11 +41,11 @@ export const FilterAssignees: React.FC<Props> = observer((props) => {
<> <>
{filteredOptions.slice(0, itemsToRender).map((member) => ( {filteredOptions.slice(0, itemsToRender).map((member) => (
<FilterOption <FilterOption
key={`assignees-${member?.member?.id}`} key={`assignees-${member.id}`}
isChecked={appliedFilters?.includes(member.member?.id) ? true : false} isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.member?.id)} onClick={() => handleUpdate(member.id)}
icon={<Avatar user={member.member} height="18px" width="18px" />} icon={<Avatar user={member} height="18px" width="18px" />}
title={member.member?.display_name} title={member.display_name}
/> />
))} ))}
{viewButtons} {viewButtons}
@ -68,4 +64,4 @@ export const FilterAssignees: React.FC<Props> = observer((props) => {
)} )}
</> </>
); );
}); };

View File

@ -1,34 +1,30 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// ui // ui
import { Avatar, Loader } from "components/ui"; import { Avatar, Loader } from "components/ui";
// types
import { IUserLite } from "types";
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
handleUpdate: (val: string) => void; handleUpdate: (val: string) => void;
itemsToRender: number; itemsToRender: number;
projectId: string; members: IUserLite[] | undefined;
searchQuery: string; searchQuery: string;
viewButtons: React.ReactNode; viewButtons: React.ReactNode;
}; };
export const FilterCreatedBy: React.FC<Props> = observer((props) => { export const FilterCreatedBy: React.FC<Props> = (props) => {
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props; const { appliedFilters, handleUpdate, itemsToRender, members, searchQuery, viewButtons } = props;
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { project: projectStore } = store;
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = projectStore.members?.[projectId?.toString() ?? ""]?.filter((member) => const filteredOptions = members?.filter((member) =>
member.member.display_name.toLowerCase().includes(searchQuery.toLowerCase()) member.display_name.toLowerCase().includes(searchQuery.toLowerCase())
); );
return ( return (
@ -45,11 +41,11 @@ export const FilterCreatedBy: React.FC<Props> = observer((props) => {
<> <>
{filteredOptions.slice(0, itemsToRender).map((member) => ( {filteredOptions.slice(0, itemsToRender).map((member) => (
<FilterOption <FilterOption
key={`created-by-${member.member?.id}`} key={`created-by-${member.id}`}
isChecked={appliedFilters?.includes(member.member?.id) ? true : false} isChecked={appliedFilters?.includes(member.id) ? true : false}
onClick={() => handleUpdate(member.member?.id)} onClick={() => handleUpdate(member.id)}
icon={<Avatar user={member.member} height="18px" width="18px" />} icon={<Avatar user={member} height="18px" width="18px" />}
title={member.member?.display_name} title={member.display_name}
/> />
))} ))}
{viewButtons} {viewButtons}
@ -68,4 +64,4 @@ export const FilterCreatedBy: React.FC<Props> = observer((props) => {
)} )}
</> </>
); );
}); };

View File

@ -31,6 +31,29 @@ type Props = {
projectId: string; 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) => { export const FilterSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, projectId } = props; const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, projectId } = props;
@ -157,25 +180,12 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
itemsToRender={filtersToRender.priority?.currentLength ?? 0} itemsToRender={filtersToRender.priority?.currentLength ?? 0}
searchQuery={filtersSearchQuery} searchQuery={filtersSearchQuery}
viewButtons={ viewButtons={
<div className="flex items-center gap-2 ml-7 mt-1"> <ViewButtons
{/* TODO: handle view more and less in a better way */} isViewLessVisible={isViewLessVisible("priority")}
{isViewMoreVisible("priority") && ( isViewMoreVisible={isViewMoreVisible("priority")}
<button handleLess={() => handleViewLess("priority")}
className="text-custom-primary-100 text-xs font-medium" handleMore={() => handleViewMore("priority")}
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>
} }
/> />
</div> </div>
@ -189,25 +199,15 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
handleUpdate={(val) => handleFiltersUpdate("state_group", val)} handleUpdate={(val) => handleFiltersUpdate("state_group", val)}
itemsToRender={filtersToRender.state_group?.currentLength ?? 0} itemsToRender={filtersToRender.state_group?.currentLength ?? 0}
searchQuery={filtersSearchQuery} 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> </div>
)} )}
@ -219,23 +219,16 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
handleUpdate={(val) => handleFiltersUpdate("state", val)} handleUpdate={(val) => handleFiltersUpdate("state", val)}
itemsToRender={filtersToRender.state?.currentLength ?? 0} itemsToRender={filtersToRender.state?.currentLength ?? 0}
searchQuery={filtersSearchQuery} 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> </div>
)} )}
@ -246,27 +239,15 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
appliedFilters={filters.assignees ?? null} appliedFilters={filters.assignees ?? null}
handleUpdate={(val) => handleFiltersUpdate("assignees", val)} handleUpdate={(val) => handleFiltersUpdate("assignees", val)}
itemsToRender={filtersToRender.assignees?.currentLength ?? 0} itemsToRender={filtersToRender.assignees?.currentLength ?? 0}
projectId={projectId} members={projectStore.members?.[projectId]?.map((m) => m.member) ?? undefined}
searchQuery={filtersSearchQuery} searchQuery={filtersSearchQuery}
viewButtons={ viewButtons={
<div className="flex items-center gap-2 ml-7 mt-1"> <ViewButtons
{isViewMoreVisible("assignees") && ( isViewLessVisible={isViewLessVisible("assignees")}
<button isViewMoreVisible={isViewMoreVisible("assignees")}
className="text-custom-primary-100 text-xs font-medium ml-7" handleLess={() => handleViewLess("assignees")}
onClick={() => handleViewMore("assignees")} handleMore={() => handleViewMore("assignees")}
> />
View more
</button>
)}
{isViewLessVisible("assignees") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewLess("assignees")}
>
View less
</button>
)}
</div>
} }
/> />
</div> </div>
@ -279,27 +260,15 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
appliedFilters={filters.created_by ?? null} appliedFilters={filters.created_by ?? null}
handleUpdate={(val) => handleFiltersUpdate("created_by", val)} handleUpdate={(val) => handleFiltersUpdate("created_by", val)}
itemsToRender={filtersToRender.created_by?.currentLength ?? 0} itemsToRender={filtersToRender.created_by?.currentLength ?? 0}
projectId={projectId} members={projectStore.members?.[projectId]?.map((m) => m.member) ?? undefined}
searchQuery={filtersSearchQuery} searchQuery={filtersSearchQuery}
viewButtons={ viewButtons={
<div className="flex items-center gap-2 ml-7 mt-1"> <ViewButtons
{isViewMoreVisible("created_by") && ( isViewLessVisible={isViewLessVisible("created_by")}
<button isViewMoreVisible={isViewMoreVisible("created_by")}
className="text-custom-primary-100 text-xs font-medium ml-7" handleLess={() => handleViewLess("created_by")}
onClick={() => handleViewMore("created_by")} handleMore={() => 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>
} }
/> />
</div> </div>
@ -312,27 +281,15 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
appliedFilters={filters.labels ?? null} appliedFilters={filters.labels ?? null}
handleUpdate={(val) => handleFiltersUpdate("labels", val)} handleUpdate={(val) => handleFiltersUpdate("labels", val)}
itemsToRender={filtersToRender.labels?.currentLength ?? 0} itemsToRender={filtersToRender.labels?.currentLength ?? 0}
projectId={projectId} labels={projectStore.labels?.[projectId] ?? undefined}
searchQuery={filtersSearchQuery} searchQuery={filtersSearchQuery}
viewButtons={ viewButtons={
<div className="flex items-center gap-2 ml-7 mt-1"> <ViewButtons
{isViewMoreVisible("labels") && ( isViewLessVisible={isViewLessVisible("labels")}
<button isViewMoreVisible={isViewMoreVisible("labels")}
className="text-custom-primary-100 text-xs font-medium" handleLess={() => handleViewLess("labels")}
onClick={() => handleViewMore("labels")} handleMore={() => handleViewMore("labels")}
> />
View more
</button>
)}
{isViewLessVisible("labels") && (
<button
className="text-custom-primary-100 text-xs font-medium"
onClick={() => handleViewLess("labels")}
>
View less
</button>
)}
</div>
} }
/> />
</div> </div>

View File

@ -1,12 +1,11 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// ui // ui
import { Loader } from "components/ui"; import { Loader } from "components/ui";
// types
import { IIssueLabels } from "types";
const LabelIcons = ({ color }: { color: string }) => ( const LabelIcons = ({ color }: { color: string }) => (
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} /> <span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
@ -16,24 +15,19 @@ type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
handleUpdate: (val: string) => void; handleUpdate: (val: string) => void;
itemsToRender: number; itemsToRender: number;
projectId: string; labels: IIssueLabels[] | undefined;
searchQuery: string; searchQuery: string;
viewButtons: React.ReactNode; viewButtons: React.ReactNode;
}; };
export const FilterLabels: React.FC<Props> = observer((props) => { export const FilterLabels: React.FC<Props> = (props) => {
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery, viewButtons } = props; const { appliedFilters, handleUpdate, itemsToRender, labels, searchQuery, viewButtons } = props;
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore();
const { project: projectStore } = store;
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const filteredOptions = projectStore.labels?.[projectId?.toString() ?? ""]?.filter((label) => const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase()));
label.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return ( return (
<> <>
@ -72,4 +66,4 @@ export const FilterLabels: React.FC<Props> = observer((props) => {
)} )}
</> </>
); );
}); };

View File

@ -49,15 +49,10 @@ export const FilterStartDate: React.FC<Props> = observer((props) => {
isChecked={appliedFilters?.includes(option.value) ? true : false} isChecked={appliedFilters?.includes(option.value) ? true : false}
onClick={() => handleUpdate(option.value)} onClick={() => handleUpdate(option.value)}
title={option.name} title={option.name}
multiple={false} multiple
/> />
))} ))}
<FilterOption <FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
isChecked={false}
onClick={() => setIsDateFilterModalOpen(true)}
title="Custom"
multiple={false}
/>
</> </>
) : ( ) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p> <p className="text-xs text-custom-text-400 italic">No matches found</p>

View File

@ -13,10 +13,11 @@ type Props = {
handleUpdate: (val: string) => void; handleUpdate: (val: string) => void;
itemsToRender: number; itemsToRender: number;
searchQuery: string; searchQuery: string;
viewButtons: React.ReactNode;
}; };
export const FilterStateGroup: React.FC<Props> = observer((props) => { 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); const [previewEnabled, setPreviewEnabled] = useState(true);
@ -34,9 +35,8 @@ export const FilterStateGroup: React.FC<Props> = observer((props) => {
{previewEnabled && ( {previewEnabled && (
<div> <div>
{filteredOptions.length > 0 ? ( {filteredOptions.length > 0 ? (
filteredOptions <>
.slice(0, itemsToRender) {filteredOptions.slice(0, itemsToRender).map((stateGroup) => (
.map((stateGroup) => (
<FilterOption <FilterOption
key={stateGroup.key} key={stateGroup.key}
isChecked={appliedFilters?.includes(stateGroup.key) ? true : false} isChecked={appliedFilters?.includes(stateGroup.key) ? true : false}
@ -44,7 +44,9 @@ export const FilterStateGroup: React.FC<Props> = observer((props) => {
icon={<StateGroupIcon stateGroup={stateGroup.key} />} icon={<StateGroupIcon stateGroup={stateGroup.key} />}
title={stateGroup.title} title={stateGroup.title}
/> />
)) ))}
{viewButtons}
</>
) : ( ) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p> <p className="text-xs text-custom-text-400 italic">No matches found</p>
)} )}

View File

@ -1,8 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { FilterHeader, FilterOption } from "components/issues"; import { FilterHeader, FilterOption } from "components/issues";
// ui // ui
@ -11,25 +8,24 @@ import { Loader } from "components/ui";
import { StateGroupIcon } from "components/icons"; import { StateGroupIcon } from "components/icons";
// helpers // helpers
import { getStatesList } from "helpers/state.helper"; import { getStatesList } from "helpers/state.helper";
// types
import { IStateResponse } from "types";
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
handleUpdate: (val: string) => void; handleUpdate: (val: string) => void;
itemsToRender: number; itemsToRender: number;
projectId: string;
searchQuery: string; searchQuery: string;
states: IStateResponse | undefined;
viewButtons: React.ReactNode;
}; };
export const FilterState: React.FC<Props> = observer((props) => { export const FilterState: React.FC<Props> = (props) => {
const { appliedFilters, handleUpdate, itemsToRender, projectId, searchQuery } = props; const { appliedFilters, handleUpdate, itemsToRender, searchQuery, states, viewButtons } = props;
const [previewEnabled, setPreviewEnabled] = useState(true); const [previewEnabled, setPreviewEnabled] = useState(true);
const store = useMobxStore(); const statesList = getStatesList(states);
const { project: projectStore } = store;
const statesByGroups = projectStore.states?.[projectId?.toString() ?? ""];
const statesList = getStatesList(statesByGroups);
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
@ -56,6 +52,7 @@ export const FilterState: React.FC<Props> = observer((props) => {
title={state.name} title={state.name}
/> />
))} ))}
{viewButtons}
</> </>
) : ( ) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p> <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) => {
)} )}
</> </>
); );
}); };

View File

@ -49,15 +49,10 @@ export const FilterTargetDate: React.FC<Props> = observer((props) => {
isChecked={appliedFilters?.includes(option.value) ? true : false} isChecked={appliedFilters?.includes(option.value) ? true : false}
onClick={() => handleUpdate(option.value)} onClick={() => handleUpdate(option.value)}
title={option.name} title={option.name}
multiple={false} multiple
/> />
))} ))}
<FilterOption <FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
isChecked={false}
onClick={() => setIsDateFilterModalOpen(true)}
title="Custom"
multiple={false}
/>
</> </>
) : ( ) : (
<p className="text-xs text-custom-text-400 italic">No matches found</p> <p className="text-xs text-custom-text-400 italic">No matches found</p>