feat(CommunityPermissions): prevent adding tokens and ens names that are already chosen

Closes: #8817
This commit is contained in:
Michał Cieślak 2023-01-24 22:49:07 +01:00 committed by Michał
parent 25bb970ea4
commit 9ac0c159fe
9 changed files with 203 additions and 24 deletions

View File

@ -14,12 +14,16 @@ ColumnLayout {
property alias domainNameValid: domainNameInput.valid
property alias addButtonEnabled: addOrUpdateButton.enabled
property var reservedNames: []
signal addClicked
signal updateClicked
signal removeClicked
spacing: 0
onReservedNamesChanged: domainNameInput.validate()
StatusInput {
id: domainNameInput
@ -33,13 +37,22 @@ ColumnLayout {
font.pixelSize: 13
input.placeholderText: "name.eth"
errorMessageCmp.visible: false
validators: StatusRegularExpressionValidator {
// TODO: check ens domain validator
regularExpression: /^(\*\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?$/i
errorMessage: qsTr("Subdomain not recognized")
errorMessage: alreadyUsed
? qsTr("This condition has already been added")
: qsTr("This is not ENS name")
property bool alreadyUsed: false
validate: function (value) {
return value === "*.eth" || regularExpression.test(value)
alreadyUsed = reservedNames.includes(value)
return (value === "*.eth" || regularExpression.test(value))
&& !alreadyUsed
}
}
@ -64,11 +77,34 @@ ColumnLayout {
lineHeightMode: Text.FixedHeight
}
Item {
Layout.topMargin: 8
Layout.fillWidth: true
Layout.preferredHeight: Math.max(18, errorText.height) // by design
StatusBaseText {
id: errorText
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
text: domainNameInput.errorMessageCmp.text
font.pixelSize: 13
lineHeight: 18
lineHeightMode: Text.FixedHeight
color: Theme.palette.dangerColor1
wrapMode: Text.Wrap
}
}
StatusButton {
id: addOrUpdateButton
text: (root.mode === HoldingTypes.Mode.Add) ? qsTr("Add") : qsTr("Update")
Layout.topMargin: 40
Layout.topMargin: 13 // by design
Layout.preferredHeight: 44 // by design
Layout.fillWidth: true
onClicked: root.mode === HoldingTypes.Mode.Add
@ -80,7 +116,7 @@ ColumnLayout {
Layout.topMargin: 16 // by design
Layout.preferredHeight: 44 // by design
Layout.fillWidth: true
visible: root.mode === HoldingTypes.Mode.Update
visible: root.mode === HoldingTypes.Mode.UpdateOrRemove
type: StatusBaseButton.Type.Danger
onClicked: root.removeClicked()

View File

@ -16,6 +16,7 @@ Item {
id: root
property var store
property var checkedKeys: []
property int type: ExtendedDropdownContent.Type.Assets
readonly property bool canGoBack: root.state !== d.listView_depth1_State
@ -272,10 +273,14 @@ Item {
TokenItem {
id: tokenGroupItem
Layout.fillWidth: true
key: d.currentItemKey
name: d.currentItemName
iconSource: d.currentItemSource
selected: root.checkedKeys.includes(key)
enabled: true
onItemClicked: root.itemClicked(d.currentItemKey,
d.currentItemName,
@ -301,6 +306,8 @@ Item {
}
isHeaderVisible: false // TEMPORARILY hidden. These 2 header options will be implemented after MVP.
model: d.currentModel
checkedKeys: root.checkedKeys
onHeaderItemClicked: {
if(key === "MINT") console.log("TODO: Mint asset")
else if(key === "IMPORT") console.log("TODO: Import existing asset")
@ -319,6 +326,7 @@ Item {
}
model: d.currentModel
checkedKeys: root.checkedKeys
onHeaderItemClicked: {
if(key === "MINT") console.log("TODO: Mint collectible")
@ -351,6 +359,8 @@ Item {
padding: 0
model: d.currentModel
checkedKeys: root.checkedKeys
onItemClicked: {
d.reset()
root.itemClicked(key, name, iconSource)

View File

@ -6,6 +6,6 @@ QtObject {
}
enum Mode {
Add, Update
Add, Update, UpdateOrRemove
}
}

View File

@ -13,6 +13,8 @@ StatusDropdown {
id: root
property var store
property var usedTokens: []
property var usedEnsNames: []
property string assetKey: ""
property real assetAmount: 0
@ -37,7 +39,7 @@ StatusDropdown {
}
function openUpdateFlow() {
d.currentHoldingMode = HoldingTypes.Mode.Update
d.initialHoldingMode = HoldingTypes.Mode.UpdateOrRemove
if(d.currentHoldingType !== HoldingTypes.Type.Ens) {
if(statesStack.size === 0)
statesStack.push(HoldingsDropdown.FlowType.List_Deep1)
@ -53,16 +55,13 @@ StatusDropdown {
function reset() {
d.currentHoldingType = HoldingTypes.Type.Asset
d.currentHoldingMode = HoldingTypes.Mode.Add
d.initialHoldingMode = HoldingTypes.Mode.Add
d.assetAmountText = ""
d.collectibleAmountText = ""
root.assetKey = ""
root.collectibleKey = ""
root.assetAmount = 0
root.collectibleAmount = 1
root.ensDomainName = ""
d.setDefaultAmounts()
d.setInitialFlow()
}
@ -79,7 +78,14 @@ StatusDropdown {
property int extendedDropdownType: ExtendedDropdownContent.Type.Assets
property int currentHoldingType: HoldingTypes.Type.Asset
property int currentHoldingMode: HoldingTypes.Mode.Add
property bool updateSelected: false
property int initialHoldingMode: HoldingTypes.Mode.Add
property int effectiveHoldingMode: initialHoldingMode === HoldingTypes.Mode.UpdateOrRemove
? HoldingTypes.Mode.UpdateOrRemove
: (updateSelected ? HoldingTypes.Mode.Update : HoldingTypes.Mode.Add)
property bool extendedDeepNavigation: false
property var currentSubItems
property string currentItemKey: ""
@ -105,6 +111,13 @@ StatusDropdown {
else
statesStack.push(HoldingsDropdown.FlowType.Selected)
}
function setDefaultAmounts() {
d.assetAmountText = ""
d.collectibleAmountText = ""
root.assetAmount = 0
root.collectibleAmount = 1
}
}
QtObject {
@ -236,9 +249,27 @@ StatusDropdown {
id: listPanel
store: root.store
checkedKeys: root.usedTokens.map(entry => entry.key)
type: d.extendedDropdownType
onItemClicked: {
d.assetAmountText = ""
d.collectibleAmountText = ""
if (checkedKeys.includes(key)) {
const amount = root.usedTokens.find(entry => entry.key === key).amount
if(d.extendedDropdownType === ExtendedDropdownContent.Type.Assets)
root.assetAmount = amount
else
root.collectibleAmount = amount
d.updateSelected = true
} else {
d.setDefaultAmounts()
d.updateSelected = false
}
if(d.extendedDropdownType === ExtendedDropdownContent.Type.Assets)
root.assetKey = key
else
@ -293,7 +324,7 @@ StatusDropdown {
amountText: d.assetAmountText
tokenCategoryText: qsTr("Asset")
addOrUpdateButtonEnabled: d.assetsReady
mode: d.currentHoldingMode
mode: d.effectiveHoldingMode
onEffectiveAmountChanged: root.assetAmount = effectiveAmount
onAmountTextChanged: d.assetAmountText = amountText
@ -329,7 +360,7 @@ StatusDropdown {
tokenCategoryText: qsTr("Collectible")
addOrUpdateButtonEnabled: d.collectiblesReady
allowDecimals: false
mode: d.currentHoldingMode
mode: d.effectiveHoldingMode
onEffectiveAmountChanged: root.collectibleAmount = effectiveAmount
onAmountTextChanged: d.collectibleAmountText = amountText
@ -356,7 +387,8 @@ StatusDropdown {
EnsPanel {
addButtonEnabled: d.ensReady
domainName: root.ensDomainName
mode: d.currentHoldingMode
mode: d.initialHoldingMode
reservedNames: root.usedEnsNames
onDomainNameChanged: root.ensDomainName = domainName
onDomainNameValidChanged: d.ensDomainNameValid = domainNameValid

View File

@ -12,6 +12,8 @@ import StatusQ.Components 0.1
StatusListView {
id: root
property var checkedKeys: []
property var headerModel
property bool isHeaderVisible: true
property int maxHeight: 381 // default by design
@ -60,7 +62,8 @@ StatusListView {
shortName: !!model.shortName ? model.shortName : ""
iconSource: model.iconSource
subItems: model.subItems
enabled: true
selected: root.checkedKeys.includes(model.key)
onItemClicked: root.itemClicked(model.key,
model.name,
model.shortName,

View File

@ -13,6 +13,8 @@ StatusScrollView {
property url titleImage: ""
property string subtitle: ""
property ListModel model
property var checkedKeys: []
property int maxHeight: 381 // default by design
signal itemClicked(var key, string name, url iconSource)
@ -47,6 +49,29 @@ StatusScrollView {
Image {
source: model.imageSource ? model.imageSource : ""
anchors.fill: parent
Rectangle {
width: 32
height: 32
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 8
radius: width / 2
visible: root.checkedKeys.includes(model.key)
// TODO: use color from theme when defined properly in the design
color: "#F5F6F8"
StatusIcon {
anchors.centerIn: parent
icon: "checkmark"
color: Theme.palette.baseColor1
width: 16
height: 16
}
}
}
MouseArea {
anchors.fill: parent

View File

@ -15,6 +15,7 @@ Control {
property string shortName
property url iconSource
property var subItems
property bool selected: false
signal itemClicked(string key, string name, string shortName, url iconSource, var subItems)
@ -71,8 +72,10 @@ Control {
}
StatusIcon {
icon: "tiny/chevron-right"
visible: !!root.subItems && root.subItems.count > 0
readonly property bool hasSubItems: !!root.subItems && root.subItems.count > 0
icon: root.selected && !hasSubItems ? "checkmark" : "tiny/chevron-right"
visible: root.selected || hasSubItems
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 16
color: Theme.palette.baseColor1

View File

@ -83,7 +83,7 @@ ColumnLayout {
Layout.preferredHeight: d.defaultHeight
Layout.fillWidth: true
Layout.topMargin: d.defaultSpacing
visible: root.mode === HoldingTypes.Mode.Update
visible: root.mode === HoldingTypes.Mode.UpdateOrRemove
type: StatusBaseButton.Type.Danger
onClicked: root.removeClicked()

View File

@ -87,6 +87,48 @@ StatusScrollView {
}
property bool isPrivateDirty: false
function getIndexOfKey(key) {
const count = holdingsModel.count
for (let i = 0; i < count; i++)
if (holdingsModel.get(i).key === key)
return i
return -1
}
function getTokenKeysAndAmounts() {
const keysAndAmounts = []
const count = holdingsModel.count
for (let i = 0; i < count; i++) {
const item = holdingsModel.get(i)
if (item.type === HoldingTypes.Type.Ens)
continue
keysAndAmounts.push({ key: item.key, amount: item.amount })
}
return keysAndAmounts
}
function getEnsNames() {
const names = []
const count = holdingsModel.count
for (let i = 0; i < count; i++) {
const item = holdingsModel.get(i)
if (item.type !== HoldingTypes.Type.Ens)
continue
names.push(item.name)
}
return names
}
// TODO: Channels
}
@ -195,7 +237,7 @@ StatusScrollView {
StatusItemSelector {
id: tokensSelector
property int editedIndex
property int editedIndex: -1
Layout.fillWidth: true
icon: Style.svg("contact_verified")
@ -227,6 +269,28 @@ StatusScrollView {
d.dirtyValues.holdingsModel.append({ type, key, name, amount, imageSource })
}
function prepareUpdateIndex(key) {
const itemIndex = tokensSelector.editedIndex
const existingIndex = d.dirtyValues.getIndexOfKey(key)
if (itemIndex !== -1 && existingIndex !== -1 && itemIndex !== existingIndex) {
const previousKey = d.dirtyValues.holdingsModel.get(itemIndex).key
d.dirtyValues.holdingsModel.remove(existingIndex)
return d.dirtyValues.getIndexOfKey(previousKey)
}
if (itemIndex === -1) {
return existingIndex
}
return itemIndex
}
onOpened: {
usedTokens = d.dirtyValues.getTokenKeysAndAmounts()
usedEnsNames = d.dirtyValues.getEnsNames().filter(item => item !== ensDomainName)
}
onAddAsset: {
const modelItem = CommunityPermissionsHelpers.getTokenByKey(store.assetsModel, key)
addItem(HoldingTypes.Type.Asset, modelItem, amount)
@ -240,7 +304,7 @@ StatusScrollView {
}
onAddEns: {
const key = "ENS"
const key = "ENS_" + domain
const icon = Style.svg("profile/ensUsernames")
d.dirtyValues.holdingsModel.append({type: HoldingTypes.Type.Ens, key, name: domain, amount: 1, imageSource: icon })
@ -248,27 +312,31 @@ StatusScrollView {
}
onUpdateAsset: {
const itemIndex = prepareUpdateIndex(key)
const modelItem = CommunityPermissionsHelpers.getTokenByKey(store.assetsModel, key)
const name = modelItem.shortName ? modelItem.shortName : modelItem.name
const imageSource = modelItem.iconSource.toString()
d.dirtyValues.holdingsModel.set(tokensSelector.editedIndex, { type: HoldingTypes.Type.Asset, key, name, amount, imageSource })
d.dirtyValues.holdingsModel.set(itemIndex, { type: HoldingTypes.Type.Asset, key, name, amount, imageSource })
d.triggerDirtyTool = !d.triggerDirtyTool
dropdown.close()
}
onUpdateCollectible: {
const itemIndex = prepareUpdateIndex(key)
const modelItem = CommunityPermissionsHelpers.getTokenByKey(store.collectiblesModel, key)
const name = modelItem.name
const imageSource = modelItem.iconSource.toString()
d.dirtyValues.holdingsModel.set(tokensSelector.editedIndex, { type: HoldingTypes.Type.Collectible, key, name, amount, imageSource })
d.dirtyValues.holdingsModel.set(itemIndex, { type: HoldingTypes.Type.Collectible, key, name, amount, imageSource })
d.triggerDirtyTool = !d.triggerDirtyTool
dropdown.close()
}
onUpdateEns: {
const key = "ENS"
const key = "ENS_" + domain
const icon = Style.svg("profile/ensUsernames")
d.dirtyValues.holdingsModel.set(tokensSelector.editedIndex, { type: HoldingTypes.Type.Ens, key, name: domain, amount: 1, imageSource: icon })
@ -287,6 +355,8 @@ StatusScrollView {
dropdown.x = tokensSelector.addButton.width + d.dropdownHorizontalOffset
dropdown.y = 0
dropdown.open()
editedIndex = -1
}
onItemClicked: {