Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 13 additions & 0 deletions packages/components/modules/__shared__/common/types.ts
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ 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) => (
<Box display="grid" justifyItems="center" gridAutoRows="min-content" gap={1.5} padding={4}>
<SearchingImage sx={{ color: 'grey.500' }} />
<Box display="grid" justifyItems="center" gridAutoRows="min-content" gap={0.5}>
<Typography variant="subtitle2" color="text.primary">
No results found
</Typography>
<Typography variant="caption" color="text.secondary">
Check your spelling or try another search.
{message}
</Typography>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DateFilterChipProps> = ({ fetchParameters, executeRefetch }) => {
const [drawerOpen, setDrawerOpen] = useState(false)
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const isMobile = useMediaQuery<Theme>((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<HTMLElement>) => {
if (isMobile) {
handleDrawerOpen()
} else {
setAnchorEl(event.currentTarget)
}
}

const renderDateFilterComponent = (onClose: () => void) => (
<>
<Typography variant="subtitle1" m={4} align="center">
Period
</Typography>
<Divider sx={{ marginBottom: 2 }} />
<DateFilterComponent
createdFrom={parseDate(createdFrom)}
createdTo={parseDate(createdTo)}
executeRefetch={executeRefetch}
onApply={onClose}
onClearFilter={onClose}
/>
</>
)

return (
<>
<Chip
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span>{labelRender()}</span>
<KeyboardArrowDown color="action" />
</Box>
}
onClick={handleOnClickOnChip}
variant={hasDateSelected ? 'filled' : 'soft'}
color="default"
/>
{isMobile ? (
<SwipeableDrawer globalHeight="auto" open={drawerOpen} onClose={handleDrawerClose}>
{renderDateFilterComponent(handleDrawerClose)}
</SwipeableDrawer>
) : (
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
<Box padding={2}>{renderDateFilterComponent(handleMenuClose)}</Box>
</Menu>
)}
</>
)
}

export default DateFilterChip
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { FetchParameters } from '../common/types'

export interface DateFilterChipProps {
fetchParameters: FetchParameters
executeRefetch: (params: Partial<FetchParameters>) => void
}
Original file line number Diff line number Diff line change
@@ -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<DateFilterComponentProps> = ({
createdFrom,
createdTo,
executeRefetch,
onApply,
onClearFilter,
}) => {
const [tempCreatedFrom, setTempCreatedFrom] = useState<DateTime | null>(createdFrom ?? null)
const [tempCreatedTo, setTempCreatedTo] = useState<DateTime | null>(createdTo ?? null)

const [error, setError] = useState<DateValidationError | null>(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 (
<Box display="flex" gap={2} flexDirection="column">
<LocalizationProvider dateAdapter={AdapterLuxon}>
<DatePicker
label="Start date"
onChange={(newValue) => setTempCreatedFrom(newValue)}
disableFuture
value={tempCreatedFrom}
shouldDisableDate={disableStartDate}
slotProps={{
day: (dayProps) => ({
sx: {
...(disableStartDate(dayProps.day) && {
backgroundColor: 'error.lighter',
}),
},
}),
}}
/>
<DatePicker
label="End date"
value={tempCreatedTo}
onChange={(newValue) => setTempCreatedTo(newValue)}
onError={(newError) => setError(newError)}
disableFuture
shouldDisableDate={disableEndDate}
slotProps={{
textField: {
helperText: errorMessage,
},
day: (dayProps) => ({
sx: {
...(disableEndDate(dayProps.day) && {
backgroundColor: 'error.lighter',
}),
},
}),
}}
/>
</LocalizationProvider>
<Box display="flex" gap={2} flexDirection="column" mt={4} mb={2}>
<Button onClick={handleApply} variant="contained" color="inherit">
Filter
</Button>
<Button onClick={handleClear} variant="outlined" color="inherit">
Clear Filter
</Button>
</Box>
</Box>
)
}

export default DateFilterComponent
Original file line number Diff line number Diff line change
@@ -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<FetchParameters>) => void
onApply?: () => void
onClearFilter?: () => void
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/components/modules/activity-log/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -18,23 +19,38 @@ const ActivityLogComponent: FC<ActivityLogComponentProps> = ({
LogGroups = DefaultLogGroups,
LogGroupsProps,
}) => {
const [selectedFilters, setSelectedFilters] = useState<EventFilterOption[]>(['All'])
const [fetchParameters, setFetchParameters] = useState<FetchParameters>({
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<EventFilterOption[]>(['All'])
const { control, reset, watch } = useForm({ defaultValues: { search: '' } })
const searchValue = watch('search')

const { logGroups, loadNext, hasNext, isLoadingNext, refetch } = useActivityLogs(queryRef)
const executeRefetch = (updatedParameters: Partial<FetchParameters>) => {
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<HTMLInputElement>) => {
const value = e.target.value || ''
startTransition(() => {
refetch({ userName: value, count: 10, cursor: null }, { fetchPolicy: 'store-and-network' })
executeRefetch({ userName: value })
})
}

Expand All @@ -54,14 +70,20 @@ const ActivityLogComponent: FC<ActivityLogComponentProps> = ({
onClear={handleSearchClear}
isPending={isPending}
/>
<Box display="flex" mt={2} mb={2}>
<Box display="flex" mt={2} mb={2} flexDirection="row" gap={2}>
<DateFilterChip fetchParameters={fetchParameters} executeRefetch={executeRefetch} />
<EventFilterChip
options={['All', 'Comments', 'Reactions', 'Posts']}
selectedOptions={selectedFilters}
onChange={setSelectedFilters}
/>
</Box>
{!isPending && searchValue && emptyLogsList && <SearchNotFoundState />}
{!isPending &&
(fetchParameters.createdFrom != null || fetchParameters.createdTo != null) &&
emptyLogsList && (
<SearchNotFoundState message="No results found for the selected date range." />
)}
<LogGroups
logGroups={logGroups}
loadNext={loadNext}
Expand Down
2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@baseapp-frontend/components",
"description": "BaseApp components modules such as comments, notifications, messages, and more.",
"version": "1.0.11",
"version": "1.0.12",
"sideEffects": false,
"scripts": {
"babel:transpile": "babel modules -d tmp-babel --extensions .ts,.tsx --ignore '**/__tests__/**','**/__storybook__/**'",
Expand Down
Loading
Loading