diff --git a/lib/api/transactions.js b/lib/api/transactions.js index 3de473072..01d2e9e7d 100644 --- a/lib/api/transactions.js +++ b/lib/api/transactions.js @@ -21,6 +21,7 @@ const coreUtils = require('../../utils/core.js'); module.exports = function (app) { const self = this; const knowledge = app.knownAddresses; + const invalidAddress = '__invalid_address'; const param = (p, d) => { p = parseInt(p, 10); @@ -263,13 +264,26 @@ module.exports = function (app) { }); }; - const normalizeTransactionParams = (params) => { + const parseLiskAddress = async (address) => { + const liskIdPattern = /^[0-9]{1,21}[L|l]$/g; + + if (!address || typeof address !== 'string') return ''; + if (address.match(liskIdPattern)) return coreUtils.parseAddress(address); + address = await knowledge.getByUser(address); + if (typeof address === 'object' && typeof address.address === 'string') return address.address; + return invalidAddress; + }; + + const normalizeTransactionParams = async (params) => { const directionQueries = []; const offset = param(params.offset, 0); const limit = param(params.limit, 100); const sort = params.sort || 'timestamp:desc'; - const address = coreUtils.parseAddress(params.address); + + const address = await parseLiskAddress(params.address); + params.senderId = await parseLiskAddress(params.senderId); + params.recipientId = await parseLiskAddress(params.recipientId); let baseQuery = `sort=${sort}&offset=${offset}&limit=${limit}`; @@ -295,11 +309,13 @@ module.exports = function (app) { filters = filters.filter(item => item !== 'senderId' && item !== 'recipientId'); } - Object.keys(params).forEach((key) => { - if ((filters.indexOf(key) >= 0)) { - baseQuery += `&${key}=${params[key]}`; - } - }); + Object.keys(params) + .filter(p => !!params[p]) + .forEach((key) => { + if ((filters.indexOf(key) >= 0)) { + baseQuery += `&${key}=${params[key]}`; + } + }); // type might be comma separate or undefined if (params.type) { @@ -352,16 +368,16 @@ module.exports = function (app) { }); }; - this.getTransactions = function (query, error, success) { - const queryList = normalizeTransactionParams(query); + this.getTransactions = async function (query, error, success) { + const queryList = await normalizeTransactionParams(query); this.getTransactionsCall(queryList, error, success); }; - this.getTransactionsByAddress = function (query, error, success) { + this.getTransactionsByAddress = async function (query, error, success) { if (!query || (!query.address && !query.senderId && !query.recipientId)) { return error({ success: false, error: 'Missing/Invalid address parameter' }); } - const queryList = normalizeTransactionParams(query); + const queryList = await normalizeTransactionParams(query); return this.getTransactionsCall(queryList, error, success); }; diff --git a/src/app/states.js b/src/app/states.js index 7907dafb4..d2012bdd0 100644 --- a/src/app/states.js +++ b/src/app/states.js @@ -33,7 +33,7 @@ App.config(($stateProvider, $urlRouterProvider, $locationProvider) => { component: 'block', }) .state('transactions', { - url: '/txs/:page', + url: '/txs/:page?senderId&senderPublicKey&recipientId&recipientPublicKey&minAmount&maxAmount&type&height&blockId&fromTimestamp&toTimestamp&sort&limit&offset', parentDir: 'home', component: 'transactions', }) diff --git a/src/assets/styles/common.css b/src/assets/styles/common.css index 86ff53920..8436b9807 100644 --- a/src/assets/styles/common.css +++ b/src/assets/styles/common.css @@ -1604,3 +1604,32 @@ li.dropdown.disabled a { #footer .social-networks { padding-top: 5px; } + +.transaction-filter button { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + user-select: none; + background-image: none; + border: 1px solid #ccc; + border-style: solid none; + height: auto; + width: auto; +} + +.transaction-filter button:first-of-type { + border-style: solid none solid none; +} + +.transaction-filter button:last-of-type { + border-style: solid solid solid none; +} + diff --git a/src/components/address/address.html b/src/components/address/address.html index 766820b97..0dee48da6 100644 --- a/src/components/address/address.html +++ b/src/components/address/address.html @@ -108,7 +108,7 @@

- + diff --git a/src/components/transactions/transactions.component.js b/src/components/transactions/transactions.component.js index 81770db12..7e76d018a 100644 --- a/src/components/transactions/transactions.component.js +++ b/src/components/transactions/transactions.component.js @@ -18,21 +18,36 @@ import template from './transactions.html'; const TransactionsConstructor = function ($rootScope, $stateParams, $state, $http, $interval) { const vm = this; + + const filters = Object.keys($stateParams) + .filter(key => key !== 'page') + .filter(key => key !== '#') + .filter(key => typeof $stateParams[key] !== 'undefined') + .map(key => `${key}=${$stateParams[key]}`); + vm.getLastTransactions = (n) => { const limit = 20 + 1; let offset = 0; if (n) offset = (n - 1) * limit; - $http.get(`/api/getTransactions?limit=${limit}&offset=${offset}`).then((resp) => { + let requestUrl = `/api/getTransactions?limit=${limit}&offset=${offset}`; + requestUrl += filters.length ? `&${filters.join('&')}` : ''; + + $http.get(requestUrl).then((resp) => { if (resp.data.success) { - const removedTx = resp.data.transactions.splice(-1, 1); + let removedTx; + if (resp.data.transactions.length > limit - 1) { + removedTx = resp.data.transactions.splice(-1, 1); + } vm.txs = { results: resp.data.transactions }; vm.txs.hasPrev = !!offset; vm.txs.hasNext = !!removedTx; - vm.txs.page = $stateParams.page || 0; + vm.txs.page = $stateParams.page || 1; vm.txs.loadPageOffset = vm.loadPageOffset; vm.txs.loadPage = vm.loadPage; + vm.txs.activeSort = vm.activeSort; + vm.txs.applySort = vm.applySort; } else { vm.txs = {}; } @@ -47,6 +62,15 @@ const TransactionsConstructor = function ($rootScope, $stateParams, $state, $htt $state.go($state.current.component, { page: pageNumber }); }; + vm.applySort = (predicate) => { + const direction = (predicate === vm.activeSort.predicate && vm.activeSort.direction === 'asc') ? 'desc' : 'asc'; + $state.go($state.current.component, { sort: `${predicate}:${direction}` }); + }; + + vm.activeSort = typeof $stateParams.sort === 'string' + ? { predicate: $stateParams.sort.split(':')[0], direction: $stateParams.sort.split(':')[1] } + : { predicate: 'timestamp', direction: 'desc' }; + const update = () => { vm.getLastTransactions($stateParams.page || 1); }; diff --git a/src/components/transactions/transactions.html b/src/components/transactions/transactions.html index 56b1940d3..a6dd7da12 100644 --- a/src/components/transactions/transactions.html +++ b/src/components/transactions/transactions.html @@ -17,13 +17,13 @@ -->

Transactions

+
+ + +
Loading transactions list
-
- - -
diff --git a/src/services/pagination.js b/src/services/pagination.js index cce960d37..305ad61d9 100644 --- a/src/services/pagination.js +++ b/src/services/pagination.js @@ -25,6 +25,7 @@ const Pagination = function ($http, $q, params) { this.key = params.key || ''; this.offset = Number(params.offset) || 0; this.currentPage = Number(params.currentPage) || 1; + this.page = this.currentPage; this.limit = Number(params.limit) || 25; ['url', 'parent', 'key', 'offset', 'limit'].forEach((key) => { diff --git a/src/shared/transactions-filter/transactions-filter.directive.js b/src/shared/transactions-filter/transactions-filter.directive.js index 863c93955..1ab23775f 100644 --- a/src/shared/transactions-filter/transactions-filter.directive.js +++ b/src/shared/transactions-filter/transactions-filter.directive.js @@ -16,108 +16,78 @@ import AppTools from '../../app/app-tools.module'; import template from './transactions-filter.html'; -const TransactionsConstructor = function ($rootScope, $scope, $timeout, allTxs) { +const TransactionsConstructor = function ($rootScope, $scope, $stateParams, $element, $state) { $scope.searchModel = []; $scope.searchParams = []; $scope.availableSearchParams = [ - { key: 'senderId', name: 'Sender ID', placeholder: 'Sender...' }, - { key: 'senderPublicKey', name: 'Sender Public Key', placeholder: 'Sender Public Key...' }, - { key: 'recipientId', name: 'Recipient ID', placeholder: 'Recipient...' }, - { key: 'recipientPublicKey', name: 'Recipient Public Key', placeholder: 'Recipient Public Key...' }, - { key: 'minAmount', name: 'Min Amount', placeholder: 'Min Amount...' }, - { key: 'maxAmount', name: 'Max Amount', placeholder: 'Max Amount...' }, - { key: 'type', name: 'Transaction Type', placeholder: 'Comma separated...' }, - { key: 'height', name: 'Block Height', placeholder: 'Block Height...' }, - { key: 'blockId', name: 'Block Id', placeholder: 'Block Id...' }, - { key: 'fromTimestamp', name: 'From Timestamp', placeholder: 'From Timestamp...' }, - { key: 'toTimestamp', name: 'To Timestamp', placeholder: 'To Timestamp...' }, - { key: 'limit', name: 'Limit', placeholder: 'Limit...' }, - { key: 'offset', name: 'Offset', placeholder: 'Offset...' }, - { - key: 'sort', - name: 'Order By', - placeholder: 'Order By...', - restrictToSuggestedValues: true, - suggestedValues: ['amount:asc', 'amount:desc', 'fee:asc', 'fee:desc', 'type:asc', 'type:desc', 'timestamp:asc', 'timestamp:desc'], - }, + { key: 'senderId', name: 'Sender ID', placeholder: 'Sender...', example: '12317412804123L' }, + { key: 'senderPublicKey', name: 'Sender Public Key', placeholder: 'Sender Public Key...', example: 'b550ede5...a26c78d8' }, + { key: 'recipientId', name: 'Recipient ID', placeholder: 'Recipient...', example: '12317412804123L' }, + { key: 'recipientPublicKey', name: 'Recipient Public Key', placeholder: 'Recipient Public Key...', example: 'b550ede5...a26c78d8' }, + { key: 'minAmount', name: 'Min Amount', placeholder: 'Min Amount...', example: '1.25' }, + { key: 'maxAmount', name: 'Max Amount', placeholder: 'Max Amount...', example: '1000.5' }, + { key: 'type', name: 'Comma separated transaction types', placeholder: 'Comma separated...', example: '1,3' }, + { key: 'height', name: 'Block height', placeholder: 'Block Height...', example: '2963014' }, + { key: 'blockId', name: 'Block Id', placeholder: 'Block Id...', example: '17238091754034756025' }, + { key: 'fromTimestamp', name: 'From Timestamp', placeholder: 'From Timestamp...', example: '17238091754034756025' }, + { key: 'toTimestamp', name: 'To Timestamp', placeholder: 'To Timestamp...', example: '12317412804123L' }, + // { key: 'limit', name: 'Limit', placeholder: 'Limit...', example: '12317412804123L' }, + // { key: 'offset', name: 'Offset', placeholder: 'Offset...', example: '12317412804123L' }, + // { + // key: 'sort', + // name: 'Order By', + // placeholder: 'Order By...', + // restrictToSuggestedValues: true, + // suggestedValues: + // ['amount:asc', 'amount:desc', 'fee:asc', 'fee:desc', 'type:asc', + // 'type:desc', 'timestamp:asc', 'timestamp:desc'], + // }, ]; - $scope.parametersDisplayLimit = $scope.availableSearchParams.length; - - const onSearchBoxCleaned = () => { - if ($scope.cleanByFilters) { - $scope.cleanByFilters = false; - } else { - $scope.invalidParams = false; - $scope.txs = allTxs(); - $scope.txs.loadData(); + const convertToUrl = (key, param) => { + switch (key) { + case 'minAmount': + case 'maxAmount': + return Number(param) * Math.pow(10, 8); + default: + return param; } }; - const searchByParams = (params) => { - if ($scope.direction !== 'search') { - $scope.lastDirection = $scope.direction; - $scope.direction = 'search'; + const convertFromUrl = (key, param) => { + switch (key) { + case 'minAmount': + case 'maxAmount': + return Number(param) / Math.pow(10, 8); + default: + return encodeURI(param); } - $scope.invalidParams = false; - $scope.txs = allTxs(params); - $scope.txs.loadData(); }; - const onSearchChange = () => { - const params = {}; - Object.keys($scope.searchModel).forEach((key) => { - if ($scope.searchModel[key] !== undefined && $scope.searchModel[key] !== '') { - params[key] = $scope.searchModel[key]; - } - if ((key === 'minAmount' || key === 'maxAmount') && params[key] !== '') { - params[key] = Math.floor(parseFloat(params[key]) * 1e8); - } - }); - - if (params.query) { - params.senderId = params.query; - params.recipientId = params.query; - } - - if (Object.keys(params).length > 0) { - searchByParams(params); - } else { - onSearchBoxCleaned(); - } + $scope.addToQueryText = (key) => { + $scope.queryText += ` ${key}=`; }; - $rootScope.$on('advanced-searchbox:modelUpdated', (event, model) => { - if ($scope.searchModel.query !== model.query) { - $scope.searchModel = Object.assign({}, model); - return onSearchChange(); - } - - return $scope.searchModel = Object.assign({}, model); - }); - - $rootScope.$on('advanced-searchbox:removedSearchParam', (event, searchParameter) => { - delete $scope.searchModel[searchParameter.key]; - onSearchChange(); - }); - - $rootScope.$on('advanced-searchbox:removedAllSearchParam', () => { - onSearchBoxCleaned(); - }); - - $rootScope.$on('advanced-searchbox:leavedEditMode', (event, searchParameter) => { - $scope.searchModel[searchParameter.key] = searchParameter.value; - onSearchChange(); - }); - - // Sets autocomplete attr off - const disableAutocomplete = () => { - $timeout(() => { - document.getElementsByClassName('search-parameter-input')[0].setAttribute('autocomplete', 'off'); - }, 0); + $scope.performSearch = () => { + const query = $scope.queryText.split(' ') + .map(param => param.split('=')) + .reduce((acc, param) => { + acc[param[0]] = convertToUrl(param[0], param[1]); + return acc; + }, {}); + $state.go($state.current.component, Object.assign({ page: 1 }, query), { inherit: false }); }; - disableAutocomplete(); + $scope.queryText = Object.keys($stateParams) + .filter(key => key !== 'page') + .filter(key => key !== 'address') + .filter(key => key !== 'sort') + .filter(key => key !== '#') + .filter(key => typeof $stateParams[key] !== 'undefined') + .map(key => `${key}=${convertFromUrl(key, $stateParams[key])}`) + .join(' '); + + $scope.parametersDisplayLimit = $scope.availableSearchParams.length; }; const transactionsFilter = AppTools.directive('transactionsFilter', () => ({ diff --git a/src/shared/transactions-filter/transactions-filter.html b/src/shared/transactions-filter/transactions-filter.html index 2aecf281f..9d72c97cc 100644 --- a/src/shared/transactions-filter/transactions-filter.html +++ b/src/shared/transactions-filter/transactions-filter.html @@ -15,16 +15,53 @@ * */ --> -
- - -