plane/web/components/cycles/sidebar.tsx
sriram veeraghanta e178bba9c0
feat: session authentication and god-mode implementation (#4302)
* dev: move authentication to base class for credentials

* chore: new account creation

* dev: return error as query parameter

* dev: accounts and profile endpoints for user

* fix: user store updates

* fix: store fixes

* fix: type fixes

* dev: set is_password_autoset and is_email_verifier for auth providers

* dev: move all auth configuration to different apps

* dev: fix circular imports

* dev: remove unused imports

* dev: fix imports for authentication

* dev: update endpoints to use rest framework api viewa

* fix: onboarding fixes

* dev: session model changes

* fix: session model and add check for last name first name and avatar

* dev: fix referer redirect

* dev: remove auth imports

* dev: fix imports

* dev: update migrations

* fix: instance admin login

* comflict: conflicts resolved

* dev: fix import errors and email check endpoint

* fix: error messages and redirects after login

* dev: configs api

* fix: is github enabled boolean

* dev: merge config and instance api

* conflict: merge conflict resolved

* dev: instance admin sign up endpoint

* dev: enable magic link login

* dev: configure instance variables for github and google enabled

* chore: typo fixes

* fix: god mode docker file changes

* build-error: resolved build errors

* fix: docker compose changes

* dev: add email credential check endpoint

* fix: minor package changes

* fix: docker related changes

* dev: add nginx rules in the nginx template

* dev: refactor the url patterns

* fix: docker changes

* fix: docker files for god-mode

* fix: static export

* fix: nginx conf

* dev: smtp sender refused exception

* fix: godmode fixes

* chore: god mode revamp.

* dev: add csrf secured flag

* fix: oauth redirect uri and session settings

* chore: god mode app changes.  (#3982)

* chore: send test email functionality.

* style: authentication methods page UI revamp.

* chore: create workspace popup.

* fix: user me endpoint

* dev: fix redirection after authentication

* dev: handle god mode redirection

* fix: redirections

* fix: auth related hooks

* fix: store related fixes

* dev: fix session authentication for rest apis

* fix: linting errors

* fix: removing references of useStore=

* dev: fix redirection and password validation

* dev: add useUser hook

* fix: build fixes and lint issues

* fix: removing useApplication hook

* fix: build errors

* fix: delete unused files

* fix: auth build fixes

* fix: bugfixes

* dev: alter avatar to support more than 255 chars

* dev: fix profile endpoint and increase session expiry time and update session on every request

* chore: resolved the migration

* chore: resolved merge conflicts

* dev: error codes and error messages for the auth flow

* dev: instance admin sign up and sign in endpoint

* dev: use zxcvbn to validate password strength

* dev: add extra parameters when error handling on instance god mode

* chore: auth init

* chore: signin/ signup form ui updates and password strength meter.

* chore: update password fields.

* chore: validations and error handling.

* chore: updated sign-up form

* chore: updated workflow and updated the code structure

* chore: instance empty state for god-mode.

* chore: instance and auth wrappers update

* fix: renaming godmode

* fix: docker changes

* chore: updated authentication wrappers

* chore: updated the authentication workflow and rendered all pages

* fix: build errors

* fix: docker related fixes

* fix: tailing slash added to space and admin for valid nginx locations

* chore: seperate pages for signup and login

* git-action modified for admin file changes

* feature build action updated for admin app

* self host modified

* chore: resolved build errors and handled signin and signup in a seperate route

* chore: sign-in and sign-up revamp.

* fix: migration conflicts

* dev: migrations

* chore: handled redirection

* dev: admin url

* dev: create seperate endpoint for instance admin me

* dev: instance admin endpoint

* git action fixed

* chore: handled auth wrappers

* dev: add serializer and remove print logs

* fix: build errors

* dev: fix migrations

* dev: instance folder structuring

* fix: linting errors

* chore: resolved build errors

* chore: updated store and auth workflow and updates api service types

* chore: Replaced Next Link with Anchoer tag for god-mode redirection

* add 3333 port to allowed origins

* make password login working again

* dev: fix redirection, add admin signout endpoint and fix email credential check endpoint

* fix unique code sign in

* fix small build error

* enable sign out

* dev: add google client secret variable to configure instance

* dev: add referer for redirection

* fix origin urls for oauths

* admin setup and login separation

* dev: fix user redirection and tour completed endpoint

* fix build errors

* dev: add set password endpoint

* dev: remove user creation logic for redirection

* fix unique code page

* fix forgot password

* chore: onboarding revamp.

* dev: fix workspace slug redirection in login

* chore: invited user onboarding flow update.

* chore: fix switch or delete account modal.

* fix members exception

* refactor auth flows and add invitations to auth flow

* fix sig in sign up url

* fix action url

* fix build errors

* dev: fix user set password when logging in

* dev: reset password endpoint

* chore: confirm password validation for signup and onboarding.

* enable reset password

* fix build error

* chore: minor UI updates.

* chore: forgot and reset password UI revamp.

* fix authentication re directions

* dev: auth redirections

* change url paths for signup and signin

* dev: make the user logged in when changing passwords

* dev: next path redirection for web and space app

* dev: next path for magic sign in endpoint

* dev: github space endpoint

* chore: minor ui updates and fixes in web app.

* set password screen

* fix multiple unique code generation

* dev: next path base redirection

* dev: remove print logs

* dev: auth space endpoints

* fix build errors

* dev: invalidate cache on configuration update, god mode exception errors and authentication failed code

* dev: fix space endpoints and add extra endpoints

* chore: space auth revamp.

* dev: add sign up for space app

* fix: build errors.

* fix: auth redirection logic.

* chore: space app onboarding revamp.

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Manish Gupta <manish@mgupta.me>
Co-authored-by: = <=>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>
2024-04-29 12:12:33 +05:30

608 lines
25 KiB
TypeScript

import React, { useCallback, useEffect, useState } from "react";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
// icons
import {
ArchiveRestoreIcon,
ChevronDown,
LinkIcon,
Trash2,
UserCircle2,
AlertCircle,
ChevronRight,
CalendarClock,
} from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// types
import { ICycle, IIssueFilterOptions } from "@plane/types";
// ui
import { Avatar, ArchiveIcon, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
// components
import { SidebarProgressStats } from "@/components/core";
import ProgressChart from "@/components/core/sidebar/progress-chart";
import { ArchiveCycleModal, CycleDeleteModal } from "@/components/cycles";
import { DateRangeDropdown } from "@/components/dropdowns";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_UPDATED } from "@/constants/event-tracker";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useEventTracker, useCycle, useUser, useMember, useIssues } from "@/hooks/store";
// services
import { CycleService } from "@/services/cycle.service";
type Props = {
cycleId: string;
handleClose: () => void;
isArchived?: boolean;
};
const defaultValues: Partial<ICycle> = {
start_date: null,
end_date: null,
};
// services
const cycleService = new CycleService();
// TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { cycleId, handleClose, isArchived } = props;
// states
const [archiveCycleModal, setArchiveCycleModal] = useState(false);
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId, peekCycle } = router.query;
// store hooks
const { setTrackElement, captureCycleEvent } = useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
const { getCycleById, updateCycleDetails, restoreCycle } = useCycle();
const { getUserDetails } = useMember();
// derived values
const cycleDetails = getCycleById(cycleId);
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined;
// form info
const { control, reset } = useForm({
defaultValues,
});
const submitChanges = (data: Partial<ICycle>, changedProperty: string) => {
if (!workspaceSlug || !projectId || !cycleId) return;
updateCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data)
.then((res) => {
captureCycleEvent({
eventName: CYCLE_UPDATED,
payload: {
...res,
changed_properties: [changedProperty],
element: "Right side-peek",
state: "SUCCESS",
},
});
})
.catch(() => {
captureCycleEvent({
eventName: CYCLE_UPDATED,
payload: {
...data,
element: "Right side-peek",
state: "FAILED",
},
});
});
};
const handleCopyText = () => {
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Link Copied!",
message: "Cycle link copied to clipboard.",
});
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Some error occurred",
});
});
};
const handleRestoreCycle = async () => {
if (!workspaceSlug || !projectId) return;
await restoreCycle(workspaceSlug.toString(), projectId.toString(), cycleId)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Restore success",
message: "Your cycle can be found in project cycles.",
});
router.push(`/${workspaceSlug.toString()}/projects/${projectId.toString()}/archives/cycles`);
})
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Cycle could not be restored. Please try again.",
})
);
};
useEffect(() => {
if (cycleDetails)
reset({
...cycleDetails,
});
}, [cycleDetails, reset]);
const dateChecker = async (payload: any) => {
try {
const res = await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload);
return res.status;
} catch (err) {
return false;
}
};
const handleDateChange = async (startDate: Date | undefined, endDate: Date | undefined) => {
if (!startDate || !endDate) return;
let isDateValid = false;
const payload = {
start_date: renderFormattedPayloadDate(startDate),
end_date: renderFormattedPayloadDate(endDate),
};
if (cycleDetails && cycleDetails.start_date && cycleDetails.end_date)
isDateValid = await dateChecker({
...payload,
cycle_id: cycleDetails.id,
});
else isDateValid = await dateChecker(payload);
if (isDateValid) {
submitChanges(payload, "date_range");
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Cycle updated successfully.",
});
} else {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message:
"You already have a cycle on the given dates, if you want to create a draft cycle, you can do that by removing both the dates.",
});
reset({ ...cycleDetails });
}
};
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{ [key]: newValues },
cycleId
);
},
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
);
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed";
const startDate = getDate(cycleDetails?.start_date);
const endDate = getDate(cycleDetails?.end_date);
const isStartValid = startDate && startDate <= new Date();
const isEndValid = endDate && startDate && endDate >= startDate;
const progressPercentage = cycleDetails
? isCompleted && cycleDetails?.progress_snapshot
? Math.round(
(cycleDetails.progress_snapshot.completed_issues / cycleDetails.progress_snapshot.total_issues) * 100
)
: Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
: null;
if (!cycleDetails)
return (
<Loader className="px-5">
<div className="space-y-2">
<Loader.Item height="15px" width="50%" />
<Loader.Item height="15px" width="30%" />
</div>
<div className="mt-8 space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</div>
</Loader>
);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const issueCount =
isCompleted && !isEmpty(cycleDetails.progress_snapshot)
? cycleDetails.progress_snapshot.total_issues === 0
? "0 Issue"
: `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}`
: cycleDetails.total_issues === 0
? "0 Issue"
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
return (
<div className="relative">
{cycleDetails && workspaceSlug && projectId && (
<>
<ArchiveCycleModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleId}
isOpen={archiveCycleModal}
handleClose={() => setArchiveCycleModal(false)}
/>
<CycleDeleteModal
cycle={cycleDetails}
isOpen={cycleDeleteModal}
handleClose={() => setCycleDeleteModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
</>
)}
<>
<div className="sticky z-10 top-0 flex items-center justify-between bg-custom-sidebar-background-100 py-5">
<div>
<button
className="flex h-5 w-5 items-center justify-center rounded-full bg-custom-border-300"
onClick={() => handleClose()}
>
<ChevronRight className="h-3 w-3 stroke-2 text-white" />
</button>
</div>
<div className="flex items-center gap-3.5">
{!isArchived && (
<button onClick={handleCopyText}>
<LinkIcon className="h-3 w-3 text-custom-text-300" />
</button>
)}
{isEditingAllowed && (
<CustomMenu placement="bottom-end" ellipsis>
{!isArchived && (
<CustomMenu.MenuItem onClick={() => setArchiveCycleModal(true)} disabled={!isCompleted}>
{isCompleted ? (
<div className="flex items-center gap-2">
<ArchiveIcon className="h-3 w-3" />
Archive cycle
</div>
) : (
<div className="flex items-start gap-2">
<ArchiveIcon className="h-3 w-3" />
<div className="-mt-1">
<p>Archive cycle</p>
<p className="text-xs text-custom-text-400">
Only completed cycle <br /> can be archived.
</p>
</div>
</div>
)}
</CustomMenu.MenuItem>
)}
{isArchived && (
<CustomMenu.MenuItem onClick={handleRestoreCycle}>
<span className="flex items-center justify-start gap-2">
<ArchiveRestoreIcon className="h-3 w-3" />
<span>Restore cycle</span>
</span>
</CustomMenu.MenuItem>
)}
{!isCompleted && (
<CustomMenu.MenuItem
onClick={() => {
setTrackElement("CYCLE_PAGE_SIDEBAR");
setCycleDeleteModal(true);
}}
>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" />
<span>Delete cycle</span>
</span>
</CustomMenu.MenuItem>
)}
</CustomMenu>
)}
</div>
</div>
<div className="flex flex-col gap-3 pt-2">
<div className="flex items-center gap-5">
{currentCycle && (
<span
className="flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs"
style={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
}}
>
{currentCycle.value === "current" && daysLeft !== undefined
? `${daysLeft} ${currentCycle.label}`
: `${currentCycle.label}`}
</span>
)}
</div>
<h4 className="w-full break-words text-xl font-semibold text-custom-text-100">{cycleDetails.name}</h4>
</div>
{cycleDetails.description && (
<TextArea
className="outline-none ring-none w-full max-h-max bg-transparent !p-0 !m-0 !border-0 resize-none text-sm leading-5 text-custom-text-200"
value={cycleDetails.description}
disabled
/>
)}
<div className="flex flex-col gap-5 pb-6 pt-2.5">
<div className="flex items-center justify-start gap-1">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<CalendarClock className="h-4 w-4" />
<span className="text-base">Date range</span>
</div>
<div className="h-7 w-3/5">
<Controller
control={control}
name="start_date"
render={({ field: { value: startDateValue, onChange: onChangeStartDate } }) => (
<Controller
control={control}
name="end_date"
render={({ field: { value: endDateValue, onChange: onChangeEndDate } }) => (
<DateRangeDropdown
className="h-7"
buttonContainerClassName="w-full"
buttonVariant="background-with-text"
minDate={new Date()}
value={{
from: getDate(startDateValue),
to: getDate(endDateValue),
}}
onSelect={(val) => {
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
handleDateChange(val?.from, val?.to);
}}
placeholder={{
from: "Start date",
to: "End date",
}}
required={cycleDetails.status !== "draft"}
disabled={!isEditingAllowed || isArchived}
/>
)}
/>
)}
/>
</div>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<UserCircle2 className="h-4 w-4" />
<span className="text-base">Lead</span>
</div>
<div className="flex w-3/5 items-center rounded-sm">
<div className="flex items-center gap-2.5">
<Avatar name={cycleOwnerDetails?.display_name} src={cycleOwnerDetails?.avatar} />
<span className="text-sm text-custom-text-200">{cycleOwnerDetails?.display_name}</span>
</div>
</div>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
<LayersIcon className="h-4 w-4" />
<span className="text-base">Issues</span>
</div>
<div className="flex w-3/5 items-center">
<span className="px-1.5 text-sm text-custom-text-300">{issueCount}</span>
</div>
</div>
</div>
<div className="flex flex-col">
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 px-1.5 py-5">
<Disclosure defaultOpen>
{({ open }) => (
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
<Disclosure.Button
className="flex w-full items-center justify-between gap-2 p-1.5"
disabled={!isStartValid || !isEndValid}
>
<div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-custom-text-200">Progress</span>
</div>
<div className="flex items-center gap-2.5">
{progressPercentage ? (
<span className="flex h-5 w-9 items-center justify-center rounded bg-amber-500/20 text-xs font-medium text-amber-500">
{progressPercentage ? `${progressPercentage}%` : ""}
</span>
) : (
""
)}
{isStartValid && isEndValid ? (
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
) : (
<div className="flex items-center gap-1">
<AlertCircle height={14} width={14} className="text-custom-text-200" />
<span className="text-xs italic text-custom-text-200">
{cycleDetails?.start_date && cycleDetails?.end_date
? "This cycle isn't active yet."
: "Invalid date. Please enter valid date."}
</span>
</div>
)}
</div>
</Disclosure.Button>
<Transition show={open}>
<Disclosure.Panel>
<div className="flex flex-col gap-3">
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
<>
{cycleDetails.progress_snapshot.distribution?.completion_chart &&
cycleDetails.start_date &&
cycleDetails.end_date && (
<div className="h-full w-full pt-4">
<div className="flex items-start gap-4 py-2 text-xs">
<div className="flex items-center gap-3 text-custom-text-100">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
</div>
<div className="relative h-40 w-80">
<ProgressChart
distribution={cycleDetails.progress_snapshot.distribution?.completion_chart}
startDate={cycleDetails.start_date}
endDate={cycleDetails.end_date}
totalIssues={cycleDetails.progress_snapshot.total_issues}
/>
</div>
</div>
)}
</>
) : (
<>
{cycleDetails.distribution?.completion_chart &&
cycleDetails.start_date &&
cycleDetails.end_date && (
<div className="h-full w-full pt-4">
<div className="flex items-start gap-4 py-2 text-xs">
<div className="flex items-center gap-3 text-custom-text-100">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
</div>
<div className="relative h-40 w-80">
<ProgressChart
distribution={cycleDetails.distribution?.completion_chart}
startDate={cycleDetails.start_date}
endDate={cycleDetails.end_date}
totalIssues={cycleDetails.total_issues}
/>
</div>
</div>
)}
</>
)}
{/* stats */}
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
<>
{cycleDetails.progress_snapshot.total_issues > 0 &&
cycleDetails.progress_snapshot.distribution && (
<div className="h-full w-full border-t border-custom-border-200 pt-5">
<SidebarProgressStats
distribution={cycleDetails.progress_snapshot.distribution}
groupedIssues={{
backlog: cycleDetails.progress_snapshot.backlog_issues,
unstarted: cycleDetails.progress_snapshot.unstarted_issues,
started: cycleDetails.progress_snapshot.started_issues,
completed: cycleDetails.progress_snapshot.completed_issues,
cancelled: cycleDetails.progress_snapshot.cancelled_issues,
}}
totalIssues={cycleDetails.progress_snapshot.total_issues}
isPeekView={Boolean(peekCycle)}
isCompleted={isCompleted}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
)}
</>
) : (
<>
{cycleDetails.total_issues > 0 && cycleDetails.distribution && (
<div className="h-full w-full border-t border-custom-border-200 pt-5">
<SidebarProgressStats
distribution={cycleDetails.distribution}
groupedIssues={{
backlog: cycleDetails.backlog_issues,
unstarted: cycleDetails.unstarted_issues,
started: cycleDetails.started_issues,
completed: cycleDetails.completed_issues,
cancelled: cycleDetails.cancelled_issues,
}}
totalIssues={cycleDetails.total_issues}
isPeekView={Boolean(peekCycle)}
isCompleted={isCompleted}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
)}
</>
)}
</div>
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
</div>
</>
</div>
);
});