diff --git a/storybook/pages/CollectiblesSelectionAdaptorPage.qml b/storybook/pages/CollectiblesSelectionAdaptorPage.qml new file mode 100644 index 0000000000..dc16962d5f --- /dev/null +++ b/storybook/pages/CollectiblesSelectionAdaptorPage.qml @@ -0,0 +1,391 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Models 0.1 + +import AppLayouts.Wallet.controls 1.0 +import AppLayouts.Wallet.adaptors 1.0 +import utils 1.0 + +import Storybook 1.0 + +import SortFilterProxyModel 0.2 + +Pane { + id: root + + ListModel { + id: listModel + + readonly property var data: [ + // collection 2 + { + tokenId: "id_3", + name: "Multi-sequencer Test NFT 1", + contractAddress: "contract_2", + collectionName: "Multi-sequencer Test NFT", + collectionUid: "collection_2", + ownership: [ + { + accountAddress: "account_1", + balance: 1, + txTimestamp: 1714059810 + } + ], + imageUrl: Constants.tokenIcon("ETH", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + { + tokenId: "id_4", + name: "Multi-sequencer Test NFT 2", + contractAddress: "contract_2", + collectionName: "Multi-sequencer Test NFT", + collectionUid: "collection_2", + ownership: [ + { + accountAddress: "account_1", + balance: 1, + txTimestamp: 1714059811 + } + ], + imageUrl: Constants.tokenIcon("ETH", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + { + tokenId: "id_5", + name: "Multi-sequencer Test NFT 3", + contractAddress: "contract_2", + collectionName: "Multi-sequencer Test NFT", + collectionUid: "collection_2", + ownership: [ + { + accountAddress: "account_1", + balance: 1, + txTimestamp: 1714059899 + } + ], + imageUrl: Constants.tokenIcon("ETH", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + // collection 1 + { + tokenId: "id_1", + name: "Genesis", + contractAddress: "contract_1", + collectionName: "ERC-1155 Faucet", + collectionUid: "collection_1", + ownership: [ + { + accountAddress: "account_1", + balance: 23, + txTimestamp: 1714059862 + }, + { + accountAddress: "account_2", + balance: 29, + txTimestamp: 1714054862 + } + ], + imageUrl: Constants.tokenIcon("DAI", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + { + tokenId: "id_2", + name: "QAERC1155", + contractAddress: "contract_1", + collectionName: "ERC-1155 Faucet", + collectionUid: "collection_1", + ownership: [ + { + accountAddress: "account_1", + balance: 500, + txTimestamp: 1714059864 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "", + communityName: "", + communityImage: Qt.resolvedUrl("") + }, + // collection 3, community token + { + tokenId: "id_6", + name: "My Token", + contractAddress: "contract_3", + collectionName: "My Token", + collectionUid: "collection_3", + ownership: [ + { + accountAddress: "account_1", + balance: 1, + txTimestamp: 1714059899 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_1", + communityName: "My community", + communityImage: Constants.tokenIcon("KIN", false) + }, + { + tokenId: "id_7", + name: "My Token", + contractAddress: "contract_3", + collectionName: "My Token", + collectionUid: "collection_3", + ownership: [ + { + accountAddress: "account_1", + balance: 1, + txTimestamp: 1714059899 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_1", + communityName: "My community", + communityImage: Constants.tokenIcon("KIN", false) + }, + { + tokenId: "id_8", + name: "My Token", + contractAddress: "contract_3", + collectionName: "My Token", + collectionUid: "collection_3", + ownership: [ + { + accountAddress: "account_2", + balance: 1, + txTimestamp: 1714059999 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_1", + communityName: "My community", + communityImage: Constants.tokenIcon("KIN", false) + }, + { + tokenId: "id_9", + name: "My Other Token", + contractAddress: "contract_4", + collectionName: "My Other Token", + collectionUid: "collection_4", + ownership: [ + { + accountAddress: "account_1", + balance: 1, + txTimestamp: 1714059991 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_1", + communityName: "My community", + communityImage: Constants.tokenIcon("KIN", false) + }, + { + tokenId: "id_10", + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: "account_1", + balance: 1, + txTimestamp: 1714059777 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + }, + { + tokenId: "id_11", + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: "account_1", + balance: 1, + txTimestamp: 1714059778 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + }, + { + tokenId: "id_11", + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: "account_2", + balance: 1, + txTimestamp: 1714059779 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + }, + { + tokenId: "id_12", + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: "account_3", + balance: 1, + txTimestamp: 1714059779 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + }, + { + tokenId: "id_13", + name: "My Community 2 Token", + contractAddress: "contract_5", + collectionName: "My Community 2 Token", + collectionUid: "collection_5", + ownership: [ + { + accountAddress: "account_3", + balance: 1, + txTimestamp: 1714059788 + } + ], + imageUrl: Constants.tokenIcon("ZRX", false), + mediaUrl: Qt.resolvedUrl(""), + communityId: "community_2", + communityName: "My community 2", + communityImage: Constants.tokenIcon("ICOS", false) + } + ] + + Component.onCompleted: { + append(data) + + const accounts = new Set() + + data.forEach(e => e.ownership.forEach( + e => { accounts.add(e.accountAddress) })) + + accountsSelector.model = [...accounts.values()] + } + } + + CollectiblesSelectionAdaptor { + id: adaptor + + collectiblesModel: listModel + accountKey: accountsSelector.selection + } + + ColumnLayout { + anchors.fill: parent + + TokenSelectorNew { + collectiblesModel: adaptor.model + } + + RowLayout { + Label { text: "Accounts:" } + + RadioButtonFlowSelector { + id: accountsSelector + + Layout.fillWidth: true + } + } + + RowLayout { + GenericListView { + label: "Input model" + + model: listModel + + Layout.fillWidth: true + Layout.fillHeight: true + + skipEmptyRoles: true + } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollBar.vertical: ScrollBar {} + clip: true + spacing: 5 + + model: adaptor.model + + delegate: ColumnLayout { + width: ListView.view.width + + readonly property var submodel: model.subitems + readonly property int submodelCount: + model.subitems.ModelCount.count + + Label { + text: (model.communityId ? "Community" : "Collection") + + `: ${model.groupName} (count: ${submodelCount})` + } + + Repeater { + model: submodel + + Label { + text: "\t" + model.name + " (balance: " + model.balance + "), key: " + model.key + } + } + } + + section.property: "type" + section.delegate: Label { + text: section + font.underline: true + font.bold: true + bottomPadding: 10 + } + } + } + } +} + +// category: Adaptors diff --git a/storybook/pages/TokenSelectorCollectibleDelegatePage.qml b/storybook/pages/TokenSelectorCollectibleDelegatePage.qml new file mode 100644 index 0000000000..dd856aac82 --- /dev/null +++ b/storybook/pages/TokenSelectorCollectibleDelegatePage.qml @@ -0,0 +1,110 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import AppLayouts.Wallet.views 1.0 +import StatusQ.Core.Theme 0.1 +import utils 1.0 + +import Qt.labs.settings 1.0 + +SplitView { + id: root + orientation: Qt.Vertical + + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + Rectangle { + color: Theme.palette.statusListItem.backgroundColor + border.color: Theme.palette.primaryColor1 + border.width: 1 + + anchors.fill: delegate + anchors.margins: -30 + } + + TokenSelectorCollectibleDelegate { + id: delegate + + implicitWidth: 330 + anchors.centerIn: parent + + name: nameTextField.text + balance: balanceSpinBox.value ? balanceSpinBox.value : "" + image: Constants.tokenIcon("ETH") + + goDeeperIconVisible: goDeeperSwitch.checked + + interactive: interactiveSwitch.checked + highlighted: highlightedSwitch.checked + } + } + + Pane { + SplitView.minimumHeight: 250 + SplitView.preferredHeight: 250 + + RowLayout { + anchors.fill: parent + + ColumnLayout { + RowLayout { + Label { + text: "name:" + } + + TextField { + id: nameTextField + + text: "Crypto Kitties" + } + } + + RowLayout { + Label { + text: "balance:" + } + + SpinBox { + id: balanceSpinBox + + value: 12 + + from: 0 + to: 20 + } + } + + Switch { + id: interactiveSwitch + text: "Interactive" + checked: true + } + + Switch { + id: highlightedSwitch + text: "Highlighted" + checked: false + } + + Switch { + id: goDeeperSwitch + text: "Go deeper icon visible" + checked: false + } + + Item { Layout.fillHeight: true } + } + } + } + + Settings { + property alias interactiveSwitchChecked: interactiveSwitch.checked + property alias highlightedSwitchChecked: highlightedSwitch.checked + property alias goDeeperSwitchChecked: goDeeperSwitch.checked + } +} + +// category: Delegates diff --git a/storybook/pages/TokenSelectorNewPage.qml b/storybook/pages/TokenSelectorNewPage.qml new file mode 100644 index 0000000000..ba79811a41 --- /dev/null +++ b/storybook/pages/TokenSelectorNewPage.qml @@ -0,0 +1,187 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import AppLayouts.Wallet.controls 1.0 +import StatusQ.Core.Theme 0.1 +import utils 1.0 + +Pane { + readonly property var assetsData: [ + { + tokensKey: "key_1", + communityId: "", + name: "Status Test Token", + currencyBalanceAsString: "42,23 USD", + symbol: "STT", + iconSource: Constants.tokenIcon("STT"), + tokensKey: "STT", + + balances: [ + { + balanceAsString: "0,56", + iconUrl: "network/Network=Ethereum" + }, + { + balanceAsString: "0,22", + iconUrl: "network/Network=Arbitrum" + }, + { + balanceAsString: "0,12", + iconUrl: "network/Network=Optimism" + } + ] + }, + { + tokensKey: "key_2", + communityId: "", + name: "Ether", + currencyBalanceAsString: "4 276,86 USD", + symbol: "ETH", + iconSource: Constants.tokenIcon("ETH"), + tokensKey: "ETH", + + balances: [ + { + balanceAsString: "1,01", + iconUrl: "network/Network=Optimism" + }, + { + balanceAsString: "0,47", + iconUrl: "network/Network=Arbitrum" + }, + { + balanceAsString: "0,12", + iconUrl: "network/Network=Ethereum" + } + ] + }, + { + tokensKey: "key_2", + communityId: "", + name: "Dai Stablecoin", + currencyBalanceAsString: "45,92 USD", + symbol: "DAI", + iconSource: Constants.tokenIcon("DAI"), + tokensKey: "DAI", + + balances: [ + { + balanceAsString: "45,12", + iconUrl: "network/Network=Arbitrum" + }, + { + balanceAsString: "0,56", + iconUrl: "network/Network=Optimism" + }, + { + balanceAsString: "0,12", + iconUrl: "network/Network=Ethereum" + } + ] + } + ] + + readonly property var collectiblesData: [ + { + groupName: "My community", + icon: Constants.tokenIcon("BQX"), + type: "community", + subitems: [ + { + key: "my_community_key_1", + name: "My token", + balance: 1, + icon: Constants.tokenIcon("CFI"), + } + ] + }, + { + groupName: "Crypto Kitties", + icon: Constants.tokenIcon("ENJ"), + type: "other", + subitems: [ + { + key: "collection_1_key_1", + name: "Furbeard", + balance: 1, + icon: Constants.tokenIcon("FUEL"), + }, + { + key: "collection_1_key_2", + name: "Magicat", + balance: 1, + icon: Constants.tokenIcon("ENJ"), + }, + { + key: "collection_1_key_3", + name: "Happy Meow", + balance: 1, + icon: Constants.tokenIcon("FUN"), + } + ] + }, + { + groupName: "Super Rare", + icon: Constants.tokenIcon("CVC"), + type: "other", + subitems: [ + { + key: "collection_2_key_1", + name: "Unicorn 1", + balance: 12, + icon: Constants.tokenIcon("CVC") + }, + { + key: "collection_2_key_2", + name: "Unicorn 2", + balance: 1, + icon: Constants.tokenIcon("CVC") + } + ] + }, + { + groupName: "Unicorn", + icon: Constants.tokenIcon("ELF"), + type: "other", + subitems: [ + { + key: "collection_3_key_1", + name: "Unicorn", + balance: 1, + icon: Constants.tokenIcon("ELF") + } + ] + } + ] + + ListModel { + id: assetsModel + + Component.onCompleted: append(assetsData) + } + + ListModel { + id: collectiblesModel + + Component.onCompleted: append(collectiblesData) + } + + background: Rectangle { + color: Theme.palette.baseColor3 + } + + TokenSelectorNew { + id: panel + + anchors.centerIn: parent + + assetsModel: assetsModel + collectiblesModel: collectiblesModel + + onCollectibleSelected: console.log("collectible selected:", key) + onCollectionSelected: console.log("collection selected:", key) + onAssetSelected: console.log("asset selected:", key) + } +} + +// category: Controls diff --git a/storybook/pages/TokenSelectorPanelPage.qml b/storybook/pages/TokenSelectorPanelPage.qml new file mode 100644 index 0000000000..d4b0f9fa8b --- /dev/null +++ b/storybook/pages/TokenSelectorPanelPage.qml @@ -0,0 +1,236 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import AppLayouts.Wallet.panels 1.0 +import utils 1.0 + +Pane { + readonly property var assetsData: [ + { + tokensKey: "key_1", + communityId: "", + name: "Status Test Token", + currencyBalanceAsString: "42,23 USD", + symbol: "STT", + iconSource: Constants.tokenIcon("STT"), + tokensKey: "STT", + + balances: [ + { + balanceAsString: "0,56", + iconUrl: "network/Network=Ethereum" + }, + { + balanceAsString: "0,22", + iconUrl: "network/Network=Arbitrum" + }, + { + balanceAsString: "0,12", + iconUrl: "network/Network=Optimism" + } + ] + }, + { + tokensKey: "key_2", + communityId: "", + name: "Ether", + currencyBalanceAsString: "4 276,86 USD", + symbol: "ETH", + iconSource: Constants.tokenIcon("ETH"), + tokensKey: "ETH", + + balances: [ + { + balanceAsString: "1,01", + iconUrl: "network/Network=Optimism" + }, + { + balanceAsString: "0,47", + iconUrl: "network/Network=Arbitrum" + }, + { + balanceAsString: "0,12", + iconUrl: "network/Network=Ethereum" + } + ] + }, + { + tokensKey: "key_2", + communityId: "", + name: "Dai Stablecoin", + currencyBalanceAsString: "45,92 USD", + symbol: "DAI", + iconSource: Constants.tokenIcon("DAI"), + tokensKey: "DAI", + + balances: [ + { + balanceAsString: "45,12", + iconUrl: "network/Network=Arbitrum" + }, + { + balanceAsString: "0,56", + iconUrl: "network/Network=Optimism" + }, + { + balanceAsString: "0,12", + iconUrl: "network/Network=Ethereum" + } + ] + } + ] + + readonly property var collectiblesData: [ + { + groupName: "My community", + icon: Constants.tokenIcon("BQX"), + type: "community", + subitems: [ + { + key: "my_community_key_1", + name: "My token", + balance: 1, + icon: Constants.tokenIcon("CFI"), + } + ] + }, + { + groupName: "Crypto Kitties", + icon: Constants.tokenIcon("ENJ"), + type: "other", + subitems: [ + { + key: "collection_1_key_1", + name: "Furbeard", + balance: 1, + icon: Constants.tokenIcon("FUEL"), + }, + { + key: "collection_1_key_2", + name: "Magicat", + balance: 1, + icon: Constants.tokenIcon("ENJ"), + }, + { + key: "collection_1_key_3", + name: "Happy Meow", + balance: 1, + icon: Constants.tokenIcon("FUN"), + } + ] + }, + { + groupName: "Super Rare", + icon: Constants.tokenIcon("CVC"), + type: "other", + subitems: [ + { + key: "collection_2_key_1", + name: "Unicorn 1", + balance: 12, + icon: Constants.tokenIcon("CVC") + }, + { + key: "collection_2_key_2", + name: "Unicorn 2", + balance: 1, + icon: Constants.tokenIcon("CVC") + } + ] + }, + { + groupName: "Unicorn", + icon: Constants.tokenIcon("ELF"), + type: "other", + subitems: [ + { + key: "collection_3_key_1", + name: "Unicorn", + balance: 1, + icon: Constants.tokenIcon("ELF") + } + ] + } + ] + + ListModel { + id: assetsModel + + Component.onCompleted: append(assetsData) + } + + ListModel { + id: collectiblesModel + + Component.onCompleted: append(collectiblesData) + } + + Rectangle { + anchors.fill: panel + anchors.margins: -1 + + color: "transparent" + border.color: "lightgray" + } + + TokenSelectorPanel { + id: panel + + anchors.centerIn: parent + + width: 350 + + assetsModel: assetsModelCheckBox.checked ? assetsModel : null + collectiblesModel: collectiblesModelCheckBox.checked + ? collectiblesModel : null + + onCollectibleSelected: { + highlightedKey = key + console.log("collectible selected:", key) + } + + onCollectionSelected: { + highlightedKey = key + console.log("collection selected:", key) + } + + onAssetSelected: { + highlightedKey = key + console.log("asset selected:", key) + } + } + + RowLayout { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + + Button { + text: "Select assets tab" + + onClicked: panel.currentTab = TokenSelectorPanel.Tabs.Assets + } + + Button { + text: "Select collectibles tab" + + onClicked: panel.currentTab = TokenSelectorPanel.Tabs.Collectibles + } + + CheckBox { + id: assetsModelCheckBox + + checked: true + text: "Assets model assigned" + } + + CheckBox { + id: collectiblesModelCheckBox + + checked: true + text: "Collectibles model assigned" + } + } +} + +// category: Controls diff --git a/ui/app/AppLayouts/Wallet/adaptors/CollectiblesSelectionAdaptor.qml b/ui/app/AppLayouts/Wallet/adaptors/CollectiblesSelectionAdaptor.qml new file mode 100644 index 0000000000..c924b11162 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/adaptors/CollectiblesSelectionAdaptor.qml @@ -0,0 +1,185 @@ +import QtQuick 2.15 + +import StatusQ 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 + +import SortFilterProxyModel 0.2 + +/** + Adaptor transforming input flat model of collectibles into grouped model, + grouped by communities and collections to form expected by components like + e.g. TokenSelector. + + 1. Ownership submodels filtered to have only entries for given `account` + 2. Total balance calculated for remaining ownership entries in submodels + 3. Balance exposed to the top level model + 4. Grouping value exposed depending if token comes from community or not + (community collectibles are grouped by communtiyId, other by collectionId) + 5. Entries with zero balance filtered out + 6. Items are sorted by communityId and collectionId in order to group them correctly + 7. Grouping by groupingValue + 8. For community groups, group once again by collectionId + 9. Expose groupName and type according of it's community group or collection + 10. Expose sub-sub-groups count as a balance for sub-groups +**/ +QObject { + id: root + + /** Account key used for filtering **/ + property string accountKey + + /** + Expected model structure: + + tokenId [string] - unique identifier of a collectible + collectionUid [string] - unique identifier of a collection + contractAddress [string] - collectible's contract address + name [string] - collectible's name e.g. "Magicat" + collectionName [string] - collection name e.g. "Crypto Kitties" + mediaUrl [url] - collectible's media url + imageUrl [url] - collectible's image url + communityId [string] - unique identifier of a community for community collectible or empty + ownership [model] - submodel of balances per chain/account + balance [int] - balance (always 1 for ERC-721) + accountAddress [string] - unique identifier of an account + **/ + property var collectiblesModel + + /** + Model structure: + + groupName [string] - group name (from collection or community name) + icon [url] - from imageUrl or mediaUrl + type [string] - can be "community" or "other" + subitems [model] - submodel of collectibles/collections of the group + key [string] - key of collection (community type) or collectible (other type) + name [string] - name of the subitem (of collectible or collection) + balance [int] - balance of collection (in case of community collectibles) + or collectible (in case of ERC-1155) + icon [url] - icon of the subitem + **/ + readonly property alias model: communityGroupsGrouppedByCollection + + SortFilterProxyModel { + id: initiallyFilteredAndSorted + + objectName: "collectiblesSelectionAdaptor_initiallyFilteredAndSorted" + + sourceModel: ObjectProxyModel { + sourceModel: collectiblesModel ?? null + + delegate: QObject { + readonly property int balance: balanceAggregator.value /* 3 */ + readonly property string groupingValue: model.communityId /* 4 */ + ? model.communityId + : model.collectionUid + + readonly property string key: model.tokenId + + readonly property url icon: + model.imageUrl || model.mediaUrl || Qt.resolvedUrl("") + + SortFilterProxyModel { /* 1 */ + id: ownershipFiltered + + sourceModel: model.ownership + + filters: ValueFilter { + roleName: "accountAddress" + value: root.accountKey + } + } + + SumAggregator { /* 2 */ + id: balanceAggregator + + model: ownershipFiltered + roleName: "balance" + } + } + + expectedRoles: [ + "ownership", "communityId", "collectionUid", "imageUrl", + "mediaUrl", "tokenId" + ] + exposedRoles: ["balance", "groupingValue", "icon", "key"] + } + + filters: RangeFilter { /* 5 */ + roleName: "balance" + minimumValue: 1 + } + + sorters: [ /* 6 */ + RoleSorter { + roleName: "communityId" + sortOrder: Qt.DescendingOrder + }, + RoleSorter { + roleName: "collectionUid" + } + ] + } + + GroupingModel { /* 7 */ + id: grouppedByCollectionOrCommunity + + objectName: "collectiblesSelectionAdaptor_grouppedByCollectionOrCommunity" + + sourceModel: initiallyFilteredAndSorted + groupingRoleName: "groupingValue" + submodelRoleName: "subitems" + } + + ObjectProxyModel { + id: communityGroupsGrouppedByCollection + + objectName: "collectiblesSelectionAdaptor_communityGroupsGrouppedByCollection" + + sourceModel: grouppedByCollectionOrCommunity + + delegate: QObject { + readonly property var subitems: + model.communityId ? collectionCountProxyLoader.item + : model.subitems + readonly property string type: /* 9 */ + model.communityId ? "community" : "other" + readonly property string groupName: + model.communityName || model.collectionName + + readonly property url icon: + model.communityId ? model.communityImage + : (model.icon || Qt.resolvedUrl("")) + + Loader { + id: collectionCountProxyLoader + + active: !!model.communityId + + sourceComponent: ObjectProxyModel { + sourceModel: GroupingModel { /* 8 */ + sourceModel: model.communityId ? model.subitems : null + + groupingRoleName: "collectionUid" + submodelRoleName: "subitems" + } + + delegate: QtObject { /* 10 */ + readonly property int balance: model.subitems.ModelCount.count + readonly property string key: model.collectionUid + } + + expectedRoles: ["subitems", "collectionUid"] + exposedRoles: ["balance", "key"] + } + } + } + + expectedRoles: [ + "subitems", "collectionName", "communityId", + "communityName", "communityImage", "icon" + ] + exposedRoles: ["subitems", "type", "groupName", "icon"] + } +} diff --git a/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml b/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml index 45a6072e22..1c055d1f2c 100644 --- a/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml +++ b/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml @@ -173,7 +173,7 @@ QObject { RolesRenamingModel { id: renamedTokensBySymbolModel - sourceModel: root.plainTokensBySymbolModel + sourceModel: root.plainTokensBySymbolModel || null mapping: [ RoleRename { from: "key" diff --git a/ui/app/AppLayouts/Wallet/adaptors/qmldir b/ui/app/AppLayouts/Wallet/adaptors/qmldir index 088eb02a1b..ed8eee3848 100644 --- a/ui/app/AppLayouts/Wallet/adaptors/qmldir +++ b/ui/app/AppLayouts/Wallet/adaptors/qmldir @@ -1 +1,2 @@ +CollectiblesSelectionAdaptor 1.0 CollectiblesSelectionAdaptor.qml TokenSelectorViewAdaptor 1.0 TokenSelectorViewAdaptor.qml diff --git a/ui/app/AppLayouts/Wallet/controls/TokenSelectorNew.qml b/ui/app/AppLayouts/Wallet/controls/TokenSelectorNew.qml new file mode 100644 index 0000000000..4c7a32ba25 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/controls/TokenSelectorNew.qml @@ -0,0 +1,178 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Components 0.1 +import StatusQ.Components.private 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 AppLayouts.Wallet.panels 1.0 + +import utils 1.0 + +Control { + id: root + + /** Expected model structure: see TokenSelectorPanel::assetsModel **/ + property alias assetsModel: tokenSelectorPanel.assetsModel + + /** Expected model structure: see TokenSelectorPanel::collectiblesModel **/ + property alias collectiblesModel: tokenSelectorPanel.collectiblesModel + + signal assetSelected(string key) + signal collectionSelected(string key) + signal collectibleSelected(string key) + + // Index of the current tab, indexes ​​correspond to the + // TokensSelectorPanel.Tabs enum values. + property alias currentTab: tokenSelectorPanel.currentTab + + function setCustom(name: string, icon: url, key: string) { + d.isTokenSelected = true + d.currentName = name + d.currentIcon = icon + tokenSelectorPanel.highlightedKey = key ?? "" + } + + padding: 10 + + QtObject { + id: d + + property bool isTokenSelected: false + + property string currentName + property url currentIcon + } + + background: StatusComboboxBackground { + border.width: 0 + color: { + if (d.isTokenSelected) + return "transparent" + + return root.hovered || dropdown.opened + ? Theme.palette.primaryColor2 + : Theme.palette.primaryColor3 + } + } + + contentItem: Loader { + sourceComponent: d.isTokenSelected ? selectedContent + : notSelectedContent + } + + Component { + id: notSelectedContent + + RowLayout { + spacing: 10 + + StatusBaseText { + objectName: "tokenSelectorContentItemText" + font.pixelSize: root.font.pixelSize + font.weight: Font.Medium + color: Theme.palette.primaryColor1 + text: qsTr("Select token") + } + + StatusComboboxIndicator { + color: Theme.palette.primaryColor1 + } + } + } + + Component { + id: selectedContent + + RowLayout { + spacing: Style.current.halfPadding + + StatusRoundedImage { + objectName: "tokenSelectorIcon" + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + image.source: d.currentIcon + } + + StatusBaseText { + objectName: "tokenSelectorContentItemText" + font.pixelSize: 28 + color: root.hovered ? Theme.palette.blue : Theme.palette.darkBlue + + text: d.currentName + } + + StatusComboboxIndicator { + color: Theme.palette.primaryColor1 + } + } + } + + StatusDropdown { + id: dropdown + + y: parent.height + 4 + + closePolicy: Popup.CloseOnPressOutsideParent + bottomPadding: 0 + + contentItem: TokenSelectorPanel { + id: tokenSelectorPanel + + function findSubitem(key) { + const count = collectiblesModel.rowCount() + + for (let i = 0; i < count; i++) { + const entry = ModelUtils.get(collectiblesModel, i) + const subitem = ModelUtils.getByKey( + entry.subitems, "key", key) + if (subitem) + return subitem + } + } + + function setCurrentAndClose(name, icon) { + d.currentName = name + d.currentIcon = icon + d.isTokenSelected = true + dropdown.close() + } + + onAssetSelected: { + const entry = ModelUtils.getByKey(assetsModel, "tokensKey", key) + highlightedKey = key + + setCurrentAndClose(entry.symbol, + Constants.tokenIcon(entry.symbol)) + root.assetSelected(key) + } + + onCollectibleSelected: { + highlightedKey = key + + const subitem = findSubitem(key) + setCurrentAndClose(subitem.name, subitem.icon) + + root.collectibleSelected(key) + } + + onCollectionSelected: { + highlightedKey = key + + const subitem = findSubitem(key) + setCurrentAndClose(subitem.name, subitem.icon) + + root.collectionSelected(key) + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: dropdown.opened ? dropdown.close() : dropdown.open() + } +} diff --git a/ui/app/AppLayouts/Wallet/controls/qmldir b/ui/app/AppLayouts/Wallet/controls/qmldir index 2d50e00b3a..8f6225cb09 100644 --- a/ui/app/AppLayouts/Wallet/controls/qmldir +++ b/ui/app/AppLayouts/Wallet/controls/qmldir @@ -1,22 +1,23 @@ -NetworkFilter 1.0 NetworkFilter.qml -NetworkSelectItemDelegate 1.0 NetworkSelectItemDelegate.qml AccountHeaderGradient 1.0 AccountHeaderGradient.qml -StatusTxProgressBar 1.0 StatusTxProgressBar.qml -StatusDateRangePicker 1.0 StatusDateRangePicker.qml ActivityFilterTagItem 1.0 ActivityFilterTagItem.qml -SortOrderComboBox 1.0 SortOrderComboBox.qml +CollectibleBalanceTag 1.0 CollectibleBalanceTag.qml +CollectibleLinksTags 1.0 CollectibleLinksTags.qml +DappsComboBox 1.0 DappsComboBox.qml +EditSlippagePanel 1.0 EditSlippagePanel.qml FilterComboBox 1.0 FilterComboBox.qml +InformationTileAssetDetails 1.0 InformationTileAssetDetails.qml ManageTokenMenuButton 1.0 ManageTokenMenuButton.qml ManageTokensCommunityTag 1.0 ManageTokensCommunityTag.qml ManageTokensDelegate 1.0 ManageTokensDelegate.qml ManageTokensGroupDelegate 1.0 ManageTokensGroupDelegate.qml MaxSendButton 1.0 MaxSendButton.qml -InformationTileAssetDetails 1.0 InformationTileAssetDetails.qml +NetworkFilter 1.0 NetworkFilter.qml +NetworkSelectItemDelegate 1.0 NetworkSelectItemDelegate.qml +SortOrderComboBox 1.0 SortOrderComboBox.qml +StatusDateRangePicker 1.0 StatusDateRangePicker.qml StatusNetworkListItemTag 1.0 StatusNetworkListItemTag.qml -CollectibleBalanceTag 1.0 CollectibleBalanceTag.qml -CollectibleLinksTags 1.0 CollectibleLinksTags.qml -DappsComboBox 1.0 DappsComboBox.qml +StatusTxProgressBar 1.0 StatusTxProgressBar.qml SwapExchangeButton 1.0 SwapExchangeButton.qml -EditSlippagePanel 1.0 EditSlippagePanel.qml -TokenSelector 1.0 TokenSelector.qml SwapModalFooterInfoComponent 1.0 SwapModalFooterInfoComponent.qml +TokenSelector 1.0 TokenSelector.qml +TokenSelectorNew 1.0 TokenSelectorNew.qml diff --git a/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml b/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml new file mode 100644 index 0000000000..579ea9e24f --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml @@ -0,0 +1,378 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.15 + +import StatusQ 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 StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Wallet.views 1.0 +import shared.controls 1.0 +import utils 1.0 + +import SortFilterProxyModel 0.2 + +/** + Two-tabs panel holding searchable lists of assets (single level) and + collectibles (two levels). + + Structure: + + TabBar (assets, collectibles) + StackLayout (current index bound to tab bar's current index) + Assets List (assets part) + StackView (collectibles part) + Collectibles List (top level - groups by collection/community) + Collectibles List (nested level, on demand) +*/ +Control { + id: root + + enum Tabs { + Assets = 0, + Collectibles = 1 + } + + /** + Expected model structure: + + tokensKey [string] - unique asset's identifier + name [string] - asset's name + symbol [string] - asset's symbol + iconSource [url] - asset's icon + currencyBalanceAsString [string] - formatted balance + balances [model] - submodel of balances per chain + balanceAsString [string] - formatted balance per chain + iconUrl [url] - chain's icon + **/ + property alias assetsModel: assetsSfpm.sourceModel + + /** + Expected model structure: + + groupName [string] - group name + icon [url] - icon image of a group + type [string] - group type, can be "community" or "other" + subitems [model] - submodel of collectibles/collections of the group + key [string] - balance + name [string] - name of the subitem + balance [int] - balance of the subitem + icon [url] - icon of the subitem + **/ + property alias collectiblesModel: collectiblesSfpm.sourceModel + + // Index of the current tab, indexes ​​correspond to the Tabs enum values. + property alias currentTab: tabBar.currentIndex + + signal assetSelected(string key) + signal collectionSelected(string key) + signal collectibleSelected(string key) + + property string highlightedKey: "" + + SortFilterProxyModel { + id: assetsSfpm + + filters: AnyOf { + SearchFilter { + roleName: "name" + searchPhrase: assetsSearchBox.text + } + SearchFilter { + roleName: "symbol" + searchPhrase: assetsSearchBox.text + } + } + } + + SortFilterProxyModel { + id: collectiblesSfpm + + filters: SearchFilter { + roleName: "groupName" + searchPhrase: collectiblesSearchBox.text + } + } + + component SearchFilter: RegExpFilter { + required property string searchPhrase + + pattern: `*${searchPhrase}*` + caseSensitivity : Qt.CaseInsensitive + syntax: RegExpFilter.Wildcard + } + + component Search: SearchBox { + input.leftPadding: root.leftPadding + input.rightPadding: root.leftPadding + minimumHeight: 56 + maximumHeight: 56 + input.showBackground: false + focus: visible + } + + contentItem: ColumnLayout { + StatusTabBar { + id: tabBar + + visible: !!root.assetsModel && !!root.collectiblesModel + + currentIndex: !!root.assetsModel + ? TokenSelectorPanel.Tabs.Assets + : TokenSelectorPanel.Tabs.Collectibles + + StatusTabButton { + text: qsTr("Assets") + width: implicitWidth + + visible: !!root.assetsModel + } + + StatusTabButton { + text: qsTr("Collectibles") + width: implicitWidth + + visible: !!root.collectiblesModel + } + } + + StackLayout { + Layout.maximumHeight: 400 + + visible: !!root.assetsModel || !!root.collectiblesModel + currentIndex: tabBar.currentIndex + + ColumnLayout { + Layout.preferredHeight: visible ? implicitHeight : 0 + spacing: 0 + + Search { + id: assetsSearchBox + + Layout.fillWidth: true + placeholderText: qsTr("Search assets") + } + + StatusDialogDivider { + Layout.fillWidth: true + visible: assetsListView.count + } + + StatusListView { + id: assetsListView + + clip: true + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: contentHeight + + model: assetsSfpm + + delegate: TokenSelectorAssetDelegate { + required property var model + required property int index + + highlighted: tokensKey === root.highlightedKey + + tokensKey: model.tokensKey + name: model.name + symbol: model.symbol + currencyBalanceAsString: model.currencyBalanceAsString + iconSource: model.iconSource + balancesModel: model.balances + + onClicked: root.assetSelected(model.tokensKey) + } + } + } + + StackView { + id: collectiblesStackView + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: visible ? currentItem.implicitHeight : 0 + + initialItem: ColumnLayout { + spacing: 0 + + Search { + id: collectiblesSearchBox + + Layout.fillWidth: true + placeholderText: qsTr("Search collectibles") + } + + StatusDialogDivider { + Layout.fillWidth: true + visible: collectiblesListView.count + } + + StatusListView { + id: collectiblesListView + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: contentHeight + + clip: true + model: collectiblesSfpm + + delegate: TokenSelectorCollectibleDelegate { + required property var model + + readonly property int subitemsCount: + model.subitems.ModelCount.count + + readonly property bool isCommunity: + model.type === "community" + + readonly property bool showCount: + subitemsCount > 1 || isCommunity + + name: model.groupName + balance: showCount ? subitemsCount : "" + image: model.icon + goDeeperIconVisible: subitemsCount > 1 + || isCommunity + highlighted: subitemsCount === 1 && !isCommunity + ? ModelUtils.get(model.subitems, 0, "key") + === root.highlightedKey + : false + + onClicked: { + if (subitemsCount === 1 && !isCommunity) { + const key = ModelUtils.get(model.subitems, 0, "key") + root.collectibleSelected(key) + return + } + + const parameters = { + index: collectiblesSfpm.index(model.index, 0), + model: model.subitems, + isCommunity: isCommunity + } + + collectiblesStackView.push( + collectiblesSublistComponent, + parameters, + StackView.Immediate) + } + } + + section.property: "type" + section.delegate: StatusBaseText { + id: sectionTitle + + color: Theme.palette.baseColor1 + topPadding: Style.current.padding + + text: section === "community" + ? qsTr("Community minted") + : qsTr("Other") + } + } + } + } + } + } + + Component { + id: collectiblesSublistComponent + + ColumnLayout { + property var index + property alias model: sublistSfpm.sourceModel + property bool isCommunity + + spacing: 0 + + SortFilterProxyModel { + id: sublistSfpm + + filters: SearchFilter { + roleName: "name" + searchPhrase: collectiblesSublistSearchBox.text + } + } + + StatusIconTextButton { + id: backButton + + statusIcon: "previous" + icon.width: 12 + icon.height: 12 + text: qsTr("Back") + + onClicked: collectiblesStackView.pop(StackView.Immediate) + } + + StatusDialogDivider { + Layout.fillWidth: true + visible: collectiblesListView.count + } + + Search { + id: collectiblesSublistSearchBox + + Layout.fillWidth: true + placeholderText: qsTr("Search collectibles") + } + + StatusDialogDivider { + Layout.fillWidth: true + visible: collectiblesListView.count + } + + StatusListView { + id: sublist + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: contentHeight + + model: sublistSfpm + + clip: true + + delegate: TokenSelectorCollectibleDelegate { + required property var model + + name: model.name + balance: model.balance > 1 ? model.balance : "" + image: model.icon + goDeeperIconVisible: false + highlighted: model.key === root.highlightedKey + + onClicked: { + if (isCommunity) + root.collectionSelected(model.key) + else + root.collectibleSelected(model.key) + } + } + } + + // Detection if the related model entry has been removed. + // Using model.Component.destruction.connect is not reliable because + // is not called for submodels maintained in c++ by the parent model. + ItemSelectionModel { + id: selection + + model: collectiblesSfpm + + onHasSelectionChanged: { + if (!hasSelection) + collectiblesStackView.pop(StackView.Immediate) + } + + Component.onCompleted: select(index, ItemSelectionModel.Select) + } + } + } +} diff --git a/ui/app/AppLayouts/Wallet/panels/qmldir b/ui/app/AppLayouts/Wallet/panels/qmldir index 7fedbc09d4..c3ce03421c 100644 --- a/ui/app/AppLayouts/Wallet/panels/qmldir +++ b/ui/app/AppLayouts/Wallet/panels/qmldir @@ -1,11 +1,12 @@ -WalletHeader 1.0 WalletHeader.qml -WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml -WalletNftPreview 1.0 WalletNftPreview.qml ActivityFilterPanel 1.0 ActivityFilterPanel.qml +ContractInfoButtonWithMenu 1.0 ContractInfoButtonWithMenu.qml +DAppsWorkflow 1.0 DAppsWorkflow.qml ManageAssetsPanel 1.0 ManageAssetsPanel.qml ManageCollectiblesPanel 1.0 ManageCollectiblesPanel.qml ManageHiddenPanel 1.0 ManageHiddenPanel.qml -DAppsWorkflow 1.0 DAppsWorkflow.qml -SwapInputPanel 1.0 SwapInputPanel.qml -ContractInfoButtonWithMenu 1.0 ContractInfoButtonWithMenu.qml SignInfoBox 1.0 SignInfoBox.qml +SwapInputPanel 1.0 SwapInputPanel.qml +TokenSelectorPanel 1.0 TokenSelectorPanel.qml +WalletHeader 1.0 WalletHeader.qml +WalletNftPreview 1.0 WalletNftPreview.qml +WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml diff --git a/ui/app/AppLayouts/Wallet/stores/CollectiblesStore.qml b/ui/app/AppLayouts/Wallet/stores/CollectiblesStore.qml index ca98cd584f..09e0675080 100644 --- a/ui/app/AppLayouts/Wallet/stores/CollectiblesStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/CollectiblesStore.qml @@ -18,6 +18,8 @@ QtObject { readonly property var _allCollectiblesModel: !!root._allCollectiblesModule ? root._allCollectiblesModule.allCollectiblesModel : null readonly property var allCollectiblesModel: RolesRenamingModel { + objectName: "allCollectiblesModel" + sourceModel: root._allCollectiblesModel mapping: [ @@ -85,6 +87,8 @@ QtObject { /* PRIVATE: This model joins the "Tokens By Symbol Model" and "Communities Model" by communityId */ property LeftJoinModel _jointCollectiblesBySymbolModel: LeftJoinModel { + objectName: "jointCollectiblesBySymbolModel" + leftModel: allCollectiblesModel rightModel: _renamedCommunitiesModel joinRole: "communityId" diff --git a/ui/app/AppLayouts/Wallet/views/TokenSelectorCollectibleDelegate.qml b/ui/app/AppLayouts/Wallet/views/TokenSelectorCollectibleDelegate.qml new file mode 100644 index 0000000000..ae9dcbc3f9 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/views/TokenSelectorCollectibleDelegate.qml @@ -0,0 +1,100 @@ +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 + + required property string name + required property string balance + required property url image + + property bool goDeeperIconVisible: true + property bool interactive: true + + 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: root.image + + 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 + + StatusBaseText { + id: nameText + + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + text: root.name + font.weight: Font.Medium + elide: Text.ElideRight + } + + StatusBaseText { + Layout.alignment: Qt.AlignVCenter + + text: root.balance + visible: root.balance !== "" + color: Theme.palette.baseColor1 + font.pixelSize: 13 + font.weight: Font.Medium + elide: Text.ElideRight + } + + StatusIcon { + Layout.alignment: Qt.AlignVCenter + + icon: "tiny/chevron-right" + visible: root.goDeeperIconVisible + color: Theme.palette.baseColor1 + width: 16 + height: 16 + } + } + } + } +} diff --git a/ui/app/AppLayouts/Wallet/views/qmldir b/ui/app/AppLayouts/Wallet/views/qmldir index 425b22e63f..af6595b3d7 100644 --- a/ui/app/AppLayouts/Wallet/views/qmldir +++ b/ui/app/AppLayouts/Wallet/views/qmldir @@ -4,4 +4,5 @@ NetworkSelectionView 1.0 NetworkSelectionView.qml NetworkSelectorView 1.0 NetworkSelectorView.qml SavedAddresses 1.0 SavedAddresses.qml TokenSelectorAssetDelegate 1.0 TokenSelectorAssetDelegate.qml +TokenSelectorCollectibleDelegate 1.0 TokenSelectorCollectibleDelegate.qml TokenSelectorView 1.0 TokenSelectorView.qml diff --git a/ui/imports/shared/views/AssetsViewAdaptor.qml b/ui/imports/shared/views/AssetsViewAdaptor.qml index 18ab718de7..76f8d51823 100644 --- a/ui/imports/shared/views/AssetsViewAdaptor.qml +++ b/ui/imports/shared/views/AssetsViewAdaptor.qml @@ -90,6 +90,8 @@ QObject { ObjectProxyModel { id: proxyModel + objectName: "assetsViewAdaptorProxyModel" + sourceModel: root.tokensModel ?? null delegate: QObject {