From 8ab7d13d2fbfeb8532f7f1c70527024c6519e1a2 Mon Sep 17 00:00:00 2001 From: Mathieu Bouhelier Date: Tue, 28 Jan 2025 20:35:53 -0300 Subject: [PATCH 1/6] add filter --- .../modules/__shared__/MobileDrawer/index.tsx | 52 +++++++++ .../modules/__shared__/common/types.ts | 13 +++ .../web/SearchNotFoundState/index.tsx | 8 +- .../activity-log/DateFilterChip/index.tsx | 85 +++++++++++++++ .../activity-log/DateFilterChip/types.ts | 6 ++ .../DateFilterComponent/index.tsx | 100 ++++++++++++++++++ .../activity-log/DateFilterComponent/types.ts | 17 +++ .../graphql/queries/ActivityLogsFragment.ts | 15 ++- .../web/ActivityLogComponent/index.tsx | 33 +++++- packages/utils/constants/date.ts | 1 + 10 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 packages/components/modules/__shared__/MobileDrawer/index.tsx create mode 100644 packages/components/modules/activity-log/DateFilterChip/index.tsx create mode 100644 packages/components/modules/activity-log/DateFilterChip/types.ts create mode 100644 packages/components/modules/activity-log/DateFilterComponent/index.tsx create mode 100644 packages/components/modules/activity-log/DateFilterComponent/types.ts diff --git a/packages/components/modules/__shared__/MobileDrawer/index.tsx b/packages/components/modules/__shared__/MobileDrawer/index.tsx new file mode 100644 index 00000000..b5c22e3f --- /dev/null +++ b/packages/components/modules/__shared__/MobileDrawer/index.tsx @@ -0,0 +1,52 @@ +import { FC } from 'react' + +import { Box, Divider, Drawer, Typography, styled } from '@mui/material' + +import { MobileDrawerProps } from '../types' + +const Puller = styled('div')(({ theme }) => ({ + width: 64, + height: 6, + backgroundColor: theme.palette.grey[500], + borderRadius: 3, + position: 'absolute', + top: 8, + left: 'calc(50% - 32px)', +})) + +const MobileDrawer: FC = ({ + open, + onClose, + title = 'Drawer Title', + children, +}) => ( + + + + + {title} + + + {children} + + +) + +export default MobileDrawer diff --git a/packages/components/modules/__shared__/common/types.ts b/packages/components/modules/__shared__/common/types.ts index 27187687..b787b310 100644 --- a/packages/components/modules/__shared__/common/types.ts +++ b/packages/components/modules/__shared__/common/types.ts @@ -1,3 +1,16 @@ +import { ReactNode } from 'react' + export type SocialUpsertForm = { body: string } + +export interface SearchNotFoundStateProps { + message?: string +} + +export interface MobileDrawerProps { + open: boolean + onClose: () => void + title?: string + children: ReactNode +} diff --git a/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx b/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx index 43a9b85c..498397d1 100644 --- a/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx +++ b/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx @@ -2,7 +2,11 @@ import { SearchingImage } from '@baseapp-frontend/design-system/components/web/i import { Box, Typography } from '@mui/material' -const SearchNotFoundState = () => ( +import { SearchNotFoundStateProps } from '../types' + +const SearchNotFoundState = ({ + message = 'Check your spelling or try another search.', +}: SearchNotFoundStateProps) => ( @@ -10,7 +14,7 @@ const SearchNotFoundState = () => ( No results found - Check your spelling or try another search. + {message} diff --git a/packages/components/modules/activity-log/DateFilterChip/index.tsx b/packages/components/modules/activity-log/DateFilterChip/index.tsx new file mode 100644 index 00000000..547c665d --- /dev/null +++ b/packages/components/modules/activity-log/DateFilterChip/index.tsx @@ -0,0 +1,85 @@ +import { FC, useState } from 'react' + +import { DATE_FORMAT, formatDate } from '@baseapp-frontend/utils' + +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import { Box, Chip, Menu, Theme, useMediaQuery } from '@mui/material' +import { DateTime } from 'luxon' + +import MobileDrawer from '../../__shared__/MobileDrawer' +import DateFilterComponent from '../DateFilterComponent' +import { DateFilterChipProps } from './types' + +const DateFilterChip: FC = ({ fetchParameters, executeRefetch }) => { + const [drawerOpen, setDrawerOpen] = useState(false) + const [anchorEl, setAnchorEl] = useState(null) + const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')) + + const handleDrawerOpen = () => setDrawerOpen(true) + const handleDrawerClose = () => setDrawerOpen(false) + const handleMenuClose = () => setAnchorEl(null) + + const { createdFrom, createdTo } = fetchParameters + const hasDateSelected = createdFrom || createdTo + + const labelRender = () => { + if (createdFrom && createdTo) { + return `${formatDate(createdFrom, { toFormat: DATE_FORMAT[3] })} - ${formatDate(createdTo, { toFormat: DATE_FORMAT[3] })}` + } + if (createdFrom) { + return `From ${formatDate(createdFrom, { toFormat: DATE_FORMAT[3] })}` + } + if (createdTo) { + return `Until ${formatDate(createdTo, { toFormat: DATE_FORMAT[3] })}` + } + return 'Period' + } + + const handleOnClickOnChip = (event: React.MouseEvent) => { + if (isMobile) { + handleDrawerOpen() + } else { + setAnchorEl(event.currentTarget) + } + } + + return ( + <> + + {labelRender()} + + + } + onClick={handleOnClickOnChip} + variant={hasDateSelected ? 'filled' : 'soft'} + color="default" + /> + + + + + + + + + + + + ) +} + +export default DateFilterChip diff --git a/packages/components/modules/activity-log/DateFilterChip/types.ts b/packages/components/modules/activity-log/DateFilterChip/types.ts new file mode 100644 index 00000000..c8f9dc6e --- /dev/null +++ b/packages/components/modules/activity-log/DateFilterChip/types.ts @@ -0,0 +1,6 @@ +import { IFetchParameters } from '../DateFilterComponent/types' + +export interface DateFilterChipProps { + fetchParameters: IFetchParameters + executeRefetch: (params: Partial) => void +} diff --git a/packages/components/modules/activity-log/DateFilterComponent/index.tsx b/packages/components/modules/activity-log/DateFilterComponent/index.tsx new file mode 100644 index 00000000..e9c7566b --- /dev/null +++ b/packages/components/modules/activity-log/DateFilterComponent/index.tsx @@ -0,0 +1,100 @@ +import React, { FC, useState } from 'react' + +import { DATE_FORMAT, formatDate } from '@baseapp-frontend/utils' + +import { Box, Button } from '@mui/material' +import { DateValidationError, LocalizationProvider } from '@mui/x-date-pickers' +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon' +import { DatePicker } from '@mui/x-date-pickers/DatePicker' +import { DateTime } from 'luxon' + +import { DateFilterComponentProps } from './types' + +const DateFilterComponent: FC = ({ + createdFrom, + createdTo, + executeRefetch, + onApply, + onClearFilter, +}) => { + const [tempCreatedFrom, setTempCreatedFrom] = useState(createdFrom ?? null) + const [tempCreatedTo, setTempCreatedTo] = useState(createdTo ?? null) + + const [error, setError] = useState(null) + + const errorMessage = React.useMemo(() => { + switch (error) { + case 'minDate': + return 'End date cannot be earlier than start date.' + case 'invalidDate': + return 'Your date is not valid' + default: + return '' + } + }, [error]) + + const handleApply = () => { + if (tempCreatedTo && tempCreatedFrom && tempCreatedTo < tempCreatedFrom) { + setError('minDate') + return + } + + setError(null) + executeRefetch({ + createdFrom: tempCreatedFrom + ? formatDate(tempCreatedFrom, { toFormat: DATE_FORMAT.api }) + : null, + createdTo: tempCreatedTo ? formatDate(tempCreatedTo, { toFormat: DATE_FORMAT.api }) : null, + }) + onApply?.() + } + + const handleClear = () => { + if (createdFrom || createdTo) { + executeRefetch({ + createdFrom: null, + createdTo: null, + }) + } + setTempCreatedFrom(null) + setTempCreatedTo(null) + onClearFilter?.() + } + + return ( + + + + setTempCreatedFrom(newValue)} + disableFuture + value={tempCreatedFrom} + /> + setTempCreatedTo(newValue)} + onError={(newError) => setError(newError)} + disableFuture + slotProps={{ + textField: { + helperText: errorMessage, + }, + }} + /> + + + + + + + + ) +} + +export default DateFilterComponent diff --git a/packages/components/modules/activity-log/DateFilterComponent/types.ts b/packages/components/modules/activity-log/DateFilterComponent/types.ts new file mode 100644 index 00000000..12005b1b --- /dev/null +++ b/packages/components/modules/activity-log/DateFilterComponent/types.ts @@ -0,0 +1,17 @@ +import { DateTime } from 'luxon' + +export interface IFetchParameters { + createdFrom: string | null + createdTo: string | null + userName: string + count: number + cursor: string | null +} + +export interface DateFilterComponentProps { + createdFrom: DateTime | null + createdTo: DateTime | null + executeRefetch: (params: Partial) => void + onApply?: () => void + onClearFilter?: () => void +} diff --git a/packages/components/modules/activity-log/common/graphql/queries/ActivityLogsFragment.ts b/packages/components/modules/activity-log/common/graphql/queries/ActivityLogsFragment.ts index 3703a992..9521f7ef 100644 --- a/packages/components/modules/activity-log/common/graphql/queries/ActivityLogsFragment.ts +++ b/packages/components/modules/activity-log/common/graphql/queries/ActivityLogsFragment.ts @@ -13,9 +13,20 @@ export const ActivityLogsFragmentQuery = graphql` count: { type: "Int", defaultValue: 10 } cursor: { type: "String" } userName: { type: "String", defaultValue: null } + createdFrom: { type: "Date", defaultValue: null } + createdTo: { type: "Date", defaultValue: null } ) { - activityLogs(first: $count, after: $cursor, userName: $userName) - @connection(key: "ActivityLogs_activityLogs", filters: ["userName"]) { + activityLogs( + first: $count + after: $cursor + userName: $userName + createdFrom: $createdFrom + createdTo: $createdTo + ) + @connection( + key: "ActivityLogs_activityLogs" + filters: ["userName", "createdFrom", "createdTo"] + ) { edges { node { id diff --git a/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx b/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx index a25356b6..cb923fc7 100644 --- a/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx @@ -8,6 +8,8 @@ import { Box, Typography } from '@mui/material' import { useForm } from 'react-hook-form' import { SearchNotFoundState } from '../../../__shared__/web' +import DateFilterChip from '../DateFilterChip' +import { IFetchParameters } from '../DateFilterComponent/types' import { useActivityLogs } from '../../common' import EventFilterChip from './EventFilterChip' import DefaultLogGroups from './LogGroups' @@ -18,23 +20,38 @@ const ActivityLogComponent: FC = ({ LogGroups = DefaultLogGroups, LogGroupsProps, }) => { - const [selectedFilters, setSelectedFilters] = useState(['All']) + const [fetchParameters, setFetchParameters] = useState({ + createdFrom: null, + createdTo: null, + userName: '', + count: 10, + cursor: null, + }) + const { logGroups, loadNext, hasNext, isLoadingNext, refetch } = useActivityLogs(queryRef) const [isPending, startTransition] = useTransition() + const [selectedFilters, setSelectedFilters] = useState(['All']) const { control, reset, watch } = useForm({ defaultValues: { search: '' } }) const searchValue = watch('search') - const { logGroups, loadNext, hasNext, isLoadingNext, refetch } = useActivityLogs(queryRef) + const executeRefetch = (updatedParameters: Partial) => { + const newFetchParameters = { ...fetchParameters, ...updatedParameters } + setFetchParameters(newFetchParameters) + startTransition(() => { + refetch(newFetchParameters, { fetchPolicy: 'store-and-network' }) + }) + } const handleSearchClear = () => { startTransition(() => { reset() - refetch({ userName: '' }) + executeRefetch({ userName: '' }) }) } + const handleSearchChange = (e: ChangeEvent) => { const value = e.target.value || '' startTransition(() => { - refetch({ userName: value, count: 10, cursor: null }, { fetchPolicy: 'store-and-network' }) + executeRefetch({ userName: value }) }) } @@ -54,7 +71,8 @@ const ActivityLogComponent: FC = ({ onClear={handleSearchClear} isPending={isPending} /> - + + = ({ /> {!isPending && searchValue && emptyLogsList && } + {!isPending && + (fetchParameters.createdFrom != null || fetchParameters.createdTo != null) && + emptyLogsList && ( + + )} Date: Thu, 13 Feb 2025 14:50:52 -0300 Subject: [PATCH 2/6] refactoring --- .../modules/__shared__/MobileDrawer/index.tsx | 2 +- .../web/SearchNotFoundState/index.tsx | 2 +- .../activity-log/DateFilterChip/index.tsx | 54 +++++++------- .../DateFilterComponent/index.tsx | 70 ++++++++++++------- .../web/ActivityLogComponent/index.tsx | 4 +- 5 files changed, 73 insertions(+), 59 deletions(-) diff --git a/packages/components/modules/__shared__/MobileDrawer/index.tsx b/packages/components/modules/__shared__/MobileDrawer/index.tsx index b5c22e3f..fd6a28bc 100644 --- a/packages/components/modules/__shared__/MobileDrawer/index.tsx +++ b/packages/components/modules/__shared__/MobileDrawer/index.tsx @@ -2,7 +2,7 @@ import { FC } from 'react' import { Box, Divider, Drawer, Typography, styled } from '@mui/material' -import { MobileDrawerProps } from '../types' +import { MobileDrawerProps } from '../common/types' const Puller = styled('div')(({ theme }) => ({ width: 64, diff --git a/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx b/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx index 498397d1..9fcd8346 100644 --- a/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx +++ b/packages/components/modules/__shared__/web/SearchNotFoundState/index.tsx @@ -2,7 +2,7 @@ import { SearchingImage } from '@baseapp-frontend/design-system/components/web/i import { Box, Typography } from '@mui/material' -import { SearchNotFoundStateProps } from '../types' +import { SearchNotFoundStateProps } from '../../common/types' const SearchNotFoundState = ({ message = 'Check your spelling or try another search.', diff --git a/packages/components/modules/activity-log/DateFilterChip/index.tsx b/packages/components/modules/activity-log/DateFilterChip/index.tsx index 547c665d..0bd6e024 100644 --- a/packages/components/modules/activity-log/DateFilterChip/index.tsx +++ b/packages/components/modules/activity-log/DateFilterChip/index.tsx @@ -2,7 +2,7 @@ import { FC, useState } from 'react' import { DATE_FORMAT, formatDate } from '@baseapp-frontend/utils' -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import { KeyboardArrowDown } from '@mui/icons-material' import { Box, Chip, Menu, Theme, useMediaQuery } from '@mui/material' import { DateTime } from 'luxon' @@ -22,16 +22,19 @@ const DateFilterChip: FC = ({ fetchParameters, executeRefet const { createdFrom, createdTo } = fetchParameters const hasDateSelected = createdFrom || createdTo + const parseDate = (date: string | null): DateTime | null => + date ? DateTime.fromFormat(date, DATE_FORMAT.api) : null + + const formatLabelDate = (date: string | null): string | null => + date ? formatDate(date, { toFormat: DATE_FORMAT[3] }) : null + const labelRender = () => { - if (createdFrom && createdTo) { - return `${formatDate(createdFrom, { toFormat: DATE_FORMAT[3] })} - ${formatDate(createdTo, { toFormat: DATE_FORMAT[3] })}` - } - if (createdFrom) { - return `From ${formatDate(createdFrom, { toFormat: DATE_FORMAT[3] })}` - } - if (createdTo) { - return `Until ${formatDate(createdTo, { toFormat: DATE_FORMAT[3] })}` - } + const fromDate = formatLabelDate(createdFrom) + const toDate = formatLabelDate(createdTo) + if (createdFrom && createdTo) return `${fromDate} - ${toDate}` + + if (createdFrom) return `From ${fromDate}` + if (createdTo) return `Until ${toDate}` return 'Period' } @@ -43,40 +46,35 @@ const DateFilterChip: FC = ({ fetchParameters, executeRefet } } + const renderDateFilterComponent = (onClose: () => void) => ( + + ) + return ( <> {labelRender()} - + } onClick={handleOnClickOnChip} variant={hasDateSelected ? 'filled' : 'soft'} color="default" /> - - + {renderDateFilterComponent(handleDrawerClose)} - - - + {renderDateFilterComponent(handleMenuClose)} ) diff --git a/packages/components/modules/activity-log/DateFilterComponent/index.tsx b/packages/components/modules/activity-log/DateFilterComponent/index.tsx index e9c7566b..6b2abda6 100644 --- a/packages/components/modules/activity-log/DateFilterComponent/index.tsx +++ b/packages/components/modules/activity-log/DateFilterComponent/index.tsx @@ -1,7 +1,5 @@ import React, { FC, useState } from 'react' -import { DATE_FORMAT, formatDate } from '@baseapp-frontend/utils' - import { Box, Button } from '@mui/material' import { DateValidationError, LocalizationProvider } from '@mui/x-date-pickers' import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon' @@ -41,10 +39,8 @@ const DateFilterComponent: FC = ({ setError(null) executeRefetch({ - createdFrom: tempCreatedFrom - ? formatDate(tempCreatedFrom, { toFormat: DATE_FORMAT.api }) - : null, - createdTo: tempCreatedTo ? formatDate(tempCreatedTo, { toFormat: DATE_FORMAT.api }) : null, + createdFrom: tempCreatedFrom ? tempCreatedFrom.toISODate() : null, + createdTo: tempCreatedTo ? tempCreatedTo.toISODate() : null, }) onApply?.() } @@ -61,30 +57,50 @@ const DateFilterComponent: FC = ({ onClearFilter?.() } + const disableStartDate = (date: DateTime) => (tempCreatedTo ? date > tempCreatedTo : false) + + const disableEndDate = (date: DateTime) => (tempCreatedFrom ? date < tempCreatedFrom : false) + return ( - - - setTempCreatedFrom(newValue)} - disableFuture - value={tempCreatedFrom} - /> - setTempCreatedTo(newValue)} - onError={(newError) => setError(newError)} - disableFuture - slotProps={{ - textField: { - helperText: errorMessage, + + setTempCreatedFrom(newValue)} + disableFuture + value={tempCreatedFrom} + shouldDisableDate={disableStartDate} + slotProps={{ + day: (dayProps) => ({ + sx: { + ...(disableStartDate(dayProps.day) && { + backgroundColor: 'error.lighter', + }), }, - }} - /> - - + }), + }} + /> + setTempCreatedTo(newValue)} + onError={(newError) => setError(newError)} + disableFuture + shouldDisableDate={disableEndDate} + slotProps={{ + textField: { + helperText: errorMessage, + }, + day: (dayProps) => ({ + sx: { + ...(disableEndDate(dayProps.day) && { + backgroundColor: 'error.lighter', + }), + }, + }), + }} + /> +