feat(ProfileShowcase): Base components for managing dirty state
This commit is contained in:
parent
5b5b19fc7a
commit
809af0ac90
|
@ -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
|
|
@ -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}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
ProfileShowcaseDirtyState 1.0 ProfileShowcaseDirtyState.qml
|
||||||
|
VisibilityAndPositionDirtyStateModel 1.0 VisibilityAndPositionDirtyStateModel.qml
|
Loading…
Reference in New Issue