fix(TokenHoldersPanel): Added sorting feature

Closes #11032
This commit is contained in:
Alexandra Betouni 2023-07-05 18:17:51 +03:00
parent ba89daa737
commit a1e1e316db
9 changed files with 379 additions and 163 deletions

View File

@ -34,8 +34,7 @@ SplitView {
sourceModel: tokenHoldersModel
sortBy: holdersList.sortBy
sortOrder: holdersList.sorting === SortableTokenHoldersList.Sorting.Descending
? Qt.DescendingOrder : Qt.AscendingOrder
sortOrder: holdersList.sortOrder ? Qt.DescendingOrder : Qt.AscendingOrder
}
onClicked: logs.logEvent("holdersList.clicked: " + index)

View File

@ -20,10 +20,10 @@ SplitView {
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
TokenHoldersPanel {
anchors.centerIn: parent
width: 568
height: 364
anchors.centerIn: parent
tokenName: "Aniversary"
model: TokenHoldersModel {}
isSelectorMode: editorSelectorMode.checked

View File

@ -33,7 +33,7 @@ ListModel {
},
{
name: "",
walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579268",
walletAddress: "0xb794f5ea0ba394782634hhh3fffba74279579264",
imageSource: "",
amount: 1,
noOfMessages: 0,
@ -41,7 +41,7 @@ ListModel {
},
{
name: "",
walletAddress: "0xc794f5ea0ba39494ce839613fffba74279579268",
walletAddress: "0xc794f577990jjjjjewaofherfffba74279579265",
imageSource: "",
amount: 11,
noOfMessages: 0,
@ -49,7 +49,7 @@ ListModel {
},
{
name: "",
walletAddress: "0xd794f5ea0ba39494ce839613fffba74279579268",
walletAddress: "0xd794f5ea009fnrsehggwe7777ffba74279579266",
imageSource: "",
amount: 14,
noOfMessages: 0,

View File

@ -20,16 +20,8 @@ import utils 1.0
StatusListView {
id: root
enum SortBy {
None, Username, NoOfMessages, Holding
}
enum Sorting {
Descending, Ascending
}
readonly property alias sortBy: d.sortBy
readonly property alias sorting: d.sorting
readonly property alias sortOrder: d.sorting
signal clicked(int index, var parent, var mouse)
@ -55,9 +47,9 @@ StatusListView {
d.resetOtherHeaders(this)
if (sorting === StatusSortableColumnHeader.Sorting.Ascending)
d.sorting = SortableTokenHoldersList.Sorting.Ascending
d.sorting = Qt.AscendingOrder
else if (sorting === StatusSortableColumnHeader.Sorting.Descending)
d.sorting = SortableTokenHoldersList.Sorting.Descending
d.sorting = Qt.DescendingOrder
}
}
@ -75,8 +67,8 @@ StatusListView {
QtObject {
id: d
property int sortBy: SortableTokenHoldersList.SortBy.None
property int sorting: SortableTokenHoldersList.Sorting.Descending
property int sortBy: TokenHoldersProxyModel.SortBy.None
property int sorting: Qt.DescendingOrder
readonly property int red2Color: 4
@ -113,9 +105,9 @@ StatusListView {
onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.Username
d.sortBy = TokenHoldersProxyModel.SortBy.Username
else
d.sortBy = SortableTokenHoldersList.SortBy.None
d.sortBy = TokenHoldersProxyModel.SortBy.None
}
}
@ -131,9 +123,9 @@ StatusListView {
onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.NoOfMessages
d.sortBy = TokenHoldersProxyModel.SortBy.NoOfMessages
else
d.sortBy = SortableTokenHoldersList.SortBy.None
d.sortBy = TokenHoldersProxyModel.SortBy.None
}
}
@ -149,9 +141,9 @@ StatusListView {
onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.Holding
d.sortBy = TokenHoldersProxyModel.SortBy.Holding
else
d.sortBy = SortableTokenHoldersList.SortBy.None
d.sortBy = TokenHoldersProxyModel.SortBy.None
}
}
}
@ -185,7 +177,7 @@ StatusListView {
}
readonly property bool showSeparator: isFirstRowAddress
&& root.sortBy === SortableTokenHoldersList.SortBy.Username
&& root.sortBy === TokenHoldersProxyModel.SortBy.Username
width: ListView.view.width

View File

@ -23,7 +23,7 @@ Control {
property alias isAirdropEnabled: infoBoxPanel.buttonEnabled
readonly property alias sortBy: holdersList.sortBy
readonly property alias sorting: holdersList.sorting
readonly property alias sorting: holdersList.sortOrder
readonly property bool empty: countCheckHelper.count === 0
@ -50,8 +50,7 @@ Control {
searchText: searcher.text
sortBy: holdersList.sortBy
sortOrder: holdersList.sorting === SortableTokenHoldersList.Sorting.Descending
? Qt.DescendingOrder : Qt.AscendingOrder
sortOrder: holdersList.sortOrder ? Qt.DescendingOrder : Qt.AscendingOrder
}
QtObject {

View File

@ -0,0 +1,288 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import QtQuick.Controls 2.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import SortFilterProxyModel 0.2
import utils 1.0
import shared.controls 1.0
/*!
\qmltype TokenHoldersList
\inherits StatusListView
\brief Shows list of users or addresses with corrensponding numbers of
messages and holding amounts.
Expected roles: name, walletAddress, imageSource, amount
*/
Item {
id: root
implicitHeight: (listView.contentHeight+header.height+12)//initial height plus top margin
property alias model: listView.model
property bool isSelectorMode: false
readonly property alias sortBy: d.sortBy
readonly property alias sortOrder: d.sorting
readonly property bool bottomSeparatorVisible: ((listView.contentY > 0) &&
(listView.contentY < (listView.contentHeight - listView.height - 40/*margins*/)))
signal selfDestructAmountChanged(string walletAddress, int amount)
signal selfDestructRemoved(string walletAddress)
QtObject {
id: d
property int sortBy: TokenHoldersProxyModel.SortBy.None
property int sorting: Qt.DescendingOrder
property var selectedTokenAmount: new Map();
property var selectedTokenChecked: new Map();
property int delegateHeight: 64
signal resetOtherHeaders(var header)
}
clip: true
Control {
id: header
width: parent.width
height: 40
readonly property alias usernameHeaderWidth: usernameHeader.width
readonly property alias holdingHeaderWidth: holdingHeader.width
background: Rectangle {
id: scrollingSeparator
width: parent.width
height: 4
anchors.bottom: parent.bottom
color: Theme.palette.baseColor2
visible: (listView.contentY > 0)
}
contentItem: Item {
anchors.fill: parent
anchors.leftMargin: Style.current.padding
anchors.rightMargin: Style.current.padding
clip: true
RowLayout {
id: row
anchors.fill: parent
spacing: Style.current.padding
RowLayout {
id: usernameHeader
ColumnHeader {
text: qsTr("Username")
traversalOrder: [
StatusSortableColumnHeader.Sorting.NoSorting,
StatusSortableColumnHeader.Sorting.Ascending,
StatusSortableColumnHeader.Sorting.Descending
]
onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = TokenHoldersProxyModel.SortBy.Username
else
d.sortBy = TokenHoldersProxyModel.SortBy.None
}
}
Item { Layout.fillWidth: true }
}
ColumnHeader {
id: holdingHeader
text: qsTr("Hodling")
onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = TokenHoldersProxyModel.SortBy.Holding
else
d.sortBy = TokenHoldersProxyModel.SortBy.None
}
}
Item {
Layout.preferredWidth: 233
Layout.rightMargin: Style.current.halfPadding
}
}
}
}
StatusListView {
id: listView
anchors.fill: parent
anchors.topMargin: header.height
currentIndex: -1
component ColumnHeader: StatusSortableColumnHeader {
id: columnHeader
leftPadding: 0
rightPadding: 0
Connections {
target: d
function onResetOtherHeaders(header) {
if (header !== columnHeader)
columnHeader.reset()
}
}
onClicked: {
d.resetOtherHeaders(this)
if (sorting === StatusSortableColumnHeader.Sorting.Ascending)
d.sorting = Qt.AscendingOrder
else if (sorting === StatusSortableColumnHeader.Sorting.Descending)
d.sorting = Qt.DescendingOrder
}
}
component NumberCell: StatusBaseText {
horizontalAlignment: Qt.AlignRight
font.weight: Font.Medium
font.pixelSize: 13
color: Theme.palette.baseColor1
elide: Qt.ElideRight
}
delegate: ItemDelegate {
id: delegate
width: ListView.view.width
height: d.delegateHeight
padding: 0
readonly property string name: model.name
readonly property bool isFirstRowAddress: {
if (model.name !== "")
return false
const item = listView.itemAtIndex(index - 1)
return item && item.name
}
readonly property bool showSeparator: isFirstRowAddress
&& root.sortBy === TokenHoldersProxyModel.SortBy.Username
background: Item {
Rectangle {
anchors.fill: parent
radius: Style.current.radius
color: (delegate.hovered || delegate.ListView.isCurrentItem)
? Theme.palette.baseColor2 : "transparent"
}
Rectangle {
visible: delegate.showSeparator
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: delegate.topPadding / 2
height: 1
color: Theme.palette.baseColor2
}
}
contentItem: Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Style.current.padding
anchors.rightMargin: Style.current.padding
RowLayout {
spacing: Style.current.halfPadding
StatusListItem {
readonly property bool unknownHolder: model.name === ""
readonly property string formattedTitle: unknownHolder ? "?" : model.name
Layout.preferredWidth: header.usernameHeaderWidth
color: "transparent"
leftPadding: 0
rightPadding: 0
sensor.enabled: false
title: formattedTitle
statusListItemTitle.visible: !unknownHolder
subTitle: Utils.getElidedPk(model.walletAddress)
asset.name: model.imageSource
asset.isImage: true
asset.isLetterIdenticon: !asset.name
asset.color: Theme.palette.getColor("red2")
}
NumberCell {
Layout.preferredWidth: header.holdingHeaderWidth
Layout.leftMargin: Style.current.halfPadding
text: LocaleUtils.numberToLocaleString(model.amount)
}
Item { Layout.preferredWidth: 100 }
StatusComboBox {
id: combo
Layout.preferredWidth: 68
Layout.preferredHeight: 44
control.spacing: Style.current.halfPadding / 2
model: amount
size: StatusComboBox.Size.Small
type: StatusComboBox.Type.Secondary
delegate: StatusItemDelegate {
width: combo.control.width
centerTextHorizontally: true
highlighted: combo.control.highlightedIndex === index
font: combo.control.font
text: Number(modelData) + 1
}
contentItem: StatusBaseText {
id: comboText
font: combo.control.font
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
color: Theme.palette.baseColor1
Component.onCompleted: {
if (d.selectedTokenAmount.get(walletAddress) === undefined) {
d.selectedTokenAmount.set(walletAddress, amount);
}
text = d.selectedTokenAmount.get(walletAddress);
}
}
control.onActivated: {
d.selectedTokenAmount.set(walletAddress, (index+1));
comboText.text = d.selectedTokenAmount.get(walletAddress);
if (checkBox.checked) {
root.selfDestructAmountChanged(walletAddress, d.selectedTokenAmount.get(walletAddress))
}
}
}
Item { Layout.preferredWidth: 28 }
StatusCheckBox {
id: checkBox
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignRight
visible: root.isSelectorMode
padding: 0
onCheckStateChanged: {
if (checked)
root.selfDestructAmountChanged(model.walletAddress, d.selectedTokenAmount.get(walletAddress))
else
root.selfDestructRemoved(model.walletAddress)
d.selectedTokenChecked.set(walletAddress, checked);
}
Component.onCompleted: {
checked = !!d.selectedTokenChecked.get(walletAddress);
}
}
}
}
}
}
}

View File

@ -1,165 +1,97 @@
import QtQuick 2.14
import QtQuick.Layouts 1.14
import QtQuick.Controls 2.14
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import SortFilterProxyModel 0.2
import StatusQ.Core.Utils 0.1
import StatusQ.Popups 0.1
import utils 1.0
import shared.controls 1.0
Control {
id: root
// Expected roles: name, walletAddress, imageSource, amount
property var model
property string tokenName
property bool isSelectorMode: false
signal selfDestructAmountChanged(string walletAddress, int amount)
signal selfDestructRemoved(string walletAddress)
bottomPadding: 16
QtObject {
id: d
TokenHoldersProxyModel {
id: filteredModel
sourceModel: root.model
searchText: searcher.text
readonly property int red2Color: 4
sortBy: holdersList.sortBy
sortOrder: holdersList.sortOrder ? Qt.DescendingOrder : Qt.AscendingOrder
}
contentItem: ColumnLayout {
spacing: Style.current.padding
id: column
anchors.fill: parent
anchors.topMargin: Style.current.padding
spacing: 0
StatusBaseText {
id: txtLabel
Layout.fillWidth: true
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.padding
wrapMode: Text.Wrap
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.baseColor1
SortFilterProxyModel {
id: filteredModel
sourceModel: root.model
filters: ExpressionFilter {
enabled: searcher.enabled
expression: {
searcher.text
return model.name.toLowerCase().includes(searcher.text.toLowerCase()) ||
model.walletAddress.toLowerCase().includes(searcher.text.toLowerCase())
}
}
text: qsTr("%1 token holders").arg(root.tokenName)
}
SearchBox {
id: searcher
Layout.fillWidth: true
Layout.topMargin: 12
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.padding
visible: !root.empty
topPadding: 0
bottomPadding: 0
minimumHeight: 36 // by design
maximumHeight: minimumHeight
enabled: root.model && root.model.count > 0
placeholderText: enabled ? qsTr("Search") : qsTr("No placeholders to search")
placeholderText: qsTr("Search hodlers")
}
StatusBaseText {
id: anotherLabel
Layout.fillWidth: true
Layout.topMargin: 12
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.padding
visible: !root.preview
wrapMode: Text.Wrap
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.baseColor1
text: searcher.text.length > 0 ? qsTr("Search results") : qsTr("All %1 token holders").arg(root.tokenName)
visible: (searcher.text.length > 0 && filteredModel.count === 0)
text: visible ? qsTr("No hodlers found") : ""
}
Item {
id: scrollViewWrapper
TokenHoldersList {
id: holdersList
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: scrollView.implicitWidth
implicitHeight: scrollView.implicitHeight
StatusListView {
id: scrollView
anchors.fill: parent
implicitHeight: contentHeight
Layout.topMargin: 12
isSelectorMode: root.isSelectorMode
model: filteredModel
ScrollBar.vertical: StatusScrollBar {
parent: scrollViewWrapper
anchors.top: scrollView.top
anchors.bottom: scrollView.bottom
anchors.left: scrollView.right
anchors.leftMargin: 1
onSelfDestructRemoved: {
root.selfDestructRemoved(walletAddress);
}
delegate: RowLayout {
width: ListView.view.width
spacing: Style.current.padding
StatusListItem {
readonly property bool unknownHolder: model.name === ""
readonly property string formattedTitle: unknownHolder ? "?" : model.name
onSelfDestructAmountChanged: {
root.selfDestructAmountChanged(walletAddress, amount);
}
}
Rectangle {
Layout.fillWidth: true
leftPadding: 0
rightPadding: 0
sensor.enabled: false
title: formattedTitle
statusListItemTitle.visible: !unknownHolder
subTitle: model.walletAddress
asset.name: model.imageSource
asset.isImage: true
asset.isLetterIdenticon: unknownHolder
asset.color: Theme.palette.userCustomizationColors[d.red2Color]
}
StatusComboBox {
id: combo
Layout.preferredWidth: 88
Layout.preferredHeight: 44
visible: root.isSelectorMode && amount > 1
control.spacing: Style.current.halfPadding / 2
model: amount
size: StatusComboBox.Size.Small
type: StatusComboBox.Type.Secondary
delegate: StatusItemDelegate {
width: combo.control.width
centerTextHorizontally: true
highlighted: combo.control.highlightedIndex === index
font: combo.control.font
text: Number(modelData) + 1
}
contentItem: StatusBaseText {
font: combo.control.font
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
text: Number(combo.control.displayText) + 1
color: Theme.palette.baseColor1
}
control.onDisplayTextChanged: {
if(checkBox.checked)
root.selfDestructAmountChanged(walletAddress, Number(combo.currentIndex) + 1)
}
}
StatusCheckBox {
id: checkBox
Layout.leftMargin: Style.current.padding
visible: root.isSelectorMode
padding: 0
onCheckStateChanged: {
if(checked)
root.selfDestructAmountChanged(model.walletAddress, Number(combo.currentIndex) + 1)
else
root.selfDestructRemoved(model.walletAddress)
}
}
}
}
Layout.preferredHeight: 4
Layout.alignment: Qt.AlignBottom
color: Theme.palette.baseColor2
opacity: holdersList.bottomSeparatorVisible ? 1.0 : 0.0
}
}
}

View File

@ -6,9 +6,13 @@ SortFilterProxyModel {
property string searchText
readonly property string searchTextLowerCase: searchText.toLowerCase()
property int sortBy: SortableTokenHoldersList.SortBy.Username
property int sortBy: TokenHoldersProxyModel.SortBy.Username
property int sortOrder: Qt.AscendingOrder
enum SortBy {
None, Username, NoOfMessages, Holding
}
filters: ExpressionFilter {
expression: {
root.searchTextLowerCase
@ -23,7 +27,7 @@ SortFilterProxyModel {
sorters: [
FilterSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username
enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username
ValueFilter {
roleName: "name"
@ -35,27 +39,27 @@ SortFilterProxyModel {
},
RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username
enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username
roleName: "name"
sortOrder: root.sortOrder
priority: 2
},
RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username
enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username
roleName: "walletAddress"
sortOrder: root.sortOrder
priority: 1
},
RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.NoOfMessages
enabled: root.sortBy === TokenHoldersProxyModel.SortBy.NoOfMessages
roleName: "noOfMessages"
sortOrder: root.sortOrder
},
RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Holding
enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Holding
roleName: "amount"
sortOrder: root.sortOrder
}

View File

@ -63,14 +63,10 @@ StatusDialog {
implicitWidth: 600 // by design
padding: 0
TokenHoldersPanel {
contentItem: TokenHoldersPanel {
id: tokenHoldersPanel
anchors.fill: parent
padding: 16
tokenName: root.collectibleName
isSelectorMode: true
onSelfDestructAmountChanged: d.updateTokensToDestruct(walletAddress, amount)
onSelfDestructRemoved: d.clearTokensToDesctruct(walletAddress)
}
@ -78,6 +74,12 @@ StatusDialog {
footer: StatusDialogFooter {
spacing: Style.current.padding
rightButtons: ObjectModel {
StatusFlatButton {
text: qsTr("Cancel")
onClicked: {
root.close()
}
}
StatusButton {
enabled: d.tokenCount > 0
text: qsTr("Remotely destruct %n token(s)", "", d.tokenCount)