feat(Community Permissions): Add search option in assets / collectibles dropdown

Closes: #9042
This commit is contained in:
Michał Cieślak 2023-01-26 15:21:35 +01:00 committed by Michał
parent fc5dbf0d2c
commit bd6dc02162
5 changed files with 287 additions and 152 deletions

View File

@ -44,6 +44,18 @@ ListModel {
iconSource: ModelsData.collectibles.kitty5, iconSource: ModelsData.collectibles.kitty5,
imageSource: ModelsData.collectibles.kitty5Big, imageSource: ModelsData.collectibles.kitty5Big,
name: "Magicat-3" name: "Magicat-3"
},
{
key: "Kitty5",
iconSource: ModelsData.collectibles.kitty4,
imageSource: ModelsData.collectibles.kitty4Big,
name: "Furbeard-3"
},
{
key: "Kitty6",
iconSource: ModelsData.collectibles.kitty5,
imageSource: ModelsData.collectibles.kitty5Big,
name: "Magicat-4"
} }
] ]
}, },

View File

@ -1,6 +1,9 @@
import QtQuick 2.13 import QtQuick 2.14
import QtQuick.Controls 2.13 import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.14
import QtQml 2.14
import Qt.labs.settings 1.0
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
@ -19,7 +22,7 @@ Item {
property var checkedKeys: [] property var checkedKeys: []
property int type: ExtendedDropdownContent.Type.Assets property int type: ExtendedDropdownContent.Type.Assets
readonly property bool canGoBack: root.state !== d.listView_depth1_State readonly property bool canGoBack: root.state !== d.depth1_ListState
signal itemClicked(string key, string name, url iconSource) signal itemClicked(string key, string name, url iconSource)
signal navigateDeep(string key, var subItems) signal navigateDeep(string key, var subItems)
@ -30,7 +33,7 @@ Item {
} }
function goBack() { function goBack() {
root.state = d.listView_depth1_State d.reset()
} }
function goForward(key, itemName, itemSource, subItems) { function goForward(key, itemName, itemSource, subItems) {
@ -38,53 +41,81 @@ Item {
d.currentItemKey = key d.currentItemKey = key
d.currentItemName = itemName d.currentItemName = itemName
d.currentItemSource = itemSource d.currentItemSource = itemSource
root.state = d.listView_depth2_State root.state = d.useThumbnailsOnDepth2
? d.depth2_ThumbnailsState : d.depth2_ListState
}
Settings {
property alias useThumbnailsOnDepth2: d.useThumbnailsOnDepth2
} }
QtObject { QtObject {
id: d id: d
readonly property int filterItemsHeight: 36 readonly property int filterItemsHeight: 36
readonly property int filterPopupWidth: 233 readonly property int filterPopupWidth: 233
readonly property int filterPopupHeight: 205
// Internal management properties // Internal management properties
property bool isFilterOptionVisible: false property bool isFilterOptionVisible: false
readonly property string thumbnailsViewState: "THUMBNAILS" property bool useThumbnailsOnDepth2: false
readonly property string listView_depth1_State: "LIST-DEPTH1"
readonly property string listView_depth2_State: "LIST-DEPTH2" readonly property string depth1_ListState: "DEPTH-1-LIST"
property var currentModel: root.store.collectiblesModel readonly property string depth2_ListState: "DEPTH-2-LIST"
property var currentSubitems readonly property string depth2_ThumbnailsState: "DEPTH-2-THUMBNAILS"
property var currentModel: null
property var currentSubitems: null
property string currentItemKey: "" property string currentItemKey: ""
property string currentItemName: "" property string currentItemName: ""
property url currentItemSource: "" property url currentItemSource: ""
readonly property SortFilterProxyModel filtered: SortFilterProxyModel { readonly property bool searchMode: searcher.text.length > 0
id: collectiblesFilteredModel
sourceModel: root.store.collectiblesModel onCurrentModelChanged: {
// Workaround for a bug in SortFilterProxyModel causing that model
// is rendered incorrectly when sourceModel is changed to a model
// with different set of roles
filteredModel.active = false
filteredModel.active = true
filters: ExpressionFilter { searcher.text = ""
expression: { filteredModel.item.sourceModel = currentModel
searcher.text contentLoader.item.model = filteredModel.item
return name.toLowerCase().includes(searcher.text.toLowerCase()) }
readonly property Loader loader_: Loader {
id: filteredModel
sourceComponent: SortFilterProxyModel {
filters: ExpressionFilter {
expression: {
searcher.text
if (model.shortName && model.shortName.toLowerCase()
.includes(searcher.text.toLowerCase()))
return true
return model.name.toLowerCase().includes(
searcher.text.toLowerCase())
}
} }
} }
} }
function reset() { function reset() {
searcher.text = ""
d.currentItemKey = "" d.currentItemKey = ""
d.currentItemName = "" d.currentItemName = ""
d.currentItemSource = "" d.currentItemSource = ""
d.currentModel = root.store.collectiblesModel d.currentSubitems = null
d.currentSubitems = undefined root.state = d.depth1_ListState
root.state = d.listView_depth1_State
} }
} }
state: d.listView_depth1_State state: d.depth1_ListState
states: [ states: [
State { State {
name: d.listView_depth1_State name: d.depth1_ListState
PropertyChanges { PropertyChanges {
target: contentLoader target: contentLoader
@ -94,20 +125,17 @@ Item {
PropertyChanges { PropertyChanges {
target: d target: d
currentModel: root.type === ExtendedDropdownContent.Type.Assets currentModel: root.type === ExtendedDropdownContent.Type.Assets
? root.store.assetsModel : collectiblesFilteredModel//root.store.collectiblesModel ? root.store.assetsModel : root.store.collectiblesModel
isFilterOptionVisible: false isFilterOptionVisible: false
} }
PropertyChanges { PropertyChanges {
target: tokenGroupItem target: tokenGroupItem
visible: false visible: false
} }
PropertyChanges {
target: searcher
visible: type === ExtendedDropdownContent.Type.Collectibles
}
}, },
State { State {
name: d.listView_depth2_State name: d.depth2_ListState
PropertyChanges { PropertyChanges {
target: contentLoader target: contentLoader
@ -124,7 +152,7 @@ Item {
} }
}, },
State { State {
name: d.thumbnailsViewState name: d.depth2_ThumbnailsState
PropertyChanges { PropertyChanges {
target: contentLoader target: contentLoader
@ -164,23 +192,29 @@ Item {
// Filter options popup: // Filter options popup:
StatusDropdown { StatusDropdown {
id: filterOptionsPopup id: filterOptionsPopup
width: d.filterPopupWidth width: d.filterPopupWidth
height: d.filterPopupHeight
contentItem: ColumnLayout { contentItem: ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 8 anchors.topMargin: 8
anchors.bottomMargin: 8 anchors.bottomMargin: 8
ListView {
Layout.fillWidth: true spacing: 0
Layout.preferredHeight: model.count * d.filterItemsHeight
// TODO: it can be simplified by using inline components after
// migration to Qt 5.15 or higher
Repeater {
model: ListModel { model: ListModel {
ListElement { text: qsTr("Most viewed"); selected: true } ListElement { text: qsTr("Most viewed"); selected: true }
ListElement { text: qsTr("Newest first"); selected: false } ListElement { text: qsTr("Newest first"); selected: false }
ListElement { text: qsTr("Oldest first"); selected: false } ListElement { text: qsTr("Oldest first"); selected: false }
} }
delegate: StatusItemPicker { delegate: StatusItemPicker {
width: ListView.view.width Layout.fillWidth: true
height: d.filterItemsHeight Layout.preferredHeight: d.filterItemsHeight
color: sensor1.containsMouse ? Theme.palette.baseColor4 : "transparent" color: sensor1.containsMouse ? Theme.palette.baseColor4 : "transparent"
name: model.text name: model.text
namePixelSize: 13 namePixelSize: 13
@ -201,25 +235,28 @@ Item {
} }
} }
} }
// Not visual element to control filter options
ButtonGroup {
id: filterRadioBtnGroup
}
} }
Separator { Layout.fillWidth: true } ButtonGroup {
id: filterRadioBtnGroup
}
ListView { Separator {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: model.count * d.filterItemsHeight Layout.topMargin: 5
Layout.bottomMargin: 5
}
Repeater {
model: ListModel { model: ListModel {
ListElement { key: "LIST"; text: qsTr("List"); selected: true } ListElement { key: "LIST"; text: qsTr("List"); selected: true }
ListElement { key: "THUMBNAILS"; text: qsTr("Thumbnails"); selected: false } ListElement { key: "THUMBNAILS"; text: qsTr("Thumbnails"); selected: false }
} }
delegate: StatusItemPicker {
width: ListView.view.width delegate: StatusItemPicker {
height: d.filterItemsHeight Layout.fillWidth: true
Layout.preferredHeight: d.filterItemsHeight
color: sensor2.containsMouse ? Theme.palette.baseColor4 : "transparent" color: sensor2.containsMouse ? Theme.palette.baseColor4 : "transparent"
name: model.text name: model.text
namePixelSize: 13 namePixelSize: 13
@ -236,21 +273,28 @@ Item {
onClicked: { onClicked: {
selected = !selected selected = !selected
if(model.key === "LIST") { if(model.key === "LIST") {
root.state = d.listView_depth2_State root.state = d.depth2_ListState
} }
else if(model.key === "THUMBNAILS") { else if(model.key === "THUMBNAILS") {
root.state = d.thumbnailsViewState root.state = d.depth2_ThumbnailsState
} }
filterOptionsPopup.close() filterOptionsPopup.close()
} }
} }
}
// Not visual element to control visualization options Binding {
ButtonGroup { target: d
id: visualizationRadioBtnGroup when: model.key === "THUMBNAILS" && selected
property: "useThumbnailsOnDepth2"
value: true
restoreMode: Binding.RestoreBindingOrValue
}
} }
} }
ButtonGroup {
id: visualizationRadioBtnGroup
}
} }
} }
@ -263,12 +307,20 @@ Item {
id: searcher id: searcher
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: root.state === d.depth1_ListState ? 0 : 8
visible: false
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
minimumHeight: 36 minimumHeight: 36
maximumHeight: 36 maximumHeight: 36
placeholderText: root.type === ExtendedDropdownContent.Type.Assets ?
qsTr("Search assets") : qsTr("Search collectibles")
Binding on placeholderText{
when: d.currentItemName !== ""
value: qsTr("Search %1").arg(d.currentItemName)
}
} }
TokenItem { TokenItem {
@ -277,7 +329,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
key: d.currentItemKey key: d.currentItemKey
name: d.currentItemName name: qsTr("Any %1").arg(d.currentItemName)
iconSource: d.currentItemSource iconSource: d.currentItemSource
selected: root.checkedKeys.includes(key) selected: root.checkedKeys.includes(key)
@ -292,7 +344,6 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
} }
} }
@ -304,9 +355,9 @@ Item {
ListElement { key: "MINT"; icon: "add"; iconSize: 16; description: qsTr("Mint asset"); rotation: 0; spacing: 8 } ListElement { key: "MINT"; icon: "add"; iconSize: 16; description: qsTr("Mint asset"); rotation: 0; spacing: 8 }
ListElement { key: "IMPORT"; icon: "invite-users"; iconSize: 16; description: qsTr("Import existing asset"); rotation: 180; spacing: 8 } ListElement { key: "IMPORT"; icon: "invite-users"; iconSize: 16; description: qsTr("Import existing asset"); rotation: 180; spacing: 8 }
} }
isHeaderVisible: false // TEMPORARILY hidden. These 2 header options will be implemented after MVP. areHeaderButtonsVisible: false // TEMPORARILY hidden. These 2 header options will be implemented after MVP.
model: d.currentModel
checkedKeys: root.checkedKeys checkedKeys: root.checkedKeys
searchMode: d.searchMode
onHeaderItemClicked: { onHeaderItemClicked: {
if(key === "MINT") console.log("TODO: Mint asset") if(key === "MINT") console.log("TODO: Mint asset")
@ -320,25 +371,26 @@ Item {
id: collectiblesListView id: collectiblesListView
ListDropdownContent { ListDropdownContent {
isHeaderVisible: root.state === d.listView_depth1_State areHeaderButtonsVisible: root.state === d.depth1_ListState
headerModel: ListModel { headerModel: ListModel {
ListElement { key: "MINT"; icon: "add"; iconSize: 16; description: qsTr("Mint collectible"); rotation: 0; spacing: 8 } ListElement { key: "MINT"; icon: "add"; iconSize: 16; description: qsTr("Mint collectible"); rotation: 0; spacing: 8 }
} }
model: d.currentModel
checkedKeys: root.checkedKeys checkedKeys: root.checkedKeys
searchMode: d.searchMode
onHeaderItemClicked: { onHeaderItemClicked: {
if(key === "MINT") console.log("TODO: Mint collectible") if(key === "MINT") console.log("TODO: Mint collectible")
} }
onItemClicked: { onItemClicked: {
if(subItems && root.state === d.listView_depth1_State) { if(subItems && root.state === d.depth1_ListState) {
// One deep navigation // One deep navigation
d.currentSubitems = subItems d.currentSubitems = subItems
d.currentItemKey = key d.currentItemKey = key
d.currentItemName = name d.currentItemName = name
d.currentItemSource = iconSource d.currentItemSource = iconSource
root.state = d.listView_depth2_State root.state = d.useThumbnailsOnDepth2
? d.depth2_ThumbnailsState : d.depth2_ListState
root.navigateDeep(key, subItems) root.navigateDeep(key, subItems)
} }
else { else {
@ -355,12 +407,10 @@ Item {
ThumbnailsDropdownContent { ThumbnailsDropdownContent {
title: d.currentItemName title: d.currentItemName
titleImage: d.currentItemSource titleImage: d.currentItemSource
checkedKeys: root.checkedKeys
padding: 0 padding: 0
model: d.currentModel
checkedKeys: root.checkedKeys
onItemClicked: { onItemClicked: {
d.reset() d.reset()
root.itemClicked(key, name, iconSource) root.itemClicked(key, name, iconSource)

View File

@ -204,7 +204,7 @@ StatusDropdown {
Repeater { Repeater {
id: tabLabelsRepeater id: tabLabelsRepeater
model: [qsTr("Asset"), qsTr("Collectible"), qsTr("ENS")] model: [qsTr("Assets"), qsTr("Collectibles"), qsTr("ENS")]
StatusSwitchTabButton { StatusSwitchTabButton {
text: modelData text: modelData

View File

@ -15,7 +15,9 @@ StatusListView {
property var checkedKeys: [] property var checkedKeys: []
property var headerModel property var headerModel
property bool isHeaderVisible: true property bool areHeaderButtonsVisible: true
property bool searchMode: false
property int maxHeight: 381 // default by design property int maxHeight: 381 // default by design
signal headerItemClicked(string key) signal headerItemClicked(string key)
@ -25,36 +27,56 @@ StatusListView {
implicitHeight: Math.min(contentHeight, root.maxHeight) implicitHeight: Math.min(contentHeight, root.maxHeight)
currentIndex: -1 currentIndex: -1
clip: true clip: true
header: Rectangle {
visible: root.isHeaderVisible header: ColumnLayout {
z: 3 // Above delegate (z=1) and above section.delegate (z = 2)
color: Theme.palette.statusMenu.backgroundColor
width: root.width width: root.width
height: root.isHeaderVisible ? columnHeader.implicitHeight + 2 * columnHeader.anchors.topMargin : 0
ColumnLayout { spacing: 0
id: columnHeader
anchors.top: parent.top Item {
anchors.left: parent.left Layout.fillWidth: true
anchors.leftMargin: 6 Layout.preferredHeight: root.areHeaderButtonsVisible
anchors.rightMargin: anchors.leftMargin ? columnHeader.implicitHeight + 2 * columnHeader.anchors.topMargin
anchors.topMargin: 8 : 0
anchors.bottomMargin: 2 * anchors.topMargin
spacing: 20 visible: root.areHeaderButtonsVisible
Repeater { z: 3 // Above delegate (z=1) and above section.delegate (z = 2)
model: root.headerModel
delegate: StatusIconTextButton { ColumnLayout {
z: 3 // Above delegate (z=1) and above section.delegate (z = 2) id: columnHeader
spacing: model.spacing
statusIcon: model.icon anchors.top: parent.top
icon.width: model.iconSize anchors.left: parent.left
icon.height: model.iconSize anchors.leftMargin: 6
iconRotation: model.rotation anchors.rightMargin: anchors.leftMargin
text: model.description anchors.topMargin: 8
onClicked: root.headerItemClicked(model.index) anchors.bottomMargin: 2 * anchors.topMargin
spacing: 20
Repeater {
model: root.headerModel
delegate: StatusIconTextButton {
z: 3 // Above delegate (z=1) and above section.delegate (z = 2)
spacing: model.spacing
statusIcon: model.icon
icon.width: model.iconSize
icon.height: model.iconSize
iconRotation: model.rotation
text: model.description
onClicked: root.headerItemClicked(model.index)
}
} }
} }
} }
}// End of Header
Loader {
Layout.preferredHeight: visible ? d.sectionHeight : 0
Layout.fillWidth: true
visible: root.searchMode
sourceComponent: sectionComponent
}
}
delegate: TokenItem { delegate: TokenItem {
width: ListView.view.width width: ListView.view.width
key: model.key key: model.key
@ -70,19 +92,52 @@ StatusListView {
model.iconSource, model.iconSource,
model.subItems) model.subItems)
} }
section.property: "category"
section.property: root.searchMode ? "" : "category"
section.criteria: ViewSection.FullString section.criteria: ViewSection.FullString
section.delegate: Item { section.delegate: Item {
width: ListView.view.width width: ListView.view.width
height: 34 // by design height: d.sectionHeight
StatusBaseText {
anchors.leftMargin: 8 Loader {
anchors.left: parent.left id: loader
anchors.verticalCenter: parent.verticalCenter anchors.fill: parent
text: section sourceComponent: sectionComponent
color: Theme.palette.baseColor1
font.pixelSize: 12 Binding {
elide: Text.ElideRight target: loader.item
property: "section"
value: section
}
} }
}// End of Category item }
}// End of Root
QtObject {
id: d
readonly property int sectionHeight: 34
}
Component {
id: sectionComponent
Item {
id: sectionDelegateRoot
property string section: root.model && root.model.count ?
qsTr("Search result") :
qsTr("No results")
StatusBaseText {
anchors.leftMargin: 8
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: sectionDelegateRoot.section
color: Theme.palette.baseColor1
font.pixelSize: 12
elide: Text.ElideRight
}
}
}
}

View File

@ -12,7 +12,7 @@ StatusScrollView {
property string title: "" property string title: ""
property url titleImage: "" property url titleImage: ""
property string subtitle: "" property string subtitle: ""
property ListModel model property var model
property var checkedKeys: [] property var checkedKeys: []
property int maxHeight: 381 // default by design property int maxHeight: 381 // default by design
@ -30,64 +30,82 @@ StatusScrollView {
clip: true clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
GridLayout { ColumnLayout {
id: grid
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
columnSpacing: 8
rowSpacing: 12
columns: d.columns
Repeater { StatusBaseText {
model: root.model Layout.leftMargin: 8
delegate: ColumnLayout { Layout.topMargin: 8
spacing: 4
Rectangle {
Layout.preferredWidth: 133
Layout.preferredHeight: 133
color: "transparent"
Image {
source: model.imageSource ? model.imageSource : ""
anchors.fill: parent
Rectangle { visible: !!model ? model.count === 0 : false
width: 32
height: 32
anchors.bottom: parent.bottom Layout.fillWidth: true
anchors.right: parent.right
anchors.margins: 8
radius: width / 2 text: qsTr("No results")
visible: root.checkedKeys.includes(model.key) color: Theme.palette.baseColor1
// TODO: use color from theme when defined properly in the design font.pixelSize: 12
color: "#F5F6F8" wrapMode: Text.Wrap
}
StatusIcon { GridLayout {
anchors.centerIn: parent id: grid
icon: "checkmark" Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
columnSpacing: 8
rowSpacing: 12
columns: d.columns
color: Theme.palette.baseColor1 Repeater {
width: 16 model: root.model
height: 16 delegate: ColumnLayout {
spacing: 4
Rectangle {
Layout.preferredWidth: 133
Layout.preferredHeight: 133
color: "transparent"
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
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: { root.itemClicked(model.key, model.name, model.iconSource) }
}
} }
MouseArea { StatusBaseText {
anchors.fill: parent Layout.alignment: Qt.AlignLeft
cursorShape: Qt.PointingHandCursor Layout.leftMargin: 8
hoverEnabled: true text: model.name
onClicked: { root.itemClicked(model.key, model.name, model.iconSource) } color: Theme.palette.directColor1
font.pixelSize: 13
elide: Text.ElideRight
} }
} }
StatusBaseText {
Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 8
text: model.name
color: Theme.palette.directColor1
font.pixelSize: 13
elide: Text.ElideRight
}
} }
} }
} }