From c514dd1501ebe223c3d45782e6983adae7a20ccc Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Thu, 14 Mar 2024 14:20:49 -0400 Subject: [PATCH 1/5] Added member API function to use pagination to get all member transactions. Modified transaction page to use new function when exporting transaction to CSV/Excel. --- src/app/@api/member.api.ts | 25 +++++- .../pages/transactions/transactions.page.ts | 90 ++++++++++--------- 2 files changed, 73 insertions(+), 42 deletions(-) diff --git a/src/app/@api/member.api.ts b/src/app/@api/member.api.ts index a6f4340..5050d8f 100644 --- a/src/app/@api/member.api.ts +++ b/src/app/@api/member.api.ts @@ -23,7 +23,7 @@ import { CustomTokenRequest, } from '@build-5/interfaces'; import dayjs from 'dayjs'; -import { Observable, combineLatest, map, of, switchMap } from 'rxjs'; +import { Observable, combineLatest, first, map, of, switchMap } from 'rxjs'; import { BaseApi } from './base.api'; export interface TokenDistributionWithAirdrops extends TokenDistribution { @@ -210,6 +210,29 @@ export class MemberApi extends BaseApi { ); } + public getAllTransactions(memberId: string, orderBy: string[] = ['createdOn']): Observable { + return new Observable(observer => { + const fetchPage = (lastValue?: string) => { + this.transactionDataset.getTopTransactionsLive(orderBy, lastValue, memberId) + .pipe(first()) + .subscribe({ + next: transactions => { + if (transactions.length > 0) { + observer.next(transactions); + const lastTransaction = transactions[transactions.length - 1]; + fetchPage(lastTransaction.uid); + } else { + observer.complete(); + } + }, + error: err => observer.error(err) + }); + }; + + fetchPage(); + }); + } + public allSpacesAsMember = (memberId: NetworkAddress, lastValue?: string) => this.spaceDataset .subset(Subset.MEMBERS) diff --git a/src/app/pages/member/pages/transactions/transactions.page.ts b/src/app/pages/member/pages/transactions/transactions.page.ts index dd547e2..b59596c 100644 --- a/src/app/pages/member/pages/transactions/transactions.page.ts +++ b/src/app/pages/member/pages/transactions/transactions.page.ts @@ -18,7 +18,7 @@ import { DataService } from '@pages/member/services/data.service'; import { HelperService } from '@pages/member/services/helper.service'; import { Member, Transaction, TransactionType } from '@build-5/interfaces'; import Papa from 'papaparse'; -import { BehaviorSubject, Observable, Subscription, first, map, of } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription, first, map, of, toArray } from 'rxjs'; @UntilDestroy() @Component({ @@ -157,47 +157,55 @@ export class TransactionsPage implements OnInit, OnDestroy { public exportTransactions(): void { if (!this.data.member$.value?.uid) return; this.exportingTransactions = true; + this.memberApi - .topTransactions(this.data.member$.value?.uid, undefined, undefined) - .pipe(first(), untilDestroyed(this)) - .subscribe((transactions: Transaction[]) => { - this.exportingTransactions = false; - const fields = [ - '', - 'tranUid', - 'network', - 'type', - 'date', - 'amount', - 'tokenAmount', - 'tokenId', - 'tokenUid', - 'nftUid', - 'collectionUid', - 'tangle', - ]; - const csv = Papa.unparse({ - fields, - data: transactions.map((t) => [ - t.uid, - t.network, - this.transactionService.getTitle(t), - t.createdOn?.toDate(), - t.payload.amount, - t.payload.nativeTokens?.[0]?.amount || '', - t.payload.nativeTokens?.[0]?.id || '', - t.payload.token, - t.payload.nft, - t.payload.collection, - this.transactionService.getExplorerLink(t), - ]), - }); - - download( - `data:text/csv;charset=utf-8${csv}`, - `soonaverse_${this.data.member$.value?.uid}_transactions.csv`, - ); - this.cd.markForCheck(); + .getAllTransactions(this.data.member$.value?.uid) + .pipe(toArray(), untilDestroyed(this)) + .subscribe({ + next: (allTransactions: Transaction[][]) => { + this.exportingTransactions = false; + const flatTransactions = allTransactions.flat(); + const fields = [ + 'tranUid', + 'network', + 'type', + 'date', + 'amount', + 'tokenAmount', + 'tokenId', + 'tokenUid', + 'nftUid', + 'collectionUid', + 'tangle', + ]; + const csv = Papa.unparse({ + fields, + data: flatTransactions.map(t => [ + t.uid, + t.network, + this.transactionService.getTitle(t), + t.createdOn?.toDate(), + t.payload.amount, + t.payload.nativeTokens?.[0]?.amount || '', + t.payload.nativeTokens?.[0]?.id || '', + t.payload.token, + t.payload.nft, + t.payload.collection, + this.transactionService.getExplorerLink(t), + ]), + }); + + download( + `data:text/csv;charset=utf-8,${encodeURIComponent(csv)}`, + `soonaverse_${this.data.member$.value?.uid}_transactions.csv` + ); + this.cd.markForCheck(); + }, + error: (error) => { + this.exportingTransactions = false; + console.error('Error fetching transactions for export', error); + + } }); } From 64c9c3b196ddcbf193e55f4cdf166039176dc570 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Thu, 14 Mar 2024 15:30:31 -0400 Subject: [PATCH 2/5] member transactions page will now fully populate with all transactions and maintains desktop paginated table and mobile infitite scroll UX designs. --- .../pages/transactions/transactions.page.html | 2 +- .../pages/transactions/transactions.page.ts | 90 ++++++++----------- 2 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/app/pages/member/pages/transactions/transactions.page.html b/src/app/pages/member/pages/transactions/transactions.page.html index af0e419..883fb2c 100644 --- a/src/app/pages/member/pages/transactions/transactions.page.html +++ b/src/app/pages/member/pages/transactions/transactions.page.html @@ -17,7 +17,7 @@
- + Date diff --git a/src/app/pages/member/pages/transactions/transactions.page.ts b/src/app/pages/member/pages/transactions/transactions.page.ts index b59596c..2ea2b96 100644 --- a/src/app/pages/member/pages/transactions/transactions.page.ts +++ b/src/app/pages/member/pages/transactions/transactions.page.ts @@ -35,6 +35,7 @@ export class TransactionsPage implements OnInit, OnDestroy { public openLockedTokenClaim?: Transaction | null; private dataStore: Transaction[][] = []; private subscriptions$: Subscription[] = []; + public allTransactions: Transaction[] = []; constructor( public deviceService: DeviceService, @@ -54,15 +55,32 @@ export class TransactionsPage implements OnInit, OnDestroy { this.exportTransactions(); this.cd.markForCheck(); } + }); - this.data.member$?.pipe(untilDestroyed(this)).subscribe((obj) => { - if (obj) { - this.listen(); - } - }); + this.data.member$?.pipe(untilDestroyed(this)).subscribe((member) => { + if (member) { + this.memberApi.getAllTransactions(member.uid) + .pipe(toArray(), first()) + .subscribe({ + next: (pages) => { + this.allTransactions = pages.flat(); + this.updateDisplayedTransactions(); + }, + error: (error) => { + console.error('Error fetching all transactions', error); + } + }); + } }); } + private updateDisplayedTransactions(lastIndex?: number): void { + const nextIndex = lastIndex ?? DEFAULT_LIST_SIZE; + const newTransactions = this.allTransactions.slice(0, nextIndex); + this.transactions$.next(newTransactions); + this.cd.markForCheck(); + } + public getDebugInfo(tran: Transaction | undefined | null): string { let msg = `uid: ${tran?.uid}, tries: ${tran?.payload?.walletReference?.count || 0}`; if (tran?.payload?.walletReference?.error) { @@ -108,24 +126,10 @@ export class TransactionsPage implements OnInit, OnDestroy { } public onScroll(): void { - // In this case there is no value, no need to infinite scroll. - if (!this.transactions$.value) { - return; - } - - // We reached maximum. - if ( - !this.dataStore[this.dataStore.length - 1] || - this.dataStore[this.dataStore.length - 1]?.length < DEFAULT_LIST_SIZE - ) { - return; + if (this.transactions$.value && this.allTransactions.length > this.transactions$.value.length) { + const nextIndex = this.transactions$.value.length + DEFAULT_LIST_SIZE; + this.updateDisplayedTransactions(nextIndex); } - - // Def order field. - const lastValue = this.transactions$.value[this.transactions$.value.length - 1]._doc; - this.subscriptions$.push( - this.getHandler(lastValue).subscribe(this.store.bind(this, this.dataStore.length)), - ); } protected store(page: number, a: any): void { @@ -135,7 +139,6 @@ export class TransactionsPage implements OnInit, OnDestroy { this.dataStore.push(a); } - // Merge arrays. this.transactions$.next(Array.prototype.concat.apply([], this.dataStore)); } @@ -158,47 +161,28 @@ export class TransactionsPage implements OnInit, OnDestroy { if (!this.data.member$.value?.uid) return; this.exportingTransactions = true; - this.memberApi - .getAllTransactions(this.data.member$.value?.uid) + this.memberApi.getAllTransactions(this.data.member$.value?.uid) .pipe(toArray(), untilDestroyed(this)) .subscribe({ next: (allTransactions: Transaction[][]) => { this.exportingTransactions = false; const flatTransactions = allTransactions.flat(); const fields = [ - 'tranUid', - 'network', - 'type', - 'date', - 'amount', - 'tokenAmount', - 'tokenId', - 'tokenUid', - 'nftUid', - 'collectionUid', - 'tangle', + 'tranUid', 'network', 'type', 'date', 'amount', 'tokenAmount', 'tokenId', 'tokenUid', 'nftUid', 'collectionUid', 'tangle' ]; const csv = Papa.unparse({ fields, data: flatTransactions.map(t => [ - t.uid, - t.network, - this.transactionService.getTitle(t), - t.createdOn?.toDate(), - t.payload.amount, - t.payload.nativeTokens?.[0]?.amount || '', - t.payload.nativeTokens?.[0]?.id || '', - t.payload.token, - t.payload.nft, - t.payload.collection, - this.transactionService.getExplorerLink(t), + t.uid, t.network, this.transactionService.getTitle(t), t.createdOn?.toDate(), t.payload.amount, + t.payload.nativeTokens?.[0]?.amount || '', t.payload.nativeTokens?.[0]?.id || '', t.payload.token, + t.payload.nft, t.payload.collection, this.transactionService.getExplorerLink(t) ]), }); - download( - `data:text/csv;charset=utf-8,${encodeURIComponent(csv)}`, - `soonaverse_${this.data.member$.value?.uid}_transactions.csv` - ); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const downloadUrl = URL.createObjectURL(blob); + download(downloadUrl, `soonaverse_${this.data.member$.value?.uid}_transactions.csv`); + URL.revokeObjectURL(downloadUrl); this.cd.markForCheck(); }, error: (error) => { @@ -209,8 +193,8 @@ export class TransactionsPage implements OnInit, OnDestroy { }); } - public trackByUid(index: number, item: any): number { - return item.uid; + public trackByUid(index: number, item: any): any { + return item ? item.uid : index; } private cancelSubscriptions(): void { From 0f8704e4b76043053b538a9c3f671de4c4cdcfa7 Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Thu, 14 Mar 2024 15:40:34 -0400 Subject: [PATCH 3/5] lint/prettier commit --- src/app/@api/member.api.ts | 14 ++++--- .../pages/transactions/transactions.page.ts | 39 ++++++++++++++----- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/app/@api/member.api.ts b/src/app/@api/member.api.ts index 5050d8f..6352f31 100644 --- a/src/app/@api/member.api.ts +++ b/src/app/@api/member.api.ts @@ -210,13 +210,17 @@ export class MemberApi extends BaseApi { ); } - public getAllTransactions(memberId: string, orderBy: string[] = ['createdOn']): Observable { - return new Observable(observer => { + public getAllTransactions( + memberId: string, + orderBy: string[] = ['createdOn'], + ): Observable { + return new Observable((observer) => { const fetchPage = (lastValue?: string) => { - this.transactionDataset.getTopTransactionsLive(orderBy, lastValue, memberId) + this.transactionDataset + .getTopTransactionsLive(orderBy, lastValue, memberId) .pipe(first()) .subscribe({ - next: transactions => { + next: (transactions) => { if (transactions.length > 0) { observer.next(transactions); const lastTransaction = transactions[transactions.length - 1]; @@ -225,7 +229,7 @@ export class MemberApi extends BaseApi { observer.complete(); } }, - error: err => observer.error(err) + error: (err) => observer.error(err), }); }; diff --git a/src/app/pages/member/pages/transactions/transactions.page.ts b/src/app/pages/member/pages/transactions/transactions.page.ts index 2ea2b96..01ae49a 100644 --- a/src/app/pages/member/pages/transactions/transactions.page.ts +++ b/src/app/pages/member/pages/transactions/transactions.page.ts @@ -59,7 +59,8 @@ export class TransactionsPage implements OnInit, OnDestroy { this.data.member$?.pipe(untilDestroyed(this)).subscribe((member) => { if (member) { - this.memberApi.getAllTransactions(member.uid) + this.memberApi + .getAllTransactions(member.uid) .pipe(toArray(), first()) .subscribe({ next: (pages) => { @@ -68,7 +69,7 @@ export class TransactionsPage implements OnInit, OnDestroy { }, error: (error) => { console.error('Error fetching all transactions', error); - } + }, }); } }); @@ -161,21 +162,40 @@ export class TransactionsPage implements OnInit, OnDestroy { if (!this.data.member$.value?.uid) return; this.exportingTransactions = true; - this.memberApi.getAllTransactions(this.data.member$.value?.uid) + this.memberApi + .getAllTransactions(this.data.member$.value?.uid) .pipe(toArray(), untilDestroyed(this)) .subscribe({ next: (allTransactions: Transaction[][]) => { this.exportingTransactions = false; const flatTransactions = allTransactions.flat(); const fields = [ - 'tranUid', 'network', 'type', 'date', 'amount', 'tokenAmount', 'tokenId', 'tokenUid', 'nftUid', 'collectionUid', 'tangle' + 'tranUid', + 'network', + 'type', + 'date', + 'amount', + 'tokenAmount', + 'tokenId', + 'tokenUid', + 'nftUid', + 'collectionUid', + 'tangle', ]; const csv = Papa.unparse({ fields, - data: flatTransactions.map(t => [ - t.uid, t.network, this.transactionService.getTitle(t), t.createdOn?.toDate(), t.payload.amount, - t.payload.nativeTokens?.[0]?.amount || '', t.payload.nativeTokens?.[0]?.id || '', t.payload.token, - t.payload.nft, t.payload.collection, this.transactionService.getExplorerLink(t) + data: flatTransactions.map((t) => [ + t.uid, + t.network, + this.transactionService.getTitle(t), + t.createdOn?.toDate(), + t.payload.amount, + t.payload.nativeTokens?.[0]?.amount || '', + t.payload.nativeTokens?.[0]?.id || '', + t.payload.token, + t.payload.nft, + t.payload.collection, + this.transactionService.getExplorerLink(t), ]), }); @@ -188,8 +208,7 @@ export class TransactionsPage implements OnInit, OnDestroy { error: (error) => { this.exportingTransactions = false; console.error('Error fetching transactions for export', error); - - } + }, }); } From 71f5c893656d75126f6423e63d33f1a97109eb2c Mon Sep 17 00:00:00 2001 From: Alec Menconi Date: Thu, 14 Mar 2024 23:16:28 -0400 Subject: [PATCH 4/5] On initial page load only 100 transactions are fetched, added select option to load more transactions for page/export. --- src/app/@api/member.api.ts | 37 ++- src/app/pages/member/member.module.ts | 4 + .../pages/transactions/transactions.page.html | 65 ++++- .../pages/transactions/transactions.page.less | 18 ++ .../pages/transactions/transactions.page.ts | 254 +++++++++--------- 5 files changed, 248 insertions(+), 130 deletions(-) diff --git a/src/app/@api/member.api.ts b/src/app/@api/member.api.ts index 6352f31..78e3290 100644 --- a/src/app/@api/member.api.ts +++ b/src/app/@api/member.api.ts @@ -215,6 +215,8 @@ export class MemberApi extends BaseApi { orderBy: string[] = ['createdOn'], ): Observable { return new Observable((observer) => { + let accumulatedTransactions: Transaction[] = []; + const fetchPage = (lastValue?: string) => { this.transactionDataset .getTopTransactionsLive(orderBy, lastValue, memberId) @@ -222,10 +224,11 @@ export class MemberApi extends BaseApi { .subscribe({ next: (transactions) => { if (transactions.length > 0) { - observer.next(transactions); + accumulatedTransactions = [...accumulatedTransactions, ...transactions]; const lastTransaction = transactions[transactions.length - 1]; fetchPage(lastTransaction.uid); } else { + observer.next(accumulatedTransactions); observer.complete(); } }, @@ -237,6 +240,38 @@ export class MemberApi extends BaseApi { }); } + public getTransactionsWithLimit( + memberId: string, + totalRequired: number, + orderBy: string[] = ['createdOn'], + ): Observable { + return new Observable((observer) => { + let accumulatedTransactions: Transaction[] = []; + + const fetchPage = (lastValue?: string) => { + this.transactionDataset + .getTopTransactionsLive(orderBy, lastValue, memberId) + .pipe(first()) + .subscribe( + (transactions) => { + accumulatedTransactions = [...accumulatedTransactions, ...transactions]; + observer.next(accumulatedTransactions.slice(0, totalRequired)); + + if (transactions.length > 0 && accumulatedTransactions.length < totalRequired) { + const newLastValue = transactions[transactions.length - 1].uid; + fetchPage(newLastValue); + } else { + observer.complete(); + } + }, + (error) => observer.error(error), + ); + }; + + fetchPage(); + }); + } + public allSpacesAsMember = (memberId: NetworkAddress, lastValue?: string) => this.spaceDataset .subset(Subset.MEMBERS) diff --git a/src/app/pages/member/member.module.ts b/src/app/pages/member/member.module.ts index 8fc6c08..d0e2af3 100644 --- a/src/app/pages/member/member.module.ts +++ b/src/app/pages/member/member.module.ts @@ -61,6 +61,8 @@ import { MemberSpacesComponent } from './pages/spaces/member-spaces.component'; import { TokensPage } from './pages/tokens/tokens.page'; import { TransactionsPage } from './pages/transactions/transactions.page'; import { DataService } from './services/data.service'; +import { NzRadioModule } from 'ng-zorro-antd/radio'; +import { NzSpinModule } from 'ng-zorro-antd/spin'; @NgModule({ declarations: [ @@ -128,6 +130,8 @@ import { DataService } from './services/data.service'; NftDepositModule, NftStakeModule, TokenTradingPairsTableModule, + NzRadioModule, + NzSpinModule, ], }) export class MemberModule {} diff --git a/src/app/pages/member/pages/transactions/transactions.page.html b/src/app/pages/member/pages/transactions/transactions.page.html index 883fb2c..06b4556 100644 --- a/src/app/pages/member/pages/transactions/transactions.page.html +++ b/src/app/pages/member/pages/transactions/transactions.page.html @@ -3,6 +3,52 @@ +
+
+ Select number of transactions to view/export: +
+ + + + + + + +
+