diff --git a/package-lock.json b/package-lock.json
index da407134f..21f5ad4d1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3051,6 +3051,12 @@
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
+ "@types/file-saver": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.1.tgz",
+ "integrity": "sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==",
+ "dev": true
+ },
"@types/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -3108,6 +3114,15 @@
"integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
"dev": true
},
+ "@types/json2csv": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@types/json2csv/-/json2csv-5.0.1.tgz",
+ "integrity": "sha512-1r5GCTyFtdQ53CRSIctzWZCmtDXvxtzM77SzOqPB4woMeGcc3rhUMzPqEQH3rokG1k/QLzlC5Qe5Ih8NuFN70Q==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/lodash": {
"version": "4.14.149",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
@@ -11610,6 +11625,11 @@
"schema-utils": "^2.5.0"
}
},
+ "file-saver": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz",
+ "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
+ },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -18082,6 +18102,23 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
+ "json2csv": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.1.tgz",
+ "integrity": "sha512-QFMifUX1y8W2tKi2TwZpnzf2rHdZvzdmgZUMEMDF46F90f4a9mUeWfx/qg4kzXSZYJYc3cWA5O+eLXk5lj9g8g==",
+ "requires": {
+ "commander": "^5.0.0",
+ "jsonparse": "^1.3.1",
+ "lodash.get": "^4.4.2"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+ "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="
+ }
+ }
+ },
"json3": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
@@ -18103,6 +18140,11 @@
"graceful-fs": "^4.1.6"
}
},
+ "jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
+ },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -18376,8 +18418,7 @@
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
- "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
- "dev": true
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"lodash.isequal": {
"version": "4.5.0",
diff --git a/package.json b/package.json
index 1526acbd1..ca2d685f7 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,8 @@
"animate.css": "^3.7.2",
"await-lock": "^2.0.1",
"axios": "^0.19.0",
+ "file-saver": "^2.0.2",
+ "json2csv": "^5.0.1",
"request": "^2.88.0",
"rss-parser": "^3.7.3",
"rxjs": "^6.5.2",
@@ -56,9 +58,11 @@
"vuex": "^3.0.1"
},
"devDependencies": {
- "@babel/core": "^7.7.2",
"@babel/compat-data": "^7.9.6",
+ "@babel/core": "^7.7.2",
+ "@types/file-saver": "^2.0.1",
"@types/jest": "^23.3.1",
+ "@types/json2csv": "^5.0.1",
"@types/lodash": "^4.14.149",
"@types/node": "^9.6.50",
"@types/request": "^2.47.1",
diff --git a/src/components/TransactionList/TransactionList.less b/src/components/TransactionList/TransactionList.less
index 45a2cd9c6..d64d2566e 100644
--- a/src/components/TransactionList/TransactionList.less
+++ b/src/components/TransactionList/TransactionList.less
@@ -89,7 +89,10 @@
text-align: right;
width: 100%;
display: block;
-
+ .download-transaction {
+ margin-top: 0.08rem;
+ margin-left: 0.3rem;
+ }
.ivu-page-item {
min-width: 0.31rem;
height: 0.3rem;
diff --git a/src/components/TransactionList/TransactionList.vue b/src/components/TransactionList/TransactionList.vue
index 1323386f6..1140fe5c2 100644
--- a/src/components/TransactionList/TransactionList.vue
+++ b/src/components/TransactionList/TransactionList.vue
@@ -10,6 +10,12 @@
/>
@@ -27,6 +33,11 @@
:transaction="activePartialTransaction"
@close="onCloseCosignatureModal"
/>
+
diff --git a/src/components/TransactionList/TransactionListTs.ts b/src/components/TransactionList/TransactionListTs.ts
index c40ab3b96..d31cf7040 100644
--- a/src/components/TransactionList/TransactionListTs.ts
+++ b/src/components/TransactionList/TransactionListTs.ts
@@ -29,8 +29,9 @@ import PageTitle from '@/components/PageTitle/PageTitle.vue'
import TransactionListFilters from '@/components/TransactionList/TransactionListFilters/TransactionListFilters.vue'
// @ts-ignore
import TransactionTable from '@/components/TransactionList/TransactionTable/TransactionTable.vue'
-import { TransactionGroupState } from '@/store/Transaction'
-
+// @ts-ignore
+import ModalTransactionExport from '@/views/modals/ModalTransactionExport/ModalTransactionExport.vue'
+import { TransactionGroup } from '@/store/Transaction'
@Component({
components: {
ModalTransactionCosignature,
@@ -38,6 +39,7 @@ import { TransactionGroupState } from '@/store/Transaction'
PageTitle,
TransactionListFilters,
TransactionTable,
+ ModalTransactionExport,
},
computed: {
...mapGetters({
@@ -126,6 +128,12 @@ export class TransactionListTs extends Vue {
*/
public isAwaitingCosignature: boolean = false
+ /**
+ * Whether currently viewing export
+ * @var {boolean}
+ */
+ public isViewingExportModal: boolean = false
+
public getEmptyMessage() {
return this.displayedTransactionStatus === TransactionGroupState.all
? 'no_data_transactions'
@@ -233,4 +241,15 @@ export class TransactionListTs extends Vue {
else if (page < 1) page = 1
this.currentPage = page
}
+
+ public get hasTransactionExportModal(): boolean {
+ return this.isViewingExportModal
+ }
+
+ public set hasTransactionExportModal(f: boolean) {
+ this.isViewingExportModal = f
+ }
+ public downloadTransaction() {
+ this.hasTransactionExportModal = true
+ }
}
diff --git a/src/core/utils/CSVHelpers.ts b/src/core/utils/CSVHelpers.ts
new file mode 100644
index 000000000..6242d3d4f
--- /dev/null
+++ b/src/core/utils/CSVHelpers.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2020 NEM Foundation (https://nem.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and limitations under the License.
+ *
+ */
+import { Parser } from 'json2csv'
+import FileSaver from 'file-saver'
+import store from '@/store/index.ts'
+import { TransactionViewFactory } from '@/core/transactions/TransactionViewFactory'
+import { TransactionView } from '@/core/transactions/TransactionView'
+import { AggregateTransaction, Transaction } from 'symbol-sdk'
+import * as _ from 'lodash'
+
+export class CSVHelpers {
+ protected static views: TransactionView[] = []
+ private static transactionsArray = []
+
+ /**
+ * Export to csv file
+ * @param data array of transactions
+ * returns new array with parsed aggregate transaction objects
+ */
+ private static constructAggregateTransactionsObject(transaction: AggregateTransaction) {
+ let merged = {}
+ const result = {}
+ this.views = [
+ TransactionViewFactory.getView(store, transaction),
+ ...transaction.innerTransactions.map((tx) => TransactionViewFactory.getView(store, tx)),
+ ]
+ this.views.forEach((value) => {
+ let mergedRow = {}
+ mergedRow = value.headerItems.concat(value.detailItems)
+ merged = _.defaults(merged, mergedRow)
+ })
+
+ const mergedArray = Object.entries(merged)
+ for (let i = 0; i < mergedArray.length; i++) {
+ if (mergedArray[i][1]['key'] == 'paid_fee') {
+ result[mergedArray[i][1]['key']] = mergedArray[i][1]['value'].maxFee.compact().toString()
+ } else if (mergedArray[i][1]['key'] == 'transfer_target') {
+ result[mergedArray[i]['key']] = mergedArray[i][1]['value'].address
+ } else {
+ result[mergedArray[i][1]['key']] = mergedArray[i][1]['value']
+ }
+ }
+ return result
+ }
+
+ /**
+ * Export to csv file
+ * @param data array of transactions
+ * returns new array with parsed transaction objects
+ */
+ private static constructTransactionsObject(transaction: Transaction) {
+ this.views = [TransactionViewFactory.getView(store, transaction)]
+ const result = {}
+ const value = this.views[0].headerItems.concat(this.views[0].detailItems)
+ for (let i = 0; i < value.length; i++) {
+ if (value[i].key == 'paid_fee') {
+ result[value[i].key] = value[i].value.maxFee.compact().toString()
+ } else if (value[i].key == 'transfer_target') {
+ result[value[i].key] = value[i].value.address
+ } else if (value[i].key.startsWith('Mosaic')) {
+ result[value[i].key] = value[i].value.amount + ' (' + value[i].value.mosaicHex + ')'
+ } else {
+ result[value[i].key] = value[i].value
+ }
+ }
+ return result
+ }
+
+ /**
+ * Export to csv file
+ * @param data array of transactions
+ * returns new array with parsed transaction objects
+ */
+
+ private static prepareTransactions(data: []): any[] {
+ this.transactionsArray = []
+ data.forEach((transaction) => {
+ let result = {}
+ if (transaction instanceof AggregateTransaction) {
+ result = this.constructAggregateTransactionsObject(transaction)
+ } else {
+ result = this.constructTransactionsObject(transaction)
+ }
+ this.transactionsArray.push(result)
+ })
+ return this.transactionsArray
+ }
+
+ /**
+ * Export to csv file
+ * @param data The json data
+ * @param filename Prefix the name of the CSV file
+ */
+ public static exportCSV(data: any, filename: string) {
+ const json2csvParser = new Parser()
+ const parsedTransactions = this.prepareTransactions(data)
+ const csvData = json2csvParser.parse(parsedTransactions)
+ const blob = new Blob(['\uFEFF' + csvData], { type: 'text/plain;charset=utf-8;' })
+ const exportFilename = `${filename}-${Date.now()}.csv`
+ return FileSaver.saveAs(blob, exportFilename)
+ }
+}
diff --git a/src/language/en-US.json b/src/language/en-US.json
index 3353534ee..7a7eac2b2 100644
--- a/src/language/en-US.json
+++ b/src/language/en-US.json
@@ -42,6 +42,8 @@
"accounts_backup_title_keystore": "Download keys",
"accounts_backup_title_mnemonic": "Mnemonic passphrase",
"accounts_backup_title_qrcode": "Encrypted Mnemonic QRCode",
+ "accounts_backup_transactions": "Export the transaction list to a CSV file",
+ "accounts_backup_transactions_description": "Download all the transaction information, save it locally and view it even if you don't have an Internet connection",
"accounts_change_password_description": "Change your password",
"accounts_change_password_title": "Change Password",
"accounts_create_invoice": "Create an invoice",
@@ -292,6 +294,7 @@
"modal_account_unlock_title": "Unlock your account",
"modal_title_backup_mnemonic_display": "Display Mnemonic Passphrase",
"modal_title_backup_mnemonic_qrcode": "Export Mnemonic QR Code",
+ "modal_title_backup_transaction": "Export Transactions",
"modal_title_debug_console": "Diagnostic Console",
"modal_title_enter_account_name": "Configure your new account",
"modal_title_extend_namespace_duration": "Extend namespace duration",
diff --git a/src/language/ja-JP.json b/src/language/ja-JP.json
index 278577043..2a94213f9 100644
--- a/src/language/ja-JP.json
+++ b/src/language/ja-JP.json
@@ -42,6 +42,8 @@
"accounts_backup_title_keystore": "キーのダウンロード",
"accounts_backup_title_mnemonic": "ニーモニックパス語群",
"accounts_backup_title_qrcode": "ニーモニックQRコードの暗号化",
+ "accounts_backup_transactions": "トランザクションリストをCSVファイルにエクスポートする",
+ "accounts_backup_transactions_description": "すべての取引情報をダウンロードしてローカルに保存し、インターネットに接続していない場合でも表示します",
"accounts_change_password_description": "パスワードを変更",
"accounts_change_password_title": "パスワードを変更",
"accounts_create_invoice": "インボイスの作成",
@@ -292,6 +294,7 @@
"modal_account_unlock_title": "アカウントのアンロック",
"modal_title_backup_mnemonic_display": "ニーモニックパス語群の表示",
"modal_title_backup_mnemonic_qrcode": "ニーモニックQRコードのエクスポート",
+ "modal_title_backup_transaction": "輸出取引",
"modal_title_debug_console": "診断コンソール",
"modal_title_enter_account_name": "新しいアカウントを設定する",
"modal_title_extend_namespace_duration": "ネームスペース期限の延長",
diff --git a/src/language/zh-CN.json b/src/language/zh-CN.json
index 42e7b7e57..5c16ed476 100644
--- a/src/language/zh-CN.json
+++ b/src/language/zh-CN.json
@@ -43,6 +43,8 @@
"accounts_backup_title_mnemonic": "查看助记词",
"accounts_backup_title_qrcode": "备份助记词",
"accounts_change_password_description": "更改密码",
+ "accounts_backup_transactions": "将交易清单汇出至CSV档案",
+ "accounts_backup_transactions_description": "下载所有交易信息,保存在本地并查看,即使您没有互联网连接",
"accounts_change_password_title": "修改密码",
"accounts_create_invoice": "创建发票",
"accounts_flags_default_account": "默认钱包",
@@ -292,6 +294,7 @@
"modal_account_unlock_title": "解锁钱包",
"modal_title_backup_mnemonic_display": "显示助记词",
"modal_title_backup_mnemonic_qrcode": "导出助记词二维码",
+ "modal_title_backup_transaction": "出口交易",
"modal_title_debug_console": "诊断控制台",
"modal_title_enter_account_name": "配置新钱包",
"modal_title_extend_namespace_duration": "延长命名空间持续时间",
diff --git a/src/views/modals/ModalTransactionExport/ModalTransactionExport.less b/src/views/modals/ModalTransactionExport/ModalTransactionExport.less
new file mode 100644
index 000000000..be9ab556f
--- /dev/null
+++ b/src/views/modals/ModalTransactionExport/ModalTransactionExport.less
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 NEM Foundation (https://nem.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and limitations under the License.
+ *
+ */
+@import '../../resources/css/variables.less';
+
+.body {
+ width: 100%;
+ display: grid;
+ grid-template-rows: 1.4rem 2rem 1rem;
+ grid-template-columns: 100%;
+
+ .explain {
+ padding: 0.2rem;
+ padding-left: 0.4rem;
+ font-size: @normalFont;
+
+ .subtitle {
+ color: @primary;
+ font-weight: @boldest;
+ }
+
+ p {
+ padding-top: 0.05rem;
+ text-align: justify;
+ }
+ }
+
+ .qr-image {
+ width: 2rem;
+ height: 2rem;
+ margin: auto;
+ }
+
+ .download-button {
+ margin: auto;
+ padding: 0.1rem 0.2rem;
+ border-radius: 0.04rem;
+ span {
+ vertical-align: text-bottom;
+ }
+ }
+}
diff --git a/src/views/modals/ModalTransactionExport/ModalTransactionExport.vue b/src/views/modals/ModalTransactionExport/ModalTransactionExport.vue
new file mode 100644
index 000000000..e26e30b07
--- /dev/null
+++ b/src/views/modals/ModalTransactionExport/ModalTransactionExport.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
{{ $t('accounts_backup_transactions') }}
+
{{ $t('accounts_backup_transactions_description') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/modals/ModalTransactionExport/ModalTransactionExportTs.ts b/src/views/modals/ModalTransactionExport/ModalTransactionExportTs.ts
new file mode 100644
index 000000000..e08a61cba
--- /dev/null
+++ b/src/views/modals/ModalTransactionExport/ModalTransactionExportTs.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2020 NEM Foundation (https://nem.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and limitations under the License.
+ *
+ */
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import { mapGetters } from 'vuex'
+import { Transaction } from 'symbol-sdk'
+
+// child components
+// @ts-ignore
+import FormProfileUnlock from '@/views/forms/FormProfileUnlock/FormProfileUnlock.vue'
+import { CSVHelpers } from '@/core/utils/CSVHelpers'
+
+@Component({
+ components: { FormProfileUnlock },
+ computed: {
+ ...mapGetters({
+ confirmedTransactions: 'transaction/confirmedTransactions',
+ partialTransactions: 'transaction/partialTransactions',
+ unconfirmedTransactions: 'transaction/unconfirmedTransactions',
+ }),
+ },
+})
+export class ModalTransactionExportTs extends Vue {
+ @Prop({
+ default: false,
+ })
+ visible: boolean
+
+ public hasTransactionExportInfo: boolean = false
+
+ /**
+ * List of confirmed transactions (per-request)
+ */
+ public confirmedTransactions: Transaction[]
+
+ /**
+ * List of unconfirmed transactions (per-request)
+ */
+ public unconfirmedTransactions: Transaction[]
+
+ /**
+ * List of confirmed transactions (per-request)
+ */
+ public partialTransactions: Transaction[]
+
+ /**
+ * Visibility state
+ * @type {boolean}
+ */
+ get show(): boolean {
+ return this.visible
+ }
+
+ /**
+ * Emits close event
+ */
+ set show(val) {
+ if (!val) {
+ this.$emit('close')
+ }
+ }
+
+ /**
+ * Hook called when the account has been unlocked
+ * @return {boolean}
+ */
+ public onAccountUnlocked(): boolean {
+ this.hasTransactionExportInfo = true
+ return true
+ }
+
+ /**
+ * Hook called when child component FormProfileUnlock or
+ * HardwareConfirmationButton emit the 'error' event.
+ * @param {string} message
+ * @return {void}
+ */
+ public onError(error: string) {
+ this.$emit('error', error)
+ }
+
+ /**
+ * The download transactions csv
+ * @return {void}
+ */
+ public onDownloadTx() {
+ const transactions = this.getTransactions()
+ CSVHelpers.exportCSV(transactions, 'transactions')
+ }
+
+ /**
+ * Transactions list
+ */
+ private getTransactions() {
+ return [...this.unconfirmedTransactions, ...this.partialTransactions, ...this.confirmedTransactions]
+ }
+}