diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 86d64d9c..93bb9bb2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,11 @@ # @baseapp-frontend/components +## 1.0.12 + +### Patch Changes + +- add filter by date to the module activity log. Filter as a modal on desktop and swap on mobile. Filtering can be don by start and/or end date + ## 1.0.11 ### Patch Changes 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..9fcd8346 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 '../../common/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..37249712 --- /dev/null +++ b/packages/components/modules/activity-log/DateFilterChip/index.tsx @@ -0,0 +1,91 @@ +import { FC, useState } from 'react' + +import { SwipeableDrawer } from '@baseapp-frontend/design-system/components/web/drawers' +import { DATE_FORMAT, formatDate } from '@baseapp-frontend/utils' + +import { KeyboardArrowDown } from '@mui/icons-material' +import { Box, Chip, Divider, Menu, Theme, Typography, useMediaQuery } from '@mui/material' +import { DateTime } from 'luxon' + +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 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 = () => { + 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' + } + + const handleOnClickOnChip = (event: React.MouseEvent) => { + if (isMobile) { + handleDrawerOpen() + } else { + setAnchorEl(event.currentTarget) + } + } + + const renderDateFilterComponent = (onClose: () => void) => ( + <> + + Period + + + + + ) + + return ( + <> + + {labelRender()} + + + } + onClick={handleOnClickOnChip} + variant={hasDateSelected ? 'filled' : 'soft'} + color="default" + /> + {isMobile ? ( + + {renderDateFilterComponent(handleDrawerClose)} + + ) : ( + + {renderDateFilterComponent(handleMenuClose)} + + )} + + ) +} + +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..aca7ed58 --- /dev/null +++ b/packages/components/modules/activity-log/DateFilterChip/types.ts @@ -0,0 +1,6 @@ +import { FetchParameters } from '../common/types' + +export interface DateFilterChipProps { + fetchParameters: FetchParameters + 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..6b2abda6 --- /dev/null +++ b/packages/components/modules/activity-log/DateFilterComponent/index.tsx @@ -0,0 +1,116 @@ +import React, { FC, useState } from 'react' + +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 ? tempCreatedFrom.toISODate() : null, + createdTo: tempCreatedTo ? tempCreatedTo.toISODate() : null, + }) + onApply?.() + } + + const handleClear = () => { + if (createdFrom || createdTo) { + executeRefetch({ + createdFrom: null, + createdTo: null, + }) + } + setTempCreatedFrom(null) + setTempCreatedTo(null) + onClearFilter?.() + } + + const disableStartDate = (date: DateTime) => (tempCreatedTo ? date > tempCreatedTo : false) + + const disableEndDate = (date: DateTime) => (tempCreatedFrom ? date < tempCreatedFrom : false) + + return ( + + + 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', + }), + }, + }), + }} + /> + + + + + + + ) +} + +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..b4322ccf --- /dev/null +++ b/packages/components/modules/activity-log/DateFilterComponent/types.ts @@ -0,0 +1,11 @@ +import { DateTime } from 'luxon' + +import { FetchParameters } from '../common/types' + +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/common/types.ts b/packages/components/modules/activity-log/common/types.ts index 76c05001..c699f164 100644 --- a/packages/components/modules/activity-log/common/types.ts +++ b/packages/components/modules/activity-log/common/types.ts @@ -8,3 +8,11 @@ export interface LogGroup { lastActivityTimestamp: string logs: ActivityLogNode[] } + +export interface FetchParameters { + createdFrom: string | null + createdTo: string | null + userName: string + count: number + cursor: string | null +} diff --git a/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx b/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx index a25356b6..7af89b6a 100644 --- a/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx +++ b/packages/components/modules/activity-log/web/ActivityLogComponent/index.tsx @@ -8,7 +8,8 @@ import { Box, Typography } from '@mui/material' import { useForm } from 'react-hook-form' import { SearchNotFoundState } from '../../../__shared__/web' -import { useActivityLogs } from '../../common' +import DateFilterChip from '../../DateFilterChip' +import { FetchParameters, useActivityLogs } from '../../common' import EventFilterChip from './EventFilterChip' import DefaultLogGroups from './LogGroups' import { ActivityLogComponentProps, EventFilterOption } from './types' @@ -18,23 +19,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 +70,8 @@ const ActivityLogComponent: FC = ({ onClear={handleSearchClear} isPending={isPending} /> - + + = ({ /> {!isPending && searchValue && emptyLogsList && } + {!isPending && + (fetchParameters.createdFrom != null || fetchParameters.createdTo != null) && + emptyLogsList && ( + + )}