Skip to content

Commit a7a079d

Browse files
committed
feat: add direction filter to address events tab
Add incoming/outgoing/all direction control to the events filter bar, with bidirectional sync between direction and source/destination filters. Extract shared applyEventDirectionSync utility for desktop and mobile.
1 parent 1e49a9a commit a7a079d

File tree

7 files changed

+258
-47
lines changed

7 files changed

+258
-47
lines changed

src/pages/network/address/components/AddressEvents.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function AddressEvents({ addressId }: Props) {
1717
events,
1818
total,
1919
eventType,
20+
direction,
2021
tickStart,
2122
tickEnd,
2223
dateRange,
@@ -30,9 +31,11 @@ export default function AddressEvents({ addressId }: Props) {
3031
tickStart,
3132
tickEnd,
3233
eventType,
34+
direction,
3335
dateRange,
3436
sourceFilter,
35-
destinationFilter
37+
destinationFilter,
38+
addressId
3639
})
3740

3841
return (
@@ -42,12 +45,15 @@ export default function AddressEvents({ addressId }: Props) {
4245
<EventsFilterBar
4346
filters={filters}
4447
eventType={eventType}
48+
direction={direction}
4549
tickStart={tickStart}
4650
tickEnd={tickEnd}
4751
dateRange={dateRange}
4852
sourceFilter={sourceFilter}
4953
destinationFilter={destinationFilter}
5054
idPrefix="addr-events"
55+
showDirectionFilter
56+
addressId={addressId}
5157
/>
5258

5359
{hasError ? (

src/pages/network/address/components/TransactionsOverview/filterUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export const DATE_PRESETS = [
250250
/**
251251
* Helper to check if an address filter contains only the page address
252252
*/
253-
function isOnlyPageAddress(filter: AddressFilter | undefined, addressId: string): boolean {
253+
export function isOnlyPageAddress(filter: AddressFilter | undefined, addressId: string): boolean {
254254
if (!filter) return false
255255
const validAddresses = filter.addresses.filter((addr) => addr.trim() !== '')
256256
return validAddresses.length === 1 && validAddresses[0] === addressId

src/pages/network/address/hooks/useAddressEvents.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
useValidatedPageSize
99
} from '@app/hooks'
1010
import { type ShouldFilter, type TransactionEvent, useGetEventsQuery } from '@app/store/apis/events'
11-
import type { AddressFilter } from '../components/TransactionsOverview/filterUtils'
11+
import type {
12+
AddressFilter,
13+
TransactionDirection
14+
} from '../components/TransactionsOverview/filterUtils'
15+
import { DIRECTION } from '../components/TransactionsOverview/filterUtils'
1216
import {
1317
buildEventAddressFilter,
1418
buildTickFilter,
@@ -23,6 +27,7 @@ export default function useAddressEvents(addressId: string): {
2327
events: TransactionEvent[]
2428
total: number
2529
eventType: number | undefined
30+
direction: TransactionDirection | undefined
2631
tickStart: string | undefined
2732
tickEnd: string | undefined
2833
dateRange: DateRangeValue | undefined
@@ -45,16 +50,40 @@ export default function useAddressEvents(addressId: string): {
4550

4651
const eventType = useSanitizedEventType()
4752

48-
const should = useMemo<ShouldFilter[]>(
49-
() => [{ terms: { source: addressId, destination: addressId } }],
50-
[addressId]
51-
)
53+
const directionRaw = searchParams.get('direction')
54+
const direction: TransactionDirection | undefined =
55+
directionRaw === DIRECTION.INCOMING || directionRaw === DIRECTION.OUTGOING
56+
? directionRaw
57+
: undefined
5258

5359
const { tickNumber, tickRange } = buildTickFilter(tickStart, tickEnd)
5460

5561
const timestampRange = buildTimestampRange(dateRange)
56-
const sourceResult = buildEventAddressFilter(sourceFilter)
57-
const destResult = buildEventAddressFilter(destinationFilter)
62+
63+
// When direction is set, the conflicting filter is ignored (disabled in UI).
64+
const isIncoming = direction === DIRECTION.INCOMING
65+
const isOutgoing = direction === DIRECTION.OUTGOING
66+
67+
const effectiveSourceFilter = isOutgoing ? undefined : sourceFilter
68+
const effectiveDestFilter = isIncoming ? undefined : destinationFilter
69+
70+
const sourceResult = buildEventAddressFilter(effectiveSourceFilter)
71+
const destResult = buildEventAddressFilter(effectiveDestFilter)
72+
73+
// Use `should` (OR) to scope results to the page address when no explicit direction is set.
74+
// When direction is set, implicit source/dest handles the scoping instead.
75+
const useShouldFilter = !direction
76+
77+
const should = useMemo<ShouldFilter[] | undefined>(
78+
() =>
79+
useShouldFilter ? [{ terms: { source: addressId, destination: addressId } }] : undefined,
80+
[addressId, useShouldFilter]
81+
)
82+
83+
// When direction is set, implicit source/dest scopes to the page address.
84+
// When direction is undefined, `should` (OR) handles scoping instead.
85+
const implicitSource = isOutgoing ? addressId : undefined
86+
const implicitDest = isIncoming ? addressId : undefined
5887

5988
const { data, isFetching, isError } = useGetEventsQuery(
6089
{
@@ -65,9 +94,9 @@ export default function useAddressEvents(addressId: string): {
6594
offset,
6695
size: pageSize,
6796
logType: eventType,
68-
source: sourceResult.include,
97+
source: sourceResult.include ?? implicitSource,
6998
excludeSource: sourceResult.exclude,
70-
destination: destResult.include,
99+
destination: destResult.include ?? implicitDest,
71100
excludeDestination: destResult.exclude
72101
},
73102
{ skip: !addressId }
@@ -81,6 +110,7 @@ export default function useAddressEvents(addressId: string): {
81110
events: data?.events ?? [],
82111
total,
83112
eventType,
113+
direction,
84114
tickStart,
85115
tickEnd,
86116
dateRange,

src/pages/network/components/filters/EventsFilterBar.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { useTranslation } from 'react-i18next'
33

44
import { FunnelIcon } from '@app/assets/icons'
55
import { getEventTypeLabel } from '@app/store/apis/events'
6-
import type { AddressFilter } from '../../address/components/TransactionsOverview/filterUtils'
6+
import type {
7+
AddressFilter,
8+
TransactionDirection
9+
} from '../../address/components/TransactionsOverview/filterUtils'
10+
import DirectionControl from '../../address/components/TransactionsOverview/DirectionControl'
711
import DateFilterContent from '../../address/components/TransactionsOverview/DateFilterContent'
812
import MultiAddressFilterContent from '../../address/components/TransactionsOverview/MultiAddressFilterContent'
913
import { formatRangeLabel } from '../../hooks'
@@ -22,6 +26,7 @@ import ResetFiltersButton from './ResetFiltersButton'
2226
type Props = {
2327
filters: EventFiltersResult
2428
eventType: number | undefined
29+
direction?: TransactionDirection | undefined
2530
tickStart?: string
2631
tickEnd?: string
2732
dateRange?: DateRangeValue
@@ -30,19 +35,24 @@ type Props = {
3035
idPrefix: string
3136
showTickFilter?: boolean
3237
showDateFilter?: boolean
38+
showDirectionFilter?: boolean
39+
addressId?: string
3340
}
3441

3542
export default function EventsFilterBar({
3643
filters,
3744
eventType,
45+
direction,
3846
tickStart,
3947
tickEnd,
4048
dateRange,
4149
sourceFilter,
4250
destinationFilter,
4351
idPrefix,
4452
showTickFilter = true,
45-
showDateFilter = true
53+
showDateFilter = true,
54+
showDirectionFilter = false,
55+
addressId
4656
}: Props) {
4757
const { t } = useTranslation('network-page')
4858
const [openDropdown, setOpenDropdown] = useState<string | null>(null)
@@ -66,6 +76,7 @@ export default function EventsFilterBar({
6676

6777
const mobileActiveFilters: EventsFilters = {
6878
eventType,
79+
direction,
6980
sourceFilter,
7081
destinationFilter,
7182
...(showTickFilter && {
@@ -76,9 +87,18 @@ export default function EventsFilterBar({
7687

7788
return (
7889
<>
79-
{/* Mobile: Filters button + active filter chips */}
90+
{/* Mobile: Direction control + filters button + active filter chips */}
8091
<div className="flex flex-col gap-10 sm:hidden">
81-
<div className="flex items-center justify-end">
92+
<div
93+
className={`flex items-center ${showDirectionFilter ? 'justify-between' : 'justify-end'}`}
94+
>
95+
{showDirectionFilter && (
96+
<DirectionControl
97+
value={direction}
98+
onChange={filters.handleDirectionChange}
99+
showTooltips
100+
/>
101+
)}
82102
<MobileFiltersButton onClick={() => setIsMobileModalOpen(true)} />
83103
</div>
84104

@@ -116,11 +136,20 @@ export default function EventsFilterBar({
116136
idPrefix={idPrefix}
117137
showTickFilter={showTickFilter}
118138
showDateFilter={showDateFilter}
139+
showDirectionFilter={showDirectionFilter}
140+
addressId={addressId}
119141
/>
120142

121143
{/* Desktop: Dropdown filters */}
122144
<div className="hidden items-center gap-8 sm:flex">
123145
<FunnelIcon className="h-16 w-16 text-gray-50" />
146+
{showDirectionFilter && (
147+
<DirectionControl
148+
value={direction}
149+
onChange={filters.handleDirectionChange}
150+
showTooltips
151+
/>
152+
)}
124153
<FilterDropdown
125154
label={eventTypeLabel}
126155
isActive={filters.isEventTypeActive}

src/pages/network/components/filters/EventsMobileFiltersModal.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import { useTranslation } from 'react-i18next'
33

44
import { useBodyScrollLock } from '@app/hooks'
55
import DateFilterContent from '../../address/components/TransactionsOverview/DateFilterContent'
6-
import type { AddressFilter } from '../../address/components/TransactionsOverview/filterUtils'
6+
import type {
7+
AddressFilter,
8+
TransactionDirection
9+
} from '../../address/components/TransactionsOverview/filterUtils'
10+
import DirectionControl from '../../address/components/TransactionsOverview/DirectionControl'
711
import MultiAddressFilterContent from '../../address/components/TransactionsOverview/MultiAddressFilterContent'
812
import type { DateRangeValue, TickRangeValue } from '../../utils/eventFilterUtils'
13+
import { applyEventDirectionSync } from '../../utils/eventFilterUtils'
914
import EventTypeChips from './EventTypeChips'
1015
import MobileFiltersModalWrapper from './MobileFiltersModalWrapper'
1116
import MobileFilterSection from './MobileFilterSection'
@@ -17,6 +22,7 @@ export type EventsFilters = {
1722
dateRange?: DateRangeValue
1823
sourceFilter?: AddressFilter
1924
destinationFilter?: AddressFilter
25+
direction?: TransactionDirection
2026
}
2127

2228
type Props = {
@@ -27,6 +33,8 @@ type Props = {
2733
idPrefix: string
2834
showTickFilter?: boolean
2935
showDateFilter?: boolean
36+
showDirectionFilter?: boolean
37+
addressId?: string
3038
}
3139

3240
export default function EventsMobileFiltersModal({
@@ -36,7 +44,9 @@ export default function EventsMobileFiltersModal({
3644
onApplyFilters,
3745
idPrefix,
3846
showTickFilter = true,
39-
showDateFilter = true
47+
showDateFilter = true,
48+
showDirectionFilter = false,
49+
addressId
4050
}: Props) {
4151
const { t } = useTranslation('network-page')
4252

@@ -55,6 +65,25 @@ export default function EventsMobileFiltersModal({
5565
const [localDestFilter, setLocalDestFilter] = useState<AddressFilter | undefined>(
5666
activeFilters.destinationFilter
5767
)
68+
const [localDirection, setLocalDirection] = useState<TransactionDirection | undefined>(
69+
activeFilters.direction
70+
)
71+
72+
const handleLocalDirectionChange = useCallback(
73+
(newDirection: TransactionDirection | undefined) => {
74+
setLocalDirection(newDirection)
75+
if (!addressId) return
76+
const { sourceFilter: newSrc, destinationFilter: newDest } = applyEventDirectionSync(
77+
newDirection,
78+
addressId,
79+
localSourceFilter,
80+
localDestFilter
81+
)
82+
setLocalSourceFilter(newSrc)
83+
setLocalDestFilter(newDest)
84+
},
85+
[addressId, localSourceFilter, localDestFilter]
86+
)
5887

5988
useEffect(() => {
6089
if (isOpen) {
@@ -63,14 +92,16 @@ export default function EventsMobileFiltersModal({
6392
setLocalDateRange(activeFilters.dateRange)
6493
setLocalSourceFilter(activeFilters.sourceFilter)
6594
setLocalDestFilter(activeFilters.destinationFilter)
95+
setLocalDirection(activeFilters.direction)
6696
}
6797
}, [
6898
isOpen,
6999
activeFilters.tickRange,
70100
activeFilters.eventType,
71101
activeFilters.dateRange,
72102
activeFilters.sourceFilter,
73-
activeFilters.destinationFilter
103+
activeFilters.destinationFilter,
104+
activeFilters.direction
74105
])
75106

76107
const handleApply = useCallback(() => {
@@ -79,7 +110,8 @@ export default function EventsMobileFiltersModal({
79110
eventType: localEventType,
80111
dateRange: localDateRange,
81112
sourceFilter: localSourceFilter,
82-
destinationFilter: localDestFilter
113+
destinationFilter: localDestFilter,
114+
direction: localDirection
83115
})
84116
onClose()
85117
}, [
@@ -88,6 +120,7 @@ export default function EventsMobileFiltersModal({
88120
localDateRange,
89121
localSourceFilter,
90122
localDestFilter,
123+
localDirection,
91124
onApplyFilters,
92125
onClose
93126
])
@@ -99,6 +132,12 @@ export default function EventsMobileFiltersModal({
99132
onClose={onClose}
100133
onApply={handleApply}
101134
>
135+
{showDirectionFilter && (
136+
<MobileFilterSection id={`${idPrefix}-mobile-direction-filter`} label={t('direction')}>
137+
<DirectionControl value={localDirection} onChange={handleLocalDirectionChange} />
138+
</MobileFilterSection>
139+
)}
140+
102141
<MobileFilterSection id={`${idPrefix}-mobile-event-type-filter`} label={t('eventType')}>
103142
<EventTypeChips selectedType={localEventType} onSelectType={setLocalEventType} />
104143
</MobileFilterSection>

0 commit comments

Comments
 (0)