feat(ProfileShowcase): Add search input in all tabs

- Added searcher input in header.
- Added 2 filter proxies for hidden and inshowcase models depending on search text.
- Added filter expression per showcase tab (accounts, collectibles and communities).
- Added specific placeholder when search empty.
- Added specific logic when search active.
- Added search validation.

Closes #13508
This commit is contained in:
Noelia 2024-03-03 17:34:08 +01:00 committed by Noelia
parent 418d6bcc35
commit a9b5d8fcf7
11 changed files with 258 additions and 118 deletions

View File

@ -63,62 +63,72 @@ SplitView {
}
}
ProfileShowcasePanel {
inShowcaseModel: inShowcaseModelItem
hiddenModel: hiddenModelItem
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
emptyInShowcasePlaceholderText: "No items in showcase"
emptyHiddenPlaceholderText: "No hidden items"
showcaseLimit: limitCounter.value
onChangePositionRequested: function (from, to) {
inShowcaseModelItem.move(from, to, 1)
}
onSetVisibilityRequested: function (key, toVisibility) {
for (var i = 0; i < inShowcaseModelItem.count; i++) {
if (inShowcaseModelItem.get(i).showcaseKey === key) {
inShowcaseModelItem.setProperty(i, "showcaseVisibility", toVisibility)
if(toVisibility === 0) {
let item = inShowcaseModelItem.get(i)
hiddenModelItem.append(item)
inShowcaseModelItem.remove(i, 1)
ProfileShowcasePanel {
id: panel
inShowcaseModel: inShowcaseModelItem
hiddenModel: hiddenModelItem
anchors.centerIn: parent
width: parent.width - 16
height: parent.height - 16
emptyInShowcasePlaceholderText: "No items in showcase"
emptyHiddenPlaceholderText: "No hidden items"
showcaseLimit: limitCounter.value
searchPlaceholderText: qsTr("Search not available in storybook")
onChangePositionRequested: function (from, to) {
inShowcaseModelItem.move(from, to, 1)
}
onSetVisibilityRequested: function (key, toVisibility) {
for (var i = 0; i < inShowcaseModelItem.count; i++) {
if (inShowcaseModelItem.get(i).showcaseKey === key) {
inShowcaseModelItem.setProperty(i, "showcaseVisibility", toVisibility)
if(toVisibility === 0) {
let item = inShowcaseModelItem.get(i)
hiddenModelItem.append(item)
inShowcaseModelItem.remove(i, 1)
}
return
}
}
for (var i = 0; i < hiddenModelItem.count; i++) {
if (hiddenModelItem.get(i).showcaseKey === key) {
hiddenModelItem.setProperty(i, "showcaseVisibility", toVisibility)
if(toVisibility !== 0) {
let item = hiddenModelItem.get(i)
inShowcaseModelItem.append(item)
hiddenModelItem.remove(i, 1)
}
return
}
return
}
}
for (var i = 0; i < hiddenModelItem.count; i++) {
if (hiddenModelItem.get(i).showcaseKey === key) {
hiddenModelItem.setProperty(i, "showcaseVisibility", toVisibility)
if(toVisibility !== 0) {
let item = hiddenModelItem.get(i)
inShowcaseModelItem.append(item)
hiddenModelItem.remove(i, 1)
delegate: ProfileShowcasePanelDelegate {
id: delegate
title: model ? model.title : ""
secondaryTitle: model ? model.secondaryTitle : ""
hasImage: model ? model.hasImage : false
icon.name: model ? model.iconName : ""
icon.source: model ? model.image : ""
icon.color: model ? model.color : ""
actionComponent: model && model.hasTag ? manageTokensCommunityTag : null
Component {
id: manageTokensCommunityTag
ManageTokensCommunityTag {
Layout.maximumWidth: delegate.width *.4
text: model ? model.tagText : ""
asset.name: model ? model.tagAsset : ""
loading: model ? model.tagLoading : false
}
return
}
}
}
delegate: ProfileShowcasePanelDelegate {
id: delegate
title: model ? model.title : ""
secondaryTitle: model ? model.secondaryTitle : ""
hasImage: model ? model.hasImage : false
icon.name: model ? model.iconName : ""
icon.source: model ? model.image : ""
icon.color: model ? model.color : ""
actionComponent: model && model.hasTag ? manageTokensCommunityTag : null
Component {
id: manageTokensCommunityTag
ManageTokensCommunityTag {
Layout.maximumWidth: delegate.width *.4
text: model ? model.tagText : ""
asset.name: model ? model.tagAsset : ""
loading: model ? model.tagLoading : false
}
}
}

View File

@ -16,6 +16,8 @@ import utils 1.0
* position, second one containing hidden items.
*/
QObject {
id: root
property alias sourceModel: joined.leftModel
property alias showcaseModel: joined.rightModel
@ -36,6 +38,11 @@ QObject {
*/
readonly property bool dirty: writable.dirty || !visibleModel.synced
/**
* It sets up a searcher filter on top of both the visible and hidden models.
*/
property FastExpressionFilter searcherFilter
function revert() {
visible.syncOrder()
writable.revert()
@ -105,16 +112,32 @@ QObject {
sorters: RoleSorter { roleName: "showcasePosition" }
}
SortFilterProxyModel {
id: searcherVisibleSFPM
sourceModel: visibleSFPM
delayed: true
filters: root.searcherFilter
}
MovableModel {
id: visible
sourceModel: visibleSFPM
sourceModel: searcherVisibleSFPM
}
SortFilterProxyModel {
id: searcherHiddenSFPM
sourceModel: writable
delayed: true
filters: root.searcherFilter
}
SortFilterProxyModel {
id: hidden
sourceModel: writable
sourceModel: searcherHiddenSFPM
delayed: true
filters: HiddenFilter {}

View File

@ -24,6 +24,7 @@ QObject {
// Input models
property alias communitiesSourceModel: modelAdapter.communitiesSourceModel
property alias communitiesShowcaseModel: modelAdapter.communitiesShowcaseModel
property string communitiesSearcherText
// Output models
readonly property alias communitiesVisibleModel: communities.visibleModel
@ -47,6 +48,7 @@ QObject {
// Input models
property alias accountsSourceModel: modelAdapter.accountsSourceModel
property alias accountsShowcaseModel: modelAdapter.accountsShowcaseModel
property string accountsSearcherText
// Output models
readonly property alias accountsVisibleModel: accounts.visibleModel
@ -77,6 +79,7 @@ QObject {
// Input models
property alias collectiblesSourceModel: modelAdapter.collectiblesSourceModel
property alias collectiblesShowcaseModel: modelAdapter.collectiblesShowcaseModel
property string collectiblesSearcherText
// Output models
readonly property alias collectiblesVisibleModel: collectibles.visibleModel
@ -102,8 +105,20 @@ QObject {
ProfileShowcaseDirtyState {
id: communities
function getMemberRole(memberRole) {
return ProfileUtils.getMemberRoleText(memberRole)
}
sourceModel: modelAdapter.adaptedCommunitiesSourceModel
showcaseModel: modelAdapter.adaptedCommunitiesShowcaseModel
searcherFilter: FastExpressionFilter {
expression: {
root.communitiesSearcherText
return (name.toLowerCase().includes(root.communitiesSearcherText.toLowerCase()) ||
communities.getMemberRole(memberRole).toLowerCase().includes(root.communitiesSearcherText.toLowerCase()))
}
expectedRoles: ["name", "memberRole"]
}
}
ProfileShowcaseDirtyState {
@ -111,6 +126,14 @@ QObject {
sourceModel: modelAdapter.adaptedAccountsSourceModel
showcaseModel: modelAdapter.adaptedAccountsShowcaseModel
searcherFilter: FastExpressionFilter {
expression: {
root.accountsSearcherText
return (address.toLowerCase().includes(root.accountsSearcherText.toLowerCase()) ||
name.toLowerCase().includes( root.accountsSearcherText.toLowerCase()))
}
expectedRoles: ["address", "name"]
}
}
ProfileShowcaseDirtyState {
@ -118,6 +141,16 @@ QObject {
sourceModel: collectiblesFilter
showcaseModel: modelAdapter.adaptedCollectiblesShowcaseModel
searcherFilter: FastExpressionFilter {
expression: {
root.collectiblesSearcherText
return (name.toLowerCase().includes(root.collectiblesSearcherText.toLowerCase()) ||
uid.toLowerCase().includes(root.collectiblesSearcherText.toLowerCase()) ||
communityName.toLowerCase().includes(root.collectiblesSearcherText.toLowerCase()) ||
collectionName.toLowerCase().includes(root.collectiblesSearcherText.toLowerCase()))
}
expectedRoles: ["name", "uid", "collectionName", "communityName"]
}
}
SortFilterProxyModel {

View File

@ -7,6 +7,8 @@ import AppLayouts.Wallet 1.0
import StatusQ.Core.Theme 0.1
import StatusQ 0.1
ProfileShowcasePanel {
id: root
@ -14,7 +16,8 @@ ProfileShowcasePanel {
emptyInShowcasePlaceholderText: qsTr("Accounts here will show on your profile")
emptyHiddenPlaceholderText: qsTr("Accounts here will be hidden from your profile")
emptySearchPlaceholderText: qsTr("No accounts matching search")
searchPlaceholderText: qsTr("Search account name or address")
delegate: ProfileShowcasePanelDelegate {
title: model ? model.name : ""
secondaryTitle: WalletUtils.addressToDisplay(model ? model.address ?? "" : "", "", true, containsMouse)

View File

@ -21,7 +21,8 @@ ProfileShowcasePanel {
emptyInShowcasePlaceholderText: qsTr("Assets here will show on your profile")
emptyHiddenPlaceholderText: qsTr("Assets here will be hidden from your profile")
emptySearchPlaceholderText: qsTr("No assets matching search")
searchPlaceholderText: qsTr("Search asset name, symbol or community")
delegate: ProfileShowcasePanelDelegate {
readonly property double totalValue: !!model && !!model.decimals ? balancesAggregator.value/(10 ** model.decimals): 0

View File

@ -1,6 +1,7 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
@ -19,7 +20,8 @@ ProfileShowcasePanel {
emptyInShowcasePlaceholderText: qsTr("Collectibles here will show on your profile")
emptyHiddenPlaceholderText: qsTr("Collectibles here will be hidden from your profile")
emptySearchPlaceholderText: qsTr("No collectibles matching search")
searchPlaceholderText: qsTr("Search collectible name, number, collection or community")
additionalFooterComponent: root.addAccountsButtonVisible ? addMoreAccountsComponent : null
delegate: ProfileShowcasePanelDelegate {
@ -47,9 +49,9 @@ ProfileShowcasePanel {
id: addMoreAccountsComponent
AddMoreAccountsLink {
visible: root.addAccountsButtonVisible
text: qsTr("Dont see some of your collectibles?")
onClicked: root.navigateToAccountsTab()
visible: root.addAccountsButtonVisible
text: qsTr("Dont see some of your collectibles?")
onClicked: root.navigateToAccountsTab()
}
}
}

View File

@ -1,5 +1,6 @@
import QtQuick 2.15
import StatusQ 0.1
import utils 1.0
import AppLayouts.Profile.controls 1.0
@ -9,12 +10,11 @@ ProfileShowcasePanel {
emptyInShowcasePlaceholderText: qsTr("Drag communities here to display in showcase")
emptyHiddenPlaceholderText: qsTr("Communities here will be hidden from your Profile")
emptySearchPlaceholderText: qsTr("No communities matching search")
searchPlaceholderText: qsTr("Search community name or role")
delegate: ProfileShowcasePanelDelegate {
title: model ? model.name : ""
secondaryTitle: model && (model.memberRole === Constants.memberRole.owner ||
model.memberRole === Constants.memberRole.admin ||
model.memberRole === Constants.memberRole.tokenMaster) ? qsTr("Admin") : qsTr("Member")
secondaryTitle: (model && model.memberRole) ? ProfileUtils.getMemberRoleText(model.memberRole) : qsTr("Member")
hasImage: model && !!model.image
icon.name: model ? model.name : ""

View File

@ -4,16 +4,20 @@ import QtQuick.Layouts 1.15
import QtQml 2.15
import QtQml.Models 2.15
import StatusQ 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls.Validators 0.1
import shared.controls 1.0
import utils 1.0
import AppLayouts.Profile.controls 1.0
import SortFilterProxyModel 0.2
DoubleFlickableWithFolding {
id: root
@ -29,9 +33,14 @@ DoubleFlickableWithFolding {
// Placeholder text to be shown when the list is empty
property string emptyInShowcasePlaceholderText
property string emptyHiddenPlaceholderText
property string emptySearchPlaceholderText
property int showcaseLimit: ProfileUtils.showcaseLimit
// Searcher related properties:
property string searchPlaceholderText
property string searcherText: ""
// Signal to request position change of the visible items
signal changePositionRequested(int from, int to)
@ -47,6 +56,7 @@ DoubleFlickableWithFolding {
id: d
readonly property bool limitReached: root.showcaseLimit === inShowcaseCounterTracker.count
readonly property bool searchActive: root.searcherText !== ""
readonly property var dragHiddenItemKey: ["x-status-draggable-showcase-item-hidden"]
readonly property var dragShowcaseItemKey: ["x-status-draggable-showcase-item"]
@ -84,76 +94,119 @@ DoubleFlickableWithFolding {
flickable1: EmptyShapeRectangleFooterListView {
id: inShowcaseListView
model: root.inShowcaseModel
width: root.width
placeholderText: root.emptyInShowcasePlaceholderText
placeholderText: d.searchActive ? root.emptySearchPlaceholderText : root.emptyInShowcasePlaceholderText
footerHeight: ProfileUtils.defaultDelegateHeight
footerContentVisible: !dropAreaRow.visible
spacing: Style.current.halfPadding
delegate: delegateWrapper
model: root.inShowcaseModel
header: FoldableHeader {
readonly property bool isDropAreaVisible: root.flickable1Folded && d.isAnyHiddenDragActive
header: ColumnLayout {
width: ListView.view.width
title: qsTr("In showcase")
folded: root.flickable1Folded
rightAdditionalComponent: isDropAreaVisible && d.limitReached ? limitReachedHeaderButton :
isDropAreaVisible ? dropHeaderAreaComponent : counterComponent
spacing: 0
Component {
id: counterComponent
StatusBaseText {
id: counterText
SearchBox {
id: searcher
width: d.additionalHeaderComponentWidth
height: d.additionalHeaderComponentHeight
horizontalAlignment: Text.AlignRight
text: "%1 / %2".arg(inShowcaseCounterTracker.count).arg(root.showcaseLimit)
font.pixelSize: Style.current.tertiaryTextFontSize
color: Theme.palette.baseColor1
Layout.fillWidth: true
ColorAnimation {
id: animateColor
target: counterText
properties: "color"
from: Theme.palette.successColor1
to: Theme.palette.baseColor1
duration: 2000
placeholderText: root.searchPlaceholderText
validators: [
StatusValidator {
property bool isEmoji: false
name: "check-for-no-emojis"
validate: (value) => {
if (!value) {
return true
}
isEmoji = Constants.regularExpressions.emoji.test(value)
if (isEmoji){
return false
}
return Constants.regularExpressions.alphanumericalExpanded1.test(value)
}
errorMessage: isEmoji ?
qsTr("Your search is too cool (use A-Z and 0-9, hyphens and underscores only)")
: qsTr("Your search contains invalid characters (use A-Z and 0-9, hyphens and underscores only)")
}
]
Connections {
target: d
function onStartAnimationChanged() {
animateColor.start()
Binding {
target: root
property: "searcherText"
value: searcher.text
restoreMode: Binding.RestoreBindingOrValue
}
}
FoldableHeader {
readonly property bool isDropAreaVisible: root.flickable1Folded && d.isAnyHiddenDragActive
Layout.fillWidth: true
title: qsTr("In showcase")
folded: root.flickable1Folded
rightAdditionalComponent: isDropAreaVisible && d.limitReached ? limitReachedHeaderButton :
isDropAreaVisible ? dropHeaderAreaComponent : counterComponent
Component {
id: counterComponent
StatusBaseText {
id: counterText
width: d.additionalHeaderComponentWidth
height: d.additionalHeaderComponentHeight
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
text: "%1 / %2".arg(inShowcaseCounterTracker.count).arg(root.showcaseLimit)
font.pixelSize: Style.current.tertiaryTextFontSize
color: Theme.palette.baseColor1
ColorAnimation {
id: animateColor
target: counterText
properties: "color"
from: Theme.palette.successColor1
to: Theme.palette.baseColor1
duration: 2000
}
Connections {
target: d
function onStartAnimationChanged() {
animateColor.start()
}
}
}
}
}
Component {
id: dropHeaderAreaComponent
VisibilityDropAreaButtonsRow {
margins: 0
width: d.additionalHeaderComponentWidth
height: d.additionalHeaderComponentHeight
Component {
id: dropHeaderAreaComponent
VisibilityDropAreaButtonsRow {
margins: 0
width: d.additionalHeaderComponentWidth
height: d.additionalHeaderComponentHeight
}
}
}
Component {
id: limitReachedHeaderButton
VisibilityDropAreaButton {
width: d.additionalHeaderComponentWidth
height: d.additionalHeaderComponentHeight
rightInset: 1
text: qsTr("Showcase limit of %1 reached").arg(root.showcaseLimit)
enabled: false
textColor: Theme.palette.baseColor1
iconVisible: false
Component {
id: limitReachedHeaderButton
VisibilityDropAreaButton {
width: d.additionalHeaderComponentWidth
height: d.additionalHeaderComponentHeight
rightInset: 1
text: qsTr("Showcase limit of %1 reached").arg(root.showcaseLimit)
enabled: false
textColor: Theme.palette.baseColor1
iconVisible: false
}
}
}
onToggleFolding: root.flip1Folding()
onToggleFolding: root.flip1Folding()
}
}
// Overlaid showcase listview content drop area:
@ -199,14 +252,14 @@ DoubleFlickableWithFolding {
flickable2: EmptyShapeRectangleFooterListView {
id: hiddenListView
model: root.hiddenModel
width: root.width
placeholderText: root.emptyHiddenPlaceholderText
placeholderText: d.searchActive ? root.emptySearchPlaceholderText : root.emptyHiddenPlaceholderText
footerHeight: ProfileUtils.defaultDelegateHeight
footerContentVisible: !hiddenDropAreaButton.visible
additionalFooterComponent: root.additionalFooterComponent
spacing: Style.current.halfPadding
delegate: delegateWrapper
model: root.hiddenModel
header: FoldableHeader {
width: ListView.view.width
@ -358,7 +411,7 @@ DoubleFlickableWithFolding {
color: Theme.palette.baseColor5
radius: Style.current.radius
}
Component {
id: delegateWrapper
DropArea {

View File

@ -1,6 +1,7 @@
import QtQuick 2.13
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.13
import QtQml 2.15
import utils 1.0
import shared 1.0
@ -129,12 +130,15 @@ SettingsContentBase {
property ProfileShowcaseModels showcaseModels: ProfileShowcaseModels {
communitiesSourceModel: root.communitiesModel
communitiesShowcaseModel: root.profileStore.profileShowcaseCommunitiesModel
communitiesSearcherText: profileShowcaseCommunitiesPanel.searcherText
accountsSourceModel: root.walletStore.accounts
accountsShowcaseModel: root.profileStore.profileShowcaseAccountsModel
accountsSearcherText: profileShowcaseAccountsPanel.searcherText
collectiblesSourceModel: root.profileStore.collectiblesModel
collectiblesShowcaseModel: root.profileStore.profileShowcaseCollectiblesModel
collectiblesSearcherText: profileShowcaseCollectiblesPanel.searcherText
}
function reset() {
@ -211,7 +215,7 @@ SettingsContentBase {
ProfileShowcaseCommunitiesPanel {
id: profileShowcaseCommunitiesPanel
inShowcaseModel: priv.showcaseModels.communitiesVisibleModel
hiddenModel: priv.showcaseModels.communitiesHiddenModel
hiddenModel: priv.showcaseModels.communitiesHiddenModel
onChangePositionRequested: function (from, to) {
priv.showcaseModels.changeCommunityPosition(from, to)

View File

@ -424,8 +424,6 @@ QtObject {
readonly property QtObject memberRole: QtObject{
readonly property int none: 0
readonly property int owner: 1
readonly property int manageUsers: 2
readonly property int moderateContent: 3
readonly property int admin: 4
readonly property int tokenMaster: 5
}

View File

@ -93,4 +93,17 @@ QtObject {
return "hide"
}
}
// Member role names:
function getMemberRoleText(memberRole) {
switch(memberRole) {
case Constants.memberRole.owner:
return qsTr("Owner")
case Constants.memberRole.admin:
return qsTr("Admin")
case Constants.memberRole.tokenMaster:
return qsTr("TokenMaster")
}
return qsTr("Member")
}
}