diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index f5a054be63..1888f90681 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -165,6 +165,18 @@ ListModel { title: "StatusGroupBox" section: "Components" } + ListElement { + title: "AddressesInputList" + section: "Components" + } + ListElement { + title: "AddressesSelectorPanel" + section: "Components" + } + ListElement { + title: "AirdropRecipientsSelector" + section: "Components" + } ListElement { title: "AirdropTokensSelector" section: "Components" diff --git a/storybook/figma.json b/storybook/figma.json index 4cb72872c1..0644abb063 100644 --- a/storybook/figma.json +++ b/storybook/figma.json @@ -3,6 +3,18 @@ "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1159%3A114479", "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1684%3A127762" ], + "AirdropRecipientsSelector": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-494998", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-495258", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22647-497754", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=28045-533663", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=28045-533912", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-495493", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-495928", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-496145", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22642-496092", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22647-498080" + ], "AirdropTokensSelector": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22602-495563", "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-494998", diff --git a/storybook/pages/AddressesInputListPage.qml b/storybook/pages/AddressesInputListPage.qml new file mode 100644 index 0000000000..bb392e374c --- /dev/null +++ b/storybook/pages/AddressesInputListPage.qml @@ -0,0 +1,71 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import AppLayouts.Chat.controls.community 1.0 + +import Storybook 1.0 +import Models 1.0 + +SplitView { + orientation: Qt.Vertical + + Logs { id: logs } + + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + Rectangle { + anchors.fill: parent + color: "lightgray" + } + + AddressesInputList { + width: 500 + anchors.centerIn: parent + + enabled: isEnabledCheckBox.checked + + model: AddressesModel { + id: addressesModel + } + + onAddAddressesRequested: { + addressesModel.addAddressesFromString(addresses) + clearInput() + positionListAtEnd() + } + + onRemoveAddressRequested: addressesModel.remove(index) + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 160 + + logsView.logText: logs.logText + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + CheckBox { + id: isEnabledCheckBox + + text: "Enabled" + checked: true + } + + Button { + text: "Clear" + + onClicked: addressesModel.clear() + } + } + } +} diff --git a/storybook/pages/AddressesSelectorPanelPage.qml b/storybook/pages/AddressesSelectorPanelPage.qml new file mode 100644 index 0000000000..f70f9c6bf2 --- /dev/null +++ b/storybook/pages/AddressesSelectorPanelPage.qml @@ -0,0 +1,79 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import AppLayouts.Chat.controls.community 1.0 + +import Storybook 1.0 +import Models 1.0 + +SplitView { + orientation: Qt.Vertical + + Logs { id: logs } + + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + Rectangle { + anchors.fill: parent + color: "lightgray" + } + + Timer { + id: timer + + interval: 1000 + + onTriggered: { + addressesModel.addAddressesFromString( + addressesSelectorPanel.text) + addressesSelectorPanel.clearInput() + addressesSelectorPanel.positionListAtEnd() + } + } + + AddressesSelectorPanel { + id: addressesSelectorPanel + + anchors.centerIn: parent + width: 500 + + model: AddressesModel { + id: addressesModel + } + + Binding on loading { value: isLoadingCheckBox.checked } + Binding on loading { value: timer.running } + + onAddAddressesRequested: timer.start() + onRemoveAddressRequested: addressesModel.remove(index) + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 160 + + logsView.logText: logs.logText + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + CheckBox { + id: isLoadingCheckBox + + text: "Is loading" + } + + Button { + text: "Clear" + + onClicked: addressesModel.clear() + } + } + } +} diff --git a/storybook/pages/AirdropRecipientsSelectorPage.qml b/storybook/pages/AirdropRecipientsSelectorPage.qml new file mode 100644 index 0000000000..754003b968 --- /dev/null +++ b/storybook/pages/AirdropRecipientsSelectorPage.qml @@ -0,0 +1,175 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import AppLayouts.Chat.controls.community 1.0 + +import Models 1.0 +import Storybook 1.0 + +import utils 1.0 + + +SplitView { + property bool globalUtilsReady: false + property bool mainModuleReady: false + + orientation: Qt.Vertical + + Logs { id: logs } + + QtObject { + function isCompressedPubKey(publicKey) { + return true + } + + function getColorId(publicKey) { + return Math.floor(Math.random() * 10) + } + + Component.onCompleted: { + Utils.globalUtilsInst = this + globalUtilsReady = true + + } + Component.onDestruction: { + globalUtilsReady = false + Utils.globalUtilsInst = {} + } + } + + QtObject { + function getContactDetailsAsJson() { + return JSON.stringify({ ensVerified: true }) + } + + Component.onCompleted: { + mainModuleReady = true + Utils.mainModuleInst = this + } + Component.onDestruction: { + mainModuleReady = false + Utils.mainModuleInst = {} + } + } + + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + AddressesModel { + id: addresses + } + + ListModel { + id: members + + property int counter: 0 + + function addMember() { + const i = counter++ + const key = `pub_key_${i}` + + append({ + alias: "", + colorId: "1", + displayName: `contact ${i}`, + ensName: "", + icon: "", + isContact: true, + localNickname: "", + onlineStatus: 1, + pubKey: key, + isVerified: true, + isUntrustworthy: false + }) + } + + Component.onCompleted: { + for (let i = 0; i < 4; i++) + addMember() + } + } + + Loader { + id: loader + + anchors.centerIn: parent + active: globalUtilsReady && mainModuleReady + + sourceComponent: AirdropRecipientsSelector { + id: selector + + addressesModel: addresses + loadingAddresses: timer.running + membersModel: members + showAddressesInputWhenEmpty: + showAddressesInputWhenEmptyCheckBox.checked + + onAddAddressesRequested: timer.start() + onRemoveAddressRequested: addresses.remove(index) + onRemoveMemberRequested: members.remove(index) + + Timer { + id: timer + + interval: 1000 + + onTriggered: { + addresses.addAddressesFromString( + selector.addressesInputText) + selector.clearAddressesInput() + selector.positionAddressesListAtEnd() + } + } + } + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 180 + + logsView.logText: logs.logText + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + RowLayout { + Button { + text: "Clear addresses list" + onClicked: addresses.clear() + } + + Button { + text: "Clear members list" + onClicked: members.clear() + } + + CheckBox { + id: showAddressesInputWhenEmptyCheckBox + + text: "Show addresses input when empty" + } + } + + Button { + text: "Add member" + onClicked: { + members.addMember() + loader.item.positionMembersListAtEnd() + } + } + + MenuSeparator {} + + TextEdit { + readOnly: true + selectByMouse: true + text: "valid address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4" + } + } + } +} diff --git a/storybook/src/Models/AddressesModel.qml b/storybook/src/Models/AddressesModel.qml new file mode 100644 index 0000000000..d68ae0ffdf --- /dev/null +++ b/storybook/src/Models/AddressesModel.qml @@ -0,0 +1,30 @@ +import QtQuick 2.15 + +import utils 1.0 + +ListModel { + ListElement { + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + valid: true + } + ListElement { + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756ccx" + valid: false + } + + function addAddressesFromString(addresses) { + const words = addresses.trim().split(/[\s+,]/) + const existing = new Set() + + for (let i = 0; i < count; i++) + existing.add(get(i).address) + + words.forEach(word => { + if (word === "" || existing.has(word)) + return + + const valid = Utils.isValidAddress(word) + append({ valid, address: word }) + }) + } +} diff --git a/storybook/src/Models/qmldir b/storybook/src/Models/qmldir index 99096af781..2f8e3694e8 100644 --- a/storybook/src/Models/qmldir +++ b/storybook/src/Models/qmldir @@ -1,13 +1,14 @@ -singleton ModelsData 1.0 ModelsData.qml -singleton PermissionsModel 1.0 PermissionsModel.qml -singleton NetworksModel 1.0 NetworksModel.qml -singleton MintedCollectiblesModel 1.0 MintedCollectiblesModel.qml -IconModel 1.0 IconModel.qml -BannerModel 1.0 BannerModel.qml -UsersModel 1.0 UsersModel.qml -AssetsModel 1.0 AssetsModel.qml -CollectiblesModel 1.0 CollectiblesModel.qml -ChannelsModel 1.0 ChannelsModel.qml +AddressesModel 1.0 AddressesModel.qml AssetsCollectiblesIconsModel 1.0 AssetsCollectiblesIconsModel.qml +AssetsModel 1.0 AssetsModel.qml +BannerModel 1.0 BannerModel.qml +ChannelsModel 1.0 ChannelsModel.qml +CollectiblesModel 1.0 CollectiblesModel.qml +IconModel 1.0 IconModel.qml TokenHoldersModel 1.0 TokenHoldersModel.qml +UsersModel 1.0 UsersModel.qml WalletAccountsModel 1.0 WalletAccountsModel.qml +singleton MintedCollectiblesModel 1.0 MintedCollectiblesModel.qml +singleton ModelsData 1.0 ModelsData.qml +singleton NetworksModel 1.0 NetworksModel.qml +singleton PermissionsModel 1.0 PermissionsModel.qml diff --git a/ui/app/AppLayouts/Chat/controls/community/AddressesInputList.qml b/ui/app/AppLayouts/Chat/controls/community/AddressesInputList.qml new file mode 100644 index 0000000000..2805e0f57b --- /dev/null +++ b/ui/app/AppLayouts/Chat/controls/community/AddressesInputList.qml @@ -0,0 +1,203 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + + +Control { + id: root + + property alias model: listView.model + readonly property alias count: listView.count + + property string text: listView.footerItem.text + property int maximumTextInputHeight: 156 + + property int maximumHeight: 405 + + signal addAddressesRequested(string addresses) + signal removeAddressRequested(int index) + + function clearInput() { + listView.footerItem.edit.clear() + } + + function positionListAtEnd() { + listView.positionViewAtEnd() + } + + padding: 8 + rightPadding: 13 + clip: true + + QtObject { + id: d + + readonly property int delegateHeight: 32 + readonly property int spacing: 8 + readonly property int scrollBarWidth: 4 + readonly property int scrollBarOffset: 5 + } + + background: Rectangle { + radius: Style.current.radius + color: Theme.palette.indirectColor1 + } + + contentItem: StatusListView { + id: listView + + readonly property int maximumHeight: + root.maximumHeight - root.bottomPadding - root.topPadding + + clip: false + + verticalScrollBar { + implicitWidth: d.scrollBarWidth + ScrollBar.vertical.padding * 2 + parent: listView.parent + anchors { + left: listView.right + top: listView.top + bottom: listView.bottom + leftMargin: -verticalScrollBar.leftPadding + d.scrollBarOffset + } + } + + spacing: d.spacing + implicitHeight: Math.min(contentHeight, maximumHeight) + implicitWidth: root.availableWidth + + delegate: Rectangle { + id: delegate + + radius: height / 2 + color: Theme.palette.directColor8 + + width: ListView.view.width + height: d.delegateHeight + + states: State { + when: !model.valid + + PropertyChanges { + target: delegate + + color: Theme.palette.alphaColor( + Theme.palette.dangerColor1, 0.05) + } + + PropertyChanges { + target: statusIcon + + width: 21 + height: 21 + icon: "warning" + color: Theme.palette.dangerColor1 + } + + PropertyChanges { + target: addressText + + color: Theme.palette.dangerColor1 + } + } + + StatusIcon { + id: statusIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.left + anchors.horizontalCenterOffset: 18 + + width: 16 + height: 16 + icon: "checkbox" + color: Theme.palette.successColor1 + } + + StatusBaseText { + id: addressText + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: deleteIcon.left + anchors.margins: 7 + anchors.leftMargin: 34 + + color: Theme.palette.directColor1 + + font.pixelSize: 15 + font.weight: Font.Medium + + elide: Text.ElideMiddle + text: model.address + } + + StatusIcon { + id: deleteIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 10 + + width: 16 + height: 16 + + icon: "delete" + color: Theme.palette.directColor1 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.removeAddressRequested(model.index) + } + } + } + } + + footer: StatusBaseInput { + id: input + + showBackground: false + maximumLength: 2000 + + width: root.availableWidth + + leftPadding: 0 + rightPadding: 0 + + multiline: true + + topPadding: bottomPadding + (listView.count ? d.spacing : 0) + + height: edit.implicitHeight + topPadding + bottomPadding + + placeholderText: qsTr("Example: 0x39cf...fbd2") + + Keys.onPressed: { + if ((event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter) + || event.modifiers & Qt.ShiftModifier) { + event.accepted = false + return + } + + event.accepted = true + + if (input.text.length > 0) + root.addAddressesRequested(input.text) + } + + onHeightChanged: Qt.callLater(() => listView.positionViewAtEnd()) + + verticalAlignment: Qt.AlignTop + placeholder.verticalAlignment: Qt.AlignTop + } + } +} diff --git a/ui/app/AppLayouts/Chat/controls/community/AddressesSelectorPanel.qml b/ui/app/AppLayouts/Chat/controls/community/AddressesSelectorPanel.qml new file mode 100644 index 0000000000..2d019c6e24 --- /dev/null +++ b/ui/app/AppLayouts/Chat/controls/community/AddressesSelectorPanel.qml @@ -0,0 +1,103 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Components 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import SortFilterProxyModel 0.2 + + +Control { + id: root + + property alias model: addressesInputList.model + property alias text: addressesInputList.text + + property bool loading: false + + signal addAddressesRequested(string addresses) + signal removeAddressRequested(int index) + + readonly property alias count: addressesInputList.count + readonly property alias validAddressesCount: validAddressesModel.count + readonly property int invalidAddressesCount: addressesInputList.count + - validAddressesCount + + function clearInput() { + addressesInputList.clearInput() + } + + function positionListAtEnd() { + addressesInputList.positionListAtEnd() + } + + contentItem: Column { + spacing: 8 + + RowLayout { + width: root.availableWidth + spacing: 0 + + StatusBaseText { + color: Theme.palette.baseColor1 + text: qsTr("ETH addresses") + font.pixelSize: Theme.tertiaryTextFontSize + elide: Text.ElideRight + } + + Item { Layout.fillWidth: true } + + StatusBaseText { + visible: !root.loading && root.validAddressesCount > 0 + color: Theme.palette.baseColor1 + text: qsTr("%n valid address(s)", "", root.validAddressesCount) + + (root.invalidAddressesCount > 0 ? " / " : "") + font.pixelSize: Theme.tertiaryTextFontSize + } + + StatusBaseText { + visible: !root.loading && root.invalidAddressesCount > 0 + color: Theme.palette.dangerColor1 + text: root.validAddressesCount > 0 + ? qsTr("%n invalid", + "invalid addresses, where \"addresses\" is implicit", + root.invalidAddressesCount) + : qsTr("%n invalid address(s)", "", root.invalidAddressesCount) + font.pixelSize: Theme.tertiaryTextFontSize + } + + StatusLoadingIndicator { + visible: root.loading + + Layout.preferredWidth: 10 + Layout.preferredHeight: 10 + Layout.rightMargin: 2 + } + } + + SortFilterProxyModel { + id: validAddressesModel + + sourceModel: root.model ?? null + + filters: ValueFilter { + roleName: "valid" + value: true + } + } + + AddressesInputList { + id: addressesInputList + + enabled: !root.loading + width: root.availableWidth + + Component.onCompleted: { + addAddressesRequested.connect(root.addAddressesRequested) + removeAddressRequested.connect(root.removeAddressRequested) + } + } + } +} diff --git a/ui/app/AppLayouts/Chat/controls/community/AirdropRecipientsSelector.qml b/ui/app/AppLayouts/Chat/controls/community/AirdropRecipientsSelector.qml new file mode 100644 index 0000000000..e0aec37ac5 --- /dev/null +++ b/ui/app/AppLayouts/Chat/controls/community/AirdropRecipientsSelector.qml @@ -0,0 +1,65 @@ +import QtQuick 2.15 + +import StatusQ.Components 0.1 + +import utils 1.0 + + +StatusFlowSelector { + id: root + + property alias addressesModel: addressesSelectorPanel.model + property alias membersModel: membersSelectorPanel.model + + property alias loadingAddresses: addressesSelectorPanel.loading + property alias addressesInputText: addressesSelectorPanel.text + + property bool showAddressesInputWhenEmpty: false + + signal addAddressesRequested(string addresses) + signal removeAddressRequested(int index) + signal removeMemberRequested(int index) + + placeholderItem.visible: !addressesSelectorPanel.visible && + !membersSelectorPanel.visible + + title: qsTr("To") + icon: Style.svg("member") + flowSpacing: 12 + + placeholderText: qsTr("Example: 12 addresses and 3 members") + + function clearAddressesInput() { + addressesSelectorPanel.clearInput() + } + + function positionAddressesListAtEnd() { + addressesSelectorPanel.positionListAtEnd() + } + + function positionMembersListAtEnd() { + membersSelectorPanel.positionListAtEnd() + } + + AddressesSelectorPanel { + id: addressesSelectorPanel + + visible: count > 0 || root.showAddressesInputWhenEmpty + width: root.availableWidth + + Component.onCompleted: { + addAddressesRequested.connect(root.addAddressesRequested) + removeAddressRequested.connect(root.removeAddressRequested) + } + } + + MembersSelectorPanel { + id: membersSelectorPanel + + visible: count > 0 + width: root.availableWidth + + Component.onCompleted: removeMemberRequested.connect( + root.removeMemberRequested) + } +} diff --git a/ui/app/AppLayouts/Chat/controls/community/MembersSelectorPanel.qml b/ui/app/AppLayouts/Chat/controls/community/MembersSelectorPanel.qml new file mode 100644 index 0000000000..5eeb514691 --- /dev/null +++ b/ui/app/AppLayouts/Chat/controls/community/MembersSelectorPanel.qml @@ -0,0 +1,103 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import shared.controls.delegates 1.0 + +import utils 1.0 + + +Control { + id: root + + property alias model: listView.model + property int maximumListHeight: 188 + + readonly property alias count: listView.count + + signal removeMemberRequested(int index) + + function positionListAtEnd() { + listView.positionViewAtEnd() + } + + QtObject { + id: d + + readonly property int delegateHeight: 47 + } + + contentItem: Column { + spacing: 8 + + RowLayout { + width: root.availableWidth + spacing: 0 + + component Text: StatusBaseText { + color: Theme.palette.baseColor1 + text: qsTr("Members") + font.pixelSize: Theme.tertiaryTextFontSize + elide: Text.ElideRight + } + + Text { + text: qsTr("Members") + } + + Item { Layout.fillWidth: true } + + Text { + text: qsTr("%n member(s)", "", root.count) + } + } + + Rectangle { + width: root.availableWidth + height: Math.min(root.maximumListHeight, + d.delegateHeight * root.count) + + radius: Style.current.radius + color: Theme.palette.indirectColor1 + + StatusListView { + id: listView + + anchors.fill: parent + + delegate: ContactListItemDelegate { + width: ListView.view.width + height: d.delegateHeight + asset.width: 29 + asset.height: 29 + + color: "transparent" + + StatusIcon { + id: deleteIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 10 + + width: 16 + height: 16 + + icon: "delete" + color: Theme.palette.directColor1 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + onClicked: root.removeMemberRequested(model.index) + } + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Chat/controls/community/qmldir b/ui/app/AppLayouts/Chat/controls/community/qmldir index 03439710bc..2d5da47fc0 100644 --- a/ui/app/AppLayouts/Chat/controls/community/qmldir +++ b/ui/app/AppLayouts/Chat/controls/community/qmldir @@ -1,9 +1,13 @@ +AddressesInputList 1.0 AddressesInputList.qml +AddressesSelectorPanel 1.0 AddressesSelectorPanel.qml +AirdropRecipientsSelector 1.0 AirdropRecipientsSelector.qml AirdropTokensSelector 1.0 AirdropTokensSelector.qml CommunityCategoryListItem 1.0 CommunityCategoryListItem.qml CommunityListItem 1.0 CommunityListItem.qml HoldingTypes 1.0 HoldingTypes.qml HoldingsDropdown 1.0 HoldingsDropdown.qml InDropdown 1.0 InDropdown.qml +MembersSelectorPanel 1.0 MembersSelectorPanel.qml PermissionItem 1.0 PermissionItem.qml PermissionsDropdown 1.0 PermissionsDropdown.qml singleton PermissionTypes 1.0 PermissionTypes.qml