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,14 +63,23 @@ SplitView {
} }
} }
ProfileShowcasePanel { Item {
inShowcaseModel: inShowcaseModelItem
hiddenModel: hiddenModelItem
SplitView.fillWidth: true SplitView.fillWidth: true
SplitView.fillHeight: true SplitView.fillHeight: true
ProfileShowcasePanel {
id: panel
inShowcaseModel: inShowcaseModelItem
hiddenModel: hiddenModelItem
anchors.centerIn: parent
width: parent.width - 16
height: parent.height - 16
emptyInShowcasePlaceholderText: "No items in showcase" emptyInShowcasePlaceholderText: "No items in showcase"
emptyHiddenPlaceholderText: "No hidden items" emptyHiddenPlaceholderText: "No hidden items"
showcaseLimit: limitCounter.value showcaseLimit: limitCounter.value
searchPlaceholderText: qsTr("Search not available in storybook")
onChangePositionRequested: function (from, to) { onChangePositionRequested: function (from, to) {
inShowcaseModelItem.move(from, to, 1) inShowcaseModelItem.move(from, to, 1)
} }
@ -123,6 +132,7 @@ SplitView {
} }
} }
} }
}
LogsAndControlsPanel { LogsAndControlsPanel {
id: logsAndControlsPanel id: logsAndControlsPanel

View File

@ -16,6 +16,8 @@ import utils 1.0
* position, second one containing hidden items. * position, second one containing hidden items.
*/ */
QObject { QObject {
id: root
property alias sourceModel: joined.leftModel property alias sourceModel: joined.leftModel
property alias showcaseModel: joined.rightModel property alias showcaseModel: joined.rightModel
@ -36,6 +38,11 @@ QObject {
*/ */
readonly property bool dirty: writable.dirty || !visibleModel.synced 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() { function revert() {
visible.syncOrder() visible.syncOrder()
writable.revert() writable.revert()
@ -105,16 +112,32 @@ QObject {
sorters: RoleSorter { roleName: "showcasePosition" } sorters: RoleSorter { roleName: "showcasePosition" }
} }
SortFilterProxyModel {
id: searcherVisibleSFPM
sourceModel: visibleSFPM
delayed: true
filters: root.searcherFilter
}
MovableModel { MovableModel {
id: visible id: visible
sourceModel: visibleSFPM sourceModel: searcherVisibleSFPM
}
SortFilterProxyModel {
id: searcherHiddenSFPM
sourceModel: writable
delayed: true
filters: root.searcherFilter
} }
SortFilterProxyModel { SortFilterProxyModel {
id: hidden id: hidden
sourceModel: writable sourceModel: searcherHiddenSFPM
delayed: true delayed: true
filters: HiddenFilter {} filters: HiddenFilter {}

View File

@ -24,6 +24,7 @@ QObject {
// Input models // Input models
property alias communitiesSourceModel: modelAdapter.communitiesSourceModel property alias communitiesSourceModel: modelAdapter.communitiesSourceModel
property alias communitiesShowcaseModel: modelAdapter.communitiesShowcaseModel property alias communitiesShowcaseModel: modelAdapter.communitiesShowcaseModel
property string communitiesSearcherText
// Output models // Output models
readonly property alias communitiesVisibleModel: communities.visibleModel readonly property alias communitiesVisibleModel: communities.visibleModel
@ -47,6 +48,7 @@ QObject {
// Input models // Input models
property alias accountsSourceModel: modelAdapter.accountsSourceModel property alias accountsSourceModel: modelAdapter.accountsSourceModel
property alias accountsShowcaseModel: modelAdapter.accountsShowcaseModel property alias accountsShowcaseModel: modelAdapter.accountsShowcaseModel
property string accountsSearcherText
// Output models // Output models
readonly property alias accountsVisibleModel: accounts.visibleModel readonly property alias accountsVisibleModel: accounts.visibleModel
@ -77,6 +79,7 @@ QObject {
// Input models // Input models
property alias collectiblesSourceModel: modelAdapter.collectiblesSourceModel property alias collectiblesSourceModel: modelAdapter.collectiblesSourceModel
property alias collectiblesShowcaseModel: modelAdapter.collectiblesShowcaseModel property alias collectiblesShowcaseModel: modelAdapter.collectiblesShowcaseModel
property string collectiblesSearcherText
// Output models // Output models
readonly property alias collectiblesVisibleModel: collectibles.visibleModel readonly property alias collectiblesVisibleModel: collectibles.visibleModel
@ -102,8 +105,20 @@ QObject {
ProfileShowcaseDirtyState { ProfileShowcaseDirtyState {
id: communities id: communities
function getMemberRole(memberRole) {
return ProfileUtils.getMemberRoleText(memberRole)
}
sourceModel: modelAdapter.adaptedCommunitiesSourceModel sourceModel: modelAdapter.adaptedCommunitiesSourceModel
showcaseModel: modelAdapter.adaptedCommunitiesShowcaseModel 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 { ProfileShowcaseDirtyState {
@ -111,6 +126,14 @@ QObject {
sourceModel: modelAdapter.adaptedAccountsSourceModel sourceModel: modelAdapter.adaptedAccountsSourceModel
showcaseModel: modelAdapter.adaptedAccountsShowcaseModel 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 { ProfileShowcaseDirtyState {
@ -118,6 +141,16 @@ QObject {
sourceModel: collectiblesFilter sourceModel: collectiblesFilter
showcaseModel: modelAdapter.adaptedCollectiblesShowcaseModel 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 { SortFilterProxyModel {

View File

@ -7,6 +7,8 @@ import AppLayouts.Wallet 1.0
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ 0.1
ProfileShowcasePanel { ProfileShowcasePanel {
id: root id: root
@ -14,7 +16,8 @@ ProfileShowcasePanel {
emptyInShowcasePlaceholderText: qsTr("Accounts here will show on your profile") emptyInShowcasePlaceholderText: qsTr("Accounts here will show on your profile")
emptyHiddenPlaceholderText: qsTr("Accounts here will be hidden from 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 { delegate: ProfileShowcasePanelDelegate {
title: model ? model.name : "" title: model ? model.name : ""
secondaryTitle: WalletUtils.addressToDisplay(model ? model.address ?? "" : "", "", true, containsMouse) secondaryTitle: WalletUtils.addressToDisplay(model ? model.address ?? "" : "", "", true, containsMouse)

View File

@ -21,7 +21,8 @@ ProfileShowcasePanel {
emptyInShowcasePlaceholderText: qsTr("Assets here will show on your profile") emptyInShowcasePlaceholderText: qsTr("Assets here will show on your profile")
emptyHiddenPlaceholderText: qsTr("Assets here will be hidden from 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 { delegate: ProfileShowcasePanelDelegate {
readonly property double totalValue: !!model && !!model.decimals ? balancesAggregator.value/(10 ** model.decimals): 0 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 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
@ -19,7 +20,8 @@ ProfileShowcasePanel {
emptyInShowcasePlaceholderText: qsTr("Collectibles here will show on your profile") emptyInShowcasePlaceholderText: qsTr("Collectibles here will show on your profile")
emptyHiddenPlaceholderText: qsTr("Collectibles here will be hidden from 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 additionalFooterComponent: root.addAccountsButtonVisible ? addMoreAccountsComponent : null
delegate: ProfileShowcasePanelDelegate { delegate: ProfileShowcasePanelDelegate {

View File

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

View File

@ -4,16 +4,20 @@ import QtQuick.Layouts 1.15
import QtQml 2.15 import QtQml 2.15
import QtQml.Models 2.15 import QtQml.Models 2.15
import StatusQ 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1 import StatusQ.Core.Utils 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls.Validators 0.1
import shared.controls 1.0 import shared.controls 1.0
import utils 1.0 import utils 1.0
import AppLayouts.Profile.controls 1.0 import AppLayouts.Profile.controls 1.0
import SortFilterProxyModel 0.2
DoubleFlickableWithFolding { DoubleFlickableWithFolding {
id: root id: root
@ -29,9 +33,14 @@ DoubleFlickableWithFolding {
// Placeholder text to be shown when the list is empty // Placeholder text to be shown when the list is empty
property string emptyInShowcasePlaceholderText property string emptyInShowcasePlaceholderText
property string emptyHiddenPlaceholderText property string emptyHiddenPlaceholderText
property string emptySearchPlaceholderText
property int showcaseLimit: ProfileUtils.showcaseLimit property int showcaseLimit: ProfileUtils.showcaseLimit
// Searcher related properties:
property string searchPlaceholderText
property string searcherText: ""
// Signal to request position change of the visible items // Signal to request position change of the visible items
signal changePositionRequested(int from, int to) signal changePositionRequested(int from, int to)
@ -47,6 +56,7 @@ DoubleFlickableWithFolding {
id: d id: d
readonly property bool limitReached: root.showcaseLimit === inShowcaseCounterTracker.count 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 dragHiddenItemKey: ["x-status-draggable-showcase-item-hidden"]
readonly property var dragShowcaseItemKey: ["x-status-draggable-showcase-item"] readonly property var dragShowcaseItemKey: ["x-status-draggable-showcase-item"]
@ -84,18 +94,59 @@ DoubleFlickableWithFolding {
flickable1: EmptyShapeRectangleFooterListView { flickable1: EmptyShapeRectangleFooterListView {
id: inShowcaseListView id: inShowcaseListView
model: root.inShowcaseModel
width: root.width width: root.width
placeholderText: root.emptyInShowcasePlaceholderText placeholderText: d.searchActive ? root.emptySearchPlaceholderText : root.emptyInShowcasePlaceholderText
footerHeight: ProfileUtils.defaultDelegateHeight footerHeight: ProfileUtils.defaultDelegateHeight
footerContentVisible: !dropAreaRow.visible footerContentVisible: !dropAreaRow.visible
spacing: Style.current.halfPadding spacing: Style.current.halfPadding
delegate: delegateWrapper delegate: delegateWrapper
model: root.inShowcaseModel header: ColumnLayout {
width: ListView.view.width
spacing: 0
header: FoldableHeader { SearchBox {
id: searcher
Layout.fillWidth: true
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)")
}
]
Binding {
target: root
property: "searcherText"
value: searcher.text
restoreMode: Binding.RestoreBindingOrValue
}
}
FoldableHeader {
readonly property bool isDropAreaVisible: root.flickable1Folded && d.isAnyHiddenDragActive readonly property bool isDropAreaVisible: root.flickable1Folded && d.isAnyHiddenDragActive
width: ListView.view.width Layout.fillWidth: true
title: qsTr("In showcase") title: qsTr("In showcase")
folded: root.flickable1Folded folded: root.flickable1Folded
rightAdditionalComponent: isDropAreaVisible && d.limitReached ? limitReachedHeaderButton : rightAdditionalComponent: isDropAreaVisible && d.limitReached ? limitReachedHeaderButton :
@ -109,6 +160,7 @@ DoubleFlickableWithFolding {
width: d.additionalHeaderComponentWidth width: d.additionalHeaderComponentWidth
height: d.additionalHeaderComponentHeight height: d.additionalHeaderComponentHeight
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
text: "%1 / %2".arg(inShowcaseCounterTracker.count).arg(root.showcaseLimit) text: "%1 / %2".arg(inShowcaseCounterTracker.count).arg(root.showcaseLimit)
font.pixelSize: Style.current.tertiaryTextFontSize font.pixelSize: Style.current.tertiaryTextFontSize
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
@ -155,6 +207,7 @@ DoubleFlickableWithFolding {
onToggleFolding: root.flip1Folding() onToggleFolding: root.flip1Folding()
} }
}
// Overlaid showcase listview content drop area: // Overlaid showcase listview content drop area:
DropArea { DropArea {
@ -199,14 +252,14 @@ DoubleFlickableWithFolding {
flickable2: EmptyShapeRectangleFooterListView { flickable2: EmptyShapeRectangleFooterListView {
id: hiddenListView id: hiddenListView
model: root.hiddenModel
width: root.width width: root.width
placeholderText: root.emptyHiddenPlaceholderText placeholderText: d.searchActive ? root.emptySearchPlaceholderText : root.emptyHiddenPlaceholderText
footerHeight: ProfileUtils.defaultDelegateHeight footerHeight: ProfileUtils.defaultDelegateHeight
footerContentVisible: !hiddenDropAreaButton.visible footerContentVisible: !hiddenDropAreaButton.visible
additionalFooterComponent: root.additionalFooterComponent additionalFooterComponent: root.additionalFooterComponent
spacing: Style.current.halfPadding spacing: Style.current.halfPadding
delegate: delegateWrapper delegate: delegateWrapper
model: root.hiddenModel
header: FoldableHeader { header: FoldableHeader {
width: ListView.view.width width: ListView.view.width

View File

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

View File

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

View File

@ -93,4 +93,17 @@ QtObject {
return "hide" 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")
}
} }