diff --git a/resources.qrc b/resources.qrc index ba9cdae0e0706..a372fa3eb691a 100644 --- a/resources.qrc +++ b/resources.qrc @@ -75,5 +75,6 @@ src/gui/macOS/ui/FileProviderSettings.qml src/gui/macOS/ui/FileProviderFileDelegate.qml src/gui/integration/FileActionsWindow.qml + src/gui/GovernanceLabelsDialog.qml diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index b68e5fd9287e4..a334a2a712a4d 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -207,6 +207,27 @@ set(client_SRCS filedetails/shareemodel.cpp filedetails/sortedsharemodel.h filedetails/sortedsharemodel.cpp + governance/governancenetworkjob.h + governance/governancenetworkjob.cpp + governance/getgovernancelabels.h + governance/getgovernancelabels.cpp + governance/getavailablegovernancelabels.h + governance/getavailablegovernancelabels.cpp + governance/applygovernancelabel.h + governance/applygovernancelabel.cpp + governance/deletegovernancelabel.h + governance/deletegovernancelabel.cpp + governance/typedgovernancenetworkjob.h + governance/typedgovernancenetworkjob.cpp + governance/ocsgovernancejob.h + governance/ocsgovernancejob.cpp + governance/typedwithlabelidgovernancenetworkjob.h + governance/typedwithlabelidgovernancenetworkjob.cpp + governance/governancelabelinfo.h + governance/governancelabelinfo.cpp + governance/governancelabelslistmodel.h + governance/governancelabelslistmodel.cpp + governance/governancetypes.h tray/svgimageprovider.h tray/svgimageprovider.cpp tray/syncstatussummary.h diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml new file mode 100644 index 0000000000000..90230e23bf64c --- /dev/null +++ b/src/gui/GovernanceLabelsDialog.qml @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQml +import QtQuick +import QtQuick.Window as QtWindow +import QtQuick.Layouts +import QtQuick.Controls +import QtQml.Models +import Style +import com.nextcloud.desktopclient +import "./tray" + +ApplicationWindow { + id: governanceLabelsDialog + + required property var fileName + required property var fileId + required property var account + + flags: Qt.Window | Qt.Dialog + visible: true + + LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + width: Style.minimumWidthResolveConflictsDialog + height: Style.minimumHeightResolveConflictsDialog + minimumWidth: Style.minimumWidthResolveConflictsDialog + minimumHeight: Style.minimumHeightResolveConflictsDialog + title: qsTr('Applys labels') + + onClosing: function(close) { + Systray.destroyDialog(self); + close.accepted = true + } + + GetAvailableGovernanceLabels { + id: getAvailableGovernanceLabelsForSensitivity + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Sensitivity + entityId: governanceLabelsDialog.fileId + + onFinished: function(reply) { + labelsModel.setAvailableLabelsJsonData(reply) + } + } + + GovernanceLabelsListModel { + id: labelsModel + + entityId: governanceLabelsDialog.fileId + labelType: GovernanceNetworkJob.Sensitivity + + onRefreshData: function(labelType, entityId) { + getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + } + } + + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.bottomMargin: 5 + anchors.topMargin: 10 + spacing: 15 + z: 2 + + EnforcedPlainTextLabel { + text: 'Sensitivity label:' + + font.pixelSize: Style.pixelSize + 2 + } + + ComboBox { + id: selectedNewSensitivityLabel + + font.pixelSize: Style.pixelSize + 2 + Accessible.role: Accessible.ComboBox + Accessible.name: qsTr("Select sensitivity label") + + model: labelsModel + textRole: 'name' + valueRole: 'id' + } + + DialogButtonBox { + Layout.fillWidth: true + + Button { + text: qsTr("Apply") + DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole + } + + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } + + onAccepted: function() { + Systray.destroyDialog(governanceLabelsDialog) + } + + onRejected: function() { + Systray.destroyDialog(governanceLabelsDialog) + } + } + } + + Rectangle { + color: palette.base + anchors.fill: parent + z: 1 + } +} diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 3f508659ba576..e8b7fe216c7f0 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -454,6 +454,9 @@ Application::Application(int &argc, char **argv) connect(FolderMan::instance()->socketApi(), &SocketApi::shareCommandReceived, _gui.data(), &ownCloudGui::slotShowShareDialog); + connect(FolderMan::instance()->socketApi(), &SocketApi::governanceLabelsCommandReceived, + _gui.data(), &ownCloudGui::slotShowGovernanceLabelsDialog); + connect(FolderMan::instance()->socketApi(), &SocketApi::fileActivityCommandReceived, _gui.data(), &ownCloudGui::slotShowFileActivityDialog); diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp new file mode 100644 index 0000000000000..e32f306a597ad --- /dev/null +++ b/src/gui/governance/applygovernancelabel.cpp @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "applygovernancelabel.h" + +#include "ocsgovernancejob.h" + +namespace OCC +{ + +ApplyGovernanceLabel::ApplyGovernanceLabel(QObject *parent) + : OCC::TypedWithLabelIdGovernanceNetworkJob{parent} +{ + connect(this, &ApplyGovernanceLabel::apiVersionChanged, this, &ApplyGovernanceLabel::initialize); + connect(this, &ApplyGovernanceLabel::entityTypeChanged, this, &ApplyGovernanceLabel::initialize); + connect(this, &ApplyGovernanceLabel::customEntityTypeChanged, this, &ApplyGovernanceLabel::initialize); + connect(this, &ApplyGovernanceLabel::entityIdChanged, this, &ApplyGovernanceLabel::initialize); + connect(this, &ApplyGovernanceLabel::accountChanged, this, &ApplyGovernanceLabel::initialize); + connect(this, &ApplyGovernanceLabel::labelTypeChanged, this, &ApplyGovernanceLabel::initialize); + connect(this, &ApplyGovernanceLabel::labelIdChanged, this, &ApplyGovernanceLabel::initialize); +} + +void ApplyGovernanceLabel::start() +{ + if (!checkParameters()) { + Q_EMIT finishedWitherror(500, {}); + return; + } + + setOcsGovernanceJob(QPointer{new OcsGovernanceJob{account()}}); + + connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, + this, &ApplyGovernanceLabel::jobDone); + connect(ocsGovernanceJob().data(), &OcsJob::ocsError, + this, &ApplyGovernanceLabel::finishedWitherror); + + ocsGovernanceJob()->setPath(buildPath()); + ocsGovernanceJob()->setMethod("POST"); + + ocsGovernanceJob()->start(); +} + +void ApplyGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) +{ + Q_EMIT finished(reply); +} + +void ApplyGovernanceLabel::initialize() +{ + if (checkParameters()) { + start(); + } +} + +} // namespace OCC diff --git a/src/gui/governance/applygovernancelabel.h b/src/gui/governance/applygovernancelabel.h new file mode 100644 index 0000000000000..5a7afab1b8e4f --- /dev/null +++ b/src/gui/governance/applygovernancelabel.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef APPLYGOVERNANCELABEL_H +#define APPLYGOVERNANCELABEL_H + +#include "typedwithlabelidgovernancenetworkjob.h" + +#include +#include +#include + +namespace OCC +{ + +class ApplyGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT +public: + explicit ApplyGovernanceLabel(QObject *parent = nullptr); + +public Q_SLOTS: + void start(); + +private Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); + + void initialize(); +}; + +} // namespace OCC + +#endif // APPLYGOVERNANCELABEL_H diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp new file mode 100644 index 0000000000000..f8d7a652a3721 --- /dev/null +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "deletegovernancelabel.h" + +#include "ocsgovernancejob.h" + +namespace OCC +{ + +DeleteGovernanceLabel::DeleteGovernanceLabel(QObject *parent) + : OCC::TypedWithLabelIdGovernanceNetworkJob{parent} +{ + connect(this, &DeleteGovernanceLabel::apiVersionChanged, this, &DeleteGovernanceLabel::initialize); + connect(this, &DeleteGovernanceLabel::entityTypeChanged, this, &DeleteGovernanceLabel::initialize); + connect(this, &DeleteGovernanceLabel::customEntityTypeChanged, this, &DeleteGovernanceLabel::initialize); + connect(this, &DeleteGovernanceLabel::entityIdChanged, this, &DeleteGovernanceLabel::initialize); + connect(this, &DeleteGovernanceLabel::accountChanged, this, &DeleteGovernanceLabel::initialize); + connect(this, &DeleteGovernanceLabel::labelTypeChanged, this, &DeleteGovernanceLabel::initialize); + connect(this, &DeleteGovernanceLabel::labelIdChanged, this, &DeleteGovernanceLabel::initialize); +} + +void DeleteGovernanceLabel::start() +{ + if (!checkParameters()) { + Q_EMIT finishedWitherror(500, {}); + return; + } + + setOcsGovernanceJob(QPointer{new OcsGovernanceJob{account()}}); + + connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, + this, &DeleteGovernanceLabel::jobDone); + connect(ocsGovernanceJob().data(), &OcsJob::ocsError, + this, &DeleteGovernanceLabel::finishedWitherror); + + ocsGovernanceJob()->setPath(buildPath()); + ocsGovernanceJob()->setMethod("DELETE"); + + ocsGovernanceJob()->start(); +} + +void DeleteGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) +{ + Q_EMIT finished(reply); +} + +void DeleteGovernanceLabel::initialize() +{ + if (checkParameters()) { + start(); + } +} + +} // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.h b/src/gui/governance/deletegovernancelabel.h new file mode 100644 index 0000000000000..8be8c1a22bc5f --- /dev/null +++ b/src/gui/governance/deletegovernancelabel.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef DELETEGOVERNANCELABEL_H +#define DELETEGOVERNANCELABEL_H + +#include "typedwithlabelidgovernancenetworkjob.h" + +#include +#include +#include + +namespace OCC +{ + +class DeleteGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT +public: + explicit DeleteGovernanceLabel(QObject *parent = nullptr); + +public Q_SLOTS: + void start(); + +private Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); + + void initialize(); +}; + +} // namespace OCC + +#endif // DELETEGOVERNANCELABEL_H diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp new file mode 100644 index 0000000000000..0c681082ac38f --- /dev/null +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "getavailablegovernancelabels.h" + +#include "ocsgovernancejob.h" + +using namespace Qt::StringLiterals; + +namespace OCC +{ + +GetAvailableGovernanceLabels::GetAvailableGovernanceLabels(QObject *parent) + : OCC::TypedGovernanceNetworkJob{parent} +{ + connect(this, &GetAvailableGovernanceLabels::apiVersionChanged, this, &GetAvailableGovernanceLabels::initialize); + connect(this, &GetAvailableGovernanceLabels::entityTypeChanged, this, &GetAvailableGovernanceLabels::initialize); + connect(this, &GetAvailableGovernanceLabels::customEntityTypeChanged, this, &GetAvailableGovernanceLabels::initialize); + connect(this, &GetAvailableGovernanceLabels::entityIdChanged, this, &GetAvailableGovernanceLabels::initialize); + connect(this, &GetAvailableGovernanceLabels::accountChanged, this, &GetAvailableGovernanceLabels::initialize); + connect(this, &GetAvailableGovernanceLabels::labelTypeChanged, this, &GetAvailableGovernanceLabels::initialize); +} + +void GetAvailableGovernanceLabels::start(Governance::LabelType labelType, const QString &entityId) +{ + setLabelType(labelType); + setEntityId(entityId); + + start(); +} + +void GetAvailableGovernanceLabels::start() +{ + if (!checkParameters()) { + Q_EMIT finishedWitherror(500, {}); + return; + } + + setOcsGovernanceJob(QPointer{new OcsGovernanceJob{account()}}); + + connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, + this, &GetAvailableGovernanceLabels::jobDone); + connect(ocsGovernanceJob().data(), &OcsJob::ocsError, + this, &GetAvailableGovernanceLabels::finishedWitherror); + + ocsGovernanceJob()->setPath(buildPath()); + ocsGovernanceJob()->setMethod("GET"); + + ocsGovernanceJob()->start(); +} + +void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) +{ + Q_EMIT finished(reply); +} + +void GetAvailableGovernanceLabels::initialize() +{ + if (checkParameters()) { + start(); + } +} + +QString GetAvailableGovernanceLabels::buildPath() const +{ + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/available"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelTypeAsString()); +} + +} // namespace OCC diff --git a/src/gui/governance/getavailablegovernancelabels.h b/src/gui/governance/getavailablegovernancelabels.h new file mode 100644 index 0000000000000..9bbf0242bffce --- /dev/null +++ b/src/gui/governance/getavailablegovernancelabels.h @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef GETAVAILABLEGOVERNANCELABELS_H +#define GETAVAILABLEGOVERNANCELABELS_H + +#include "typedgovernancenetworkjob.h" + +#include +#include +#include + +namespace OCC +{ + +class GetAvailableGovernanceLabels : public OCC::TypedGovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT + +public: + explicit GetAvailableGovernanceLabels(QObject *parent = nullptr); + +Q_SIGNALS: + +public Q_SLOTS: + void start(); + + void start(OCC::Governance::LabelType labelType, const QString &entityId); + +protected: + [[nodiscard]] QString buildPath() const override; + +private Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); + + void initialize(); +}; + +} // namespace OCC + +#endif // GETAVAILABLEGOVERNANCELABELS_H diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp new file mode 100644 index 0000000000000..afe43a2b888a9 --- /dev/null +++ b/src/gui/governance/getgovernancelabels.cpp @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "getgovernancelabels.h" + +#include "ocsgovernancejob.h" + +namespace OCC +{ + +GetGovernanceLabels::GetGovernanceLabels(QObject *parent) + : OCC::GovernanceNetworkJob{parent} +{ + connect(this, &GetGovernanceLabels::apiVersionChanged, this, &GetGovernanceLabels::initialize); + connect(this, &GetGovernanceLabels::entityTypeChanged, this, &GetGovernanceLabels::initialize); + connect(this, &GetGovernanceLabels::customEntityTypeChanged, this, &GetGovernanceLabels::initialize); + connect(this, &GetGovernanceLabels::entityIdChanged, this, &GetGovernanceLabels::initialize); + connect(this, &GetGovernanceLabels::accountChanged, this, &GetGovernanceLabels::initialize); +} + +void GetGovernanceLabels::start() +{ + if (!checkParameters()) { + Q_EMIT finishedWitherror(500, {}); + return; + } + + setOcsGovernanceJob(QPointer{new OcsGovernanceJob{account()}}); + + connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, + this, &GetGovernanceLabels::jobDone); + connect(ocsGovernanceJob().data(), &OcsJob::ocsError, + this, &GetGovernanceLabels::finishedWitherror); + + ocsGovernanceJob()->setPath(buildPath()); + ocsGovernanceJob()->setMethod("GET"); + + ocsGovernanceJob()->start(); +} + +void GetGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) +{ + Q_EMIT finished(reply); +} + +void GetGovernanceLabels::initialize() +{ + if (checkParameters()) { + start(); + } +} + +} // namespace OCC diff --git a/src/gui/governance/getgovernancelabels.h b/src/gui/governance/getgovernancelabels.h new file mode 100644 index 0000000000000..176944efbd30b --- /dev/null +++ b/src/gui/governance/getgovernancelabels.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef GETGOVERNANCELABELS_H +#define GETGOVERNANCELABELS_H + +#include "governancenetworkjob.h" + +#include +#include +#include + +namespace OCC +{ + +class GetGovernanceLabels : public OCC::GovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT +public: + explicit GetGovernanceLabels(QObject *parent = nullptr); + +public Q_SLOTS: + void start(); + +private Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); + + void initialize(); +}; + +} // namespace OCC + +#endif // GETGOVERNANCELABELS_H diff --git a/src/gui/governance/governancelabelinfo.cpp b/src/gui/governance/governancelabelinfo.cpp new file mode 100644 index 0000000000000..32aa1b905eb3a --- /dev/null +++ b/src/gui/governance/governancelabelinfo.cpp @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "governancelabelinfo.h" + +namespace OCC +{ + +} // namespace OCC diff --git a/src/gui/governance/governancelabelinfo.h b/src/gui/governance/governancelabelinfo.h new file mode 100644 index 0000000000000..9ee7e311792c2 --- /dev/null +++ b/src/gui/governance/governancelabelinfo.h @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef GOVERNANCELABELINFO_H +#define GOVERNANCELABELINFO_H + +#include +#include + +namespace OCC +{ + +struct GovernanceLabelInfo +{ +public: + QString _id; + + QString _name; + + int _priority = -1; + + QString _description; + + QString _color; + + QStringList _scopes; +}; + +} // namespace OCC + +#endif // GOVERNANCELABELINFO_H diff --git a/src/gui/governance/governancelabelslistmodel.cpp b/src/gui/governance/governancelabelslistmodel.cpp new file mode 100644 index 0000000000000..02684e37b334c --- /dev/null +++ b/src/gui/governance/governancelabelslistmodel.cpp @@ -0,0 +1,192 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "governancelabelslistmodel.h" + +#include +#include +#include +#include + +using namespace Qt::StringLiterals; + +namespace OCC +{ + +Q_LOGGING_CATEGORY(lcGovernanceLabelsListModel, "nextcloud.gui.governance.labelslistmodel", QtInfoMsg) + +GovernanceLabelsListModel::GovernanceLabelsListModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +int GovernanceLabelsListModel::rowCount(const QModelIndex &parent) const +{ + auto result = 0; + if (parent.isValid()) { + return result; + } + + result = _data.count(); + return result; +} + +QVariant GovernanceLabelsListModel::data(const QModelIndex &index, int role) const +{ + auto result = QVariant{}; + + if (!index.isValid()) { + return result; + } + + if (index.column() != 0) { + return result; + } + + if (index.row() < 0 || index.row() >= _data.count()) { + return result; + } + + if (role >= Qt::UserRole + 1) { + auto convertedRole = static_cast(role); + + switch (convertedRole) + { + case LabelsListModelRoles::IdRole: + result = _data[index.row()]._id; + break; + case LabelsListModelRoles::NameRole: + result = _data[index.row()]._name; + break; + case LabelsListModelRoles::PriorityRole: + result = _data[index.row()]._priority; + break; + case LabelsListModelRoles::DescriptionRole: + result = _data[index.row()]._description; + break; + case LabelsListModelRoles::ColorRole: + result = _data[index.row()]._color; + break; + case LabelsListModelRoles::ScopesRole: + result = _data[index.row()]._scopes; + break; + } + } + + return result; +} + +QHash GovernanceLabelsListModel::roleNames() const +{ + auto result = QHash{ + {static_cast(LabelsListModelRoles::IdRole), "id"_ba}, + {static_cast(LabelsListModelRoles::NameRole), "name"_ba}, + {static_cast(LabelsListModelRoles::PriorityRole), "priority"_ba}, + {static_cast(LabelsListModelRoles::DescriptionRole), "description"_ba}, + {static_cast(LabelsListModelRoles::ColorRole), "color"_ba}, + {static_cast(LabelsListModelRoles::ScopesRole), "scopes"_ba}, + }; + + return result; +} + +Governance::LabelType GovernanceLabelsListModel::labelType() const +{ + return _labelType; +} + +void GovernanceLabelsListModel::setLabelType(Governance::LabelType newLabelType) +{ + if (_labelType == newLabelType) { + return; + } + + _labelType = newLabelType; + Q_EMIT labelTypeChanged(); + + emitRefreshData(); +} + +QString GovernanceLabelsListModel::entityId() const +{ + return _entityId; +} + +void GovernanceLabelsListModel::setEntityId(const QString &newEntityId) +{ + if (_entityId == newEntityId) { + return; + } + + _entityId = newEntityId; + Q_EMIT entityIdChanged(); + + emitRefreshData(); +} + +void GovernanceLabelsListModel::setAvailableLabelsJsonData(const QJsonDocument &reply) +{ + const auto replyObject = reply.object(); + + if (!replyObject.contains(u"ocs"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "wrong format for reply" << reply.toJson(QJsonDocument::JsonFormat::Compact); + return; + } + + const auto ocsObject = replyObject.value(u"ocs"_s).toObject(); + + if (!ocsObject.contains(u"data"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "wrong format for reply" << ocsObject; + return; + } + + const auto dataArray = ocsObject.value(u"data"_s).toArray(); + + const auto convertToStringList = [] (const QJsonArray &scopesList) -> QStringList + { + auto result = QStringList{}; + + for (const auto &oneScope : scopesList) { + result << oneScope.toString(); + } + + return result; + }; + + beginResetModel(); + _data.clear(); + for (const auto oneLabel : dataArray) { + const auto oneLabelObject = oneLabel.toObject(); + _data.emplaceBack(oneLabelObject.value(u"id"_s).toString(), + oneLabelObject.value(u"name"_s).toString(), + oneLabelObject.value(u"priority"_s).toInt(), + oneLabelObject.value(u"description"_s).toString(), + oneLabelObject.value(u"color"_s).toString(), + convertToStringList(oneLabelObject.value(u"scopes"_s).toArray()) + ); + } + endResetModel(); +} + +void GovernanceLabelsListModel::setExistingLabelsJsonData(const QJsonDocument &data) +{ + qCInfo(lcGovernanceLabelsListModel()) << data.toJson(QJsonDocument::JsonFormat::Compact); +} + +void OCC::GovernanceLabelsListModel::etagChanged() +{ + Q_EMIT refreshData(_labelType, _entityId); +} + +void GovernanceLabelsListModel::emitRefreshData() +{ + if (_entityId.isEmpty() || _labelType == Governance::LabelType::InvalidLabelType) { + return; + } + + Q_EMIT refreshData(_labelType, _entityId); +} + +} // namespace OCC diff --git a/src/gui/governance/governancelabelslistmodel.h b/src/gui/governance/governancelabelslistmodel.h new file mode 100644 index 0000000000000..d4b20b7028b31 --- /dev/null +++ b/src/gui/governance/governancelabelslistmodel.h @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef GOVERNANCELABELLISTMODEL_H +#define GOVERNANCELABELLISTMODEL_H + +#include "governancetypes.h" +#include "governancelabelinfo.h" + +#include +#include +#include + +namespace OCC +{ + +class GovernanceLabelsListModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(Governance::LabelType labelType READ labelType WRITE setLabelType NOTIFY labelTypeChanged FINAL) + + Q_PROPERTY(QString entityId READ entityId WRITE setEntityId NOTIFY entityIdChanged FINAL) + +public: + enum class LabelsListModelRoles { + IdRole = Qt::UserRole + 1, + NameRole, + PriorityRole, + DescriptionRole, + ColorRole, + ScopesRole, + }; + + Q_ENUM(LabelsListModelRoles) + + explicit GovernanceLabelsListModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QHash roleNames() const override; + + [[nodiscard]] Governance::LabelType labelType() const; + + void setLabelType(Governance::LabelType newLabelType); + + [[nodiscard]] QString entityId() const; + + void setEntityId(const QString &newEntityId); + +public Q_SLOTS: + void setAvailableLabelsJsonData(const QJsonDocument &reply); + + void setExistingLabelsJsonData(const QJsonDocument &data); + + void etagChanged(); + +Q_SIGNALS: + void labelTypeChanged(); + + void entityIdChanged(); + + void refreshData(OCC::Governance::LabelType labelType, const QString &entityId); + +private: + void emitRefreshData(); + + Governance::LabelType _labelType = Governance::LabelType::InvalidLabelType; + + QString _entityId; + + QList _data; +}; + +} // namespace OCC + +#endif // GOVERNANCELABELLISTMODEL_H diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp new file mode 100644 index 0000000000000..b2ce60d48b397 --- /dev/null +++ b/src/gui/governance/governancenetworkjob.cpp @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "governancenetworkjob.h" + +using namespace Qt::StringLiterals; + +namespace OCC +{ + +Q_LOGGING_CATEGORY(lcGovernanceNetwork, "nextcloud.gui.governance.network", QtInfoMsg) + +GovernanceNetworkJob::GovernanceNetworkJob(QObject *parent) + : QObject{parent} +{ +} + +Governance::ApiVersion GovernanceNetworkJob::apiVersion() const +{ + return _apiVersion; +} + +void GovernanceNetworkJob::setApiVersion(Governance::ApiVersion newApiVersion) +{ + if (_apiVersion == newApiVersion) { + return; + } + + _apiVersion = newApiVersion; + Q_EMIT apiVersionChanged(); +} + +Governance::EntityType GovernanceNetworkJob::entityType() const +{ + return _entityType; +} + +void GovernanceNetworkJob::setEntityType(Governance::EntityType newEntityType) +{ + if (_entityType == newEntityType) { + return; + } + + _entityType = newEntityType; + Q_EMIT entityTypeChanged(); +} + +QString GovernanceNetworkJob::customEntityType() const +{ + return _customEntityType; +} + +void GovernanceNetworkJob::setCustomEntityType(const QString &newCustomEntityType) +{ + if (_customEntityType == newCustomEntityType) { + return; + } + + _customEntityType = newCustomEntityType; + Q_EMIT customEntityTypeChanged(); +} + +QString GovernanceNetworkJob::entityId() const +{ + return _entityId; +} + +void GovernanceNetworkJob::setEntityId(const QString &newEntityId) +{ + if (_entityId == newEntityId) { + return; + } + + _entityId = newEntityId; + Q_EMIT entityIdChanged(); +} + +QString GovernanceNetworkJob::buildPath() const +{ + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId()); +} + +QString GovernanceNetworkJob::apiVersionAsString() const +{ + auto result = QString{}; + + switch (_apiVersion) + { + case Governance::ApiVersion::Version_1: + result = u"v1"_s; + break; + case Governance::ApiVersion::InvalidApiVersion: + result = u"invalid"_s; + break; + } + + return result; +} + +QString GovernanceNetworkJob::entityTypeAsString() const +{ + auto result = QString{}; + + switch (_entityType) + { + case Governance::EntityType::Files: + result = u"FILES"_s; + break; + case Governance::EntityType::Mails: + result = u"MAILS"_s; + break; + case Governance::EntityType::Custom: + result = _customEntityType; + break; + } + + return result; +} + +bool GovernanceNetworkJob::checkParameters() const +{ + auto result = true; + + if (!_account) { + result = false; + return result; + } + + if (_apiVersion == Governance::ApiVersion::InvalidApiVersion) { + result = false; + return result; + } + + if (_entityType != Governance::EntityType::Files) { + result = false; + return result; + } + + if (_entityType == Governance::EntityType::Custom && _customEntityType.isEmpty()) { + result = false; + return result; + } + + if (_entityId.isEmpty()) { + result = false; + return result; + } + + return result; +} + +void GovernanceNetworkJob::setAccount(AccountPtr newAccount) +{ + if (_account == newAccount) { + return; + } + + _account = newAccount; + Q_EMIT accountChanged(); +} + +} // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h new file mode 100644 index 0000000000000..7afe83e66e1ef --- /dev/null +++ b/src/gui/governance/governancenetworkjob.h @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef GOVERNANCENETWORKJOB_H +#define GOVERNANCENETWORKJOB_H + +#include "governancetypes.h" +#include "accountfwd.h" + +#include +#include +#include +#include + +namespace OCC +{ + +Q_DECLARE_LOGGING_CATEGORY(lcGovernanceNetwork) + +class OcsGovernanceJob; + +class GovernanceNetworkJob : public QObject +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(AccountPtr account READ account WRITE setAccount NOTIFY accountChanged FINAL) + + Q_PROPERTY(Governance::ApiVersion apiVersion READ apiVersion WRITE setApiVersion NOTIFY apiVersionChanged FINAL) + + Q_PROPERTY(Governance::EntityType entityType READ entityType WRITE setEntityType NOTIFY entityTypeChanged FINAL) + + Q_PROPERTY(QString customEntityType READ customEntityType WRITE setCustomEntityType NOTIFY customEntityTypeChanged FINAL) + + Q_PROPERTY(QString entityId READ entityId WRITE setEntityId NOTIFY entityIdChanged FINAL) + +public: + explicit GovernanceNetworkJob(QObject *parent = nullptr); + + [[nodiscard]] Governance::ApiVersion apiVersion() const; + + void setApiVersion(Governance::ApiVersion newApiVersion); + + [[nodiscard]] Governance::EntityType entityType() const; + + void setEntityType(Governance::EntityType newEntityType); + + [[nodiscard]] QString customEntityType() const; + + void setCustomEntityType(const QString &newCustomEntityType); + + [[nodiscard]] QString entityId() const; + + void setEntityId(const QString &newEntityId); + + void setAccount(AccountPtr newAccount); + +Q_SIGNALS: + void apiVersionChanged(); + + void entityTypeChanged(); + + void customEntityTypeChanged(); + + void entityIdChanged(); + + void finished(QJsonDocument reply); + + void finishedWitherror(int errorCode, const QString &errorMessage); + + void accountChanged(); + +protected: + void setOcsGovernanceJob(QPointer newJob) + { + _ocsGovernanceJob = newJob; + } + + [[nodiscard]] QPointer ocsGovernanceJob() const + { + return _ocsGovernanceJob; + } + + [[nodiscard]] AccountPtr account() const + { + return _account; + } + + [[nodiscard]] virtual QString buildPath() const; + + [[nodiscard]] QString apiVersionAsString() const; + + [[nodiscard]] QString entityTypeAsString() const; + + [[nodiscard]] virtual bool checkParameters() const; + +private: + AccountPtr _account; + + Governance::ApiVersion _apiVersion = Governance::ApiVersion::Version_1; + + Governance::EntityType _entityType = Governance::EntityType::Files; + + QString _customEntityType; + + QString _entityId; + + QPointer _ocsGovernanceJob; +}; + +} // namespace OCC + +#endif // GOVERNANCENETWORKJOB_H diff --git a/src/gui/governance/governancetypes.h b/src/gui/governance/governancetypes.h new file mode 100644 index 0000000000000..b95d5eae8b530 --- /dev/null +++ b/src/gui/governance/governancetypes.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef GOVERNANCETYPES_H +#define GOVERNANCETYPES_H + +#include + +namespace OCC { + +namespace Governance { + +Q_NAMESPACE + +enum class EntityType { + Files, + Mails, + Custom, +}; + +Q_ENUM_NS(EntityType) + +enum class LabelType { + Sensitivity, + Retention, + Hold, + InvalidLabelType, +}; + +Q_ENUM_NS(LabelType) + +enum class ApiVersion { + InvalidApiVersion, + Version_1, +}; + +Q_ENUM_NS(ApiVersion) + +} + +} + +#endif // GOVERNANCETYPES_H diff --git a/src/gui/governance/ocsgovernancejob.cpp b/src/gui/governance/ocsgovernancejob.cpp new file mode 100644 index 0000000000000..bfde0274f9cb6 --- /dev/null +++ b/src/gui/governance/ocsgovernancejob.cpp @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "ocsgovernancejob.h" + +namespace OCC +{ + +OcsGovernanceJob::OcsGovernanceJob(AccountPtr account) + : OCC::OcsJob{account} +{ +} + +void OcsGovernanceJob::setMethod(const QByteArray &method) +{ + setVerb(method); +} + +void OcsGovernanceJob::start() +{ + OcsJob::start(); +} + +} // namespace OCC diff --git a/src/gui/governance/ocsgovernancejob.h b/src/gui/governance/ocsgovernancejob.h new file mode 100644 index 0000000000000..95a9fe83427b3 --- /dev/null +++ b/src/gui/governance/ocsgovernancejob.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef OCSGOVERNANCEJOB_H +#define OCSGOVERNANCEJOB_H + +#include +#include + +namespace OCC +{ + +class OcsGovernanceJob : public OCC::OcsJob +{ + Q_OBJECT +public: + explicit OcsGovernanceJob(AccountPtr account); + + void setMethod(const QByteArray &method); + + void start() override; +}; + +} // namespace OCC + +#endif // OCSGOVERNANCEJOB_H diff --git a/src/gui/governance/typedgovernancenetworkjob.cpp b/src/gui/governance/typedgovernancenetworkjob.cpp new file mode 100644 index 0000000000000..d9f2b091fcd56 --- /dev/null +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "typedgovernancenetworkjob.h" + +using namespace Qt::StringLiterals; + +namespace OCC +{ + +TypedGovernanceNetworkJob::TypedGovernanceNetworkJob(QObject *parent) + : OCC::GovernanceNetworkJob{parent} +{ +} + +Governance::LabelType TypedGovernanceNetworkJob::labelType() const +{ + return _labelType; +} + +void TypedGovernanceNetworkJob::setLabelType(Governance::LabelType newLabelType) +{ + if (_labelType == newLabelType) { + return; + } + + _labelType = newLabelType; + Q_EMIT labelTypeChanged(); +} + +QString TypedGovernanceNetworkJob::labelTypeAsString() const +{ + auto result = QString{}; + + switch (_labelType) + { + case Governance::LabelType::Sensitivity: + result = u"sensitivity"_s; + break; + case Governance::LabelType::Retention: + result = u"retention"_s; + break; + case Governance::LabelType::Hold: + result = u"hold"_s; + break; + case Governance::LabelType::InvalidLabelType: + result = u"invalid"_s; + break; + } + + return result; +} + +bool TypedGovernanceNetworkJob::checkParameters() const +{ + auto result = GovernanceNetworkJob::checkParameters(); + + if (!result) { + return result; + } + + if (_labelType == Governance::LabelType::InvalidLabelType) { + result = false; + return result; + } + + return result; +} + +} // namespace OCC diff --git a/src/gui/governance/typedgovernancenetworkjob.h b/src/gui/governance/typedgovernancenetworkjob.h new file mode 100644 index 0000000000000..1b20e2eef80b0 --- /dev/null +++ b/src/gui/governance/typedgovernancenetworkjob.h @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef TYPEDGOVERNANCENETWORKJOB_H +#define TYPEDGOVERNANCENETWORKJOB_H + +#include "governancenetworkjob.h" +#include +#include + +namespace OCC +{ + +class TypedGovernanceNetworkJob : public GovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(Governance::LabelType labelType READ labelType WRITE setLabelType NOTIFY labelTypeChanged FINAL) + +public: + TypedGovernanceNetworkJob(QObject *parent = nullptr); + + [[nodiscard]] Governance::LabelType labelType() const; + + void setLabelType(Governance::LabelType newLabelType); + +Q_SIGNALS: + void labelTypeChanged(); + +protected: + [[nodiscard]] QString labelTypeAsString() const; + + [[nodiscard]] bool checkParameters() const override; + +private: + Governance::LabelType _labelType = Governance::LabelType::InvalidLabelType; +}; + +} // namespace OCC + +#endif // TYPEDGOVERNANCENETWORKJOB_H diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp new file mode 100644 index 0000000000000..8d35ca055dcd1 --- /dev/null +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "typedwithlabelidgovernancenetworkjob.h" + +using namespace Qt::StringLiterals; + +namespace OCC +{ + +TypedWithLabelIdGovernanceNetworkJob::TypedWithLabelIdGovernanceNetworkJob(QObject *parent) + : OCC::TypedGovernanceNetworkJob{parent} +{ +} + +QString TypedWithLabelIdGovernanceNetworkJob::labelId() const +{ + return _labelId; +} + +void TypedWithLabelIdGovernanceNetworkJob::setLabelId(const QString &newLabelId) +{ + if (_labelId == newLabelId) { + return; + } + + _labelId = newLabelId; + Q_EMIT labelIdChanged(); +} + +QString TypedWithLabelIdGovernanceNetworkJob::buildPath() const +{ + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelId(), labelTypeAsString()); +} + +bool TypedWithLabelIdGovernanceNetworkJob::checkParameters() const +{ + auto result = TypedGovernanceNetworkJob::checkParameters(); + + if (!result) { + return result; + } + + if (_labelId.isEmpty()) { + result = false; + return result; + } + + return result; +} + +} // namespace OCC diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.h b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h new file mode 100644 index 0000000000000..428578e88e96a --- /dev/null +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef TYPEDWITHLABELIDGOVERNANCENETWORKJOB_H +#define TYPEDWITHLABELIDGOVERNANCENETWORKJOB_H + +#include "typedgovernancenetworkjob.h" +#include +#include + +namespace OCC +{ + +class TypedWithLabelIdGovernanceNetworkJob : public OCC::TypedGovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QString labelId READ labelId WRITE setLabelId NOTIFY labelIdChanged FINAL) + +public: + explicit TypedWithLabelIdGovernanceNetworkJob(QObject *parent = nullptr); + + [[nodiscard]] QString labelId() const; + + void setLabelId(const QString &newLabelId); + +signals: + void labelIdChanged(); + +protected: + [[nodiscard]] QString buildPath() const override; + + [[nodiscard]] bool checkParameters() const override; + +private: + QString _labelId; +}; + +} // namespace OCC + +#endif // TYPEDWITHLABELIDGOVERNANCENETWORKJOB_H diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 1fcda11fb55b9..a66977b54f941 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -36,6 +36,11 @@ #include "tray/syncstatussummary.h" #include "tray/unifiedsearchresultslistmodel.h" #include "integration/fileactionsmodel.h" +#include "governance/applygovernancelabel.h" +#include "governance/deletegovernancelabel.h" +#include "governance/getavailablegovernancelabels.h" +#include "governance/getgovernancelabels.h" +#include "governance/governancelabelslistmodel.h" #include "filesystem.h" #ifdef WITH_LIBCLOUDPROVIDERS @@ -152,6 +157,11 @@ ownCloudGui::ownCloudGui(Application *parent) qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncConflictsModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "FileActionsModel"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "AccountWizardController"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "ApplyGovernanceLabel"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "DeleteGovernanceLabel"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "GetAvailableGovernanceLabels"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "GetGovernanceLabels"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "GovernanceLabelsListModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "QAbstractItemModel", "QAbstractItemModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "activity", "Activity"); @@ -160,6 +170,9 @@ ownCloudGui::ownCloudGui(Application *parent) qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "userStatus", "Access to Status enum"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "sharee", "Access to Type enum"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "ClientSideEncryptionTokenSelector", "Access to the certificate selector"); + qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "GovernanceNetworkJob", "base abstract type for governance labels"); + qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "TypedGovernanceNetworkJob", "base abstract type for governance labels"); + qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "TypedWithLabelIdGovernanceNetworkJob", "base abstract type for governance labels"); qRegisterMetaType("ActivityListModel*"); qRegisterMetaType("UnifiedSearchResultsListModel*"); @@ -776,6 +789,13 @@ void ownCloudGui::slotShowShareDialog(const QString &localPath) const _tray->createShareDialog(localPath); } +void ownCloudGui::slotShowGovernanceLabelsDialog(AccountPtr account, + const QString &localPath, + const QString &fileId) const +{ + _tray->createGovernanceLabelsDialog(account, localPath, fileId); +} + void ownCloudGui::slotShowFileActivityDialog(const QString &localPath) const { _tray->createFileActivityDialog(localPath); diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h index 9121f205bdd21..46429bdf41588 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -97,6 +97,9 @@ public slots: * to the folder). */ void slotShowShareDialog(const QString &localPath) const; + void slotShowGovernanceLabelsDialog(AccountPtr account, + const QString &localPath, + const QString &fileId) const; void slotShowFileActivityDialog(const QString &localPath) const; void slotShowFileActionsDialog(const QString &localPath) const; #ifdef BUILD_FILE_PROVIDER_MODULE diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 496a2a4d10131..84419a78ffb54 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -774,6 +774,23 @@ void SocketApi::command_FILE_ACTIONS(const QString &localFile, SocketListener *l processFileActionsRequest(localFile); } +void SocketApi::command_FILES_GOVERNANCE_LABELS(const QString &localFile, SocketListener *listener) +{ + Q_UNUSED(listener); + + auto fileData = FileData::get(localFile); + if (!fileData.folder) { + qCWarning(lcSocketApi) << "Unknown path" << localFile; + return; + } + + auto record = fileData.journalRecord(); + if (!record.isValid()) + return; + + emit governanceLabelsCommandReceived(fileData.folder->accountState()->account(), fileData.localPath, QString::fromLatin1(record._fileId)); +} + // don't pull the share manager into socketapi unittests #ifndef OWNCLOUD_TEST @@ -1275,6 +1292,15 @@ void SocketApi::sendLockFileInfoMenuEntries(const QFileInfo &fileInfo, } } +void SocketApi::sendFilesGovernanceLabelsMenuOptions(const QFileInfo &fileInfo, + const FileData &fileData, + SocketListener *listener) +{ + if (!FileSystem::isDir(fileInfo.absoluteFilePath()) && fileData.folder->accountState()->account()->capabilities().governanceAvailable()) { + listener->sendMessage(QLatin1String("MENU_ITEM:FILES_GOVERNANCE_LABELS::") + tr("Apply labels")); + } +} + SocketApi::FileData SocketApi::FileData::get(const QString &localFile) { FileData data; @@ -1393,6 +1419,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe const auto rootE2eeFolderFlag = isE2eEncryptedRootFolder ? SharingContextItemRootEncryptedFolderFlag::RootEncryptedFolder : SharingContextItemRootEncryptedFolderFlag::NonRootEncryptedFolder; sendSharingContextMenuOptions(fileData, listener, itemEncryptionFlag, rootE2eeFolderFlag); sendFileActionsContextMenuOptions(fileData, listener); + sendFilesGovernanceLabelsMenuOptions(fileInfo, fileData, listener); // Conflict files get conflict resolution actions bool isConflict = Utility::isConflictFile(fileData.folderRelativePath); diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 41758c28c5827..b0a08ecaa328b 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -10,6 +10,7 @@ #include "common/syncfilestatus.h" #include "common/syncjournalfilerecord.h" #include "syncfileitem.h" +#include "accountfwd.h" #include "config.h" @@ -72,6 +73,7 @@ public slots: void shareCommandReceived(const QString &localPath); void fileActivityCommandReceived(const QString &localPath); void fileActionsCommandReceived(const QString &localPath); + void governanceLabelsCommandReceived(OCC::AccountPtr account, const QString &filePath, const QString &fileId); private slots: void slotNewConnection(); @@ -140,6 +142,7 @@ private slots: Q_INVOKABLE void command_LOCK_FILE(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_UNLOCK_FILE(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_FILE_ACTIONS(const QString &localFile, OCC::SocketListener *listener); + Q_INVOKABLE void command_FILES_GOVERNANCE_LABELS(const QString &localFile, OCC::SocketListener *listener); void setFileLock(const QString &localFile, const SyncFileItem::LockStatus lockState) const; @@ -168,6 +171,8 @@ private slots: const SocketListener* const listener, const SyncJournalFileRecord &record) const; + void sendFilesGovernanceLabelsMenuOptions(const QFileInfo &fileInfo, const FileData &fileData, SocketListener *listener); + /** Send the list of menu item. (added in version 1.1) * argument is a list of files for which the menu should be shown, separated by '\x1e' * Reply with GET_MENU_ITEMS:BEGIN diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index d9641b09be0fb..6e05d17c37f65 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -393,6 +393,37 @@ void Systray::createResolveConflictsDialog(const OCC::ActivityList &allConflicts dialogWindow->requestActivate(); } +void Systray::createGovernanceLabelsDialog(AccountPtr account, const QString &fileName, const QString &fileId) +{ + const auto conflictsDialog = std::make_unique(trayEngine(), QStringLiteral("qrc:/qml/src/gui/GovernanceLabelsDialog.qml")); + const QVariantMap initialProperties{ + {"fileName", QVariant::fromValue(fileName)}, + {"account", QVariant::fromValue(account)}, + {"fileId", QVariant::fromValue(fileId)}, + }; + + if(conflictsDialog->isError()) { + qCWarning(lcSystray) << conflictsDialog->errorString(); + return; + } + + // This call dialog gets deallocated on close conditions + // by a call from the QML side to the destroyDialog slot + auto dialog = std::unique_ptr(conflictsDialog->createWithInitialProperties(initialProperties)); + if (!dialog) { + return; + } + dialog->setParent(QGuiApplication::instance()); + + auto dialogWindow = qobject_cast(dialog.release()); + if (!dialogWindow) { + return; + } + dialogWindow->show(); + dialogWindow->raise(); + dialogWindow->requestActivate(); +} + void Systray::createEncryptionTokenDiscoveryDialog() { if (_encryptionTokenDiscoveryDialog) { diff --git a/src/gui/systray.h b/src/gui/systray.h index 15ecfb1998b4c..38237fc0a4426 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -130,6 +130,7 @@ public slots: void createEditFileLocallyLoadingDialog(const QString &fileName); void destroyEditFileLocallyLoadingDialog(); void createResolveConflictsDialog(const OCC::ActivityList &allConflicts); + void createGovernanceLabelsDialog(AccountPtr account, const QString &fileName, const QString &fileId); void createEncryptionTokenDiscoveryDialog(); void destroyEncryptionTokenDiscoveryDialog(); diff --git a/src/gui/wizard/qml/AccountWizardWindow.qml b/src/gui/wizard/qml/AccountWizardWindow.qml index b76f192c948cb..aa22ed9216d9c 100644 --- a/src/gui/wizard/qml/AccountWizardWindow.qml +++ b/src/gui/wizard/qml/AccountWizardWindow.qml @@ -8,6 +8,7 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import com.nextcloud.desktopclient +import "../../tray" import Style import "../../tray" diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 73b796fa2b212..e64d3aed9b888 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -13,6 +13,8 @@ #include #include +using namespace Qt::StringLiterals; + namespace OCC { Q_LOGGING_CATEGORY(lcServerCapabilities, "nextcloud.sync.server.capabilities", QtInfoMsg) @@ -534,6 +536,11 @@ DirectEditor* Capabilities::getDirectEditorForOptionalMimetype(const QMimeType & return nullptr; } +bool Capabilities::governanceAvailable() const +{ + return _capabilities.contains(u"governance"_s); +} + /*-------------------------------------------------------------------------------------*/ diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index a06d4a1036761..87861baee7087 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -183,6 +183,8 @@ class OWNCLOUDSYNC_EXPORT Capabilities DirectEditor* getDirectEditorForMimetype(const QMimeType &mimeType); DirectEditor* getDirectEditorForOptionalMimetype(const QMimeType &mimeType); + [[nodiscard]] bool governanceAvailable() const; + private: [[nodiscard]] QMap serverThemingMap() const; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cc5f9a3b72a7a..73c31a861a4d2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -77,6 +77,7 @@ nextcloud_add_test(AllFilesDeleted) nextcloud_add_test(Blacklist) nextcloud_add_test(LocalDiscovery) nextcloud_add_test(RemoteDiscovery) +nextcloud_add_test(Governance) if (NOT APPLE) nextcloud_add_test(Permissions) diff --git a/test/testgovernance.cpp b/test/testgovernance.cpp new file mode 100644 index 0000000000000..a9e1cf50b3705 --- /dev/null +++ b/test/testgovernance.cpp @@ -0,0 +1,594 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + * + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + */ + +#include "syncenginetestutils.h" + +#include "governance/applygovernancelabel.h" +#include "governance/deletegovernancelabel.h" +#include "governance/getavailablegovernancelabels.h" +#include "governance/getgovernancelabels.h" +#include "governance/governancelabelslistmodel.h" +#include "governance/governancetypes.h" + +#include +#include + +using namespace OCC; +using namespace Qt::StringLiterals; + +class GovernanceTestHelper : public QObject +{ + Q_OBJECT + +public: + GovernanceTestHelper(QObject *parent = nullptr) + : QObject{parent} + { + } + +Q_SIGNALS: + void setupSucceeded(); + +public slots: + void setup(FakeFolder &fakeFolder) + { + fakeFolder.setServerOverride([this] (FakeQNAM::Operation operation, const QNetworkRequest &request, [[maybe_unused]] QIODevice *device) -> QNetworkReply* + { + const auto requestPathString = request.url().path(); + const auto requestPath = QStringView{requestPathString}; + const auto routeIndex = requestPath.indexOf(u"/ocs/v2.php/apps/governance/v1/labels/"_s); + if (routeIndex == -1) { + return nullptr; + } + + const auto governanceRequestParameters = requestPath.mid(routeIndex + u"/ocs/v2.php/apps/governance/v1/labels/"_s.size()).split(u"/"_s); + qDebug() << requestPath << routeIndex << governanceRequestParameters << operation; + + switch (operation) + { + case QNetworkAccessManager::CustomOperation: + if (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == u"DELETE"_s) { + return new FakePayloadReply{operation, request, fakeDeleteGovernanceLabelReply(governanceRequestParameters).toUtf8(), this}; + } else if (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == u"GET"_s) { + if (governanceRequestParameters.count() == 4 && governanceRequestParameters.at(3) == u"available"_s) { + return new FakePayloadReply{operation, request, fakeGetAvailableGovernanceLabelsReply(governanceRequestParameters).toUtf8(), this}; + } else if (governanceRequestParameters.count() == 2) { + return new FakePayloadReply{operation, request, fakeGetGovernanceLabelsReply(governanceRequestParameters).toUtf8(), this}; + } + } else if (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == u"POST"_s) { + return new FakePayloadReply{operation, request, fakeApplyGovernanceLabelReply(governanceRequestParameters).toUtf8(), this}; + } + break; + case QNetworkAccessManager::HeadOperation: + break; + case QNetworkAccessManager::GetOperation: + if (governanceRequestParameters.count() == 4 && governanceRequestParameters.at(3) == u"available"_s) { + return new FakePayloadReply{operation, request, fakeGetAvailableGovernanceLabelsReply(governanceRequestParameters).toUtf8(), this}; + } else if (governanceRequestParameters.count() == 2) { + return new FakePayloadReply{operation, request, fakeGetGovernanceLabelsReply(governanceRequestParameters).toUtf8(), this}; + } + break; + case QNetworkAccessManager::PutOperation: + break; + case QNetworkAccessManager::PostOperation: + return new FakePayloadReply{operation, request, fakeApplyGovernanceLabelReply(governanceRequestParameters).toUtf8(), this}; + break; + case QNetworkAccessManager::DeleteOperation: + return new FakePayloadReply{operation, request, fakeDeleteGovernanceLabelReply(governanceRequestParameters).toUtf8(), this}; + break; + case QNetworkAccessManager::UnknownOperation: + break; + } + + return nullptr; + }); + + Q_EMIT setupSucceeded(); + } + +private: + [[nodiscard]] QString fakeGetGovernanceLabelsReply(const QList ¶meters) const + { + qDebug() << parameters; + if (parameters.at(0) == u"FILES"_s && parameters.at(1) == u"117"_s) { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": { + "hold": [], + "retention": [], + "sensitivity": null + } + } +} + )json"_s; + + return replyJson; + } else if (parameters.at(0) == u"FILES"_s && parameters.at(1) == u"117117"_s) { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "failure", + "statuscode": 404, + "message": "Entity with id 117117 not found" + }, + "data": [] + } +} + )json"_s; + + return replyJson; + } else { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "failure", + "statuscode": 400, + "message": "Invalid entity type FILEStdytf" + }, + "data": [] + } +} + )json"_s; + + return replyJson; + } + } + + [[nodiscard]] QString fakeGetAvailableGovernanceLabelsReply(const QList ¶meters) const + { + if (parameters.at(0) == u"FILES"_s && parameters.at(1) == u"117"_s) { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": [ + { + "id": "91785883351310337", + "name": "Test Sensitivity", + "priority": 0, + "description": "", + "color": "bf4040", + "scopes": [ + "FILES" + ] + } + ] + } +} + )json"_s; + + return replyJson; + } else if (parameters.at(0) == u"FILES"_s && parameters.at(1) == u"117117"_s) { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": [ + { + "id": "91785883351310337", + "name": "Test Sensitivity", + "priority": 0, + "description": "", + "color": "bf4040", + "scopes": [ + "FILES" + ] + } + ] + } +} + )json"_s; + + return replyJson; + } else { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "failure", + "statuscode": 400, + "message": "Invalid entity type FILEStdytf" + }, + "data": [] + } +} + )json"_s; + + return replyJson; + } + } + + [[nodiscard]] QString fakeApplyGovernanceLabelReply(const QList ¶meters) const + { + if (parameters.at(1) == u"117"_s) { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": { + "hold": [], + "retention": [], + "sensitivity": null + } + } +} + })json"_s; + + return replyJson; + } else { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": { + "hold": [], + "retention": [], + "sensitivity": null + } + } +} + })json"_s; + + return replyJson; + } + } + + [[nodiscard]] QString fakeDeleteGovernanceLabelReply(const QList ¶meters) const + { + if (parameters.at(1) == u"117"_s) { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": { + "hold": [], + "retention": [], + "sensitivity": null + } + } +} + })json"_s; + + return replyJson; + } else { + const auto replyJson = uR"json( +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": { + "hold": [], + "retention": [], + "sensitivity": null + } + } +} + })json"_s; + + return replyJson; + } + } +}; + +class TestGovernance : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() + { + OCC::Logger::instance()->setLogFlush(true); + OCC::Logger::instance()->setLogDebug(true); + + QStandardPaths::setTestModeEnabled(true); + } + + void testApplyGovernanceLabel() + { + FakeFolder fakeFolder{{}}; + GovernanceTestHelper testHelper; + testHelper.setup(fakeFolder); + + ApplyGovernanceLabel myJob; + QSignalSpy finishedSpy(&myJob, &ApplyGovernanceLabel::finished); + QSignalSpy finishedWithErrorSpy(&myJob, &ApplyGovernanceLabel::finishedWitherror); + + fakeFolder.remoteModifier().insert("test.txt"); + + QVERIFY(fakeFolder.syncOnce()); + + myJob.setAccount(fakeFolder.account()); + myJob.setEntityId(u"1"_s); + myJob.setEntityType(Governance::EntityType::Files); + myJob.setLabelType(Governance::LabelType::Sensitivity); + myJob.setLabelId(u"1"_s); + + myJob.start(); + + finishedSpy.wait(); + QCOMPARE(finishedSpy.count(), 1); + QCOMPARE(finishedWithErrorSpy.count(), 0); + } + + void testDeleteGovernanceLabel() + { + FakeFolder fakeFolder{{}}; + GovernanceTestHelper testHelper; + testHelper.setup(fakeFolder); + + DeleteGovernanceLabel myJob; + QSignalSpy finishedSpy(&myJob, &DeleteGovernanceLabel::finished); + QSignalSpy finishedWithErrorSpy(&myJob, &DeleteGovernanceLabel::finishedWitherror); + + fakeFolder.remoteModifier().insert("test.txt"); + + QVERIFY(fakeFolder.syncOnce()); + + myJob.setAccount(fakeFolder.account()); + myJob.setEntityId(u"1"_s); + myJob.setEntityType(Governance::EntityType::Files); + myJob.setLabelType(Governance::LabelType::Sensitivity); + myJob.setLabelId(u"1"_s); + + myJob.start(); + + finishedSpy.wait(); + QCOMPARE(finishedSpy.count(), 1); + QCOMPARE(finishedWithErrorSpy.count(), 0); + } + + void testGetAvailableGovernanceLabels_ValidIdValidType() + { + FakeFolder fakeFolder{{}}; + GovernanceTestHelper testHelper; + testHelper.setup(fakeFolder); + + GetAvailableGovernanceLabels myJob; + QSignalSpy finishedSpy(&myJob, &GetAvailableGovernanceLabels::finished); + QSignalSpy finishedWithErrorSpy(&myJob, &GetAvailableGovernanceLabels::finishedWitherror); + + fakeFolder.remoteModifier().insert("test.txt"); + + QVERIFY(fakeFolder.syncOnce()); + + myJob.setAccount(fakeFolder.account()); + myJob.setEntityId(u"117"_s); + myJob.setEntityType(Governance::EntityType::Files); + myJob.setLabelType(Governance::LabelType::Sensitivity); + + myJob.start(); + + finishedSpy.wait(); + QCOMPARE(finishedSpy.count(), 1); + QCOMPARE(finishedWithErrorSpy.count(), 0); + } + + void testGetAvailableGovernanceLabels_InvalidIdValidType() + { + FakeFolder fakeFolder{{}}; + GovernanceTestHelper testHelper; + testHelper.setup(fakeFolder); + + GetAvailableGovernanceLabels myJob; + QSignalSpy finishedSpy(&myJob, &GetAvailableGovernanceLabels::finished); + QSignalSpy finishedWithErrorSpy(&myJob, &GetAvailableGovernanceLabels::finishedWitherror); + + fakeFolder.remoteModifier().insert("test.txt"); + + QVERIFY(fakeFolder.syncOnce()); + + myJob.setAccount(fakeFolder.account()); + myJob.setEntityId(u"117117"_s); + myJob.setEntityType(Governance::EntityType::Files); + myJob.setLabelType(Governance::LabelType::Sensitivity); + + myJob.start(); + + finishedSpy.wait(); + QCOMPARE(finishedSpy.count(), 1); + QCOMPARE(finishedWithErrorSpy.count(), 0); + } + + void testGetAvailableGovernanceLabels_ValidIdInvalidType() + { + FakeFolder fakeFolder{{}}; + GovernanceTestHelper testHelper; + testHelper.setup(fakeFolder); + + GetAvailableGovernanceLabels myJob; + QSignalSpy finishedSpy(&myJob, &GetAvailableGovernanceLabels::finished); + QSignalSpy finishedWithErrorSpy(&myJob, &GetAvailableGovernanceLabels::finishedWitherror); + + fakeFolder.remoteModifier().insert("test.txt"); + + QVERIFY(fakeFolder.syncOnce()); + + myJob.setAccount(fakeFolder.account()); + myJob.setEntityId(u"117"_s); + myJob.setEntityType(Governance::EntityType::Custom); + myJob.setCustomEntityType(u"FILEStdytf"_s); + myJob.setLabelType(Governance::LabelType::Sensitivity); + + myJob.start(); + + finishedSpy.wait(); + QCOMPARE(finishedSpy.count(), 0); + QCOMPARE(finishedWithErrorSpy.count(), 1); + } + + void testGetGovernanceLabels_ValidIdValidEntityType() + { + FakeFolder fakeFolder{{}}; + GovernanceTestHelper testHelper; + testHelper.setup(fakeFolder); + + GetGovernanceLabels myJob; + QSignalSpy finishedSpy(&myJob, &GetGovernanceLabels::finished); + QSignalSpy finishedWithErrorSpy(&myJob, &GetGovernanceLabels::finishedWitherror); + + fakeFolder.remoteModifier().insert("test.txt"); + + QVERIFY(fakeFolder.syncOnce()); + + myJob.setAccount(fakeFolder.account()); + myJob.setEntityId(u"117"_s); + myJob.setEntityType(Governance::EntityType::Files); + + myJob.start(); + + finishedSpy.wait(); + QCOMPARE(finishedSpy.count(), 1); + QCOMPARE(finishedWithErrorSpy.count(), 0); + } + + void testGetGovernanceLabels_InvalidId() + { + FakeFolder fakeFolder{{}}; + GovernanceTestHelper testHelper; + testHelper.setup(fakeFolder); + + GetGovernanceLabels myJob; + QSignalSpy finishedSpy(&myJob, &GetGovernanceLabels::finished); + QSignalSpy finishedWithErrorSpy(&myJob, &GetGovernanceLabels::finishedWitherror); + + fakeFolder.remoteModifier().insert("test.txt"); + + QVERIFY(fakeFolder.syncOnce()); + + myJob.setAccount(fakeFolder.account()); + myJob.setEntityId(u"117117"_s); + myJob.setEntityType(Governance::EntityType::Files); + + myJob.start(); + + finishedWithErrorSpy.wait(); + QCOMPARE(finishedSpy.count(), 0); + QCOMPARE(finishedWithErrorSpy.count(), 1); + } + + void testGetGovernanceLabels_ValidIdInvalidEntityType() + { + FakeFolder fakeFolder{{}}; + GovernanceTestHelper testHelper; + testHelper.setup(fakeFolder); + + GetGovernanceLabels myJob; + QSignalSpy finishedSpy(&myJob, &GetGovernanceLabels::finished); + QSignalSpy finishedWithErrorSpy(&myJob, &GetGovernanceLabels::finishedWitherror); + + fakeFolder.remoteModifier().insert("test.txt"); + + QVERIFY(fakeFolder.syncOnce()); + + myJob.setAccount(fakeFolder.account()); + myJob.setEntityId(u"117"_s); + myJob.setEntityType(Governance::EntityType::Custom); + myJob.setCustomEntityType(u"FILEStdytf"_s); + + myJob.start(); + + finishedWithErrorSpy.wait(); + QCOMPARE(finishedSpy.count(), 0); + QCOMPARE(finishedWithErrorSpy.count(), 1); + } + + void testGovernanceLabelListModel_setData() + { + GovernanceLabelsListModel myModel; + QAbstractItemModelTester myModelTester(&myModel); + + const auto replyJson = R"json( +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": [ + { + "id": "91785883351310337", + "name": "Test Sensitivity", + "priority": 0, + "description": "This is a long description", + "color": "bf4040", + "scopes": [ + "FILES" + ] + } + ] + } +} + )json"_ba; + + const auto replyData = QJsonDocument::fromJson(replyJson); + + myModel.setAvailableLabelsJsonData(replyData); + + QCOMPARE(myModel.rowCount(), 1); + const auto labelIndex = myModel.index(0); + QVERIFY(labelIndex.isValid()); + QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::IdRole)), u"91785883351310337"_s); + QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::NameRole)), u"Test Sensitivity"_s); + QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::PriorityRole)), 0); + QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::DescriptionRole)), u"This is a long description"_s); + QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::ColorRole)), u"bf4040"_s); + QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::ScopesRole)).toStringList(), QStringList{u"FILES"_s}); + } + + void testGovernanceLabelListModel_refreshData() + { + GovernanceLabelsListModel myModel; + QAbstractItemModelTester myModelTester(&myModel); + QSignalSpy modelRefreshDataSignalSpy(&myModel, &GovernanceLabelsListModel::refreshData); + + myModel.setEntityId(u"117"_s); + myModel.setLabelType(Governance::LabelType::Sensitivity); + + QVERIFY(modelRefreshDataSignalSpy.wait()); + QCOMPARE(modelRefreshDataSignalSpy.count(), 1); + QCOMPARE(modelRefreshDataSignalSpy.at(0).count(), 2); + QCOMPARE(modelRefreshDataSignalSpy.at(0).at(0).value(), OCC::Governance::LabelType::Sensitivity); + QCOMPARE(modelRefreshDataSignalSpy.at(0).at(1), u"117"_s); + } +}; + +QTEST_GUILESS_MAIN(TestGovernance) +#include "testgovernance.moc"