Skip to content

Commit d1f2be8

Browse files
julianbakercursoragentraymondjacobson
authored
Major Table Related UX Improvements (#14102)
- Removed manual table column resizing and header resize handles from shared desktop tables. - Added shared responsive column-hide behavior with explicit hide order per table surface. - Updated track tables to support combined `Track + Artist` cells (library, playlist detail, history), including inline artwork in those combined rows. - Tightened/fixed column widths, drop timing, and header alignment so columns hide before clipping and headers align with cell content. - Updated fan clubs leaderboard and USDC purchases/withdrawals table behavior to match the new shared table rules. **Desktop routes/surfaces to test** - Library: Tracks table - Collection page: Playlist detail table, Album detail table - History page: Tracks table - Dashboard page: Tracks tab, Albums tab - Fan Clubs / Coins: Leaderboard table view - Pay & Earn: Your Purchases, Withdrawal History, Sales, Audio Transactions --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Ray Jacobson <raymondjacobson@users.noreply.github.com>
1 parent 764a436 commit d1f2be8

36 files changed

Lines changed: 1619 additions & 451 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import {
4+
TransactionMethod,
5+
TransactionType
6+
} from '~/store/ui/transaction-details/types'
7+
8+
import { audioTransactionFromSdk } from './audioTransactions'
9+
10+
const makeSdkTransaction = (overrides: Record<string, unknown> = {}) =>
11+
({
12+
signature: 'signature',
13+
transactionType: 'transfer',
14+
method: 'receive',
15+
transactionDate: '2026-01-01T00:00:00.000Z',
16+
change: '100000000',
17+
balance: '500000000',
18+
metadata: 'metadata',
19+
...overrides
20+
}) as any
21+
22+
describe('audioTransactionFromSdk', () => {
23+
it('maps tip transactions', () => {
24+
const tipTx = audioTransactionFromSdk(
25+
makeSdkTransaction({
26+
transactionType: 'tip',
27+
method: 'send'
28+
})
29+
)
30+
31+
expect(tipTx.transactionType).toBe(TransactionType.TIP)
32+
expect(tipTx.method).toBe(TransactionMethod.SEND)
33+
})
34+
35+
it('throws on unknown transaction type', () => {
36+
expect(() =>
37+
audioTransactionFromSdk(
38+
makeSdkTransaction({
39+
transactionType: 'unknown_type'
40+
})
41+
)
42+
).toThrow('Unknown Transaction')
43+
})
44+
})

packages/common/src/adapters/audioTransactions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export const audioTransactionFromSdk = (
1616
'purchase unknown': TransactionType.PURCHASE,
1717
user_reward: TransactionType.CHALLENGE_REWARD,
1818
trending_reward: TransactionType.TRENDING_REWARD,
19-
transfer: TransactionType.TRANSFER
19+
transfer: TransactionType.TRANSFER,
20+
tip: TransactionType.TIP
2021
}
2122

2223
const txType = transactionTypeMap[tx.transactionType]
@@ -48,6 +49,7 @@ export const audioTransactionFromSdk = (
4849
metadata: undefined
4950
}
5051
case TransactionType.TRANSFER:
52+
case TransactionType.TIP:
5153
return {
5254
signature: tx.signature,
5355
transactionType: txType,

packages/common/src/store/ui/transaction-details/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export enum TransactionType {
77
PURCHASE = 'PURCHASE',
88
CHALLENGE_REWARD = 'CHALLENGE_REWARD',
99
TRENDING_REWARD = 'TRENDING_REWARD',
10-
TRANSFER = 'TRANSFER'
10+
TRANSFER = 'TRANSFER',
11+
TIP = 'TIP'
1112
}
1213

1314
export enum TransactionMethod {
@@ -50,7 +51,7 @@ export type TransactionDetails =
5051
}
5152
| {
5253
signature: string
53-
transactionType: TransactionType.TRANSFER
54+
transactionType: TransactionType.TRANSFER | TransactionType.TIP
5455
method: TransactionMethod.SEND | TransactionMethod.RECEIVE
5556
date: string
5657
change: StringAudio

packages/web/src/components/audio-transaction-icon/AudioTransactionIcon.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const typeIconSvgMap: Record<TransactionType, IconComponent | null> = {
3030
[TransactionType.CHALLENGE_REWARD]: IconTrophy,
3131
[TransactionType.PURCHASE]: null, // Not needed, AppLogo is used for purchases
3232
[TransactionType.TRANSFER]: IconTransaction,
33+
[TransactionType.TIP]: IconTransaction,
3334
[TransactionType.TRENDING_REWARD]: IconTrophy
3435
} as const
3536

@@ -54,6 +55,7 @@ const typeIconMap: Record<
5455
[TransactionType.CHALLENGE_REWARD]: TypeIcon,
5556
[TransactionType.PURCHASE]: AppLogo,
5657
[TransactionType.TRANSFER]: TypeIcon,
58+
[TransactionType.TIP]: TypeIcon,
5759
[TransactionType.TRENDING_REWARD]: TypeIcon
5860
} as const
5961

packages/web/src/components/audio-transactions-table/AudioTransactionsTable.module.css

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,15 @@
1212
line-height: 1.2;
1313
}
1414

15-
.icon {
16-
margin-right: 22px;
15+
.typeText {
16+
line-height: 150%;
1717
}
1818

19-
.changeCell.increase {
20-
color: var(--harmony-green);
21-
}
22-
.changeCell.decrease {
23-
color: var(--harmony-orange);
19+
.tableWrapper {
20+
padding-bottom: 0;
21+
margin-bottom: 96px;
2422
}
2523

26-
.typeText {
27-
line-height: 150%;
24+
.tableWrapper :global([class*='showMoreContainer']) {
25+
background-color: transparent;
2826
}

packages/web/src/components/audio-transactions-table/AudioTransactionsTable.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import {
99
import { dayjs } from '@audius/common/utils'
1010
import { wAUDIO } from '@audius/fixed-decimal'
1111
import { Tooltip } from '@audius/harmony'
12-
import cn from 'classnames'
1312
import { Cell, Row } from 'react-table'
1413

15-
import { AudioTransactionIcon } from 'components/audio-transaction-icon'
1614
import { Table } from 'components/table'
1715
import { TableProps } from 'components/table/Table'
16+
import { RESPONSIVE_TABLE_POLICIES } from 'components/table/responsivePolicies'
1817

1918
import styles from './AudioTransactionsTable.module.css'
2019

2120
const transactionTypeLabelMap: Record<TransactionType, string> = {
2221
[TransactionType.TRANSFER]: '$AUDIO',
22+
[TransactionType.TIP]: 'Tip',
2323
[TransactionType.CHALLENGE_REWARD]: '$AUDIO Reward Earned',
2424
[TransactionType.TRENDING_REWARD]: 'Trending Competition Award',
2525
[TransactionType.PURCHASE]: 'Purchased $AUDIO'
@@ -75,16 +75,13 @@ const renderTransactionTypeCell = (cellInfo: TransactionCell) => {
7575
const methodText =
7676
transactionMethodLabelMap[method as TransactionMethod] ?? ''
7777

78-
const isTransferType = transactionType === TransactionType.TRANSFER
78+
const isMethodType =
79+
transactionType === TransactionType.TRANSFER ||
80+
transactionType === TransactionType.TIP
7981
return (
80-
<>
81-
<div className={styles.icon}>
82-
<AudioTransactionIcon type={transactionType} method={method} />
83-
</div>
84-
<span className={styles.typeText}>
85-
{`${typeText} ${isTransferType ? methodText : ''}`.trim()}
86-
</span>
87-
</>
82+
<span className={styles.typeText}>
83+
{`${typeText} ${isMethodType ? methodText : ''}`.trim()}
84+
</span>
8885
)
8986
}
9087

@@ -113,12 +110,7 @@ const renderChangeCell = (cellInfo: TransactionCell) => {
113110
})} $AUDIO`}
114111
mount={'body'}
115112
>
116-
<div
117-
className={cn(
118-
styles.changeCell,
119-
isChangePositive(tx) ? styles.increase : styles.decrease
120-
)}
121-
>
113+
<div>
122114
{wAUDIO(BigInt(change)).toLocaleString('en-US', {
123115
maximumFractionDigits: 0
124116
})}
@@ -210,6 +202,8 @@ export const AudioTransactionsTable = ({
210202
columns={tableColumns}
211203
onClickRow={handleClickRow}
212204
isEmptyRow={isEmptyRow}
205+
responsiveColumns={RESPONSIVE_TABLE_POLICIES.audioTransactions}
206+
wrapperClassName={styles.tableWrapper}
213207
{...other}
214208
/>
215209
)

packages/web/src/components/collections-table/CollectionsTable.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ import cn from 'classnames'
1010
import { Cell, Row } from 'react-table'
1111

1212
import { TextLink } from 'components/link'
13-
import { Table, alphaSorter, dateSorter, numericSorter } from 'components/table'
13+
import {
14+
Table,
15+
COLUMN_WIDTHS,
16+
alphaSorter,
17+
dateSorter,
18+
numericSorter
19+
} from 'components/table'
20+
import type { TableProps } from 'components/table/Table'
1421

1522
import styles from './CollectionsTable.module.css'
1623
import { CollectionsTableOverflowMenuButton } from './CollectionsTableOverflowMenuButton'
@@ -48,6 +55,7 @@ type CollectionsTableProps = {
4855
showMoreLimit?: number
4956
totalRowCount?: number
5057
tableHeaderClassName?: string
58+
responsiveColumns?: TableProps['responsiveColumns']
5159
}
5260

5361
const defaultColumns: CollectionsTableColumn[] = [
@@ -75,7 +83,8 @@ export const CollectionsTable = ({
7583
scrollRef,
7684
showMoreLimit,
7785
totalRowCount,
78-
tableHeaderClassName
86+
tableHeaderClassName,
87+
responsiveColumns
7988
}: CollectionsTableProps) => {
8089
// Cell Render Functions
8190
const renderNameCell = useCallback((cellInfo: CollectionCell) => {
@@ -156,8 +165,9 @@ export const CollectionsTable = ({
156165
Header: 'Album Name',
157166
accessor: 'title',
158167
Cell: renderNameCell,
159-
maxWidth: 300,
160-
width: 120,
168+
minWidth: COLUMN_WIDTHS.artistName,
169+
width: COLUMN_WIDTHS.artistName,
170+
maxWidth: Number.MAX_SAFE_INTEGER,
161171
sortTitle: 'Album Name',
162172
sorter: alphaSorter('title'),
163173
align: 'left'
@@ -167,44 +177,53 @@ export const CollectionsTable = ({
167177
Header: 'Released',
168178
accessor: 'created_at',
169179
Cell: renderReleaseDateCell,
170-
maxWidth: 160,
180+
minWidth: COLUMN_WIDTHS.date,
181+
width: COLUMN_WIDTHS.date,
182+
maxWidth: COLUMN_WIDTHS.date,
171183
sortTitle: 'Date Released',
172184
sorter: dateSorter('created_at'),
185+
disableResizing: true,
173186
align: 'right'
174187
},
175188
reposts: {
176189
id: 'reposts',
177190
Header: 'Reposts',
178191
accessor: 'repost_count',
179192
Cell: renderRepostsCell,
180-
maxWidth: 160,
193+
minWidth: COLUMN_WIDTHS.numeric,
194+
width: COLUMN_WIDTHS.numeric,
195+
maxWidth: COLUMN_WIDTHS.numeric,
181196
sortTitle: 'Reposts',
182197
sorter: numericSorter('repost_count'),
198+
disableResizing: true,
183199
align: 'right'
184200
},
185201
saves: {
186202
id: 'saves',
187203
Header: 'Favorites',
188204
accessor: 'save_count',
189205
Cell: renderSavesCell,
190-
maxWidth: 160,
206+
minWidth: COLUMN_WIDTHS.numeric,
207+
width: COLUMN_WIDTHS.numeric,
208+
maxWidth: COLUMN_WIDTHS.numeric,
191209
sortTitle: 'Favorites',
192210
sorter: numericSorter('save_count'),
211+
disableResizing: true,
193212
align: 'right'
194213
},
195214
overflowMenu: {
196215
id: 'overflowMenu',
197216
Cell: renderOverflowMenuCell,
198-
minWidth: 64,
199-
maxWidth: 64,
200-
width: 64,
217+
minWidth: COLUMN_WIDTHS.overflowMenu,
218+
maxWidth: COLUMN_WIDTHS.overflowMenu,
219+
width: COLUMN_WIDTHS.overflowMenu,
201220
disableResizing: true,
202221
disableSortBy: true
203222
},
204223
spacer: {
205224
id: 'spacer',
206-
maxWidth: 24,
207-
minWidth: 24,
225+
maxWidth: COLUMN_WIDTHS.spacer,
226+
minWidth: COLUMN_WIDTHS.spacer,
208227
disableSortBy: true,
209228
disableResizing: true
210229
}
@@ -262,6 +281,7 @@ export const CollectionsTable = ({
262281
showMoreLimit={showMoreLimit}
263282
totalRowCount={totalRowCount}
264283
tableHeaderClassName={tableHeaderClassName}
284+
responsiveColumns={responsiveColumns}
265285
/>
266286
)
267287
}

packages/web/src/components/table/Table.module.css

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,40 +50,44 @@
5050
.tableHeader {
5151
position: relative;
5252
background-color: var(--harmony-white);
53-
padding-left: 12px;
53+
padding-left: 0px;
5454
padding-right: 12px;
5555
line-height: 41px;
5656
user-select: none;
5757
}
5858

59-
.tableHeader.hasSorter .textCell {
60-
padding-right: 6px;
61-
}
62-
63-
.titleHeader + .titleHeader::before {
64-
content: '';
65-
position: absolute;
66-
height: 18px;
67-
width: 1px;
68-
top: 50%;
69-
left: 0;
70-
transform: translateY(-50%);
71-
background-color: var(--harmony-n-100);
72-
}
73-
7459
.textCell {
7560
white-space: nowrap;
7661
overflow: hidden;
7762
text-overflow: ellipsis;
7863
}
7964

65+
.headerContent {
66+
display: flex;
67+
align-items: center;
68+
gap: 4px;
69+
width: 100%;
70+
min-width: 0;
71+
}
72+
73+
.tableHeader.leftAlign .headerContent {
74+
justify-content: flex-start;
75+
padding-left: 12px;
76+
}
77+
78+
.tableHeader.rightAlign .headerContent {
79+
justify-content: flex-end;
80+
}
81+
82+
.tableHeader:not(.leftAlign):not(.rightAlign) .headerContent {
83+
justify-content: center;
84+
}
85+
8086
.sortCaretContainer {
81-
position: absolute;
8287
display: flex;
8388
flex-direction: column;
84-
top: 50%;
85-
right: 6px;
86-
transform: translateY(-50%);
89+
flex: 0 0 auto;
90+
justify-content: center;
8791
}
8892

8993
.sortCaret {
@@ -259,15 +263,6 @@
259263
text-overflow: ellipsis;
260264
}
261265

262-
.resizer {
263-
position: absolute;
264-
z-index: 1;
265-
top: 0;
266-
bottom: 0;
267-
right: -2px;
268-
width: 6px;
269-
}
270-
271266
.leftAlign {
272267
text-align: left;
273268
justify-content: left;

0 commit comments

Comments
 (0)