Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use DataViews component to render Billing History list #97625

Open
wants to merge 45 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ab9893a
Initial commit
Dec 18, 2024
66d271a
Use page.redirect() for view receipt action
Dec 19, 2024
dfd9e43
Moves to hybrid approach to layout
Dec 19, 2024
2847e3c
Fix type errors and refactor
Dec 20, 2024
39b9dd5
Removes unused legacy code
Dec 20, 2024
b17fdc4
Refactors to functional component
Dec 20, 2024
60b957a
Prettier adjustment
Dec 20, 2024
893c707
Adds filtering/sorting/searching
Dec 30, 2024
bcbb737
Fixes filtered pagination totals
Jan 1, 2025
b9fb544
Removes unused files and adds filter for purchase type
Jan 2, 2025
ea123e1
Removes unused legacy filter function
Jan 2, 2025
14eae29
Adds date filter and dynamic service filter
Jan 2, 2025
92e1741
Refactoring and column swapping/visibility added
Jan 2, 2025
6ace1e4
Removes unnecessary params
Jan 3, 2025
129a7cf
Moves DataView list back to billing-history folder
Jan 3, 2025
beedd4b
Refactors into hooks for cleaner organization
Jan 3, 2025
48c2525
Adds scroll to top after page changes
Jan 3, 2025
03daefb
Fixes deprecated param
Jan 3, 2025
66d418b
Adds missing translation support and dynamic order types for filtering
Jan 3, 2025
7e91069
Refines action hooks
Jan 6, 2025
04c4519
Renaming
Jan 6, 2025
d0c13b4
Renames file and fixes linting
Jan 6, 2025
78acebf
Removes column hiding
Jan 6, 2025
160b00f
Adds helper method for searching transaction amounts.
Jan 9, 2025
a83fbac
Adds testing for custom hooks
Jan 9, 2025
f9519dd
Updates tests
Jan 9, 2025
8f692a8
Remove amount search
Jan 9, 2025
45f1984
Add filtering and view tests
Jan 9, 2025
ee82640
Remove use of isEqual from Lodash
Jan 13, 2025
244b480
Improve type usage
Jan 13, 2025
3185ef9
Incorporate useCallback() in useViewStateUpdate()
Jan 13, 2025
506d5e0
Assign viewState directly to useViewStateUpdate()
Jan 13, 2025
03c38f0
Call useTranslate() directly in useFieldDefinitions()
Jan 13, 2025
5b2db55
Account for unexpected item values
Jan 13, 2025
c2591d1
Remove Moment reference from BillingHistoryListDataView
Jan 13, 2025
a5fb805
Remove use of moment library
Jan 13, 2025
42678f8
Refactor usePagination
Jan 13, 2025
86fa5c1
Refactor useReceiptActions() to use top-level functions
Jan 13, 2025
6c70e4f
Refactor ususeViewStateUpdate() to use top-level functions
Jan 13, 2025
3588557
Refactor useTransactionsFiltering() to use top-level functions
Jan 13, 2025
9b06f36
Refactor useTransactionsSorting() to use top-level functions
Jan 13, 2025
ad6a7bd
Move the defaultLayout outside of the BillingHistoryListDataView func…
Jan 13, 2025
1e26d02
Add useMemo() to useViewStateUpdate()
Jan 13, 2025
88f12fb
Convert arrow functions in field-definitions.tsx
Jan 14, 2025
b34e432
Convert arrow functions in utils.tsx
Jan 14, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Gridicon } from '@automattic/components';
import { DataViews, View } from '@wordpress/dataviews';
import { useTranslate } from 'i18n-calypso';
import { useSelector } from 'react-redux';
import getPastBillingTransactions from 'calypso/state/selectors/get-past-billing-transactions';
import isRequestingBillingTransactions from 'calypso/state/selectors/is-requesting-billing-transactions';
import { useFieldDefinitions } from './hooks/use-field-definitions';
import { usePagination } from './hooks/use-pagination';
import { useReceiptActions } from './hooks/use-receipt-actions';
import { useTransactionsFiltering } from './hooks/use-transactions-filtering';
import { useTransactionsSorting } from './hooks/use-transactions-sorting';
import { useViewStateUpdate } from './hooks/use-view-state-update';
import type { ViewStateUpdate } from './data-views-types';

import 'calypso/components/dataviews/style.scss';
import './style-data-view.scss';

const DEFAULT_LAYOUT = { table: {} };

export interface BillingHistoryListProps {
getReceiptUrlFor: ( receiptId: string ) => string;
}

export default function BillingHistoryListDataView( {
getReceiptUrlFor,
}: BillingHistoryListProps ) {
const transactions = useSelector( getPastBillingTransactions );
const isLoading = useSelector( isRequestingBillingTransactions );
const viewState = useViewStateUpdate();
const receiptActions = useReceiptActions( getReceiptUrlFor );

const actions = receiptActions.map( ( action ) => ( {
...action,
icon: <Gridicon icon={ action.iconName } />,
} ) );

const filteredTransactions = useTransactionsFiltering( transactions, viewState.view );
const sortedTransactions = useTransactionsSorting( filteredTransactions, viewState.view );
const { paginatedItems, totalPages, totalItems } = usePagination(
sortedTransactions,
viewState.view.page,
viewState.view.perPage
);
const translate = useTranslate();
const fields = useFieldDefinitions( transactions );

const handleViewChange = ( view: View ) => viewState.updateView( view as ViewStateUpdate );

return (
<div className="billing-history">
<div className="dataviews-wrapper">
<DataViews
data={ paginatedItems }
paginationInfo={ {
totalItems,
totalPages,
} }
fields={ fields }
view={ viewState.view }
search
searchLabel={ translate( 'Search receipts' ) }
onChangeView={ handleViewChange }
defaultLayouts={ DEFAULT_LAYOUT }
actions={ actions }
isLoading={ isLoading }
/>
</div>
</div>
);
}
34 changes: 34 additions & 0 deletions client/me/purchases/billing-history/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ViewState } from './data-views-types';

export const DEFAULT_PAGE = 1;
export const DEFAULT_PER_PAGE = 10;

export const defaultDataViewsState: ViewState = {
type: 'table',
search: '',
filters: [],
page: DEFAULT_PAGE,
perPage: DEFAULT_PER_PAGE,
sort: {
field: 'date',
direction: 'desc',
},
fields: [ 'date', 'service', 'type', 'amount' ],
hiddenFields: [],
layout: {
styles: {
date: {
width: '15%',
},
service: {
width: '45%',
},
type: {
width: '20%',
},
amount: {
width: '20%',
},
},
},
};
39 changes: 39 additions & 0 deletions client/me/purchases/billing-history/data-views-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Operator } from '@wordpress/dataviews';

export type SortableField = 'date' | 'service' | 'type' | 'amount';
export type ViewType = 'table';
export type SortDirection = 'asc' | 'desc';

export interface Sort {
field: SortableField;
direction: SortDirection;
}

export interface Filter {
field: string;
operator: Operator;
value: string | string[];
}

export interface ViewStateUpdate {
page?: number;
perPage?: number;
sort?: Sort;
filters?: Filter[];
search?: string;
fields?: string[];
}

export interface ViewState {
type: ViewType;
search: string;
filters: Filter[];
page: number;
perPage: number;
sort: Sort;
fields: string[];
hiddenFields: string[];
layout?: {
styles?: Record< string, { width: string } >;
};
}
196 changes: 196 additions & 0 deletions client/me/purchases/billing-history/field-definitions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { type Operator } from '@wordpress/dataviews';
import { useTranslate } from 'i18n-calypso';
import { capitalPDangit } from 'calypso/lib/formatting';
import {
getTransactionTermLabel,
groupDomainProducts,
TransactionAmount,
renderTransactionQuantitySummary,
formatDisplayDate,
formatMonthYear,
formatMonthYearLabel,
} from './utils';
import type {
BillingTransaction,
BillingTransactionItem,
} from 'calypso/state/billing-transactions/types';

function renderServiceNameDescription(
transaction: BillingTransactionItem,
translate: ReturnType< typeof useTranslate >
) {
const plan = capitalPDangit( transaction.variation );
const termLabel = getTransactionTermLabel( transaction, translate );
return (
<div>
<strong>{ plan }</strong>
{ transaction.domain && <small>{ transaction.domain }</small> }
{ termLabel && <small>{ termLabel }</small> }
{ transaction.licensed_quantity && (
<small>{ renderTransactionQuantitySummary( transaction, translate ) }</small>
) }
</div>
);
}

function renderServiceName(
transaction: BillingTransaction,
translate: ReturnType< typeof useTranslate >
) {
const [ transactionItem, ...moreTransactionItems ] = groupDomainProducts(
transaction.items,
translate
);

if ( moreTransactionItems.length > 0 ) {
return <strong>{ translate( 'Multiple items' ) }</strong>;
}

if ( transactionItem.product === transactionItem.variation ) {
return transactionItem.product;
}

return renderServiceNameDescription( transactionItem, translate );
}

function getUniqueMonths(
transactions: BillingTransaction[]
): Array< { value: string; label: string } > {
const monthsMap = new Map< string, Date >();

transactions.forEach( ( transaction ) => {
const date = new Date( transaction.date );
const formatted = formatMonthYear( date );
monthsMap.set( formatted, date );
} );

return Array.from( monthsMap.entries() )
.sort( ( [ , dateA ], [ , dateB ] ) => dateB.getTime() - dateA.getTime() )
.map( ( [ value, date ] ) => ( {
value,
label: formatMonthYearLabel( date ),
} ) );
}

function getUniqueServices(
transactions: BillingTransaction[]
): Array< { value: string; label: string } > {
const uniqueServices = new Set( transactions.map( ( transaction ) => transaction.service ) );

return Array.from( uniqueServices )
.sort()
.map( ( service ) => ( {
value: service,
label: service,
} ) );
}

function getUniqueTransactionTypes(
transactions: BillingTransaction[]
): Array< { value: string; label: string } > {
const typeMap = new Map< string, string >();

transactions
.flatMap( ( transaction ) => transaction.items )
.forEach( ( item ) => {
if ( item.type && ! typeMap.has( item.type ) ) {
typeMap.set( item.type, item.type_localized || item.type );
}
} );

return Array.from( typeMap.entries() )
.sort( ( [ a ], [ b ] ) => a.localeCompare( b ) )
.map( ( [ value, label ] ) => ( {
value,
label,
} ) );
}

export function getFieldDefinitions(
transactions: BillingTransaction[] | null,
translate: ReturnType< typeof useTranslate >
) {
return {
date: {
id: 'date',
label: translate( 'Date' ),
type: 'text' as const,
width: '15%',
elements: getUniqueMonths( transactions ?? [] ),
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: BillingTransaction } ) => {
return formatMonthYear( new Date( item.date ) );
},
render: ( { item }: { item: BillingTransaction } ) => {
return <time>{ formatDisplayDate( new Date( item.date ) ) }</time>;
},
},
service: {
id: 'service',
label: translate( 'App' ),
type: 'text' as const,
width: '45%',
elements: getUniqueServices( transactions ?? [] ),
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
render: ( { item }: { item: BillingTransaction } ) => {
return <div>{ renderServiceName( item, translate ) }</div>;
},
getValue: ( { item }: { item: BillingTransaction } ) => {
const [ transactionItem ] = groupDomainProducts( item.items, translate );
if ( transactionItem.product === transactionItem.variation ) {
return transactionItem.product;
}
return capitalPDangit( transactionItem.variation );
},
},
type: {
id: 'type',
label: translate( 'Type' ),
type: 'text' as const,
width: '20%',
elements: getUniqueTransactionTypes( transactions ?? [] ),
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
render: ( { item }: { item: BillingTransaction } ) => {
const [ transactionItem ] = groupDomainProducts( item.items, translate );
return <div>{ transactionItem.type_localized || transactionItem.type }</div>;
},
getValue: ( { item }: { item: BillingTransaction } ) => {
const [ transactionItem ] = groupDomainProducts( item.items, translate );
return transactionItem.type;
},
},
amount: {
id: 'amount',
label: translate( 'Amount' ),
type: 'text' as const,
width: '20%',
enableGlobalSearch: true,
enableSorting: true,
enableHiding: false,
filterBy: {
operators: [ 'is' as Operator ],
},
getValue: ( { item }: { item: BillingTransaction } ) => {
return item.amount_integer;
},
render: ( { item }: { item: BillingTransaction } ) => {
return <TransactionAmount transaction={ item } />;
},
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useTranslate } from 'i18n-calypso';
import { useMemo } from 'react';
import { getFieldDefinitions } from '../field-definitions';
import type { BillingTransaction } from 'calypso/state/billing-transactions/types';

export function useFieldDefinitions( transactions: BillingTransaction[] | null ) {
const translate = useTranslate();

return useMemo( () => {
const fieldDefinitions = getFieldDefinitions( transactions, translate );
return Object.values( fieldDefinitions );
}, [ transactions, translate ] );
}
39 changes: 39 additions & 0 deletions client/me/purchases/billing-history/hooks/use-pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useMemo } from 'react';
import type { BillingTransaction } from 'calypso/state/billing-transactions/types';

interface PaginationResult {
paginatedItems: BillingTransaction[];
totalPages: number;
totalItems: number;
currentPage: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}

function calculatePagination(
items: BillingTransaction[],
page: number,
perPage: number
): PaginationResult {
const startIndex = ( page - 1 ) * perPage;
const paginatedItems = items.slice( startIndex, startIndex + perPage );
const totalItems = items.length;
const totalPages = Math.ceil( totalItems / perPage );

return {
paginatedItems,
totalPages,
totalItems,
currentPage: page,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
};
}

export function usePagination(
items: BillingTransaction[],
page: number,
perPage: number
): PaginationResult {
return useMemo( () => calculatePagination( items, page, perPage ), [ items, page, perPage ] );
}
Loading
Loading