feat(CommunityPermissions): Implement duplication checking

Moreover:
- adapt flow to the design
- introduce ModelChangeTracker utility component

Closes: #9048
This commit is contained in:
Michał Cieślak 2023-02-14 21:39:18 +01:00 committed by Michał
parent c78eaef2b6
commit a97c8a720e
12 changed files with 236 additions and 112 deletions

View File

@ -27,6 +27,7 @@ SplitView {
isEditState: isEditStateCheckBox.checked isEditState: isEditStateCheckBox.checked
isPrivate: isPrivateCheckBox.checked isPrivate: isPrivateCheckBox.checked
duplicationWarningVisible: isDuplicationWarningVisibleCheckBox.checked
store: CommunitiesStore { store: CommunitiesStore {
readonly property var assetsModel: AssetsModel {} readonly property var assetsModel: AssetsModel {}
@ -50,10 +51,6 @@ SplitView {
logs.logEvent("CommunitiesStore::editPermission - index: " + index) logs.logEvent("CommunitiesStore::editPermission - index: " + index)
} }
function duplicatePermission(index) {
logs.logEvent("CommunitiesStore::duplicatePermission - index: " + index)
}
function removePermission(index) { function removePermission(index) {
logs.logEvent("CommunitiesStore::removePermission - index: " + index) logs.logEvent("CommunitiesStore::removePermission - index: " + index)
} }
@ -111,6 +108,12 @@ SplitView {
text: "Is private" text: "Is private"
} }
CheckBox {
id: isDuplicationWarningVisibleCheckBox
text: "Is duplication warning visible"
}
} }
Button { Button {

View File

@ -39,10 +39,6 @@ SplitView {
readonly property var collectiblesModel: CollectiblesModel { readonly property var collectiblesModel: CollectiblesModel {
id: collectiblesModel id: collectiblesModel
} }
function duplicatePermission(index) {
logs.logEvent("CommunitiesStore::duplicatePermission - index: " + index)
}
} }
rootStore: QtObject { rootStore: QtObject {

View File

@ -0,0 +1,41 @@
import QtQml 2.14
QtObject {
property alias model: d.target
readonly property alias revision: d.revision
function reset() {
d.revision = 0
}
readonly property Connections _d: Connections {
id: d
property int revision: 0
function onRowsInserted() {
revision++
}
function onRowsMoved() {
revision++
}
function onRowsRemoved() {
revision++
}
function onLayoutChanged() {
revision++
}
function onModelReset() {
revision++
}
function onDataChanged() {
revision++
}
}
}

View File

@ -36,4 +36,63 @@ QtObject {
return -1 return -1
} }
function checkItemsEquality(itemA, itemB, roles) {
return roles.every((role) => itemA[role] === itemB[role])
}
function checkEqualityStrict(modelA, modelB, roles) {
if (modelA === modelB)
return true
const countA = modelA === null ? 0 : modelA.rowCount()
const countB = modelB === null ? 0 : modelB.rowCount()
if (countA !== countB)
return false
if (countA === 0)
return true
for (let i = 0; i < countA; i++) {
const itemA = modelA.get(i)
const itemB = modelB.get(i)
if (!checkItemsEquality(itemA, itemB, roles))
return false
}
return true
}
function checkEqualitySet(modelA, modelB, roles) {
if (modelA === modelB)
return true
const countA = modelA === null ? 0 : modelA.rowCount()
const countB = modelB === null ? 0 : modelB.rowCount()
if (countA !== countB)
return false
if (countA === 0)
return true
for (let i = 0; i < countA; i++) {
const itemA = modelA.get(i)
let found = false
for (let j = 0; j < countB; j++) {
const itemB = modelB.get(j)
if (checkItemsEquality(itemA, itemB, roles))
found = true
}
if (!found)
return false
}
return true
}
} }

View File

@ -16,101 +16,24 @@ QtObject {
readonly property QtObject _d: QtObject { readonly property QtObject _d: QtObject {
id: d id: d
component ModelObserver: Connections { readonly property ModelChangeTracker trackerA: ModelChangeTracker {
function onRowsInserted() { model: modelA
d.changeCounter++
}
function onRowsMoved() {
d.changeCounter++
}
function onRowsRemoved() {
d.changeCounter++
}
function onLayoutChanged() {
d.changeCounter++
}
function onModelReset() {
d.changeCounter++
}
function onDataChanged() {
d.changeCounter++
}
} }
property int changeCounter: 0 readonly property ModelChangeTracker trackerB: ModelChangeTracker {
model: modelB
}
readonly property int revision: trackerA.revision + trackerB.revision
readonly property bool equal: checkEquality(modelA, modelB, roles, mode, readonly property bool equal: checkEquality(modelA, modelB, roles, mode,
changeCounter) revision)
readonly property Connections observerA: ModelObserver {
target: modelA
}
readonly property Connections observerB: ModelObserver {
target: modelB
}
function checkEquality(modelA, modelB, roles, mode, dummy) { function checkEquality(modelA, modelB, roles, mode, dummy) {
if (modelA === modelB)
return true
const countA = modelA === null ? 0 : modelA.rowCount()
const countB = modelB === null ? 0 : modelB.rowCount()
if (countA !== countB)
return false
if (countA === 0)
return true
if (mode === ModelsComparator.CompareMode.Strict) if (mode === ModelsComparator.CompareMode.Strict)
return checkEqualityStrict(modelA, modelB, roles) return ModelUtils.checkEqualityStrict(modelA, modelB, roles)
return checkEqualitySet(modelA, modelB, roles) return ModelUtils.checkEqualitySet(modelA, modelB, roles)
}
function checkEqualityStrict(modelA, modelB, roles) {
const count = modelA.rowCount()
for (let i = 0; i < count; i++) {
const itemA = modelA.get(i)
const itemB = modelB.get(i)
if (!checkItemsEquality(itemA, itemB, roles))
return false
}
return true
}
function checkEqualitySet(modelA, modelB, roles) {
const count = modelA.rowCount()
for (let i = 0; i < count; i++) {
const itemA = modelA.get(i)
let found = false
for (let j = 0; j < count; j++) {
const itemB = modelB.get(j)
if (checkItemsEquality(itemA, itemB, roles))
found = true
}
if (!found)
return false
}
return true
}
function checkItemsEquality(itemA, itemB, roles) {
return roles.every((role) => itemA[role] === itemB[role])
} }
} }
} }

View File

@ -2,6 +2,7 @@ module StatusQ.Core.Utils
EmojiJSON 1.0 emojiList.js EmojiJSON 1.0 emojiList.js
JSONListModel 0.1 JSONListModel.qml JSONListModel 0.1 JSONListModel.qml
ModelChangeTracker 0.1 ModelChangeTracker.qml
ModelsComparator 0.1 ModelsComparator.qml ModelsComparator 0.1 ModelsComparator.qml
XSS 1.0 xss.js XSS 1.0 xss.js
singleton Emoji 0.1 Emoji.qml singleton Emoji 0.1 Emoji.qml

View File

@ -195,5 +195,6 @@
<file>StatusQ/Components/LoadingComponent.qml</file> <file>StatusQ/Components/LoadingComponent.qml</file>
<file>StatusQ/Core/Utils/ModelUtils.qml</file> <file>StatusQ/Core/Utils/ModelUtils.qml</file>
<file>StatusQ/Core/Utils/ModelsComparator.qml</file> <file>StatusQ/Core/Utils/ModelsComparator.qml</file>
<file>StatusQ/Core/Utils/ModelChangeTracker.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -5,6 +5,7 @@ import AppLayouts.Chat.layouts 1.0
import AppLayouts.Chat.stores 1.0 import AppLayouts.Chat.stores 1.0
import AppLayouts.Chat.views.communities 1.0 import AppLayouts.Chat.views.communities 1.0
import StatusQ.Core.Utils 0.1
import utils 1.0 import utils 1.0
SettingsPageLayout { SettingsPageLayout {
@ -57,8 +58,8 @@ SettingsPageLayout {
? d.permissionsViewState : d.welcomeViewState ? d.permissionsViewState : d.welcomeViewState
function initializeData() { function initializeData() {
holdingsToEditModel = defaultListObject.createObject(d) holdingsToEditModel = emptyModel
channelsToEditModel = defaultListObject.createObject(d) channelsToEditModel = emptyModel
permissionTypeToEdit = PermissionTypes.Type.None permissionTypeToEdit = PermissionTypes.Type.None
isPrivateToEditValue = false isPrivateToEditValue = false
} }
@ -146,6 +147,8 @@ SettingsPageLayout {
id: newPermissionView id: newPermissionView
CommunityNewPermissionView { CommunityNewPermissionView {
id: communityNewPermissionView
viewWidth: root.viewWidth viewWidth: root.viewWidth
rootStore: root.rootStore rootStore: root.rootStore
@ -190,6 +193,55 @@ SettingsPageLayout {
property: "dirty" property: "dirty"
value: isEditState && dirty value: isEditState && dirty
} }
ModelChangeTracker {
id: holdingsTracker
model: communityNewPermissionView.dirtyValues.holdingsModel
}
ModelChangeTracker {
id: channelsTracker
model: communityNewPermissionView.dirtyValues.channelsModel
}
Binding {
target: root
property: "saveChangesButtonEnabled"
value: !communityNewPermissionView.duplicationWarningVisible
}
duplicationWarningVisible: {
// dependencies
holdingsTracker.revision
channelsTracker.revision
communityNewPermissionView.dirtyValues.permissionType
communityNewPermissionView.dirtyValues.isPrivate
const model = root.store.permissionsModel
for (let i = 0; i < model.count; i++) {
if (root.state === d.editPermissionViewState
&& d.permissionIndexToEdit === i)
continue
const item = model.get(i)
const holdings = item.holdingsListModel
const channels = item.channelsListModel
const permissionType = item.permissionType
const same = (a, b) => ModelUtils.checkEqualitySet(a, b, ["key"])
if (same(dirtyValues.holdingsModel, holdings)
&& same(dirtyValues.channelsModel, channels)
&& dirtyValues.permissionType === permissionType)
return true
}
return false
}
} }
} }
@ -201,19 +253,24 @@ SettingsPageLayout {
rootStore: root.rootStore rootStore: root.rootStore
store: root.store store: root.store
onEditPermissionRequested: { function setInitialValuesFromIndex(index) {
const item = root.store.permissionsModel.get(index) const item = root.store.permissionsModel.get(index)
d.permissionIndexToEdit = index
d.holdingsToEditModel = item.holdingsListModel d.holdingsToEditModel = item.holdingsListModel
d.channelsToEditModel = item.channelsListModel d.channelsToEditModel = item.channelsListModel
d.permissionTypeToEdit = item.permissionType d.permissionTypeToEdit = item.permissionType
d.isPrivateToEditValue = item.isPrivate d.isPrivateToEditValue = item.isPrivate
}
onEditPermissionRequested: {
setInitialValuesFromIndex(index)
d.permissionIndexToEdit = index
root.state = d.editPermissionViewState root.state = d.editPermissionViewState
} }
onDuplicatePermissionRequested: { onDuplicatePermissionRequested: {
root.store.duplicatePermission(index) setInitialValuesFromIndex(index)
root.state = d.newPermissionViewState
} }
onRemovePermissionRequested: { onRemovePermissionRequested: {
@ -222,8 +279,7 @@ SettingsPageLayout {
} }
} }
Component { ListModel {
id: defaultListObject id: emptyModel
ListModel {}
} }
} }

View File

@ -0,0 +1,40 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
Control {
id: root
spacing: Style.current.halfPadding
QtObject {
id: d
property int iconSize: 20
}
contentItem: RowLayout {
spacing: root.spacing
StatusIcon {
Layout.preferredWidth: d.iconSize
Layout.preferredHeight: d.iconSize
Layout.alignment: Qt.AlignTop
color: Theme.palette.dangerColor1
icon: "warning"
}
StatusBaseText {
Layout.fillWidth: true
wrapMode: Text.Wrap
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.dangerColor1
text: qsTr("Permission with same properties is already active, edit properties to create a new permission.")
}
}
}

View File

@ -6,4 +6,5 @@ CommunityProfilePopupInviteMessagePanel 1.0 CommunityProfilePopupInviteMessagePa
HidePermissionPanel 1.0 HidePermissionPanel.qml HidePermissionPanel 1.0 HidePermissionPanel.qml
JoinPermissionsOverlayPanel 1.0 JoinPermissionsOverlayPanel.qml JoinPermissionsOverlayPanel 1.0 JoinPermissionsOverlayPanel.qml
PermissionConflictWarningPanel 1.0 PermissionConflictWarningPanel.qml PermissionConflictWarningPanel 1.0 PermissionConflictWarningPanel.qml
PermissionDuplicationWarningPanel 1.0 PermissionDuplicationWarningPanel.qml
PermissionQualificationPanel 1.0 PermissionQualificationPanel.qml PermissionQualificationPanel 1.0 PermissionQualificationPanel.qml

View File

@ -179,14 +179,6 @@ QtObject {
createPermission(holdings, permissionType, isPrivate, channels, index) createPermission(holdings, permissionType, isPrivate, channels, index)
} }
function duplicatePermission(index) {
// TO BE REPLACED: Call to backend
console.log("TODO: Duplicate permissions - backend call")
const permission = root.permissionsModel.get(index)
createPermission(permission.holdingsListModel, permission.permissionType,
permission.isPrivate, permission.channelsListModel)
}
function removePermission(index) { function removePermission(index) {
console.log("TODO: Remove permissions - backend call") console.log("TODO: Remove permissions - backend call")
root.permissionsModel.remove(index) root.permissionsModel.remove(index)

View File

@ -44,6 +44,8 @@ StatusScrollView {
readonly property alias dirtyValues: d.dirtyValues readonly property alias dirtyValues: d.dirtyValues
property alias duplicationWarningVisible: duplicationPanel.visible
signal createPermissionClicked signal createPermissionClicked
function resetChanges() { function resetChanges() {
@ -156,7 +158,7 @@ StatusScrollView {
contentWidth: mainLayout.width contentWidth: mainLayout.width
contentHeight: mainLayout.height contentHeight: mainLayout.height
onPermissionTypeChanged: d.loadInitValues() onPermissionTypeChanged: Qt.callLater(() => d.loadInitValues())
ColumnLayout { ColumnLayout {
id: mainLayout id: mainLayout
@ -517,6 +519,14 @@ StatusScrollView {
channels: store.permissionConflict.channels channels: store.permissionConflict.channels
} }
PermissionDuplicationWarningPanel {
id: duplicationPanel
visible: false
Layout.fillWidth: true
Layout.topMargin: 50 // by desing
}
StatusButton { StatusButton {
visible: !root.isEditState visible: !root.isEditState
Layout.topMargin: conflictPanel.visible ? conflictPanel.Layout.topMargin : 24 // by design Layout.topMargin: conflictPanel.visible ? conflictPanel.Layout.topMargin : 24 // by design
@ -524,6 +534,7 @@ StatusScrollView {
enabled: d.dirtyValues.holdingsModel.count > 0 enabled: d.dirtyValues.holdingsModel.count > 0
&& d.dirtyValues.permissionType !== PermissionTypes.Type.None && d.dirtyValues.permissionType !== PermissionTypes.Type.None
&& (d.dirtyValues.channelsModel.count > 0 || d.isCommunityPermission) && (d.dirtyValues.channelsModel.count > 0 || d.isCommunityPermission)
&& !root.duplicationWarningVisible
Layout.preferredHeight: 44 Layout.preferredHeight: 44
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true