feat(ProfileShowcase): Base components for managing dirty state

This commit is contained in:
Michał Cieślak 2024-02-16 12:31:40 +01:00 committed by Michał
parent 5b5b19fc7a
commit 809af0ac90
4 changed files with 388 additions and 0 deletions

View File

@ -0,0 +1,197 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
import Storybook 1.0
import AppLayouts.Profile.helpers 1.0
Item {
id: root
ListModel {
id: communitiesModel
ListElement { key: "1"; name: "Crypto Kitties" }
ListElement { key: "2"; name: "Status" }
ListElement { key: "3"; name: "Fun Stuff" }
ListElement { key: "4"; name: "Other Stuff" }
}
ListModel {
id: communitiesShowcaseModel
ListElement { key: "1"; visibility: 1; position: 0 }
ListElement { key: "3"; visibility: 2; position: 9 }
}
ProfileShowcaseDirtyState {
id: dirtyState
sourceModel: communitiesModel
showcaseModel: communitiesShowcaseModel
}
MovableModel {
id: movableModel
sourceModel: dirtyState.visibleModel
}
ColumnLayout {
anchors.fill: parent
Grid {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 10
rows: 3
columns: 3
spacing: 10
flow: Grid.TopToBottom
Label {
text: "Backend models"
font.pixelSize: 22
padding: 10
}
GenericListView {
width: 200
height: 300
model: communitiesModel
label: "COMMUNITIES MODEL"
}
GenericListView {
width: 200
height: 300
model: communitiesShowcaseModel
label: "SHOWCASE MODEL"
roles: ["key", "visibility", "position"]
}
Label {
text: "Internal models"
font.pixelSize: 22
padding: 10
}
GenericListView {
width: 350
height: 300
model: dirtyState.joined_
label: "JOINED MODEL"
}
GenericListView {
width: 350
height: 300
model: dirtyState.writable_
label: "WRITABLE MODEL"
roles: ["key", "visibility", "position", "name"]
}
Label {
text: "Display models"
font.pixelSize: 22
padding: 10
}
GenericListView {
width: 450
height: 300
model: movableModel
label: "IN SHOWCASE"
movable: true
roles: ["key", "visibility", "position"]
onMoveRequested: {
movableModel.move(from, to)
const key = ModelUtils.get(movableModel, to, "key")
dirtyState.changePosition(key, to);
}
insetComponent: RowLayout {
readonly property var topModel: model
RoundButton {
text: "❌"
onClicked: dirtyState.setVisibility(model.key, 0)
}
ComboBox {
id: combo
model: ListModel {
ListElement { text: "contacts"; value: 1 }
ListElement { text: "verified"; value: 2 }
ListElement { text: "all"; value: 3 }
}
onCurrentValueChanged: {
if (!completed || topModel.index < 0)
return
dirtyState.setVisibility(topModel.key, currentValue)
}
property bool completed: false
Component.onCompleted: {
currentIndex = indexOfValue(topModel.visibility)
completed = true
}
textRole: "text"
valueRole: "value"
}
}
}
GenericListView {
width: 450
height: 300
model: dirtyState.hiddenModel
label: "HIDDEN"
roles: ["key", "visibility", "position"]
insetComponent: Button {
text: "unhide"
onClicked: dirtyState.setVisibility(model.key, 1)
}
}
}
Button {
text: "SAVE"
onClicked: {
const toBeSaved = dirtyState.currentState()
communitiesShowcaseModel.clear()
communitiesShowcaseModel.append(toBeSaved)
}
Layout.alignment: Qt.AlignHCenter
Layout.margins: 10
}
}
}
// category: Models

View File

@ -0,0 +1,83 @@
import QtQml 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
import SortFilterProxyModel 0.2
/**
* Building block for managing temporary state in the "Profile Showcase"
* functionality. Provides combining raw source model (like e.g. communities
* model or accounts model) with lean showcase model (providing info regarding
* visibility and position), managing dirty state (visibility, position) and
* providing two output models - one containing visible items sorted by
* position, second one containing hidden items.
*/
QObject {
property alias sourceModel: joined.leftModel
property alias showcaseModel: joined.rightModel
/**
* Model holding elements from 'sourceModel' intended to be visible in the
* showcase, sorted by 'position' role. Includes roles from both input models.
*/
readonly property alias visibleModel: visible
/**
* Model holding elements from 'sourceModel' intended to be hidden, no
* sorting applied. Includes roles from both input models.
*/
readonly property alias hiddenModel: hidden
function currentState() {
return writable.currentState()
}
function setVisibility(key, visibility) {
writable.setVisibility(key, visibility)
}
function changePosition(key, to) {
writable.changePosition(key, to)
}
// internals, debug purpose only
readonly property alias writable_: writable
readonly property alias joined_: joined
component VisibilityFilter: RangeFilter {
roleName: "visibility"
minimumValue: 1
}
LeftJoinModel {
id: joined
joinRole: "key"
}
VisibilityAndPositionDirtyStateModel {
id: writable
sourceModel: joined
}
SortFilterProxyModel {
id: visible
sourceModel: writable
delayed: true
filters: VisibilityFilter {}
sorters: RoleSorter { roleName: "position" }
}
SortFilterProxyModel {
id: hidden
sourceModel: writable
delayed: true
filters: VisibilityFilter { inverted: true}
}
}

View File

@ -0,0 +1,106 @@
import QtQml 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
/**
* Basic building block for storing temporary state in the "Profile Showcase"
* functionality. Allows to store on the UI side the temporary position and
* visibility level. Can store temporary state for Communities, Accounts,
* Collectibles and Assets.
*/
WritableProxyModel {
id: root
/* Provides the list of objects representing the current state in the
* in the following format:
* [ {
* key: <string or integer>
* position: <integer>
* visibility: <integer>
* }
* ]
*
* The entries with visibility 0 (hidden) are not included in the list.
*/
function currentState() {
const visible = d.getVisibleEntries()
const minPos = Math.min(...visible.map(e => e.position))
return visible.map(e => { e.position -= minPos; return e })
}
/* Sets the visibility of the given item. If the element was hidden, it is
* positioned last.
*/
function setVisibility(key, visibility) {
const sourceIdx = d.indexByKey(key)
const oldVisibility = d.getVisibility(sourceIdx)
if (oldVisibility === visibility)
return
// hiding, changing visibility level
if (visibility === 0 || (visibility > 0 && oldVisibility > 0)) {
set(sourceIdx, { visibility: visibility })
return
}
// unhiding
const positions = d.getVisibleEntries().map(e => e.position)
const position = Math.max(-1, ...positions) + 1
set(sourceIdx, { visibility, position })
}
/* Sets the position of the item. The "to" parameter is expected to be
* a target index in the list and must be in range [0; count - 1].
*/
function changePosition(key, to) {
const visible = d.getVisibleEntries()
visible.sort((a, b) => a.position - b.position)
const idx = visible.findIndex(item => item.key === key)
if (idx === -1) {
console.warn(`Entry with key ${key} not found`)
return
}
const count = visible.length
if (to < 0 || to >= count) {
console.warn(`Destination position out of range: ${to}`)
return
}
// swap
[visible[idx], visible[to]] = [visible[to], visible[idx]]
visible.forEach((e, i) => {
if (e.position === i)
return
const idx = d.indexByKey(e.key)
set(idx, { position: i })
})
}
readonly property QtObject d_: QtObject {
id: d
function indexByKey(key) {
return ModelUtils.indexOf(root, "key", key)
}
function getVisibleEntries() {
const roles = ["key", "position", "visibility"]
const keysAndPos = ModelUtils.modelToArray(root, roles)
return keysAndPos.filter(p => p.visibility)
}
function getVisibility(idx) {
return ModelUtils.get(root, idx, "visibility") || 0
}
}
}

View File

@ -0,0 +1,2 @@
ProfileShowcaseDirtyState 1.0 ProfileShowcaseDirtyState.qml
VisibilityAndPositionDirtyStateModel 1.0 VisibilityAndPositionDirtyStateModel.qml