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"