status-desktop/storybook/pages/NetworkSelectPopupPage.qml
Stefan 691de11211 fix(Wallet) network selection and unify network implementations
Major changes:

- Don't allow empty network selection. End up using the nim model
  directly instead because of individual row changes issues
  encountered with nim models
- Made the clone model a generic implementation to be used in other
places where we need to clone a model: ReceiveModal,
AddEditSavedAddressPopup
- Use cloned model as alternative to NetworksExtraStoreProxy in
  ReceiveModal
- Added tristate support to our generic checkbox control
- UX improvements as per design
- Fix save address tests naming and zero address issue
- Various fixes

Notes:
- Failed to make NetworkSelectPopup follow ground-truth: show partially
  checked as user intention until the network is selected in the
  source model. Got stuck on nim models not being stable models and
  report wrong entry change when reset. Tried sorting and only updating
  changes without reset but it didn't work.
- Moved grouped property SingleSelectionInfo to its own file from
  an inline component after finding out that it fails to load on Linux
  with error "Cannot assign to property of unknown type: "*".".
  It works on MacOS as expected

Closes: #10119
2023-04-20 19:34:24 +02:00

324 lines
12 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import utils 1.0
import AppLayouts.Wallet.popups 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.stores 1.0
import Models 1.0
import SortFilterProxyModel 0.2
SplitView {
id: root
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
ColumnLayout {
id: controlLayout
anchors.fill: parent
// Leave some space so that the popup will be opened without accounting for Layer
ColumnLayout {
Layout.maximumHeight: 50
}
NetworkFilter {
id: networkFilter
Layout.preferredWidth: 200
Layout.preferredHeight: 100
Layout.alignment: Qt.AlignHCenter
allNetworks: simulatedNimModel
layer1Networks: SortFilterProxyModel {
function rowData(index, propName) {
return get(index)[propName]
}
sourceModel: simulatedNimModel
filters: ValueFilter { roleName: "layer"; value: 1; }
}
layer2Networks: SortFilterProxyModel {
sourceModel: simulatedNimModel
filters: [ValueFilter { roleName: "layer"; value: 2; },
ValueFilter { roleName: "isTest"; value: false; }]
}
testNetworks: SortFilterProxyModel {
sourceModel: simulatedNimModel
filters: [ValueFilter { roleName: "layer"; value: 2; },
ValueFilter { roleName: "isTest"; value: true; }]
}
enabledNetworks: SortFilterProxyModel {
sourceModel: simulatedNimModel
filters: ValueFilter { roleName: "isEnabled"; value: true; }
}
onToggleNetwork: (network) => {
if(multiSelection) {
simulatedNimModel.toggleNetwork(network)
} else {
lastSingleSelectionLabel.text = `[${network.chainName}] (NL) - ID: ${network.chainId}, Icon: ${network.iconUrl}`
}
}
multiSelection: multiSelectionCheckbox.checked
isChainVisible: isChainVisibleCheckbox.checked
}
// Dummy item to make space for popup
Item {
id: popupPlaceholder
Layout.preferredWidth: networkSelectPopup.width
Layout.preferredHeight: networkSelectPopup.height
NetworkSelectPopup {
id: networkSelectPopup
layer1Networks: networkFilter.layer1Networks
layer2Networks: networkFilter.layer2Networks
testNetworks: networkFilter.testNetworks
useEnabledRole: false
visible: true
closePolicy: Popup.NoAutoClose
// Simulates a network toggle
onToggleNetwork: (network, networkModel, index) => simulatedNimModel.toggleNetwork(network)
}
}
ColumnLayout {
Layout.preferredHeight: 30
Layout.maximumHeight: 30
}
RowLayout {
Button {
text: "Single Selection Popup"
onClicked: selectPopupLoader.active = true
}
Label {
id: lastSingleSelectionLabel
text: "-"
}
}
Item {
id: singleSelectionPopupPlaceholder
Layout.preferredWidth: selectPopupLoader.item ? selectPopupLoader.item.width : 0
Layout.preferredHeight: selectPopupLoader.item ? selectPopupLoader.item.height : 0
property var currentModel: networkFilter.layer2Networks
property int currentIndex: 0
Loader {
id: selectPopupLoader
active: false
sourceComponent: NetworkSelectPopup {
layer1Networks: networkFilter.layer1Networks
layer2Networks: networkFilter.layer2Networks
testNetworks: networkFilter.testNetworks
singleSelection {
enabled: true
currentModel: singleSelectionPopupPlaceholder.currentModel
currentIndex: singleSelectionPopupPlaceholder.currentIndex
}
onToggleNetwork: (network, networkModel, index) => {
lastSingleSelectionLabel.text = `[${network.chainName}] - ID: ${network.chainId}, Icon: ${network.iconUrl}`
singleSelectionPopupPlaceholder.currentModel = networkModel
singleSelectionPopupPlaceholder.currentIndex = index
}
onClosed: selectPopupLoader.active = false
}
onLoaded: item.open()
}
}
// Vertical separator
ColumnLayout {}
}
}
Pane {
SplitView.minimumWidth: 300
SplitView.fillWidth: true
SplitView.minimumHeight: 300
ColumnLayout {
anchors.fill: parent
ListView {
id: allNetworksListView
Layout.fillWidth: true
Layout.fillHeight: true
model: simulatedNimModel
delegate: ItemDelegate {
width: allNetworksListView.width
implicitHeight: delegateRowLayout.implicitHeight
highlighted: ListView.isCurrentItem
RowLayout {
id: delegateRowLayout
anchors.fill: parent
Column {
Layout.margins: 5
spacing: 3
Label { text: model.chainName }
Row {
spacing: 5
Label { text: `<b>${model.shortName}</b>` }
Label { text: `ID <b>${model.chainId}</b>` }
CheckBox {
checkState: model.isEnabled ? Qt.Checked : Qt.Unchecked
tristate: true
nextCheckState: () => {
const nextEnabled = (checkState !== Qt.Checked)
availableNetworks.sourceModel.setProperty(availableNetworks.mapToSource(index), "isEnabled", nextEnabled)
Qt.callLater(() => { simulatedNimModel.cloneModel(availableNetworks) })
return nextEnabled ? Qt.Checked : Qt.Unchecked
}
}
}
}
}
onClicked: allNetworksListView.currentIndex = index
}
}
CheckBox {
id: multiSelectionCheckbox
Layout.margins: 5
text: "Multi Selection"
checked: true
}
CheckBox {
id: testModeCheckbox
Layout.margins: 5
text: "Test Networks Mode"
checked: false
onCheckedChanged: Qt.callLater(simulatedNimModel.cloneModel, availableNetworks)
}
CheckBox {
id: isChainVisibleCheckbox
Layout.margins: 5
text: "Is chain visible"
checked: true
}
}
}
SortFilterProxyModel {
id: availableNetworks
// Simulate Nim's way of providing access to data
function rowData(index, propName) {
return get(index)[propName]
}
sourceModel: NetworksModel.allNetworks
filters: ValueFilter { roleName: "isTest"; value: testModeCheckbox.checked; }
}
// Keep a clone so that the UX can be modified without affecting the original model
CloneModel {
id: simulatedNimModel
sourceModel: availableNetworks
roles: ["chainId", "layer", "chainName", "isTest", "isEnabled", "iconUrl", "shortName", "chainColor"]
rolesOverride: [{ role: "enabledState", transform: (mD) => {
return simulatedNimModel.areAllEnabled(sourceModel)
? NetworkSelectPopup.UxEnabledState.AllEnabled
: mD.isEnabled
? NetworkSelectPopup.UxEnabledState.Enabled
: NetworkSelectPopup.UxEnabledState.Disabled
}
}]
/// Simulate the Nim model
function toggleNetwork(network) {
const chainId = network.chainId
let chainIdOnlyEnabled = true
let chainIdOnlyDisabled = true
let allEnabled = true
for (let i = 0; i < simulatedNimModel.count; i++) {
const item = simulatedNimModel.get(i)
if(item.enabledState === NetworkSelectPopup.UxEnabledState.Enabled) {
if(item.chainId !== chainId) {
chainIdOnlyEnabled = false
}
} else if(item.enabledState === NetworkSelectPopup.UxEnabledState.Disabled) {
if(item.chainId !== chainId) {
chainIdOnlyDisabled = false
}
allEnabled = false
} else {
if(item.chainId === chainId) {
chainIdOnlyDisabled = false
chainIdOnlyEnabled = false
}
}
}
for (let i = 0; i < simulatedNimModel.count; i++) {
const item = simulatedNimModel.get(i)
if(allEnabled) {
simulatedNimModel.setProperty(i, "enabledState", item.chainId === chainId ? NetworkSelectPopup.UxEnabledState.Enabled : NetworkSelectPopup.UxEnabledState.Disabled)
} else if(chainIdOnlyEnabled || chainIdOnlyDisabled) {
simulatedNimModel.setProperty(i, "enabledState", NetworkSelectPopup.UxEnabledState.AllEnabled)
} else if(item.chainId === chainId) {
simulatedNimModel.setProperty(i, "enabledState", item.enabledState === NetworkSelectPopup.UxEnabledState.Enabled
? NetworkSelectPopup.UxEnabledState.Disabled
:NetworkSelectPopup.UxEnabledState.Enabled)
}
const haveEnabled = item.enabledState !== NetworkSelectPopup.UxEnabledState.Disabled
if(item.isEnabled !== haveEnabled) {
simulatedNimModel.setProperty(i, "isEnabled", haveEnabled)
}
}
}
function areAllEnabled(modelToCheck) {
for (let i = 0; i < modelToCheck.count; i++) {
if(!(modelToCheck.get(i).isEnabled)) {
return false
}
}
return true
}
}
}