feat[UI - Wallet Stability] Create generic/reusable assets listview component
TLDR: later this should form a basic building block for a new TokenSelector picker component, potentially replacing the current HoldingSelector* and TokenListView components (support for collectibles TBD as part of https://github.com/status-im/status-desktop/issues/15121) - create reusable `TokenSelectorAssetDelegate` and `TokenSelectorView` - add corresponding SB page, showcasing the flow/integration and the separation of concerns between the view, adaptor and delegate layers - add QML testcase for TokenSelectorView - don't display crypto symbol for token balances per chain tags - update the stores and SB pages - add some missing formatter functions to LocaleUtils and CurrenciesStore Fixes #14716
This commit is contained in:
parent
6d96745c04
commit
a12a6a4894
|
@ -30,6 +30,7 @@ SplitView {
|
|||
id: d
|
||||
|
||||
readonly property SwapInputParamsForm swapInputParamsForm: SwapInputParamsForm {
|
||||
selectedNetworkChainId: ctrlSelectedNetworkChainId.currentValue
|
||||
fromTokensKey: ctrlFromTokensKey.text
|
||||
fromTokenAmount: ctrlFromTokenAmount.text
|
||||
toTokenKey: ctrlToTokenKey.text
|
||||
|
@ -74,7 +75,7 @@ SplitView {
|
|||
}
|
||||
|
||||
currencyStore: d.adaptor.currencyStore
|
||||
flatNetworksModel: d.adaptor.filteredFlatNetworksModel
|
||||
flatNetworksModel: d.adaptor.swapStore.flatNetworks
|
||||
processedAssetsModel: d.adaptor.processedAssetsModel
|
||||
|
||||
tokenKey: d.swapInputParamsForm.fromTokensKey
|
||||
|
@ -125,6 +126,22 @@ SplitView {
|
|||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: "Chain:"
|
||||
}
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
id: ctrlSelectedNetworkChainId
|
||||
model: d.adaptor.swapStore.flatNetworks
|
||||
textRole: "chainName"
|
||||
valueRole: "chainId"
|
||||
displayText: currentIndex === -1 ? "All chains" : currentText
|
||||
currentIndex: -1 // all chains
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
|
|
|
@ -65,19 +65,9 @@ SplitView {
|
|||
id: swapInputForm
|
||||
selectedAccountIndex: accountComboBox.currentIndex
|
||||
selectedNetworkChainId: d.getNetwork()
|
||||
fromTokensKey: {
|
||||
if (d.tokenBySymbolModel.count > 0) {
|
||||
return ModelUtils.get(d.tokenBySymbolModel, fromTokenComboBox.currentIndex, "key")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
fromTokensKey: fromTokenComboBox.currentValue
|
||||
fromTokenAmount: swapInput.text
|
||||
toTokenKey: {
|
||||
if (d.tokenBySymbolModel.count > 0) {
|
||||
return ModelUtils.get(d.tokenBySymbolModel, toTokenComboBox.currentIndex, "key")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
toTokenKey: toTokenComboBox.currentValue
|
||||
toTokenAmount: swapOutputAmount.text
|
||||
}
|
||||
|
||||
|
@ -87,33 +77,34 @@ SplitView {
|
|||
visible: true
|
||||
modal: false
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
destroyOnClose: true
|
||||
swapInputParamsForm: swapInputForm
|
||||
swapAdaptor: SwapModalAdaptor {
|
||||
swapProposalLoading: loadingCheckBox.checked
|
||||
swapProposalReady: swapProposalReadyCheckBox.checked
|
||||
swapStore: SwapStore {
|
||||
readonly property var accounts: d.accountsModel
|
||||
readonly property var flatNetworks: d.flatNetworksModel
|
||||
readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked
|
||||
swapAdaptor: SwapModalAdaptor {
|
||||
swapProposalLoading: loadingCheckBox.checked
|
||||
swapProposalReady: swapProposalReadyCheckBox.checked
|
||||
swapStore: SwapStore {
|
||||
readonly property var accounts: d.accountsModel
|
||||
readonly property var flatNetworks: d.flatNetworksModel
|
||||
readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked
|
||||
|
||||
signal suggestedRoutesReady(var txRoutes)
|
||||
signal suggestedRoutesReady(var txRoutes)
|
||||
|
||||
function fetchSuggestedRoutes(accountFrom, accountTo, amount, tokenFrom, tokenTo,
|
||||
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) {}
|
||||
function authenticateAndTransfer(uuid, accountFrom, accountTo,
|
||||
tokenFrom, tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) {}
|
||||
}
|
||||
walletAssetsStore: WalletAssetsStore {
|
||||
id: thisWalletAssetStore
|
||||
walletTokensStore: TokensStore {
|
||||
readonly property var plainTokensBySymbolModel: TokensBySymbolModel {}
|
||||
}
|
||||
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
|
||||
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
|
||||
}
|
||||
currencyStore: CurrenciesStore {}
|
||||
swapFormData: swapInputForm
|
||||
function fetchSuggestedRoutes(accountFrom, accountTo, amount, tokenFrom, tokenTo,
|
||||
disabledFromChainIDs, disabledToChainIDs, preferredChainIDs, sendType, lockedInAmounts) {}
|
||||
function authenticateAndTransfer(uuid, accountFrom, accountTo,
|
||||
tokenFrom, tokenTo, sendType, tokenName, tokenIsOwnerToken, paths) {}
|
||||
}
|
||||
walletAssetsStore: WalletAssetsStore {
|
||||
id: thisWalletAssetStore
|
||||
walletTokensStore: TokensStore {
|
||||
plainTokensBySymbolModel: TokensBySymbolModel {}
|
||||
}
|
||||
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
|
||||
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
|
||||
}
|
||||
currencyStore: CurrenciesStore {}
|
||||
swapFormData: swapInputForm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +122,7 @@ SplitView {
|
|||
id: areTestNetworksEnabledCheckbox
|
||||
text: "areTestNetworksEnabled"
|
||||
checked: true
|
||||
onCheckedChanged: networksComboBox.currentIndex = 0
|
||||
onToggled: networksComboBox.currentIndex = 0
|
||||
}
|
||||
|
||||
StatusBaseText {
|
||||
|
@ -173,6 +164,7 @@ SplitView {
|
|||
ComboBox {
|
||||
id: fromTokenComboBox
|
||||
textRole: "name"
|
||||
valueRole: "key"
|
||||
model: d.tokenBySymbolModel
|
||||
currentIndex: 0
|
||||
}
|
||||
|
@ -190,6 +182,7 @@ SplitView {
|
|||
ComboBox {
|
||||
id: toTokenComboBox
|
||||
textRole: "name"
|
||||
valueRole: "key"
|
||||
model: d.tokenBySymbolModel
|
||||
currentIndex: 1
|
||||
}
|
||||
|
@ -197,7 +190,7 @@ SplitView {
|
|||
StatusInput {
|
||||
id: swapOutputAmount
|
||||
Layout.preferredWidth: 100
|
||||
label: "Token amount to receive"
|
||||
label: "Token amount to receive"
|
||||
text: "100"
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
|
||||
import Storybook 1.0
|
||||
import Models 1.0
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import AppLayouts.Wallet.views 1.0
|
||||
|
||||
SplitView {
|
||||
id: root
|
||||
orientation: Qt.Vertical
|
||||
|
||||
Logs { id: logs }
|
||||
|
||||
Pane {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.fillHeight: true
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.baseColor3
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 380
|
||||
height: 200
|
||||
color: Theme.palette.statusListItem.backgroundColor
|
||||
border.color: Theme.palette.primaryColor1
|
||||
border.width: 1
|
||||
anchors.centerIn: parent
|
||||
|
||||
TokenSelectorAssetDelegate {
|
||||
implicitWidth: 333
|
||||
anchors.centerIn: parent
|
||||
|
||||
tokensKey: "ETH"
|
||||
name: "Ethereum"
|
||||
symbol: "ETH"
|
||||
currencyBalanceAsString: "14,456.42 USD"
|
||||
balancesModel: ListModel {
|
||||
readonly property var data: [
|
||||
{ chainId: 1, balanceAsString: "1234.50", iconUrl: "network/Network=Ethereum" },
|
||||
{ chainId: 42161, balanceAsString: "55.91", iconUrl: "network/Network=Arbitrum" },
|
||||
{ chainId: 10, balanceAsString: "45.12", iconUrl: "network/Network=Optimism" },
|
||||
{ chainId: 420, balanceAsString: "1.23", iconUrl: "network/Network=Testnet" }
|
||||
]
|
||||
Component.onCompleted: append(data)
|
||||
}
|
||||
|
||||
interactive: ctrlInteractive.checked
|
||||
highlighted: ctrlHighlighted.checked
|
||||
|
||||
onAssetSelected: (tokensKey) => {
|
||||
console.warn("!!! TOKEN SELECTED:", tokensKey)
|
||||
logs.logEvent("TokenSelectorAssetDelegate::onTokenSelected", ["tokensKey"], arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogsAndControlsPanel {
|
||||
SplitView.minimumHeight: 300
|
||||
SplitView.preferredHeight: 300
|
||||
|
||||
logsView.logText: logs.logText
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
ColumnLayout {
|
||||
Switch {
|
||||
id: ctrlInteractive
|
||||
text: "Interactive"
|
||||
checked: true
|
||||
}
|
||||
Switch {
|
||||
id: ctrlHighlighted
|
||||
text: "Highlighted"
|
||||
checked: false
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Delegates
|
|
@ -0,0 +1,206 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
|
||||
import Storybook 1.0
|
||||
import Models 1.0
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import AppLayouts.Wallet.views 1.0
|
||||
import AppLayouts.Wallet.stores 1.0
|
||||
import AppLayouts.Wallet.adaptors 1.0
|
||||
|
||||
import shared.stores 1.0
|
||||
import utils 1.0
|
||||
|
||||
SplitView {
|
||||
id: root
|
||||
orientation: Qt.Vertical
|
||||
|
||||
Logs { id: logs }
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
property var enabledChainIds: []
|
||||
function addFilter(chainId) {
|
||||
if (d.enabledChainIds.includes(chainId))
|
||||
return
|
||||
const newFilters = d.enabledChainIds.concat(chainId)
|
||||
d.enabledChainIds = newFilters
|
||||
}
|
||||
function removeFilter(chainId) {
|
||||
const newFilters = d.enabledChainIds.filter((filter) => filter !== chainId)
|
||||
d.enabledChainIds = newFilters
|
||||
}
|
||||
function rebuildFilter() {
|
||||
let newFilters = []
|
||||
for (let i = 0; i < chainIdsRepeater.count; i++) {
|
||||
const item = chainIdsRepeater.itemAt(i)
|
||||
if (!!item && item.checked) {
|
||||
newFilters.push(item.chainId)
|
||||
}
|
||||
}
|
||||
d.enabledChainIds = newFilters
|
||||
}
|
||||
|
||||
readonly property string enabledChainIdsString: enabledChainIds.join(":")
|
||||
|
||||
readonly property var flatNetworks: NetworksModel.flatNetworks
|
||||
readonly property var currencyStore: CurrenciesStore {}
|
||||
readonly property var assetsStore: WalletAssetsStore {
|
||||
id: thisWalletAssetStore
|
||||
walletTokensStore: TokensStore {
|
||||
plainTokensBySymbolModel: TokensBySymbolModel {}
|
||||
}
|
||||
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
|
||||
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
|
||||
}
|
||||
|
||||
readonly property var walletAccountsModel: WalletAccountsModel {}
|
||||
|
||||
readonly property var adaptor: TokenSelectorViewAdaptor {
|
||||
assetsModel: d.assetsStore.groupedAccountAssetsModel
|
||||
flatNetworksModel: d.flatNetworks
|
||||
enabledChainIds: d.enabledChainIds
|
||||
currentCurrency: d.currencyStore.currentCurrency
|
||||
|
||||
accountAddress: ctrlAccount.currentValue ?? ""
|
||||
showCommunityAssets: ctrlShowCommunityAssets.checked
|
||||
searchString: ctrlSearch.text
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: d.rebuildFilter()
|
||||
|
||||
Pane {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.fillHeight: true
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.baseColor3
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 380
|
||||
height: 200
|
||||
color: Theme.palette.statusListItem.backgroundColor
|
||||
border.color: Theme.palette.primaryColor1
|
||||
border.width: 1
|
||||
anchors.centerIn: parent
|
||||
|
||||
// tokensKey, name, symbol, decimals, currentCurrencyBalance (computed), marketDetails, balances -> [ chainId, address, balance, iconUrl ]
|
||||
TokenSelectorView {
|
||||
anchors.fill: parent
|
||||
|
||||
model: d.adaptor.outputAssetsModel
|
||||
|
||||
onTokenSelected: (tokensKey) => {
|
||||
console.warn("!!! TOKEN SELECTED:", tokensKey)
|
||||
logs.logEvent("TokenSelectorView::onTokenSelected", ["tokensKey"], arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogsAndControlsPanel {
|
||||
SplitView.minimumHeight: 400
|
||||
SplitView.preferredHeight: 400
|
||||
|
||||
logsView.logText: logs.logText
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
ColumnLayout {
|
||||
CheckBox {
|
||||
id: ctrlTestNetworks
|
||||
text: "Test networks enabled"
|
||||
tristate: true
|
||||
checkState: Qt.PartiallyChecked
|
||||
onClicked: d.rebuildFilter()
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: chainIdsRepeater
|
||||
model: SortFilterProxyModel {
|
||||
sourceModel: d.flatNetworks
|
||||
filters: ValueFilter {
|
||||
roleName: "isTest"
|
||||
value: ctrlTestNetworks.checked
|
||||
enabled: ctrlTestNetworks.checkState !== Qt.PartiallyChecked
|
||||
}
|
||||
}
|
||||
delegate: CheckBox {
|
||||
required property int chainId
|
||||
required property string chainName
|
||||
required property string shortName
|
||||
required property bool isEnabled
|
||||
checked: isEnabled
|
||||
opacity: enabled ? 1 : 0.3
|
||||
text: "%1 (%2) - %3".arg(chainName).arg(shortName).arg(chainId)
|
||||
onToggled: {
|
||||
if (checked)
|
||||
d.addFilter(chainId)
|
||||
else
|
||||
d.removeFilter(chainId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: "Enabled chain ids: %1".arg(d.enabledChainIdsString)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Search:" }
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
id: ctrlSearch
|
||||
placeholderText: "Token name or symbol"
|
||||
}
|
||||
}
|
||||
Switch {
|
||||
id: ctrlShowCommunityAssets
|
||||
text: "Show community assets"
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Account:" }
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
id: ctrlAccount
|
||||
textRole: "name"
|
||||
valueRole: "address"
|
||||
displayText: currentIndex === -1 ? "All accounts" : currentText
|
||||
model: SortFilterProxyModel {
|
||||
sourceModel: d.walletAccountsModel
|
||||
sorters: RoleSorter { roleName: "position" }
|
||||
}
|
||||
currentIndex: -1
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: "Selected: %1".arg(ctrlAccount.currentValue ?? "all")
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// category: Views
|
|
@ -0,0 +1,82 @@
|
|||
import QtQuick 2.15
|
||||
import QtTest 1.15
|
||||
|
||||
import Models 1.0
|
||||
|
||||
import AppLayouts.Wallet.views 1.0
|
||||
import AppLayouts.Wallet.stores 1.0
|
||||
import AppLayouts.Wallet.adaptors 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 600
|
||||
height: 400
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
readonly property var flatNetworks: NetworksModel.flatNetworks
|
||||
readonly property var assetsStore: WalletAssetsStore {
|
||||
id: thisWalletAssetStore
|
||||
walletTokensStore: TokensStore {
|
||||
plainTokensBySymbolModel: TokensBySymbolModel {}
|
||||
}
|
||||
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
|
||||
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
|
||||
}
|
||||
|
||||
readonly property var adaptor: TokenSelectorViewAdaptor {
|
||||
assetsModel: d.assetsStore.groupedAccountAssetsModel
|
||||
flatNetworksModel: d.flatNetworks
|
||||
currentCurrency: "USD"
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: componentUnderTest
|
||||
TokenSelectorView {
|
||||
anchors.fill: parent
|
||||
|
||||
model: d.adaptor.outputAssetsModel
|
||||
}
|
||||
}
|
||||
|
||||
SignalSpy {
|
||||
id: signalSpy
|
||||
target: controlUnderTest
|
||||
signalName: "tokenSelected"
|
||||
}
|
||||
|
||||
property TokenSelectorView controlUnderTest: null
|
||||
|
||||
TestCase {
|
||||
name: "TokenSelectorView"
|
||||
when: windowShown
|
||||
|
||||
function init() {
|
||||
controlUnderTest = createTemporaryObject(componentUnderTest, root)
|
||||
signalSpy.clear()
|
||||
}
|
||||
|
||||
function test_basicGeometry() {
|
||||
verify(!!controlUnderTest)
|
||||
verify(controlUnderTest.width > 0)
|
||||
verify(controlUnderTest.height > 0)
|
||||
}
|
||||
|
||||
function test_clickEthToken() {
|
||||
verify(!!controlUnderTest)
|
||||
|
||||
const tokensKey = "ETH"
|
||||
|
||||
const delegate = findChild(controlUnderTest, "tokenSelectorAssetDelegate_%1".arg(tokensKey))
|
||||
verify(!!delegate)
|
||||
tryCompare(delegate, "tokensKey", tokensKey)
|
||||
|
||||
// click the delegate, verify the signal has been fired and has the correct "tokensKey" as argument
|
||||
mouseClick(delegate)
|
||||
tryCompare(signalSpy, "count", 1)
|
||||
compare(signalSpy.signalArguments[0][0], tokensKey)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import QtQuick 2.15
|
||||
import QtTest 1.15
|
||||
|
||||
import Models 1.0
|
||||
|
||||
import StatusQ.Core.Utils 0.1
|
||||
|
||||
import AppLayouts.Wallet.stores 1.0
|
||||
import AppLayouts.Wallet.adaptors 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 600
|
||||
height: 400
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
readonly property var flatNetworks: NetworksModel.flatNetworks
|
||||
readonly property var assetsStore: WalletAssetsStore {
|
||||
id: thisWalletAssetStore
|
||||
walletTokensStore: TokensStore {
|
||||
plainTokensBySymbolModel: TokensBySymbolModel {}
|
||||
}
|
||||
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
|
||||
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: componentUnderTest
|
||||
TokenSelectorViewAdaptor {
|
||||
assetsModel: d.assetsStore.groupedAccountAssetsModel
|
||||
flatNetworksModel: d.flatNetworks
|
||||
currentCurrency: "USD"
|
||||
}
|
||||
}
|
||||
|
||||
property TokenSelectorViewAdaptor controlUnderTest: null
|
||||
|
||||
TestCase {
|
||||
name: "TokenSelectorViewAdaptor"
|
||||
when: windowShown
|
||||
|
||||
function init() {
|
||||
controlUnderTest = createTemporaryObject(componentUnderTest, root)
|
||||
}
|
||||
|
||||
function test_search() {
|
||||
verify(!!controlUnderTest)
|
||||
|
||||
const searchText = "dAi"
|
||||
const originalCount = controlUnderTest.outputAssetsModel.count
|
||||
controlUnderTest.searchString = searchText
|
||||
|
||||
// search yields 1 result
|
||||
tryCompare(controlUnderTest.outputAssetsModel, "count", 1)
|
||||
|
||||
// resetting search string resets the view back to original count
|
||||
controlUnderTest.searchString = ""
|
||||
tryCompare(controlUnderTest.outputAssetsModel, "count", originalCount)
|
||||
}
|
||||
|
||||
function test_showCommunityAssets() {
|
||||
verify(!!controlUnderTest)
|
||||
|
||||
const originalCount = controlUnderTest.outputAssetsModel.count
|
||||
|
||||
// turn on showing the community assets, verify we now have more items
|
||||
controlUnderTest.showCommunityAssets = true
|
||||
tryVerify(() => controlUnderTest.outputAssetsModel.count > originalCount)
|
||||
|
||||
// turning them back off, verify we are back to the original number of items
|
||||
controlUnderTest.showCommunityAssets = false
|
||||
tryCompare(controlUnderTest.outputAssetsModel, "count", originalCount)
|
||||
}
|
||||
|
||||
function test_enabledChainIds() {
|
||||
verify(!!controlUnderTest)
|
||||
|
||||
// enable just "1" (Eth Mainnet) chain
|
||||
controlUnderTest.enabledChainIds = [1]
|
||||
|
||||
// grab the "DAI" entry
|
||||
const delegate = ModelUtils.getByKey(controlUnderTest.outputAssetsModel, "tokensKey", "DAI")
|
||||
verify(!!delegate)
|
||||
const origBalance = delegate.currencyBalance
|
||||
|
||||
// should have 0 balance
|
||||
tryCompare(delegate, "currencyBalance", 0)
|
||||
|
||||
// re-enable all chains, DAI should again have the original balance
|
||||
controlUnderTest.enabledChainIds = []
|
||||
tryCompare(delegate, "currencyBalance", origBalance)
|
||||
}
|
||||
|
||||
function test_accountAddress() {
|
||||
verify(!!controlUnderTest)
|
||||
|
||||
// enable the "Hot wallet" account address filter (0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881)
|
||||
controlUnderTest.accountAddress = "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881"
|
||||
|
||||
// grab the "STT" entry
|
||||
const delegate = ModelUtils.getByKey(controlUnderTest.outputAssetsModel, "tokensKey", "STT")
|
||||
verify(!!delegate)
|
||||
|
||||
// should have ~45.90 balance
|
||||
fuzzyCompare(delegate.currencyBalance, 45.90, 0.01)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,29 +5,33 @@ ListModel {
|
|||
{
|
||||
tokensKey: "DAI",
|
||||
balances: [
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 5, balance: "0" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 10, balance: "559133758939097000" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 420, balance: "0" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 11155111, balance: "0" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 5, balance: "123456789123456789" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 11155111, balance: "123456789123456789" }
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 420, balance: "123456789123456789" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 11155111, balance: "123456789123456789" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 42161, balance: "45123456789123456789" },
|
||||
]
|
||||
},
|
||||
{
|
||||
tokensKey: "ETH",
|
||||
balances: [
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 1, balance: "122082928968121891" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 420, balance: "1013151281976507736" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 421613, balance: "473057568699284613" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 5, balance: "307400931315122839" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 420, balance: "307400931315122839" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 11155111, balance: "307400931315122839" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 420, balance: "122082928968121891" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 421613, balance: "0" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 5, balance: "559133758939097000" }
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 420, balance: "559133758939097000" }
|
||||
]
|
||||
},
|
||||
{
|
||||
tokensKey: "STT",
|
||||
balances: [
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 5, balance: "999999999998998500000000000016777216" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 5, balance: "1077000000000000000000" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 1, balance: "45123456789123456789" },
|
||||
{ account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", chainId: 420, balance: "999999999998998500000000000016777216" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 10, balance: "1077000000000000000000" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 420, balance: "122082928968121891" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 421613, balance: "222000000000000000" },
|
||||
{ account: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881", chainId: 11155111, balance: "559133758939097000" }
|
||||
|
|
|
@ -56,7 +56,7 @@ QtObject {
|
|||
Component.onCompleted: append([
|
||||
{
|
||||
chainId: 1,
|
||||
chainName: "Ethereum Mainnet",
|
||||
chainName: "Mainnet",
|
||||
blockExplorerUrl: "https://etherscan.io/",
|
||||
iconUrl: "network/Network=Ethereum",
|
||||
chainColor: "#627EEA",
|
||||
|
@ -217,7 +217,6 @@ QtObject {
|
|||
chainName: "Arbitrum",
|
||||
iconUrl: ModelsData.networks.arbitrum,
|
||||
isActive: false,
|
||||
isEnabled: true,
|
||||
shortName: "ARB",
|
||||
chainColor: "purple",
|
||||
layer: 2,
|
||||
|
@ -337,7 +336,6 @@ QtObject {
|
|||
chainName: "Arbitrum",
|
||||
iconUrl: ModelsData.networks.arbitrum,
|
||||
isActive: false,
|
||||
isEnabled: true,
|
||||
shortName: "ARB",
|
||||
chainColor: "purple",
|
||||
layer: 2,
|
||||
|
|
|
@ -9,7 +9,6 @@ ListModel {
|
|||
readonly property string nativeSource: "native" //SourceOfTokensModel.custom
|
||||
|
||||
readonly property var data: [
|
||||
|
||||
{
|
||||
key: "ETH",
|
||||
name: "Ether",
|
||||
|
@ -117,7 +116,7 @@ ListModel {
|
|||
changePctDay: 0,
|
||||
changePct24hour: 0,
|
||||
change24hour: 0,
|
||||
currencyPrice: ({amount: 0, symbol: "USD", displayDecimals: 2, stripTrailingZeroes: false})
|
||||
currencyPrice: ({amount: 0.07, symbol: "USD", displayDecimals: 2, stripTrailingZeroes: false})
|
||||
},
|
||||
detailsLoading: false,
|
||||
marketDetailsLoading: false
|
||||
|
|
|
@ -17,10 +17,10 @@ QtObject {
|
|||
return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale)
|
||||
}
|
||||
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) {
|
||||
let bigIntBalance = SQUtils.AmountsArithmetic.fromString(balance)
|
||||
let decimalBalance = SQUtils.AmountsArithmetic.toNumber(bigIntBalance, decimals)
|
||||
return formatCurrencyAmount(decimalBalance, symbol)
|
||||
return formatCurrencyAmount(decimalBalance, symbol, options)
|
||||
}
|
||||
|
||||
function getFiatValue(balance, cryptoSymbol) {
|
||||
|
|
|
@ -182,7 +182,7 @@ QtObject {
|
|||
let listOfChains = chainIds.split(":")
|
||||
let listOfChainIds = []
|
||||
for (let k =0;k<listOfChains.length;k++) {
|
||||
listOfChainIds.push(SQUtils.ModelUtils.getByKey(NetworksModel.flatNetworks, "shortName", listOfChains[k], "chainId"))
|
||||
listOfChainIds.push(SQUtils.ModelUtils.getByKey(flatNetworksModel, "shortName", listOfChains[k], "chainId"))
|
||||
}
|
||||
return listOfChainIds
|
||||
}
|
||||
|
@ -211,6 +211,9 @@ QtObject {
|
|||
root.showUnPreferredChains = !root.showUnPreferredChains
|
||||
}
|
||||
|
||||
function setRouteEnabledFromChains(chainId) {
|
||||
}
|
||||
|
||||
function setSelectedTokenIsOwnerToken(isOwnerToken) {
|
||||
}
|
||||
|
||||
|
@ -258,13 +261,11 @@ QtObject {
|
|||
}
|
||||
|
||||
function getNetworkName(chainId) {
|
||||
return SQUtils.ModelUtils.getByKey(NetworksModel.flatNetworks, "chainId", chainId, "chainName")
|
||||
return SQUtils.ModelUtils.getByKey(flatNetworksModel, "chainId", chainId, "chainName")
|
||||
}
|
||||
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
|
||||
let bigIntBalance = SQUtils.AmountsArithmetic.fromString(balance)
|
||||
let decimalBalance = SQUtils.AmountsArithmetic.toNumber(bigIntBalance, decimals)
|
||||
return currencyStore.formatCurrencyAmount(decimalBalance, symbol)
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) {
|
||||
return currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options)
|
||||
}
|
||||
|
||||
// Property and methods below are used to apply advanced token management settings to the SendModal
|
||||
|
@ -318,11 +319,9 @@ QtObject {
|
|||
},
|
||||
FastExpressionFilter {
|
||||
expression: {
|
||||
if (model.isCommunityAsset)
|
||||
return true
|
||||
return model.currentCurrencyBalance > balanceThresholdAmount
|
||||
}
|
||||
expectedRoles: ["isCommunityAsset", "currentCurrencyBalance"]
|
||||
expectedRoles: ["currentCurrencyBalance"]
|
||||
enabled: balanceThresholdEnabled
|
||||
}
|
||||
]
|
||||
|
|
|
@ -122,7 +122,7 @@ private:
|
|||
m_persistentIndexes.clear();
|
||||
m_persistentIndexes.reserve(count);
|
||||
|
||||
for (decltype(count) i = 0; i < count; i++)
|
||||
for (auto i = 0; i < count; i++)
|
||||
m_persistentIndexes.push_back(model->index(i, 0));
|
||||
}
|
||||
|
||||
|
|
|
@ -66,9 +66,8 @@ private:
|
|||
void updateRoleNames();
|
||||
void updateIndexes(int from, int to);
|
||||
|
||||
QHash<int, QByteArray> findExpectedRoles(
|
||||
const QHash<int, QByteArray>& roleNames,
|
||||
const QStringList& expectedRoles);
|
||||
QHash<int, QByteArray> findExpectedRoles(const QHash<int, QByteArray> &roleNames,
|
||||
const QStringList &expectedRoles);
|
||||
|
||||
QPointer<QQmlComponent> m_delegate;
|
||||
QHash<int, QByteArray> m_expectedRoleNames;
|
||||
|
|
|
@ -73,19 +73,25 @@ QtObject {
|
|||
|
||||
function stripTrailingZeroes(numStr, locale) {
|
||||
locale = locale || Qt.locale()
|
||||
let regEx = locale.decimalPoint == "." ? /(\.[0-9]*[1-9])0+$|\.0*$/ : /(\,[0-9]*[1-9])0+$|\,0*$/
|
||||
let regEx = locale.decimalPoint === "." ? /(\.[0-9]*[1-9])0+$|\.0*$/ : /(\,[0-9]*[1-9])0+$|\,0*$/
|
||||
return numStr.replace(regEx, '$1')
|
||||
}
|
||||
|
||||
function numberToLocaleString(num, precision = -1, locale = null) {
|
||||
function numberToLocaleString(num, precision = -128 /* QLocale::FloatingPointShortest */, locale = null) {
|
||||
locale = locale || Qt.locale()
|
||||
|
||||
if (precision === -1)
|
||||
precision = fractionalPartLength(num)
|
||||
|
||||
return num.toLocaleString(locale, 'f', precision)
|
||||
}
|
||||
|
||||
function currencyNumberToLocaleString(num, symbol = "", locale = null) {
|
||||
locale = locale || Qt.locale()
|
||||
|
||||
if (typeof num === "string")
|
||||
num = Number(num)
|
||||
|
||||
return num.toLocaleCurrencyString(locale, symbol)
|
||||
}
|
||||
|
||||
function numberToLocaleStringInCompactForm(num, locale = null) {
|
||||
locale = locale || Qt.locale()
|
||||
const numberOfDigits = integralPartLength(num)
|
||||
|
|
|
@ -23,7 +23,7 @@ void LeftJoinModel::initialize(bool reset)
|
|||
auto rightRoleNames = m_rightModel->roleNames();
|
||||
|
||||
auto leftNames = leftRoleNames.values();
|
||||
QList<QByteArray> rightNames;
|
||||
QByteArrayList rightNames;
|
||||
|
||||
if (m_rolesToJoin.empty()) {
|
||||
rightNames = rightRoleNames.values();
|
||||
|
@ -41,7 +41,7 @@ void LeftJoinModel::initialize(bool reset)
|
|||
|
||||
if (roles.empty()) {
|
||||
qWarning().noquote()
|
||||
<< QString("Role to join %1 not found in the right model!")
|
||||
<< QStringLiteral("Role to join %1 not found in the right model!")
|
||||
.arg(roleName);
|
||||
return;
|
||||
}
|
||||
|
@ -264,11 +264,11 @@ QVariant LeftJoinModel::data(const QModelIndex& index, int role) const
|
|||
m_rightModel->index(0, 0), m_rightModelJoinRole,
|
||||
joinRoleLeftValue, 1, Qt::MatchExactly);
|
||||
|
||||
if (match.empty())
|
||||
if (match.isEmpty())
|
||||
return {};
|
||||
|
||||
m_lastUsedRightModelIndex = match.first();
|
||||
return match.first().data(role - m_rightModelRolesOffset);
|
||||
m_lastUsedRightModelIndex = match.constFirst();
|
||||
return m_lastUsedRightModelIndex.data(role - m_rightModelRolesOffset);
|
||||
}
|
||||
|
||||
void LeftJoinModel::classBegin()
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
#include <QQmlEngine>
|
||||
#include <QQmlProperty>
|
||||
|
||||
#include <memory>
|
||||
|
||||
ObjectProxyModel::ObjectProxyModel(QObject* parent)
|
||||
: QIdentityProxyModel{parent}
|
||||
{
|
||||
|
@ -184,8 +182,8 @@ QObject* ObjectProxyModel::proxyObject(int index)
|
|||
rowData->insert(i.value(), model->data(model->index(index, 0), i.key()));
|
||||
}
|
||||
|
||||
rowData->insert("index", index);
|
||||
context->setContextProperty("model", rowData);
|
||||
rowData->insert(QStringLiteral("index"), index);
|
||||
context->setContextProperty(QStringLiteral("model"), rowData);
|
||||
|
||||
QObject* instance = m_delegate->create(context);
|
||||
context->setParent(instance);
|
||||
|
@ -300,7 +298,7 @@ void ObjectProxyModel::updateIndexes(int from, int to)
|
|||
auto& entry = m_container[i];
|
||||
|
||||
if (entry.proxy)
|
||||
entry.rowData->insert("index", i);
|
||||
entry.rowData->insert(QStringLiteral("index"), i);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -308,22 +306,24 @@ QHash<int, QByteArray> ObjectProxyModel::findExpectedRoles(
|
|||
const QHash<int, QByteArray> &roleNames,
|
||||
const QStringList &expectedRoles)
|
||||
{
|
||||
if (roleNames.empty() || expectedRoles.isEmpty())
|
||||
if (roleNames.isEmpty() || expectedRoles.isEmpty())
|
||||
return {};
|
||||
|
||||
QHash<int, QByteArray> expected;
|
||||
|
||||
for (auto& role : expectedRoles) {
|
||||
for (auto &role : expectedRoles) {
|
||||
auto expectedKeys = roleNames.keys(role.toUtf8());
|
||||
auto expectedKeysCount = expectedKeys.size();
|
||||
|
||||
if (expectedKeysCount == 1)
|
||||
expected.insert(expectedKeys.first(), role.toUtf8());
|
||||
else if (expectedKeysCount == 0) {
|
||||
qWarning() << "Expected role not found!";
|
||||
qWarning() << Q_FUNC_INFO;
|
||||
qWarning() << "Expected role" << role << "not found!";
|
||||
} else {
|
||||
qWarning() << "Malformed source model - multiple roles found for given "
|
||||
"expected role name!";
|
||||
qWarning() << Q_FUNC_INFO;
|
||||
qWarning()
|
||||
<< "Malformed source model - multiple roles found for given expected role name!";
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -263,9 +263,12 @@ private slots:
|
|||
model.setSourceModel(sourceModel);
|
||||
model.setDelegate(delegate.get());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "Expected role not found!");
|
||||
const auto expRoleName = QStringLiteral("undefined");
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
QRegularExpression(QStringLiteral(".*findExpectedRoles*.")));
|
||||
QTest::ignoreMessage(QtWarningMsg, QStringLiteral("Expected role \"%1\" not found!").arg(expRoleName).toLatin1());
|
||||
|
||||
model.setExpectedRoles({ QStringLiteral("undefined") });
|
||||
model.setExpectedRoles({ expRoleName });
|
||||
|
||||
QCOMPARE(model.rowCount(), 3);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
import QtQuick 2.15
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Core.Utils 0.1
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
QObject {
|
||||
id: root
|
||||
|
||||
/**
|
||||
Transforms and prepares input data (assets) for TokenSelectorView needs. The assets model is internally
|
||||
joined with `flatNetworksModel` for the `balances` submodel
|
||||
|
||||
Expected assets model structure:
|
||||
- tokensKey: string -> unique string ID of the token (asset); e.g. "ETH" or contract address
|
||||
- name: string -> user visible token name (e.g. "Ethereum")
|
||||
- symbol: string -> user visible token symbol (e.g. "ETH")
|
||||
- decimals: int -> number of decimal places
|
||||
- communityId: string -> optional; ID of the community this token belongs to, if any
|
||||
- marketDetails: var -> object containing props like `currencyPrice` for the computed values below
|
||||
- balances: submodel -> [ chainId:int, account:string, balance:BigIntString, iconUrl:string ]
|
||||
|
||||
Computed values:
|
||||
- currencyBalance: double (e.g. `1000.42` in user's fiat currency)
|
||||
- currencyBalanceAsString: string (e.g. "1 000,42 CZK" formatted as a string according to the user's locale)
|
||||
- balanceAsString: string (`1.42` formatted as e.g. "1,42" in user's locale)
|
||||
*/
|
||||
|
||||
// input API
|
||||
required property var assetsModel
|
||||
required property var flatNetworksModel
|
||||
required property string currentCurrency // CurrenciesStore.currentCurrency, e.g. "USD"
|
||||
|
||||
// optional filter properties; empty/default values means no filtering
|
||||
property var enabledChainIds: []
|
||||
property string accountAddress
|
||||
property bool showCommunityAssets
|
||||
property string searchString
|
||||
|
||||
// output model
|
||||
readonly property SortFilterProxyModel outputAssetsModel: SortFilterProxyModel {
|
||||
sourceModel: assetsObjectProxyModel
|
||||
|
||||
filters: [
|
||||
AnyOf {
|
||||
RegExpFilter {
|
||||
roleName: "name"
|
||||
pattern: root.searchString
|
||||
caseSensitivity: Qt.CaseInsensitive
|
||||
}
|
||||
RegExpFilter {
|
||||
roleName: "symbol"
|
||||
pattern: root.searchString
|
||||
caseSensitivity: Qt.CaseInsensitive
|
||||
}
|
||||
},
|
||||
ValueFilter {
|
||||
roleName: "communityId"
|
||||
value: ""
|
||||
enabled: !root.showCommunityAssets
|
||||
}
|
||||
]
|
||||
|
||||
// FIXME optionally sort/filter by wallet controller as well
|
||||
sorters: [
|
||||
RoleSorter {
|
||||
roleName: "currencyBalance"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// internals
|
||||
ObjectProxyModel {
|
||||
id: assetsObjectProxyModel
|
||||
sourceModel: root.assetsModel
|
||||
|
||||
delegate: SortFilterProxyModel {
|
||||
id: delegateRoot
|
||||
|
||||
// properties exposed as roles to the top-level model
|
||||
readonly property int decimals: model.decimals
|
||||
readonly property double currentBalance: aggregator.value
|
||||
readonly property double currencyBalance: {
|
||||
if (!!model.marketDetails) {
|
||||
return currentBalance * model.marketDetails.currencyPrice.amount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
readonly property int displayDecimals: !!model.marketDetails ? model.marketDetails.currencyPrice.displayDecimals : 0
|
||||
readonly property string currencyBalanceAsString:
|
||||
currencyBalance ? LocaleUtils.currencyAmountToLocaleString({amount: currencyBalance, symbol: root.currentCurrency, displayDecimals})
|
||||
: ""
|
||||
|
||||
readonly property var balances: this
|
||||
|
||||
sourceModel: joinModel
|
||||
|
||||
proxyRoles: [
|
||||
FastExpressionRole {
|
||||
name: "balanceAsDouble"
|
||||
function balanceToDouble(balance: string, decimals: int) {
|
||||
if (typeof balance !== 'string')
|
||||
return 0
|
||||
let bigIntBalance = AmountsArithmetic.fromString(balance)
|
||||
return AmountsArithmetic.toNumber(bigIntBalance, decimals)
|
||||
}
|
||||
expression: balanceToDouble(model.balance, delegateRoot.decimals)
|
||||
expectedRoles: ["balance"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "balanceAsString"
|
||||
function convert(amount: double) {
|
||||
return LocaleUtils.currencyAmountToLocaleString({amount, displayDecimals: 2}, {noSymbol: true})
|
||||
}
|
||||
|
||||
expression: convert(model.balanceAsDouble)
|
||||
expectedRoles: ["balanceAsDouble"]
|
||||
}
|
||||
]
|
||||
|
||||
filters: [
|
||||
ValueFilter {
|
||||
roleName: "balance"
|
||||
value: "0"
|
||||
inverted: true
|
||||
},
|
||||
FastExpressionFilter {
|
||||
expression: root.enabledChainIds.includes(model.chainId)
|
||||
expectedRoles: ["chainId"]
|
||||
enabled: root.enabledChainIds.length
|
||||
},
|
||||
RegExpFilter {
|
||||
roleName: "account"
|
||||
pattern: root.accountAddress
|
||||
caseSensitivity: Qt.CaseInsensitive
|
||||
enabled: root.accountAddress !== ""
|
||||
}
|
||||
]
|
||||
|
||||
sorters: [
|
||||
// sort by biggest (sub)balance first
|
||||
RoleSorter {
|
||||
roleName: "balanceAsDouble"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
}
|
||||
]
|
||||
|
||||
readonly property LeftJoinModel joinModel: LeftJoinModel {
|
||||
leftModel: model.balances
|
||||
rightModel: root.flatNetworksModel
|
||||
joinRole: "chainId"
|
||||
}
|
||||
|
||||
readonly property SumAggregator aggregator: SumAggregator {
|
||||
model: delegateRoot
|
||||
roleName: "balanceAsDouble"
|
||||
}
|
||||
}
|
||||
|
||||
exposedRoles: ["balances", "currencyBalance", "currencyBalanceAsString", "balanceAsString"]
|
||||
expectedRoles: ["communityId", "balances", "decimals", "marketDetails"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
TokenSelectorViewAdaptor 1.0 TokenSelectorViewAdaptor.qml
|
|
@ -231,7 +231,7 @@ Control {
|
|||
return root.currencyStore.formatCurrencyAmount(balance, root.currencyStore.currentCurrency)
|
||||
}
|
||||
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals) {
|
||||
return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
|
||||
return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true})
|
||||
}
|
||||
onItemSelected: {
|
||||
d.setSelectedHoldingId(holdingId, holdingType)
|
||||
|
|
|
@ -22,6 +22,8 @@ QObject {
|
|||
property bool swapProposalReady: false
|
||||
property bool swapProposalLoading: false
|
||||
|
||||
property bool showCommunityTokens
|
||||
|
||||
// To expose the selected from and to Token from the SwapModal
|
||||
readonly property var fromToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.fromTokensKey)
|
||||
readonly property var toToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.toTokenKey)
|
||||
|
@ -90,8 +92,8 @@ QObject {
|
|||
return root.currencyStore.formatCurrencyAmount(balance, symbol, options, locale)
|
||||
}
|
||||
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
|
||||
return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) {
|
||||
return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options)
|
||||
}
|
||||
|
||||
function getAllChainIds() {
|
||||
|
@ -122,14 +124,13 @@ QObject {
|
|||
property real displayAssetsBelowBalanceThresholdAmount: root.walletAssetsStore.walletTokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount()
|
||||
sourceModel: __assetsWithFilteredBalances
|
||||
proxyRoles: [
|
||||
FastExpressionRole {
|
||||
name: "isCommunityAsset"
|
||||
expression: !!model.communityId
|
||||
expectedRoles: ["communityId"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
name: "currentBalance"
|
||||
expression: __getTotalBalance(model.balances, model.decimals)
|
||||
expression: {
|
||||
// FIXME recalc when selectedNetworkChainId changes
|
||||
root.swapFormData.selectedNetworkChainId
|
||||
return __getTotalBalance(model.balances, model.decimals)
|
||||
}
|
||||
expectedRoles: ["balances", "decimals"]
|
||||
},
|
||||
FastExpressionRole {
|
||||
|
@ -150,19 +151,16 @@ QObject {
|
|||
|
||||
if (!root.walletAssetsStore.assetsController.filterAcceptsSymbol(model.symbol)) // explicitely hidden
|
||||
return false
|
||||
if (model.isCommunityAsset) // do not show community assets
|
||||
return false
|
||||
if (!!model.communityId)
|
||||
return root.showCommunityTokens
|
||||
if (root.walletAssetsStore.walletTokensStore.displayAssetsBelowBalance)
|
||||
return model.currentCurrencyBalance > processedAssetsModel.displayAssetsBelowBalanceThresholdAmount
|
||||
return true
|
||||
}
|
||||
expectedRoles: ["symbol", "isCommunityAsset", "currentCurrencyBalance"]
|
||||
expectedRoles: ["symbol", "communityId", "currentCurrencyBalance"]
|
||||
}
|
||||
]
|
||||
// FIXME sort by assetsController instead, to have the sorting/order as in the main wallet view
|
||||
// sorters: RoleSorter {
|
||||
// roleName: "isCommunityAsset"
|
||||
// }
|
||||
}
|
||||
|
||||
// Internal properties and functions -----------------------------------------------------------------------------------------------------------------------------
|
||||
|
@ -203,7 +201,7 @@ QObject {
|
|||
}
|
||||
readonly property LeftJoinModel joinModel: LeftJoinModel {
|
||||
leftModel: submodel
|
||||
rightModel: root.filteredFlatNetworksModel
|
||||
rightModel: root.swapStore.flatNetworks
|
||||
|
||||
joinRole: "chainId"
|
||||
}
|
||||
|
@ -227,11 +225,12 @@ QObject {
|
|||
}
|
||||
|
||||
/* Internal function to calculate total balance */
|
||||
function __getTotalBalance(balances, decimals) {
|
||||
function __getTotalBalance(balances, decimals, chainIds = [root.swapFormData.selectedNetworkChainId]) {
|
||||
let totalBalance = 0
|
||||
for(let i=0; i<balances.count; i++) {
|
||||
let balancePerAddressPerChain = ModelUtils.get(balances, i)
|
||||
totalBalance+=AmountsArithmetic.toNumber(balancePerAddressPerChain.balance, decimals)
|
||||
if (chainIds.includes(-1) || chainIds.includes(balancePerAddressPerChain.chainId))
|
||||
totalBalance += AmountsArithmetic.toNumber(balancePerAddressPerChain.balance, decimals)
|
||||
}
|
||||
return totalBalance
|
||||
}
|
||||
|
|
|
@ -57,10 +57,10 @@ QtObject {
|
|||
sourceModel: root._joinFlatTokensModel
|
||||
|
||||
proxyRoles: [
|
||||
FastExpressionRole {
|
||||
JoinRole {
|
||||
name: "explorerUrl"
|
||||
expression: model.blockExplorerURL + "/token/" + model.address
|
||||
expectedRoles: ["blockExplorerURL", "address"]
|
||||
roleNames: ["blockExplorerURL", "address"]
|
||||
separator: "/token/"
|
||||
},
|
||||
FastExpressionRole {
|
||||
function tokenIcon(symbol) {
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Components 0.1
|
||||
import StatusQ.Core.Theme 0.1
|
||||
|
||||
import utils 1.0
|
||||
|
||||
ItemDelegate {
|
||||
id: root
|
||||
objectName: "tokenSelectorAssetDelegate_" + tokensKey
|
||||
|
||||
required property string tokensKey
|
||||
required property string name
|
||||
required property string symbol
|
||||
required property string currencyBalanceAsString
|
||||
// expected structure: balancesModel -> model.balances submodel [chainId: int, balance: BigIntString] + flatNetworks [account:string, iconUrl: string]
|
||||
required property var balancesModel
|
||||
|
||||
property bool interactive: true
|
||||
|
||||
signal assetSelected(string tokensKey)
|
||||
|
||||
spacing: Style.current.halfPadding
|
||||
horizontalPadding: Style.current.padding
|
||||
verticalPadding: 4
|
||||
|
||||
opacity: interactive ? 1 : 0.3
|
||||
|
||||
implicitWidth: ListView.view.width
|
||||
implicitHeight: 60
|
||||
|
||||
icon.width: 32
|
||||
icon.height: 32
|
||||
icon.source: Constants.tokenIcon(symbol)
|
||||
|
||||
enabled: interactive
|
||||
|
||||
background: Rectangle {
|
||||
radius: Style.current.radius
|
||||
color: (root.interactive && root.hovered) || root.highlighted ? Theme.palette.statusListItem.highlightColor
|
||||
: "transparent"
|
||||
HoverHandler {
|
||||
cursorShape: root.interactive ? Qt.PointingHandCursor : undefined
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: root.spacing
|
||||
|
||||
// asset icon
|
||||
StatusRoundedImage {
|
||||
Layout.preferredWidth: root.icon.width
|
||||
Layout.preferredHeight: root.icon.height
|
||||
image.source: root.icon.source
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
// name, symbol, total balance
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: root.spacing
|
||||
Item {
|
||||
id: nameRow
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
StatusBaseText {
|
||||
id: nameText
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.min(implicitWidth, nameRow.width - symbolText.width - symbolText.anchors.leftMargin)
|
||||
text: root.name
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
StatusBaseText {
|
||||
id: symbolText
|
||||
anchors.left: nameText.right
|
||||
anchors.leftMargin: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.symbol
|
||||
color: Theme.palette.baseColor1
|
||||
}
|
||||
}
|
||||
StatusBaseText {
|
||||
font.weight: Font.Medium
|
||||
text: root.currencyBalanceAsString
|
||||
}
|
||||
}
|
||||
|
||||
// balances per network chain
|
||||
StatusListView {
|
||||
Layout.maximumWidth: parent.width
|
||||
Layout.preferredWidth: contentWidth
|
||||
Layout.preferredHeight: 22
|
||||
orientation: ListView.Horizontal
|
||||
spacing: root.spacing
|
||||
visible: count
|
||||
interactive: !root.ListView.view.moving
|
||||
ScrollBar.horizontal: null
|
||||
|
||||
model: root.balancesModel
|
||||
delegate: RowLayout {
|
||||
height: ListView.view.height
|
||||
spacing: 4
|
||||
StatusRoundedImage {
|
||||
Layout.preferredWidth: 16
|
||||
Layout.preferredHeight: 16
|
||||
image.source: Style.svg("tiny/%1".arg(model.iconUrl))
|
||||
}
|
||||
StatusBaseText {
|
||||
font.pixelSize: Theme.tertiaryTextFontSize
|
||||
text: model.balanceAsString
|
||||
}
|
||||
}
|
||||
// let the root handle the click
|
||||
TapHandler {
|
||||
onTapped: root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.assetSelected(root.tokensKey)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import QtQuick 2.15
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
|
||||
StatusListView {
|
||||
id: root
|
||||
|
||||
// expected model structure:
|
||||
// tokensKey, name, symbol, decimals, currencyBalanceAsString (computed), marketDetails, balances -> [ chainId, address, balance, iconUrl ]
|
||||
|
||||
// output API
|
||||
signal tokenSelected(string tokensKey)
|
||||
|
||||
currentIndex: -1
|
||||
|
||||
delegate: TokenSelectorAssetDelegate {
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
tokensKey: model.tokensKey
|
||||
name: model.name
|
||||
symbol: model.symbol
|
||||
currencyBalanceAsString: model.currencyBalanceAsString
|
||||
balancesModel: model.balances
|
||||
|
||||
onAssetSelected: (tokensKey) => root.tokenSelected(tokensKey)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
CollectiblesView 1.0 CollectiblesView.qml
|
||||
AssetsDetailView 1.0 AssetsDetailView.qml
|
||||
CollectiblesView 1.0 CollectiblesView.qml
|
||||
SavedAddresses 1.0 SavedAddresses.qml
|
||||
TokenSelectorAssetDelegate 1.0 TokenSelectorAssetDelegate.qml
|
||||
TokenSelectorView 1.0 TokenSelectorView.qml
|
||||
|
|
|
@ -267,7 +267,7 @@ StatusDialog {
|
|||
return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency)
|
||||
}
|
||||
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){
|
||||
return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
|
||||
return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -391,7 +391,7 @@ StatusDialog {
|
|||
return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency)
|
||||
}
|
||||
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals) {
|
||||
return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
|
||||
return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,21 +14,13 @@ QtObject {
|
|||
property var _profileSectionModuleInst: profileSectionModule
|
||||
|
||||
function getModelIndexForKey(key) {
|
||||
for (var i=0; i<currenciesModel.count; i++) {
|
||||
if (currenciesModel.get(i).key === key) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
const idx = SQUtils.ModelUtils.indexOf(currenciesModel, "key", key)
|
||||
return idx === -1 ? 0 : idx
|
||||
}
|
||||
|
||||
function getModelIndexForShortName(shortName) {
|
||||
for (var i=0; i<currenciesModel.count; i++) {
|
||||
if (currenciesModel.get(i).shortName === shortName) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
const idx = SQUtils.ModelUtils.indexOf(currenciesModel, "shortName", shortName)
|
||||
return idx === -1 ? 0 : idx
|
||||
}
|
||||
|
||||
readonly property string currentCurrency: Global.appIsReady ? walletSection.currentCurrency : ""
|
||||
|
@ -989,10 +981,10 @@ QtObject {
|
|||
return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale)
|
||||
}
|
||||
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) {
|
||||
let bigIntBalance = SQUtils.AmountsArithmetic.fromString(balance)
|
||||
let decimalBalance = SQUtils.AmountsArithmetic.toNumber(bigIntBalance, decimals)
|
||||
return formatCurrencyAmount(decimalBalance, symbol)
|
||||
return formatCurrencyAmount(decimalBalance, symbol, options)
|
||||
}
|
||||
|
||||
function getFiatValue(cryptoAmount, cryptoSymbol) {
|
||||
|
|
|
@ -225,11 +225,11 @@ QtObject {
|
|||
}
|
||||
|
||||
function getNetworkName(chainId) {
|
||||
return fromNetworksModel.getNetworkName(chainId)
|
||||
return fromNetworksModel.getNetworkName(chainId)
|
||||
}
|
||||
|
||||
function updateRoutePreferredChains(chainIds) {
|
||||
walletSectionSendInst.updateRoutePreferredChains(chainIds)
|
||||
walletSectionSendInst.updateRoutePreferredChains(chainIds)
|
||||
}
|
||||
|
||||
function toggleShowUnPreferredChains() {
|
||||
|
@ -261,10 +261,8 @@ QtObject {
|
|||
return walletSectionSendInst.getShortChainIds(chainShortNames)
|
||||
}
|
||||
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
|
||||
let bigIntBalance = AmountsArithmetic.fromString(balance)
|
||||
let decimalBalance = AmountsArithmetic.toNumber(bigIntBalance, decimals)
|
||||
return currencyStore.formatCurrencyAmount(decimalBalance, symbol)
|
||||
function formatCurrencyAmountFromBigInt(balance, symbol, decimals, options = null) {
|
||||
return currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals, options)
|
||||
}
|
||||
|
||||
// Property set from TokenLIstView and HoldingSelector to search token by name, symbol or contract address
|
||||
|
@ -340,13 +338,11 @@ QtObject {
|
|||
|
||||
if (!root.walletAssetStore.assetsController.filterAcceptsSymbol(model.symbol)) // explicitely hidden
|
||||
return false
|
||||
if (model.isCommunityAsset)
|
||||
return true
|
||||
if (tokensStore.displayAssetsBelowBalance)
|
||||
return model.currentCurrencyBalance > processedAssetsModel.displayAssetsBelowBalanceThresholdAmount
|
||||
return true
|
||||
}
|
||||
expectedRoles: ["symbol", "isCommunityAsset", "currentCurrencyBalance"]
|
||||
expectedRoles: ["symbol", "currentCurrencyBalance"]
|
||||
}
|
||||
]
|
||||
sorters: RoleSorter {
|
||||
|
|
Loading…
Reference in New Issue