From a62eef705546b1ea797e0cf0eaafd9f0854bd1b3 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 2 Jun 2026 17:05:25 +0200 Subject: [PATCH 01/26] feat(governance): add base class for governance related network jobs will enable to share type definitions in all real jobs Signed-off-by: Matthieu Gallien --- src/gui/CMakeLists.txt | 2 + src/gui/governance/governancenetworkjob.cpp | 16 +++++++ src/gui/governance/governancenetworkjob.h | 47 +++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 src/gui/governance/governancenetworkjob.cpp create mode 100644 src/gui/governance/governancenetworkjob.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 0c0ad0145318c..5bfa9323f876b 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -208,6 +208,8 @@ set(client_SRCS filedetails/shareemodel.cpp filedetails/sortedsharemodel.h filedetails/sortedsharemodel.cpp + governance/governancenetworkjob.h + governance/governancenetworkjob.cpp tray/svgimageprovider.h tray/svgimageprovider.cpp tray/syncstatussummary.h diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp new file mode 100644 index 0000000000000..76fdfe24b4281 --- /dev/null +++ b/src/gui/governance/governancenetworkjob.cpp @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "governancenetworkjob.h" + +namespace OCC +{ + +GovernanceNetworkJob::GovernanceNetworkJob(QObject *parent) + : QObject{parent} +{ +} + +} // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h new file mode 100644 index 0000000000000..50b1704d69e7c --- /dev/null +++ b/src/gui/governance/governancenetworkjob.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef GOVERNANCENETWORKJOB_H +#define GOVERNANCENETWORKJOB_H + +#include +#include + +namespace OCC +{ + +class GovernanceNetworkJob : public QObject +{ + Q_OBJECT + QML_ELEMENT +public: + enum class EntityType { + Files, + Mails, + Custom, + }; + + Q_ENUM(EntityType) + + enum class LabelType { + Sensitivity, + REtention, + Hold, + }; + + Q_ENUM(LabelType) + + enum class ApiVersion { + Version_1, + }; + + Q_ENUM(ApiVersion) + + explicit GovernanceNetworkJob(QObject *parent = nullptr); +}; + +} // namespace OCC + +#endif // GOVERNANCENETWORKJOB_H From 2f4f97cc9fe2ffbae7073e4d9bb163baf566fca9 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 2 Jun 2026 17:06:09 +0200 Subject: [PATCH 02/26] feat(governance): very basic getter to test governance capability Signed-off-by: Matthieu Gallien --- src/libsync/capabilities.cpp | 7 +++++++ src/libsync/capabilities.h | 2 ++ 2 files changed, 9 insertions(+) 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; From f1722f774e06c103793560ed94e557903f7c1894 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 2 Jun 2026 22:50:11 +0200 Subject: [PATCH 03/26] feat(governance): add more job types for each requests also improve the available properties for the job parameters Signed-off-by: Matthieu Gallien --- src/gui/CMakeLists.txt | 10 ++++ src/gui/governance/applygovernancelabel.cpp | 16 +++++ src/gui/governance/applygovernancelabel.h | 26 ++++++++ src/gui/governance/deletegovernancelabel.cpp | 16 +++++ src/gui/governance/deletegovernancelabel.h | 26 ++++++++ .../getavailablegovernancelabels.cpp | 16 +++++ .../governance/getavailablegovernancelabels.h | 31 ++++++++++ src/gui/governance/getgovernancelabels.cpp | 16 +++++ src/gui/governance/getgovernancelabels.h | 26 ++++++++ src/gui/governance/governancenetworkjob.cpp | 60 +++++++++++++++++++ src/gui/governance/governancenetworkjob.h | 44 +++++++++++++- .../governance/typedgovernancenetworkjob.cpp | 31 ++++++++++ .../governance/typedgovernancenetworkjob.h | 39 ++++++++++++ 13 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 src/gui/governance/applygovernancelabel.cpp create mode 100644 src/gui/governance/applygovernancelabel.h create mode 100644 src/gui/governance/deletegovernancelabel.cpp create mode 100644 src/gui/governance/deletegovernancelabel.h create mode 100644 src/gui/governance/getavailablegovernancelabels.cpp create mode 100644 src/gui/governance/getavailablegovernancelabels.h create mode 100644 src/gui/governance/getgovernancelabels.cpp create mode 100644 src/gui/governance/getgovernancelabels.h create mode 100644 src/gui/governance/typedgovernancenetworkjob.cpp create mode 100644 src/gui/governance/typedgovernancenetworkjob.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 5bfa9323f876b..43dc05a39a14e 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -210,6 +210,16 @@ set(client_SRCS 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 tray/svgimageprovider.h tray/svgimageprovider.cpp tray/syncstatussummary.h diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp new file mode 100644 index 0000000000000..edfaa3aadf08a --- /dev/null +++ b/src/gui/governance/applygovernancelabel.cpp @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "applygovernancelabel.h" + +namespace OCC +{ + +ApplyGovernanceLabel::ApplyGovernanceLabel(QObject *parent) + : OCC::TypedGovernanceNetworkJob{parent} +{ +} + +} // namespace OCC diff --git a/src/gui/governance/applygovernancelabel.h b/src/gui/governance/applygovernancelabel.h new file mode 100644 index 0000000000000..695706a718e9e --- /dev/null +++ b/src/gui/governance/applygovernancelabel.h @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef APPLYGOVERNANCELABEL_H +#define APPLYGOVERNANCELABEL_H + +#include "typedgovernancenetworkjob.h" +#include +#include + +namespace OCC +{ + +class ApplyGovernanceLabel : public OCC::TypedGovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT +public: + explicit ApplyGovernanceLabel(QObject *parent = nullptr); +}; + +} // 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..550af2df618f7 --- /dev/null +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "deletegovernancelabel.h" + +namespace OCC +{ + +DeleteGovernanceLabel::DeleteGovernanceLabel(QObject *parent) + : OCC::TypedGovernanceNetworkJob{parent} +{ +} + +} // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.h b/src/gui/governance/deletegovernancelabel.h new file mode 100644 index 0000000000000..4bdc658650820 --- /dev/null +++ b/src/gui/governance/deletegovernancelabel.h @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef DELETEGOVERNANCELABEL_H +#define DELETEGOVERNANCELABEL_H + +#include "typedgovernancenetworkjob.h" +#include +#include + +namespace OCC +{ + +class DeleteGovernanceLabel : public OCC::TypedGovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT +public: + explicit DeleteGovernanceLabel(QObject *parent = nullptr); +}; + +} // 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..c9659cf5593b8 --- /dev/null +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "getavailablegovernancelabels.h" + +namespace OCC +{ + +GetAvailableGovernanceLabels::GetAvailableGovernanceLabels(QObject *parent) + : OCC::TypedGovernanceNetworkJob{parent} +{ +} + +} // namespace OCC diff --git a/src/gui/governance/getavailablegovernancelabels.h b/src/gui/governance/getavailablegovernancelabels.h new file mode 100644 index 0000000000000..9849225d47a1d --- /dev/null +++ b/src/gui/governance/getavailablegovernancelabels.h @@ -0,0 +1,31 @@ +/* + * 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 + +namespace OCC +{ + +class GetAvailableGovernanceLabels : public OCC::TypedGovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT + +public: + explicit GetAvailableGovernanceLabels(QObject *parent = nullptr); + +Q_SIGNALS: + +private: +}; + +} // 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..65a57b20589b4 --- /dev/null +++ b/src/gui/governance/getgovernancelabels.cpp @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "getgovernancelabels.h" + +namespace OCC +{ + +GetGovernanceLabels::GetGovernanceLabels(QObject *parent) + : OCC::GovernanceNetworkJob{parent} +{ +} + +} // namespace OCC diff --git a/src/gui/governance/getgovernancelabels.h b/src/gui/governance/getgovernancelabels.h new file mode 100644 index 0000000000000..a488e1b7e8b1d --- /dev/null +++ b/src/gui/governance/getgovernancelabels.h @@ -0,0 +1,26 @@ +/* + * 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 + +namespace OCC +{ + +class GetGovernanceLabels : public OCC::GovernanceNetworkJob +{ + Q_OBJECT + QML_ELEMENT +public: + explicit GetGovernanceLabels(QObject *parent = nullptr); +}; + +} // namespace OCC + +#endif // GETGOVERNANCELABELS_H diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp index 76fdfe24b4281..593b7b6e8a58f 100644 --- a/src/gui/governance/governancenetworkjob.cpp +++ b/src/gui/governance/governancenetworkjob.cpp @@ -13,4 +13,64 @@ GovernanceNetworkJob::GovernanceNetworkJob(QObject *parent) { } +GovernanceNetworkJob::ApiVersion GovernanceNetworkJob::apiVersion() const +{ + return _apiVersion; +} + +void GovernanceNetworkJob::setApiVersion(ApiVersion newApiVersion) +{ + if (_apiVersion == newApiVersion) { + return; + } + + _apiVersion = newApiVersion; + Q_EMIT apiVersionChanged(); +} + +GovernanceNetworkJob::EntityType GovernanceNetworkJob::entityType() const +{ + return _entityType; +} + +void GovernanceNetworkJob::setEntityType(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(); +} + } // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h index 50b1704d69e7c..a28a716bad2a8 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -16,6 +16,15 @@ class GovernanceNetworkJob : public QObject { Q_OBJECT QML_ELEMENT + + Q_PROPERTY(ApiVersion apiVersion READ apiVersion WRITE setApiVersion NOTIFY apiVersionChanged FINAL) + + Q_PROPERTY(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: enum class EntityType { Files, @@ -27,7 +36,7 @@ class GovernanceNetworkJob : public QObject enum class LabelType { Sensitivity, - REtention, + Retention, Hold, }; @@ -40,6 +49,39 @@ class GovernanceNetworkJob : public QObject Q_ENUM(ApiVersion) explicit GovernanceNetworkJob(QObject *parent = nullptr); + + [[nodiscard]] ApiVersion apiVersion() const; + + void setApiVersion(ApiVersion newApiVersion); + + [[nodiscard]] EntityType entityType() const; + + void setEntityType(EntityType newEntityType); + + [[nodiscard]] QString customEntityType() const; + + void setCustomEntityType(const QString &newCustomEntityType); + + QString entityId() const; + void setEntityId(const QString &newEntityId); + +Q_SIGNALS: + void apiVersionChanged(); + + void entityTypeChanged(); + + void customEntityTypeChanged(); + + void entityIdChanged(); + +private: + ApiVersion _apiVersion = ApiVersion::Version_1; + + EntityType _entityType = EntityType::Files; + + QString _customEntityType; + + QString _entityId; }; } // namespace OCC diff --git a/src/gui/governance/typedgovernancenetworkjob.cpp b/src/gui/governance/typedgovernancenetworkjob.cpp new file mode 100644 index 0000000000000..e17f9bf7f7c64 --- /dev/null +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "typedgovernancenetworkjob.h" + +namespace OCC +{ + +TypedGovernanceNetworkJob::TypedGovernanceNetworkJob(QObject *parent) + : OCC::GovernanceNetworkJob{parent} +{ +} + +GovernanceNetworkJob::LabelType TypedGovernanceNetworkJob::labelType() const +{ + return _labelType; +} + +void TypedGovernanceNetworkJob::setLabelType(LabelType newLabelType) +{ + if (_labelType == newLabelType) { + return; + } + + _labelType = newLabelType; + Q_EMIT labelTypeChanged(); +} + +} // namespace OCC diff --git a/src/gui/governance/typedgovernancenetworkjob.h b/src/gui/governance/typedgovernancenetworkjob.h new file mode 100644 index 0000000000000..d114624b02403 --- /dev/null +++ b/src/gui/governance/typedgovernancenetworkjob.h @@ -0,0 +1,39 @@ +/* + * 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(LabelType labelType READ labelType WRITE setLabelType NOTIFY labelTypeChanged FINAL) + +public: + TypedGovernanceNetworkJob(QObject *parent = nullptr); + + [[nodiscard]] LabelType labelType() const; + + void setLabelType(LabelType newLabelType); + +Q_SIGNALS: + void labelTypeChanged(); + +private: + LabelType _labelType; +}; + +} // namespace OCC + +#endif // TYPEDGOVERNANCENETWORKJOB_H From c318111a995860d6a36e836bf95e329230cdcb05 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 3 Jun 2026 18:32:31 +0200 Subject: [PATCH 04/26] feat(governance): getting ready to be able to use governance API finish implementing most of the code needed to send requests to the governance API will need further work to see how to read the replies and make something usefull Signed-off-by: Matthieu Gallien --- src/gui/CMakeLists.txt | 4 ++ src/gui/governance/applygovernancelabel.cpp | 27 +++++++++++- src/gui/governance/applygovernancelabel.h | 15 +++++-- src/gui/governance/deletegovernancelabel.cpp | 27 +++++++++++- src/gui/governance/deletegovernancelabel.h | 15 +++++-- .../getavailablegovernancelabels.cpp | 34 +++++++++++++- .../governance/getavailablegovernancelabels.h | 14 +++++- src/gui/governance/getgovernancelabels.cpp | 27 +++++++++++- src/gui/governance/getgovernancelabels.h | 11 ++++- src/gui/governance/governancenetworkjob.cpp | 44 ++++++++++++++++++- src/gui/governance/governancenetworkjob.h | 38 +++++++++++++++- src/gui/governance/ocsgovernancejob.cpp | 26 +++++++++++ src/gui/governance/ocsgovernancejob.h | 28 ++++++++++++ .../governance/typedgovernancenetworkjob.cpp | 26 ++++++++++- .../governance/typedgovernancenetworkjob.h | 6 ++- .../typedwithlabelidgovernancenetworkjob.cpp | 39 ++++++++++++++++ .../typedwithlabelidgovernancenetworkjob.h | 43 ++++++++++++++++++ 17 files changed, 401 insertions(+), 23 deletions(-) create mode 100644 src/gui/governance/ocsgovernancejob.cpp create mode 100644 src/gui/governance/ocsgovernancejob.h create mode 100644 src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp create mode 100644 src/gui/governance/typedwithlabelidgovernancenetworkjob.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 43dc05a39a14e..2611c81a84f9c 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -220,6 +220,10 @@ set(client_SRCS governance/deletegovernancelabel.cpp governance/typedgovernancenetworkjob.h governance/typedgovernancenetworkjob.cpp + governance/ocsgovernancejob.h + governance/ocsgovernancejob.cpp + governance/typedwithlabelidgovernancenetworkjob.h + governance/typedwithlabelidgovernancenetworkjob.cpp tray/svgimageprovider.h tray/svgimageprovider.cpp tray/syncstatussummary.h diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp index edfaa3aadf08a..0b5f3f6d63fdb 100644 --- a/src/gui/governance/applygovernancelabel.cpp +++ b/src/gui/governance/applygovernancelabel.cpp @@ -5,12 +5,35 @@ #include "applygovernancelabel.h" +#include "ocsgovernancejob.h" + namespace OCC { -ApplyGovernanceLabel::ApplyGovernanceLabel(QObject *parent) - : OCC::TypedGovernanceNetworkJob{parent} +ApplyGovernanceLabel::ApplyGovernanceLabel(AccountPtr account, QObject *parent) + : OCC::TypedWithLabelIdGovernanceNetworkJob{account, parent} +{ +} + +void ApplyGovernanceLabel::start() { + setOcsGovernanceJob(QPointer{new OcsGovernanceJob{account()}}); + + connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, + this, &ApplyGovernanceLabel::jobDone); + + ocsGovernanceJob()->setPath(buildPath()); + ocsGovernanceJob()->setMethod("POST"); + + ocsGovernanceJob()->start(); +} + +void ApplyGovernanceLabel::jobDone(QJsonDocument reply, int statusCode) +{ + Q_UNUSED(reply) + Q_UNUSED(statusCode) + + Q_EMIT finished(); } } // namespace OCC diff --git a/src/gui/governance/applygovernancelabel.h b/src/gui/governance/applygovernancelabel.h index 695706a718e9e..c118e9756ab19 100644 --- a/src/gui/governance/applygovernancelabel.h +++ b/src/gui/governance/applygovernancelabel.h @@ -6,19 +6,28 @@ #ifndef APPLYGOVERNANCELABEL_H #define APPLYGOVERNANCELABEL_H -#include "typedgovernancenetworkjob.h" +#include "typedwithlabelidgovernancenetworkjob.h" + #include #include +#include namespace OCC { -class ApplyGovernanceLabel : public OCC::TypedGovernanceNetworkJob +class ApplyGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob { Q_OBJECT QML_ELEMENT public: - explicit ApplyGovernanceLabel(QObject *parent = nullptr); + explicit ApplyGovernanceLabel(AccountPtr account, + QObject *parent = nullptr); + +public Q_SLOTS: + void start(); + +private Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); }; } // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp index 550af2df618f7..558ed5e294442 100644 --- a/src/gui/governance/deletegovernancelabel.cpp +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -5,12 +5,35 @@ #include "deletegovernancelabel.h" +#include "ocsgovernancejob.h" + namespace OCC { -DeleteGovernanceLabel::DeleteGovernanceLabel(QObject *parent) - : OCC::TypedGovernanceNetworkJob{parent} +DeleteGovernanceLabel::DeleteGovernanceLabel(AccountPtr account, QObject *parent) + : OCC::TypedWithLabelIdGovernanceNetworkJob{account, parent} +{ +} + +void DeleteGovernanceLabel::start() { + setOcsGovernanceJob(QPointer{new OcsGovernanceJob{account()}}); + + connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, + this, &DeleteGovernanceLabel::jobDone); + + ocsGovernanceJob()->setPath(buildPath()); + ocsGovernanceJob()->setMethod("DELETE"); + + ocsGovernanceJob()->start(); +} + +void DeleteGovernanceLabel::jobDone(QJsonDocument reply, int statusCode) +{ + Q_UNUSED(reply) + Q_UNUSED(statusCode) + + Q_EMIT finished(); } } // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.h b/src/gui/governance/deletegovernancelabel.h index 4bdc658650820..d9591d9ccfb5b 100644 --- a/src/gui/governance/deletegovernancelabel.h +++ b/src/gui/governance/deletegovernancelabel.h @@ -6,19 +6,28 @@ #ifndef DELETEGOVERNANCELABEL_H #define DELETEGOVERNANCELABEL_H -#include "typedgovernancenetworkjob.h" +#include "typedwithlabelidgovernancenetworkjob.h" + #include #include +#include namespace OCC { -class DeleteGovernanceLabel : public OCC::TypedGovernanceNetworkJob +class DeleteGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob { Q_OBJECT QML_ELEMENT public: - explicit DeleteGovernanceLabel(QObject *parent = nullptr); + explicit DeleteGovernanceLabel(AccountPtr account, + QObject *parent = nullptr); + +public Q_SLOTS: + void start(); + +private Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); }; } // namespace OCC diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp index c9659cf5593b8..0b713a45fc12a 100644 --- a/src/gui/governance/getavailablegovernancelabels.cpp +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -5,12 +5,42 @@ #include "getavailablegovernancelabels.h" +#include "ocsgovernancejob.h" + +using namespace Qt::StringLiterals; + namespace OCC { -GetAvailableGovernanceLabels::GetAvailableGovernanceLabels(QObject *parent) - : OCC::TypedGovernanceNetworkJob{parent} +GetAvailableGovernanceLabels::GetAvailableGovernanceLabels(AccountPtr account, QObject *parent) + : OCC::TypedGovernanceNetworkJob{account, parent} +{ +} + +void GetAvailableGovernanceLabels::start() +{ + setOcsGovernanceJob(QPointer{new OcsGovernanceJob{account()}}); + + connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, + this, &GetAvailableGovernanceLabels::jobDone); + + ocsGovernanceJob()->setPath(buildPath()); + ocsGovernanceJob()->setMethod("GET"); + + ocsGovernanceJob()->start(); +} + +void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, int statusCode) +{ + Q_UNUSED(reply) + Q_UNUSED(statusCode) + + Q_EMIT finished(); +} + +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 index 9849225d47a1d..0d3032d86238f 100644 --- a/src/gui/governance/getavailablegovernancelabels.h +++ b/src/gui/governance/getavailablegovernancelabels.h @@ -7,8 +7,10 @@ #define GETAVAILABLEGOVERNANCELABELS_H #include "typedgovernancenetworkjob.h" + #include #include +#include namespace OCC { @@ -19,11 +21,19 @@ class GetAvailableGovernanceLabels : public OCC::TypedGovernanceNetworkJob QML_ELEMENT public: - explicit GetAvailableGovernanceLabels(QObject *parent = nullptr); + explicit GetAvailableGovernanceLabels(AccountPtr account, + QObject *parent = nullptr); Q_SIGNALS: -private: +public Q_SLOTS: + void start(); + +protected: + [[nodiscard]] QString buildPath() const override; + +private Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); }; } // namespace OCC diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp index 65a57b20589b4..26ac486d1463c 100644 --- a/src/gui/governance/getgovernancelabels.cpp +++ b/src/gui/governance/getgovernancelabels.cpp @@ -5,12 +5,35 @@ #include "getgovernancelabels.h" +#include "ocsgovernancejob.h" + namespace OCC { -GetGovernanceLabels::GetGovernanceLabels(QObject *parent) - : OCC::GovernanceNetworkJob{parent} +GetGovernanceLabels::GetGovernanceLabels(AccountPtr account, QObject *parent) + : OCC::GovernanceNetworkJob{account, parent} +{ +} + +void GetGovernanceLabels::start() { + setOcsGovernanceJob(QPointer{new OcsGovernanceJob{account()}}); + + connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, + this, &GetGovernanceLabels::jobDone); + + ocsGovernanceJob()->setPath(buildPath()); + ocsGovernanceJob()->setMethod("GET"); + + ocsGovernanceJob()->start(); +} + +void GetGovernanceLabels::jobDone(QJsonDocument reply, int statusCode) +{ + Q_UNUSED(reply) + Q_UNUSED(statusCode) + + Q_EMIT finished(); } } // namespace OCC diff --git a/src/gui/governance/getgovernancelabels.h b/src/gui/governance/getgovernancelabels.h index a488e1b7e8b1d..a389aba6671a9 100644 --- a/src/gui/governance/getgovernancelabels.h +++ b/src/gui/governance/getgovernancelabels.h @@ -7,8 +7,10 @@ #define GETGOVERNANCELABELS_H #include "governancenetworkjob.h" + #include #include +#include namespace OCC { @@ -18,7 +20,14 @@ class GetGovernanceLabels : public OCC::GovernanceNetworkJob Q_OBJECT QML_ELEMENT public: - explicit GetGovernanceLabels(QObject *parent = nullptr); + explicit GetGovernanceLabels(AccountPtr account, + QObject *parent = nullptr); + +public Q_SLOTS: + void start(); + +private Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); }; } // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp index 593b7b6e8a58f..9515d93091d37 100644 --- a/src/gui/governance/governancenetworkjob.cpp +++ b/src/gui/governance/governancenetworkjob.cpp @@ -5,11 +5,14 @@ #include "governancenetworkjob.h" +using namespace Qt::StringLiterals; + namespace OCC { -GovernanceNetworkJob::GovernanceNetworkJob(QObject *parent) +GovernanceNetworkJob::GovernanceNetworkJob(AccountPtr account, QObject *parent) : QObject{parent} + , _account{account} { } @@ -73,4 +76,43 @@ void GovernanceNetworkJob::setEntityId(const QString &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 ApiVersion::Version_1: + result = u"v1"_s; + break; + } + + return result; +} + +QString GovernanceNetworkJob::entityTypeAsString() const +{ + auto result = QString{}; + + switch (_entityType) + { + case EntityType::Files: + result = u"FILES"_s; + break; + case EntityType::Mails: + result = u"MAILS"_s; + break; + case EntityType::Custom: + result = _customEntityType; + break; + } + + return result; +} + } // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h index a28a716bad2a8..a4368cdba49e5 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -6,12 +6,16 @@ #ifndef GOVERNANCENETWORKJOB_H #define GOVERNANCENETWORKJOB_H +#include "accountfwd.h" + #include #include namespace OCC { +class OcsGovernanceJob; + class GovernanceNetworkJob : public QObject { Q_OBJECT @@ -48,7 +52,8 @@ class GovernanceNetworkJob : public QObject Q_ENUM(ApiVersion) - explicit GovernanceNetworkJob(QObject *parent = nullptr); + explicit GovernanceNetworkJob(AccountPtr account, + QObject *parent = nullptr); [[nodiscard]] ApiVersion apiVersion() const; @@ -62,7 +67,8 @@ class GovernanceNetworkJob : public QObject void setCustomEntityType(const QString &newCustomEntityType); - QString entityId() const; + [[nodiscard]] QString entityId() const; + void setEntityId(const QString &newEntityId); Q_SIGNALS: @@ -74,7 +80,33 @@ class GovernanceNetworkJob : public QObject void entityIdChanged(); + void finished(); + +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; + private: + AccountPtr _account; + ApiVersion _apiVersion = ApiVersion::Version_1; EntityType _entityType = EntityType::Files; @@ -82,6 +114,8 @@ class GovernanceNetworkJob : public QObject QString _customEntityType; QString _entityId; + + QPointer _ocsGovernanceJob; }; } // namespace OCC 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 index e17f9bf7f7c64..71a76d5f1c01c 100644 --- a/src/gui/governance/typedgovernancenetworkjob.cpp +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -5,11 +5,13 @@ #include "typedgovernancenetworkjob.h" +using namespace Qt::StringLiterals; + namespace OCC { -TypedGovernanceNetworkJob::TypedGovernanceNetworkJob(QObject *parent) - : OCC::GovernanceNetworkJob{parent} +TypedGovernanceNetworkJob::TypedGovernanceNetworkJob(AccountPtr account, QObject *parent) + : OCC::GovernanceNetworkJob{account, parent} { } @@ -28,4 +30,24 @@ void TypedGovernanceNetworkJob::setLabelType(LabelType newLabelType) Q_EMIT labelTypeChanged(); } +QString TypedGovernanceNetworkJob::labelTypeAsString() const +{ + auto result = QString{}; + + switch (_labelType) + { + case LabelType::Sensitivity: + result = u"sensitivity"_s; + break; + case LabelType::Retention: + result = u"retention"_s; + break; + case LabelType::Hold: + result = u"hold"_s; + break; + } + + return result; +} + } // namespace OCC diff --git a/src/gui/governance/typedgovernancenetworkjob.h b/src/gui/governance/typedgovernancenetworkjob.h index d114624b02403..f0b3253b075e2 100644 --- a/src/gui/governance/typedgovernancenetworkjob.h +++ b/src/gui/governance/typedgovernancenetworkjob.h @@ -21,7 +21,8 @@ class TypedGovernanceNetworkJob : public GovernanceNetworkJob Q_PROPERTY(LabelType labelType READ labelType WRITE setLabelType NOTIFY labelTypeChanged FINAL) public: - TypedGovernanceNetworkJob(QObject *parent = nullptr); + TypedGovernanceNetworkJob(AccountPtr account, + QObject *parent = nullptr); [[nodiscard]] LabelType labelType() const; @@ -30,6 +31,9 @@ class TypedGovernanceNetworkJob : public GovernanceNetworkJob Q_SIGNALS: void labelTypeChanged(); +protected: + [[nodiscard]] QString labelTypeAsString() const; + private: LabelType _labelType; }; diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp new file mode 100644 index 0000000000000..6b777d27f664c --- /dev/null +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp @@ -0,0 +1,39 @@ +/* + * 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(AccountPtr account, + QObject *parent) + : OCC::TypedGovernanceNetworkJob{account, 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()); +} + +} // namespace OCC diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.h b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h new file mode 100644 index 0000000000000..fc4c997eda756 --- /dev/null +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h @@ -0,0 +1,43 @@ +/* + * 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(AccountPtr account, + QObject *parent = nullptr); + + [[nodiscard]] QString labelId() const; + + void setLabelId(const QString &newLabelId); + +signals: + void labelIdChanged(); + +protected: + [[nodiscard]] QString buildPath() const override; + +private: + QString _labelId; +}; + +} // namespace OCC + +#endif // TYPEDWITHLABELIDGOVERNANCENETWORKJOB_H From 261a078d31c5295868d1922b371d29710eda4db7 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 4 Jun 2026 16:38:42 +0200 Subject: [PATCH 05/26] feat(governance): open governance labels dialog from files manager Signed-off-by: Matthieu Gallien --- resources.qrc | 1 + src/gui/GovernanceLabelsDialog.qml | 157 ++++++++++++++++++ src/gui/application.cpp | 3 + src/gui/governance/applygovernancelabel.cpp | 7 +- src/gui/governance/applygovernancelabel.h | 3 +- src/gui/governance/deletegovernancelabel.cpp | 7 +- src/gui/governance/deletegovernancelabel.h | 3 +- .../getavailablegovernancelabels.cpp | 6 +- .../governance/getavailablegovernancelabels.h | 3 +- src/gui/governance/getgovernancelabels.cpp | 7 +- src/gui/governance/getgovernancelabels.h | 3 +- src/gui/governance/governancenetworkjob.cpp | 15 +- src/gui/governance/governancenetworkjob.h | 13 +- .../governance/typedgovernancenetworkjob.cpp | 4 +- .../governance/typedgovernancenetworkjob.h | 3 +- .../typedwithlabelidgovernancenetworkjob.cpp | 7 +- .../typedwithlabelidgovernancenetworkjob.h | 3 +- src/gui/owncloudgui.cpp | 18 ++ src/gui/owncloudgui.h | 3 + src/gui/socketapi/socketapi.cpp | 32 ++++ src/gui/socketapi/socketapi.h | 5 + src/gui/systray.cpp | 31 ++++ src/gui/systray.h | 1 + 23 files changed, 305 insertions(+), 30 deletions(-) create mode 100644 src/gui/GovernanceLabelsDialog.qml diff --git a/resources.qrc b/resources.qrc index 6683ed833ef1c..5496ebdf8afed 100644 --- a/resources.qrc +++ b/resources.qrc @@ -82,5 +82,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/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml new file mode 100644 index 0000000000000..9c1c8571a85d9 --- /dev/null +++ b/src/gui/GovernanceLabelsDialog.qml @@ -0,0 +1,157 @@ +/* + * 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 + } + + ApplyGovernanceLabel { + id: applyGovernanceLabel + + account: governanceLabelsDialog.account + + labelId: 'labelId' + labelType: GovernanceNetworkJob.Sensitivity + entityId: governanceLabelsDialog.fileId + } + + DeleteGovernanceLabel { + id: deleteGovernanceLabel + + account: governanceLabelsDialog.account + + labelId: 'labelId' + labelType: GovernanceNetworkJob.Sensitivity + entityId: governanceLabelsDialog.fileId + } + + GetAvailableGovernanceLabels { + id: getAvailableGovernanceLabelsForSensitivity + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Sensitivity + entityId: governanceLabelsDialog.fileId + } + + GetAvailableGovernanceLabels { + id: getAvailableGovernanceLabelsForHold + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Hold + entityId: governanceLabelsDialog.fileId + } + + GetAvailableGovernanceLabels { + id: getAvailableGovernanceLabelsForRetention + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Retention + entityId: governanceLabelsDialog.fileId + } + + GetGovernanceLabels { + id: getGovernanceLabels + + account: governanceLabelsDialog.account + + entityId: governanceLabelsDialog.fileId + } + + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.bottomMargin: 5 + anchors.topMargin: 10 + spacing: 15 + z: 2 + + Button { + text: 'Apply governance label' + onClicked: applyGovernanceLabel.start() + } + + Button { + text: 'Delete governance label' + onClicked: deleteGovernanceLabel.start() + } + + Button { + text: 'Get available governance labels for sensitivity' + onClicked: getAvailableGovernanceLabelsForSensitivity.start() + } + + Button { + text: 'Get available governance labels for retention' + onClicked: getAvailableGovernanceLabelsForRetention.start() + } + + Button { + text: 'Get available governance labels for legal hold' + onClicked: getAvailableGovernanceLabelsForHold.start() + } + + Button { + text: 'Get governance labels' + onClicked: getGovernanceLabels.start() + } + + DialogButtonBox { + Layout.fillWidth: true + + Button { + text: qsTr("Close") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + + 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 d3eba1acf3bcf..85c21b98758d3 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -462,6 +462,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 index 0b5f3f6d63fdb..09cea422a615f 100644 --- a/src/gui/governance/applygovernancelabel.cpp +++ b/src/gui/governance/applygovernancelabel.cpp @@ -10,8 +10,8 @@ namespace OCC { -ApplyGovernanceLabel::ApplyGovernanceLabel(AccountPtr account, QObject *parent) - : OCC::TypedWithLabelIdGovernanceNetworkJob{account, parent} +ApplyGovernanceLabel::ApplyGovernanceLabel(QObject *parent) + : OCC::TypedWithLabelIdGovernanceNetworkJob{parent} { } @@ -33,6 +33,9 @@ void ApplyGovernanceLabel::jobDone(QJsonDocument reply, int statusCode) Q_UNUSED(reply) Q_UNUSED(statusCode) + qCInfo(lcGovernance) << reply; + + Q_EMIT finished(); } diff --git a/src/gui/governance/applygovernancelabel.h b/src/gui/governance/applygovernancelabel.h index c118e9756ab19..a82423a432d6c 100644 --- a/src/gui/governance/applygovernancelabel.h +++ b/src/gui/governance/applygovernancelabel.h @@ -20,8 +20,7 @@ class ApplyGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob Q_OBJECT QML_ELEMENT public: - explicit ApplyGovernanceLabel(AccountPtr account, - QObject *parent = nullptr); + explicit ApplyGovernanceLabel(QObject *parent = nullptr); public Q_SLOTS: void start(); diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp index 558ed5e294442..31b92dc25413a 100644 --- a/src/gui/governance/deletegovernancelabel.cpp +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -10,8 +10,8 @@ namespace OCC { -DeleteGovernanceLabel::DeleteGovernanceLabel(AccountPtr account, QObject *parent) - : OCC::TypedWithLabelIdGovernanceNetworkJob{account, parent} +DeleteGovernanceLabel::DeleteGovernanceLabel(QObject *parent) + : OCC::TypedWithLabelIdGovernanceNetworkJob{parent} { } @@ -33,6 +33,9 @@ void DeleteGovernanceLabel::jobDone(QJsonDocument reply, int statusCode) Q_UNUSED(reply) Q_UNUSED(statusCode) + qCInfo(lcGovernance) << reply; + + Q_EMIT finished(); } diff --git a/src/gui/governance/deletegovernancelabel.h b/src/gui/governance/deletegovernancelabel.h index d9591d9ccfb5b..938c70d2d3ac9 100644 --- a/src/gui/governance/deletegovernancelabel.h +++ b/src/gui/governance/deletegovernancelabel.h @@ -20,8 +20,7 @@ class DeleteGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob Q_OBJECT QML_ELEMENT public: - explicit DeleteGovernanceLabel(AccountPtr account, - QObject *parent = nullptr); + explicit DeleteGovernanceLabel(QObject *parent = nullptr); public Q_SLOTS: void start(); diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp index 0b713a45fc12a..401f5bbe7fb34 100644 --- a/src/gui/governance/getavailablegovernancelabels.cpp +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -12,8 +12,8 @@ using namespace Qt::StringLiterals; namespace OCC { -GetAvailableGovernanceLabels::GetAvailableGovernanceLabels(AccountPtr account, QObject *parent) - : OCC::TypedGovernanceNetworkJob{account, parent} +GetAvailableGovernanceLabels::GetAvailableGovernanceLabels(QObject *parent) + : OCC::TypedGovernanceNetworkJob{parent} { } @@ -35,6 +35,8 @@ void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, int statusCode) Q_UNUSED(reply) Q_UNUSED(statusCode) + qCInfo(lcGovernance) << reply; + Q_EMIT finished(); } diff --git a/src/gui/governance/getavailablegovernancelabels.h b/src/gui/governance/getavailablegovernancelabels.h index 0d3032d86238f..29a2cab6a2a47 100644 --- a/src/gui/governance/getavailablegovernancelabels.h +++ b/src/gui/governance/getavailablegovernancelabels.h @@ -21,8 +21,7 @@ class GetAvailableGovernanceLabels : public OCC::TypedGovernanceNetworkJob QML_ELEMENT public: - explicit GetAvailableGovernanceLabels(AccountPtr account, - QObject *parent = nullptr); + explicit GetAvailableGovernanceLabels(QObject *parent = nullptr); Q_SIGNALS: diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp index 26ac486d1463c..20b75ff9101ae 100644 --- a/src/gui/governance/getgovernancelabels.cpp +++ b/src/gui/governance/getgovernancelabels.cpp @@ -10,8 +10,8 @@ namespace OCC { -GetGovernanceLabels::GetGovernanceLabels(AccountPtr account, QObject *parent) - : OCC::GovernanceNetworkJob{account, parent} +GetGovernanceLabels::GetGovernanceLabels(QObject *parent) + : OCC::GovernanceNetworkJob{parent} { } @@ -33,6 +33,9 @@ void GetGovernanceLabels::jobDone(QJsonDocument reply, int statusCode) Q_UNUSED(reply) Q_UNUSED(statusCode) + qCInfo(lcGovernance) << reply; + + Q_EMIT finished(); } diff --git a/src/gui/governance/getgovernancelabels.h b/src/gui/governance/getgovernancelabels.h index a389aba6671a9..be08db2d4be51 100644 --- a/src/gui/governance/getgovernancelabels.h +++ b/src/gui/governance/getgovernancelabels.h @@ -20,8 +20,7 @@ class GetGovernanceLabels : public OCC::GovernanceNetworkJob Q_OBJECT QML_ELEMENT public: - explicit GetGovernanceLabels(AccountPtr account, - QObject *parent = nullptr); + explicit GetGovernanceLabels(QObject *parent = nullptr); public Q_SLOTS: void start(); diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp index 9515d93091d37..f39c23e9309fe 100644 --- a/src/gui/governance/governancenetworkjob.cpp +++ b/src/gui/governance/governancenetworkjob.cpp @@ -5,14 +5,15 @@ #include "governancenetworkjob.h" +Q_LOGGING_CATEGORY(lcGovernance, "nextcloud.gui.governance", QtInfoMsg) + using namespace Qt::StringLiterals; namespace OCC { -GovernanceNetworkJob::GovernanceNetworkJob(AccountPtr account, QObject *parent) +GovernanceNetworkJob::GovernanceNetworkJob(QObject *parent) : QObject{parent} - , _account{account} { } @@ -115,4 +116,14 @@ QString GovernanceNetworkJob::entityTypeAsString() const 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 index a4368cdba49e5..a5bce972cd79c 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -10,6 +10,10 @@ #include #include +#include + +Q_DECLARE_LOGGING_CATEGORY(lcGovernance) + namespace OCC { @@ -21,6 +25,8 @@ class GovernanceNetworkJob : public QObject Q_OBJECT QML_ELEMENT + Q_PROPERTY(AccountPtr account READ account WRITE setAccount NOTIFY accountChanged FINAL) + Q_PROPERTY(ApiVersion apiVersion READ apiVersion WRITE setApiVersion NOTIFY apiVersionChanged FINAL) Q_PROPERTY(EntityType entityType READ entityType WRITE setEntityType NOTIFY entityTypeChanged FINAL) @@ -52,8 +58,7 @@ class GovernanceNetworkJob : public QObject Q_ENUM(ApiVersion) - explicit GovernanceNetworkJob(AccountPtr account, - QObject *parent = nullptr); + explicit GovernanceNetworkJob(QObject *parent = nullptr); [[nodiscard]] ApiVersion apiVersion() const; @@ -71,6 +76,8 @@ class GovernanceNetworkJob : public QObject void setEntityId(const QString &newEntityId); + void setAccount(AccountPtr newAccount); + Q_SIGNALS: void apiVersionChanged(); @@ -82,6 +89,8 @@ class GovernanceNetworkJob : public QObject void finished(); + void accountChanged(); + protected: void setOcsGovernanceJob(QPointer newJob) { diff --git a/src/gui/governance/typedgovernancenetworkjob.cpp b/src/gui/governance/typedgovernancenetworkjob.cpp index 71a76d5f1c01c..5cb3304ef04dd 100644 --- a/src/gui/governance/typedgovernancenetworkjob.cpp +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -10,8 +10,8 @@ using namespace Qt::StringLiterals; namespace OCC { -TypedGovernanceNetworkJob::TypedGovernanceNetworkJob(AccountPtr account, QObject *parent) - : OCC::GovernanceNetworkJob{account, parent} +TypedGovernanceNetworkJob::TypedGovernanceNetworkJob(QObject *parent) + : OCC::GovernanceNetworkJob{parent} { } diff --git a/src/gui/governance/typedgovernancenetworkjob.h b/src/gui/governance/typedgovernancenetworkjob.h index f0b3253b075e2..5c7464ef776a1 100644 --- a/src/gui/governance/typedgovernancenetworkjob.h +++ b/src/gui/governance/typedgovernancenetworkjob.h @@ -21,8 +21,7 @@ class TypedGovernanceNetworkJob : public GovernanceNetworkJob Q_PROPERTY(LabelType labelType READ labelType WRITE setLabelType NOTIFY labelTypeChanged FINAL) public: - TypedGovernanceNetworkJob(AccountPtr account, - QObject *parent = nullptr); + TypedGovernanceNetworkJob(QObject *parent = nullptr); [[nodiscard]] LabelType labelType() const; diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp index 6b777d27f664c..7f83d373e4cce 100644 --- a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp @@ -10,9 +10,8 @@ using namespace Qt::StringLiterals; namespace OCC { -TypedWithLabelIdGovernanceNetworkJob::TypedWithLabelIdGovernanceNetworkJob(AccountPtr account, - QObject *parent) - : OCC::TypedGovernanceNetworkJob{account, parent} +TypedWithLabelIdGovernanceNetworkJob::TypedWithLabelIdGovernanceNetworkJob(QObject *parent) + : OCC::TypedGovernanceNetworkJob{parent} { } @@ -33,7 +32,7 @@ void TypedWithLabelIdGovernanceNetworkJob::setLabelId(const QString &newLabelId) QString TypedWithLabelIdGovernanceNetworkJob::buildPath() const { - return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelId()); + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelId(), labelTypeAsString()); } } // namespace OCC diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.h b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h index fc4c997eda756..785ddd7297230 100644 --- a/src/gui/governance/typedwithlabelidgovernancenetworkjob.h +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h @@ -21,8 +21,7 @@ class TypedWithLabelIdGovernanceNetworkJob : public OCC::TypedGovernanceNetworkJ Q_PROPERTY(QString labelId READ labelId WRITE setLabelId NOTIFY labelIdChanged FINAL) public: - explicit TypedWithLabelIdGovernanceNetworkJob(AccountPtr account, - QObject *parent = nullptr); + explicit TypedWithLabelIdGovernanceNetworkJob(QObject *parent = nullptr); [[nodiscard]] QString labelId() const; diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 561a1a7b783d2..eb1934bde24df 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -37,6 +37,10 @@ #include "tray/trayaccountappsmodel.h" #include "search/unifiedsearchresultslistmodel.h" #include "integration/fileactionsmodel.h" +#include "governance/applygovernancelabel.h" +#include "governance/deletegovernancelabel.h" +#include "governance/getavailablegovernancelabels.h" +#include "governance/getgovernancelabels.h" #include "filesystem.h" #ifdef WITH_LIBCLOUDPROVIDERS @@ -153,6 +157,10 @@ 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"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "QAbstractItemModel", "QAbstractItemModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "activity", "Activity"); @@ -161,6 +169,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*"); @@ -778,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 324a5d5eb6c73..56bf72eb00062 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -774,6 +774,28 @@ 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; + } + + if (!fileData.folder->accountState()->account()->capabilities().governanceAvailable()) { + qCWarning(lcSocketApi) << "capability to use governance labels is missing"; + 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 +1297,15 @@ void SocketApi::sendLockFileInfoMenuEntries(const QFileInfo &fileInfo, } } +void SocketApi::sendFilesGovernanceLabelsMenuOptions(const QFileInfo &fileInfo, + const FileData &fileData, + SocketListener *listener) +{ + if (fileData.folder->accountState()->account()->capabilities().governanceAvailable() && !FileSystem::isDir(fileInfo.absoluteFilePath())) { + listener->sendMessage(QLatin1String("MENU_ITEM:FILES_GOVERNANCE_LABELS::") + tr("Apply labels")); + } +} + SocketApi::FileData SocketApi::FileData::get(const QString &localFile) { FileData data; @@ -1393,6 +1424,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 6eeb6e3984fbe..f1ac5d5d3109f 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -721,6 +721,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 0af43bc8b17df..29ae435439979 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -134,6 +134,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(); From 9ae0ff4a918f5e5a083088028020075dddfe44dc Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 11 Jun 2026 17:05:56 +0200 Subject: [PATCH 06/26] feat(governance): add automated tesst for network requests should enable us to test the requests one by one and ensure we decode properly the replies Signed-off-by: Matthieu Gallien --- src/gui/governance/applygovernancelabel.cpp | 12 +- src/gui/governance/deletegovernancelabel.cpp | 12 +- .../getavailablegovernancelabels.cpp | 11 +- src/gui/governance/getgovernancelabels.cpp | 12 +- src/gui/governance/governancenetworkjob.h | 5 +- test/CMakeLists.txt | 1 + test/testgovernance.cpp | 529 ++++++++++++++++++ 7 files changed, 550 insertions(+), 32 deletions(-) create mode 100644 test/testgovernance.cpp diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp index 09cea422a615f..1ea95bf78fc03 100644 --- a/src/gui/governance/applygovernancelabel.cpp +++ b/src/gui/governance/applygovernancelabel.cpp @@ -21,6 +21,8 @@ void ApplyGovernanceLabel::start() connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, this, &ApplyGovernanceLabel::jobDone); + connect(ocsGovernanceJob().data(), &OcsJob::ocsError, + this, &ApplyGovernanceLabel::finishedWithError); ocsGovernanceJob()->setPath(buildPath()); ocsGovernanceJob()->setMethod("POST"); @@ -28,15 +30,9 @@ void ApplyGovernanceLabel::start() ocsGovernanceJob()->start(); } -void ApplyGovernanceLabel::jobDone(QJsonDocument reply, int statusCode) +void ApplyGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) { - Q_UNUSED(reply) - Q_UNUSED(statusCode) - - qCInfo(lcGovernance) << reply; - - - Q_EMIT finished(); + Q_EMIT finished(reply); } } // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp index 31b92dc25413a..09dc6f073c4fe 100644 --- a/src/gui/governance/deletegovernancelabel.cpp +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -21,6 +21,8 @@ void DeleteGovernanceLabel::start() connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, this, &DeleteGovernanceLabel::jobDone); + connect(ocsGovernanceJob().data(), &OcsJob::ocsError, + this, &DeleteGovernanceLabel::finishedWithError); ocsGovernanceJob()->setPath(buildPath()); ocsGovernanceJob()->setMethod("DELETE"); @@ -28,15 +30,9 @@ void DeleteGovernanceLabel::start() ocsGovernanceJob()->start(); } -void DeleteGovernanceLabel::jobDone(QJsonDocument reply, int statusCode) +void DeleteGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) { - Q_UNUSED(reply) - Q_UNUSED(statusCode) - - qCInfo(lcGovernance) << reply; - - - Q_EMIT finished(); + Q_EMIT finished(reply); } } // namespace OCC diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp index 401f5bbe7fb34..69dbb8522a7c5 100644 --- a/src/gui/governance/getavailablegovernancelabels.cpp +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -23,6 +23,8 @@ void GetAvailableGovernanceLabels::start() connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, this, &GetAvailableGovernanceLabels::jobDone); + connect(ocsGovernanceJob().data(), &OcsJob::ocsError, + this, &GetAvailableGovernanceLabels::finishedWithError); ocsGovernanceJob()->setPath(buildPath()); ocsGovernanceJob()->setMethod("GET"); @@ -30,14 +32,9 @@ void GetAvailableGovernanceLabels::start() ocsGovernanceJob()->start(); } -void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, int statusCode) +void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) { - Q_UNUSED(reply) - Q_UNUSED(statusCode) - - qCInfo(lcGovernance) << reply; - - Q_EMIT finished(); + Q_EMIT finished(reply); } QString GetAvailableGovernanceLabels::buildPath() const diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp index 20b75ff9101ae..e3ef9ec14841d 100644 --- a/src/gui/governance/getgovernancelabels.cpp +++ b/src/gui/governance/getgovernancelabels.cpp @@ -21,6 +21,8 @@ void GetGovernanceLabels::start() connect(ocsGovernanceJob().data(), &OcsJob::jobFinished, this, &GetGovernanceLabels::jobDone); + connect(ocsGovernanceJob().data(), &OcsJob::ocsError, + this, &GetGovernanceLabels::finishedWithError); ocsGovernanceJob()->setPath(buildPath()); ocsGovernanceJob()->setMethod("GET"); @@ -28,15 +30,9 @@ void GetGovernanceLabels::start() ocsGovernanceJob()->start(); } -void GetGovernanceLabels::jobDone(QJsonDocument reply, int statusCode) +void GetGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) { - Q_UNUSED(reply) - Q_UNUSED(statusCode) - - qCInfo(lcGovernance) << reply; - - - Q_EMIT finished(); + Q_EMIT finished(reply); } } // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h index a5bce972cd79c..cb2f10a642038 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -11,6 +11,7 @@ #include #include #include +#include Q_DECLARE_LOGGING_CATEGORY(lcGovernance) @@ -87,7 +88,9 @@ class GovernanceNetworkJob : public QObject void entityIdChanged(); - void finished(); + void finished(QJsonDocument reply); + + void finishedWithError(int errorCode, const QString &errorMessage); void accountChanged(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 75c2820bec183..dcfb3e3586ad6 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..46fa66cd7d3d0 --- /dev/null +++ b/test/testgovernance.cpp @@ -0,0 +1,529 @@ +/* + * 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" + +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(ApplyGovernanceLabel::EntityType::Files); + myJob.setLabelType(ApplyGovernanceLabel::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(DeleteGovernanceLabel::EntityType::Files); + myJob.setLabelType(DeleteGovernanceLabel::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(GetAvailableGovernanceLabels::EntityType::Files); + myJob.setLabelType(GetAvailableGovernanceLabels::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(GetAvailableGovernanceLabels::EntityType::Files); + myJob.setLabelType(GetAvailableGovernanceLabels::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(GetAvailableGovernanceLabels::EntityType::Custom); + myJob.setCustomEntityType(u"FILEStdytf"_s); + myJob.setLabelType(GetAvailableGovernanceLabels::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(GetGovernanceLabels::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(GetGovernanceLabels::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(GetGovernanceLabels::EntityType::Custom); + myJob.setCustomEntityType(u"FILEStdytf"_s); + + myJob.start(); + + finishedWithErrorSpy.wait(); + QCOMPARE(finishedSpy.count(), 0); + QCOMPARE(finishedWithErrorSpy.count(), 1); + } +}; + +QTEST_GUILESS_MAIN(TestGovernance) +#include "testgovernance.moc" From 7b225550a1749d3d2a7add8ecaa489d1dd9ab952 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Mon, 15 Jun 2026 09:28:47 +0200 Subject: [PATCH 07/26] feat(governance): parse governance API replies to populate the UI Signed-off-by: Matthieu Gallien --- src/gui/CMakeLists.txt | 2 ++ src/gui/governance/governancelabelinfo.cpp | 15 ++++++++++ src/gui/governance/governancelabelinfo.h | 35 ++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 src/gui/governance/governancelabelinfo.cpp create mode 100644 src/gui/governance/governancelabelinfo.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 2611c81a84f9c..8dcbfa0ab808b 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -224,6 +224,8 @@ set(client_SRCS governance/ocsgovernancejob.cpp governance/typedwithlabelidgovernancenetworkjob.h governance/typedwithlabelidgovernancenetworkjob.cpp + governance/governancelabelinfo.h + governance/governancelabelinfo.cpp tray/svgimageprovider.h tray/svgimageprovider.cpp tray/syncstatussummary.h diff --git a/src/gui/governance/governancelabelinfo.cpp b/src/gui/governance/governancelabelinfo.cpp new file mode 100644 index 0000000000000..1f6da914a0b38 --- /dev/null +++ b/src/gui/governance/governancelabelinfo.cpp @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "governancelabelinfo.h" + +namespace OCC +{ + +GOvernanceLabelInfo::GOvernanceLabelInfo() +{ +} + +} // namespace OCC diff --git a/src/gui/governance/governancelabelinfo.h b/src/gui/governance/governancelabelinfo.h new file mode 100644 index 0000000000000..8ddf85345254f --- /dev/null +++ b/src/gui/governance/governancelabelinfo.h @@ -0,0 +1,35 @@ +/* + * 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: + GOvernanceLabelInfo(); + + QString _id; + + QString _name; + + int _priority = -1; + + QString _description; + + QString _color; + + QStringList _scopes; +}; + +} // namespace OCC + +#endif // GOVERNANCELABELINFO_H From dd5ca7962e2e7c1af141361707d1199d7d975fe4 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 18 Jun 2026 09:01:00 +0200 Subject: [PATCH 08/26] feat(governance): makes our apply labels dialog usefull Signed-off-by: Matthieu Gallien --- src/gui/CMakeLists.txt | 3 + src/gui/GovernanceLabelsDialog.qml | 56 ++--- src/gui/governance/applygovernancelabel.cpp | 19 ++ src/gui/governance/applygovernancelabel.h | 2 + src/gui/governance/deletegovernancelabel.cpp | 19 ++ src/gui/governance/deletegovernancelabel.h | 2 + .../getavailablegovernancelabels.cpp | 26 +++ .../governance/getavailablegovernancelabels.h | 4 + src/gui/governance/getgovernancelabels.cpp | 17 ++ src/gui/governance/getgovernancelabels.h | 2 + src/gui/governance/governancelabelinfo.cpp | 4 - src/gui/governance/governancelabelinfo.h | 4 +- .../governance/governancelabelslistmodel.cpp | 192 ++++++++++++++++++ .../governance/governancelabelslistmodel.h | 82 ++++++++ src/gui/governance/governancenetworkjob.cpp | 55 ++++- src/gui/governance/governancenetworkjob.h | 46 ++--- src/gui/governance/governancetypes.h | 45 ++++ .../governance/typedgovernancenetworkjob.cpp | 29 ++- .../governance/typedgovernancenetworkjob.h | 10 +- .../typedwithlabelidgovernancenetworkjob.cpp | 16 ++ .../typedwithlabelidgovernancenetworkjob.h | 2 + src/gui/owncloudgui.cpp | 2 + test/testgovernance.cpp | 91 +++++++-- 23 files changed, 631 insertions(+), 97 deletions(-) create mode 100644 src/gui/governance/governancelabelslistmodel.cpp create mode 100644 src/gui/governance/governancelabelslistmodel.h create mode 100644 src/gui/governance/governancetypes.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 8dcbfa0ab808b..cd90eb3fb2005 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -226,6 +226,9 @@ set(client_SRCS 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 index 9c1c8571a85d9..53d35e4af86ba 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -64,6 +64,10 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Sensitivity entityId: governanceLabelsDialog.fileId + + onFinished: function(reply) { + labelsModel.setAvailableLabelsJsonData(reply) + } } GetAvailableGovernanceLabels { @@ -92,6 +96,17 @@ ApplicationWindow { entityId: governanceLabelsDialog.fileId } + GovernanceLabelsListModel { + id: labelsModel + + entityId: governanceLabelsDialog.fileId + labelType: GovernanceNetworkJob.Sensitivity + + onRefreshData: function(labelType, entityId) { + getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + } + } + ColumnLayout { anchors.fill: parent anchors.leftMargin: 10 @@ -101,42 +116,33 @@ ApplicationWindow { spacing: 15 z: 2 - Button { - text: 'Apply governance label' - onClicked: applyGovernanceLabel.start() - } - - Button { - text: 'Delete governance label' - onClicked: deleteGovernanceLabel.start() - } + EnforcedPlainTextLabel { + text: 'Sensitivity label:' - Button { - text: 'Get available governance labels for sensitivity' - onClicked: getAvailableGovernanceLabelsForSensitivity.start() + font.pixelSize: Style.pixelSize + 2 } - Button { - text: 'Get available governance labels for retention' - onClicked: getAvailableGovernanceLabelsForRetention.start() - } + ComboBox { + id: selectedNewSensitivityLabel - Button { - text: 'Get available governance labels for legal hold' - onClicked: getAvailableGovernanceLabelsForHold.start() - } + font.pixelSize: Style.pixelSize + 2 + Accessible.role: Accessible.ComboBox + Accessible.name: qsTr("Select sensitivity label") - Button { - text: 'Get governance labels' - onClicked: getGovernanceLabels.start() + model: labelsModel } DialogButtonBox { Layout.fillWidth: true Button { - text: qsTr("Close") - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + text: qsTr("Apply") + DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole + } + + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole } onAccepted: function() { diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp index 1ea95bf78fc03..0df7b42f68284 100644 --- a/src/gui/governance/applygovernancelabel.cpp +++ b/src/gui/governance/applygovernancelabel.cpp @@ -13,10 +13,22 @@ 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, @@ -35,4 +47,11 @@ void ApplyGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int sta 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 index a82423a432d6c..5a7afab1b8e4f 100644 --- a/src/gui/governance/applygovernancelabel.h +++ b/src/gui/governance/applygovernancelabel.h @@ -27,6 +27,8 @@ public Q_SLOTS: private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); + + void initialize(); }; } // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp index 09dc6f073c4fe..d3320e3475f67 100644 --- a/src/gui/governance/deletegovernancelabel.cpp +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -13,10 +13,22 @@ 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, @@ -35,4 +47,11 @@ void DeleteGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int st 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 index 938c70d2d3ac9..8be8c1a22bc5f 100644 --- a/src/gui/governance/deletegovernancelabel.h +++ b/src/gui/governance/deletegovernancelabel.h @@ -27,6 +27,8 @@ public Q_SLOTS: private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); + + void initialize(); }; } // namespace OCC diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp index 69dbb8522a7c5..535d453b738d4 100644 --- a/src/gui/governance/getavailablegovernancelabels.cpp +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -15,10 +15,29 @@ 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, @@ -37,6 +56,13 @@ void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] 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()); diff --git a/src/gui/governance/getavailablegovernancelabels.h b/src/gui/governance/getavailablegovernancelabels.h index 29a2cab6a2a47..9bbf0242bffce 100644 --- a/src/gui/governance/getavailablegovernancelabels.h +++ b/src/gui/governance/getavailablegovernancelabels.h @@ -28,11 +28,15 @@ class GetAvailableGovernanceLabels : public OCC::TypedGovernanceNetworkJob 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 diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp index e3ef9ec14841d..04503fe2e1aca 100644 --- a/src/gui/governance/getgovernancelabels.cpp +++ b/src/gui/governance/getgovernancelabels.cpp @@ -13,10 +13,20 @@ 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, @@ -35,4 +45,11 @@ void GetGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int stat 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 index be08db2d4be51..176944efbd30b 100644 --- a/src/gui/governance/getgovernancelabels.h +++ b/src/gui/governance/getgovernancelabels.h @@ -27,6 +27,8 @@ public Q_SLOTS: private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); + + void initialize(); }; } // namespace OCC diff --git a/src/gui/governance/governancelabelinfo.cpp b/src/gui/governance/governancelabelinfo.cpp index 1f6da914a0b38..32aa1b905eb3a 100644 --- a/src/gui/governance/governancelabelinfo.cpp +++ b/src/gui/governance/governancelabelinfo.cpp @@ -8,8 +8,4 @@ namespace OCC { -GOvernanceLabelInfo::GOvernanceLabelInfo() -{ -} - } // namespace OCC diff --git a/src/gui/governance/governancelabelinfo.h b/src/gui/governance/governancelabelinfo.h index 8ddf85345254f..9ee7e311792c2 100644 --- a/src/gui/governance/governancelabelinfo.h +++ b/src/gui/governance/governancelabelinfo.h @@ -12,11 +12,9 @@ namespace OCC { -struct GOvernanceLabelInfo +struct GovernanceLabelInfo { public: - GOvernanceLabelInfo(); - QString _id; QString _name; diff --git a/src/gui/governance/governancelabelslistmodel.cpp b/src/gui/governance/governancelabelslistmodel.cpp new file mode 100644 index 0000000000000..4f580987ae225 --- /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::Invalid) { + 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..5dd2c0d3ce8a8 --- /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::Invalid; + + QString _entityId; + + QList _data; +}; + +} // namespace OCC + +#endif // GOVERNANCELABELLISTMODEL_H diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp index f39c23e9309fe..fa14c110cc6b4 100644 --- a/src/gui/governance/governancenetworkjob.cpp +++ b/src/gui/governance/governancenetworkjob.cpp @@ -5,24 +5,24 @@ #include "governancenetworkjob.h" -Q_LOGGING_CATEGORY(lcGovernance, "nextcloud.gui.governance", QtInfoMsg) - using namespace Qt::StringLiterals; namespace OCC { +Q_LOGGING_CATEGORY(lcGovernanceNetwork, "nextcloud.gui.governance.network", QtInfoMsg) + GovernanceNetworkJob::GovernanceNetworkJob(QObject *parent) : QObject{parent} { } -GovernanceNetworkJob::ApiVersion GovernanceNetworkJob::apiVersion() const +Governance::ApiVersion GovernanceNetworkJob::apiVersion() const { return _apiVersion; } -void GovernanceNetworkJob::setApiVersion(ApiVersion newApiVersion) +void GovernanceNetworkJob::setApiVersion(Governance::ApiVersion newApiVersion) { if (_apiVersion == newApiVersion) { return; @@ -32,12 +32,12 @@ void GovernanceNetworkJob::setApiVersion(ApiVersion newApiVersion) Q_EMIT apiVersionChanged(); } -GovernanceNetworkJob::EntityType GovernanceNetworkJob::entityType() const +Governance::EntityType GovernanceNetworkJob::entityType() const { return _entityType; } -void GovernanceNetworkJob::setEntityType(EntityType newEntityType) +void GovernanceNetworkJob::setEntityType(Governance::EntityType newEntityType) { if (_entityType == newEntityType) { return; @@ -88,9 +88,12 @@ QString GovernanceNetworkJob::apiVersionAsString() const switch (_apiVersion) { - case ApiVersion::Version_1: + case Governance::ApiVersion::Version_1: result = u"v1"_s; break; + case Governance::ApiVersion::Invalid: + result = u"invalid"_s; + break; } return result; @@ -102,13 +105,13 @@ QString GovernanceNetworkJob::entityTypeAsString() const switch (_entityType) { - case EntityType::Files: + case Governance::EntityType::Files: result = u"FILES"_s; break; - case EntityType::Mails: + case Governance::EntityType::Mails: result = u"MAILS"_s; break; - case EntityType::Custom: + case Governance::EntityType::Custom: result = _customEntityType; break; } @@ -116,6 +119,38 @@ QString GovernanceNetworkJob::entityTypeAsString() const return result; } +bool GovernanceNetworkJob::checkParameters() const +{ + auto result = true; + + if (!_account) { + result = false; + return result; + } + + if (_apiVersion == Governance::ApiVersion::Invalid) { + 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) { diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h index cb2f10a642038..25707d3f407da 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -6,6 +6,7 @@ #ifndef GOVERNANCENETWORKJOB_H #define GOVERNANCENETWORKJOB_H +#include "governancetypes.h" #include "accountfwd.h" #include @@ -13,12 +14,11 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(lcGovernance) - - namespace OCC { +Q_DECLARE_LOGGING_CATEGORY(lcGovernanceNetwork) + class OcsGovernanceJob; class GovernanceNetworkJob : public QObject @@ -28,46 +28,24 @@ class GovernanceNetworkJob : public QObject Q_PROPERTY(AccountPtr account READ account WRITE setAccount NOTIFY accountChanged FINAL) - Q_PROPERTY(ApiVersion apiVersion READ apiVersion WRITE setApiVersion NOTIFY apiVersionChanged FINAL) + Q_PROPERTY(Governance::ApiVersion apiVersion READ apiVersion WRITE setApiVersion NOTIFY apiVersionChanged FINAL) - Q_PROPERTY(EntityType entityType READ entityType WRITE setEntityType NOTIFY entityTypeChanged 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: - enum class EntityType { - Files, - Mails, - Custom, - }; - - Q_ENUM(EntityType) - - enum class LabelType { - Sensitivity, - Retention, - Hold, - }; - - Q_ENUM(LabelType) - - enum class ApiVersion { - Version_1, - }; - - Q_ENUM(ApiVersion) - explicit GovernanceNetworkJob(QObject *parent = nullptr); - [[nodiscard]] ApiVersion apiVersion() const; + [[nodiscard]] Governance::ApiVersion apiVersion() const; - void setApiVersion(ApiVersion newApiVersion); + void setApiVersion(Governance::ApiVersion newApiVersion); - [[nodiscard]] EntityType entityType() const; + [[nodiscard]] Governance::EntityType entityType() const; - void setEntityType(EntityType newEntityType); + void setEntityType(Governance::EntityType newEntityType); [[nodiscard]] QString customEntityType() const; @@ -116,12 +94,14 @@ class GovernanceNetworkJob : public QObject [[nodiscard]] QString entityTypeAsString() const; + [[nodiscard]] virtual bool checkParameters() const; + private: AccountPtr _account; - ApiVersion _apiVersion = ApiVersion::Version_1; + Governance::ApiVersion _apiVersion = Governance::ApiVersion::Version_1; - EntityType _entityType = EntityType::Files; + Governance::EntityType _entityType = Governance::EntityType::Files; QString _customEntityType; diff --git a/src/gui/governance/governancetypes.h b/src/gui/governance/governancetypes.h new file mode 100644 index 0000000000000..6fea596059203 --- /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, + Invalid, +}; + +Q_ENUM_NS(LabelType) + +enum class ApiVersion { + Invalid, + Version_1, +}; + +Q_ENUM_NS(ApiVersion) + +} + +} + +#endif // GOVERNANCETYPES_H diff --git a/src/gui/governance/typedgovernancenetworkjob.cpp b/src/gui/governance/typedgovernancenetworkjob.cpp index 5cb3304ef04dd..6d28cdf80e355 100644 --- a/src/gui/governance/typedgovernancenetworkjob.cpp +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -15,12 +15,12 @@ TypedGovernanceNetworkJob::TypedGovernanceNetworkJob(QObject *parent) { } -GovernanceNetworkJob::LabelType TypedGovernanceNetworkJob::labelType() const +Governance::LabelType TypedGovernanceNetworkJob::labelType() const { return _labelType; } -void TypedGovernanceNetworkJob::setLabelType(LabelType newLabelType) +void TypedGovernanceNetworkJob::setLabelType(Governance::LabelType newLabelType) { if (_labelType == newLabelType) { return; @@ -36,15 +36,34 @@ QString TypedGovernanceNetworkJob::labelTypeAsString() const switch (_labelType) { - case LabelType::Sensitivity: + case Governance::LabelType::Sensitivity: result = u"sensitivity"_s; break; - case LabelType::Retention: + case Governance::LabelType::Retention: result = u"retention"_s; break; - case LabelType::Hold: + case Governance::LabelType::Hold: result = u"hold"_s; break; + case Governance::LabelType::Invalid: + result = u"invalid"_s; + break; + } + + return result; +} + +bool TypedGovernanceNetworkJob::checkParameters() const +{ + auto result = GovernanceNetworkJob::checkParameters(); + + if (!result) { + return result; + } + + if (_labelType == Governance::LabelType::Invalid) { + result = false; + return result; } return result; diff --git a/src/gui/governance/typedgovernancenetworkjob.h b/src/gui/governance/typedgovernancenetworkjob.h index 5c7464ef776a1..22df022642772 100644 --- a/src/gui/governance/typedgovernancenetworkjob.h +++ b/src/gui/governance/typedgovernancenetworkjob.h @@ -18,14 +18,14 @@ class TypedGovernanceNetworkJob : public GovernanceNetworkJob Q_OBJECT QML_ELEMENT - Q_PROPERTY(LabelType labelType READ labelType WRITE setLabelType NOTIFY labelTypeChanged FINAL) + Q_PROPERTY(Governance::LabelType labelType READ labelType WRITE setLabelType NOTIFY labelTypeChanged FINAL) public: TypedGovernanceNetworkJob(QObject *parent = nullptr); - [[nodiscard]] LabelType labelType() const; + [[nodiscard]] Governance::LabelType labelType() const; - void setLabelType(LabelType newLabelType); + void setLabelType(Governance::LabelType newLabelType); Q_SIGNALS: void labelTypeChanged(); @@ -33,8 +33,10 @@ class TypedGovernanceNetworkJob : public GovernanceNetworkJob protected: [[nodiscard]] QString labelTypeAsString() const; + [[nodiscard]] bool checkParameters() const override; + private: - LabelType _labelType; + Governance::LabelType _labelType = Governance::LabelType::Invalid; }; } // namespace OCC diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp index 7f83d373e4cce..8d35ca055dcd1 100644 --- a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp @@ -35,4 +35,20 @@ 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 index 785ddd7297230..428578e88e96a 100644 --- a/src/gui/governance/typedwithlabelidgovernancenetworkjob.h +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h @@ -33,6 +33,8 @@ class TypedWithLabelIdGovernanceNetworkJob : public OCC::TypedGovernanceNetworkJ protected: [[nodiscard]] QString buildPath() const override; + [[nodiscard]] bool checkParameters() const override; + private: QString _labelId; }; diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index eb1934bde24df..032771ea1cd53 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -41,6 +41,7 @@ #include "governance/deletegovernancelabel.h" #include "governance/getavailablegovernancelabels.h" #include "governance/getgovernancelabels.h" +#include "governance/governancelabelslistmodel.h" #include "filesystem.h" #ifdef WITH_LIBCLOUDPROVIDERS @@ -161,6 +162,7 @@ ownCloudGui::ownCloudGui(Application *parent) 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"); diff --git a/test/testgovernance.cpp b/test/testgovernance.cpp index 46fa66cd7d3d0..94557c4f949cf 100644 --- a/test/testgovernance.cpp +++ b/test/testgovernance.cpp @@ -13,6 +13,11 @@ #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; @@ -331,8 +336,8 @@ private slots: myJob.setAccount(fakeFolder.account()); myJob.setEntityId(u"1"_s); - myJob.setEntityType(ApplyGovernanceLabel::EntityType::Files); - myJob.setLabelType(ApplyGovernanceLabel::LabelType::Sensitivity); + myJob.setEntityType(Governance::EntityType::Files); + myJob.setLabelType(Governance::LabelType::Sensitivity); myJob.setLabelId(u"1"_s); myJob.start(); @@ -358,8 +363,8 @@ private slots: myJob.setAccount(fakeFolder.account()); myJob.setEntityId(u"1"_s); - myJob.setEntityType(DeleteGovernanceLabel::EntityType::Files); - myJob.setLabelType(DeleteGovernanceLabel::LabelType::Sensitivity); + myJob.setEntityType(Governance::EntityType::Files); + myJob.setLabelType(Governance::LabelType::Sensitivity); myJob.setLabelId(u"1"_s); myJob.start(); @@ -385,8 +390,8 @@ private slots: myJob.setAccount(fakeFolder.account()); myJob.setEntityId(u"117"_s); - myJob.setEntityType(GetAvailableGovernanceLabels::EntityType::Files); - myJob.setLabelType(GetAvailableGovernanceLabels::LabelType::Sensitivity); + myJob.setEntityType(Governance::EntityType::Files); + myJob.setLabelType(Governance::LabelType::Sensitivity); myJob.start(); @@ -411,8 +416,8 @@ private slots: myJob.setAccount(fakeFolder.account()); myJob.setEntityId(u"117117"_s); - myJob.setEntityType(GetAvailableGovernanceLabels::EntityType::Files); - myJob.setLabelType(GetAvailableGovernanceLabels::LabelType::Sensitivity); + myJob.setEntityType(Governance::EntityType::Files); + myJob.setLabelType(Governance::LabelType::Sensitivity); myJob.start(); @@ -437,9 +442,9 @@ private slots: myJob.setAccount(fakeFolder.account()); myJob.setEntityId(u"117"_s); - myJob.setEntityType(GetAvailableGovernanceLabels::EntityType::Custom); + myJob.setEntityType(Governance::EntityType::Custom); myJob.setCustomEntityType(u"FILEStdytf"_s); - myJob.setLabelType(GetAvailableGovernanceLabels::LabelType::Sensitivity); + myJob.setLabelType(Governance::LabelType::Sensitivity); myJob.start(); @@ -464,7 +469,7 @@ private slots: myJob.setAccount(fakeFolder.account()); myJob.setEntityId(u"117"_s); - myJob.setEntityType(GetGovernanceLabels::EntityType::Files); + myJob.setEntityType(Governance::EntityType::Files); myJob.start(); @@ -489,7 +494,7 @@ private slots: myJob.setAccount(fakeFolder.account()); myJob.setEntityId(u"117117"_s); - myJob.setEntityType(GetGovernanceLabels::EntityType::Files); + myJob.setEntityType(Governance::EntityType::Files); myJob.start(); @@ -514,7 +519,7 @@ private slots: myJob.setAccount(fakeFolder.account()); myJob.setEntityId(u"117"_s); - myJob.setEntityType(GetGovernanceLabels::EntityType::Custom); + myJob.setEntityType(Governance::EntityType::Custom); myJob.setCustomEntityType(u"FILEStdytf"_s); myJob.start(); @@ -523,6 +528,66 @@ private slots: 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) From 0a146ca05deb1ac442717432dda06bc95ebae924 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 24 Jun 2026 16:40:19 +0200 Subject: [PATCH 09/26] feat(governance): fix some issues with QML bindings of enum types Signed-off-by: Matthieu Gallien --- src/gui/governance/governancelabelslistmodel.cpp | 2 +- src/gui/governance/governancelabelslistmodel.h | 2 +- src/gui/governance/governancenetworkjob.cpp | 4 ++-- src/gui/governance/governancetypes.h | 4 ++-- src/gui/governance/typedgovernancenetworkjob.cpp | 4 ++-- src/gui/governance/typedgovernancenetworkjob.h | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/gui/governance/governancelabelslistmodel.cpp b/src/gui/governance/governancelabelslistmodel.cpp index 4f580987ae225..02684e37b334c 100644 --- a/src/gui/governance/governancelabelslistmodel.cpp +++ b/src/gui/governance/governancelabelslistmodel.cpp @@ -182,7 +182,7 @@ void OCC::GovernanceLabelsListModel::etagChanged() void GovernanceLabelsListModel::emitRefreshData() { - if (_entityId.isEmpty() || _labelType == Governance::LabelType::Invalid) { + if (_entityId.isEmpty() || _labelType == Governance::LabelType::InvalidLabelType) { return; } diff --git a/src/gui/governance/governancelabelslistmodel.h b/src/gui/governance/governancelabelslistmodel.h index 5dd2c0d3ce8a8..d4b20b7028b31 100644 --- a/src/gui/governance/governancelabelslistmodel.h +++ b/src/gui/governance/governancelabelslistmodel.h @@ -70,7 +70,7 @@ public Q_SLOTS: private: void emitRefreshData(); - Governance::LabelType _labelType = Governance::LabelType::Invalid; + Governance::LabelType _labelType = Governance::LabelType::InvalidLabelType; QString _entityId; diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp index fa14c110cc6b4..b2ce60d48b397 100644 --- a/src/gui/governance/governancenetworkjob.cpp +++ b/src/gui/governance/governancenetworkjob.cpp @@ -91,7 +91,7 @@ QString GovernanceNetworkJob::apiVersionAsString() const case Governance::ApiVersion::Version_1: result = u"v1"_s; break; - case Governance::ApiVersion::Invalid: + case Governance::ApiVersion::InvalidApiVersion: result = u"invalid"_s; break; } @@ -128,7 +128,7 @@ bool GovernanceNetworkJob::checkParameters() const return result; } - if (_apiVersion == Governance::ApiVersion::Invalid) { + if (_apiVersion == Governance::ApiVersion::InvalidApiVersion) { result = false; return result; } diff --git a/src/gui/governance/governancetypes.h b/src/gui/governance/governancetypes.h index 6fea596059203..b95d5eae8b530 100644 --- a/src/gui/governance/governancetypes.h +++ b/src/gui/governance/governancetypes.h @@ -26,13 +26,13 @@ enum class LabelType { Sensitivity, Retention, Hold, - Invalid, + InvalidLabelType, }; Q_ENUM_NS(LabelType) enum class ApiVersion { - Invalid, + InvalidApiVersion, Version_1, }; diff --git a/src/gui/governance/typedgovernancenetworkjob.cpp b/src/gui/governance/typedgovernancenetworkjob.cpp index 6d28cdf80e355..d9f2b091fcd56 100644 --- a/src/gui/governance/typedgovernancenetworkjob.cpp +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -45,7 +45,7 @@ QString TypedGovernanceNetworkJob::labelTypeAsString() const case Governance::LabelType::Hold: result = u"hold"_s; break; - case Governance::LabelType::Invalid: + case Governance::LabelType::InvalidLabelType: result = u"invalid"_s; break; } @@ -61,7 +61,7 @@ bool TypedGovernanceNetworkJob::checkParameters() const return result; } - if (_labelType == Governance::LabelType::Invalid) { + if (_labelType == Governance::LabelType::InvalidLabelType) { result = false; return result; } diff --git a/src/gui/governance/typedgovernancenetworkjob.h b/src/gui/governance/typedgovernancenetworkjob.h index 22df022642772..1b20e2eef80b0 100644 --- a/src/gui/governance/typedgovernancenetworkjob.h +++ b/src/gui/governance/typedgovernancenetworkjob.h @@ -36,7 +36,7 @@ class TypedGovernanceNetworkJob : public GovernanceNetworkJob [[nodiscard]] bool checkParameters() const override; private: - Governance::LabelType _labelType = Governance::LabelType::Invalid; + Governance::LabelType _labelType = Governance::LabelType::InvalidLabelType; }; } // namespace OCC From 60cfb4bf46bd61f6a278ea308e4b562cdae573f3 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 24 Jun 2026 18:19:02 +0200 Subject: [PATCH 10/26] feat(governance): remove unused components from governance dialog Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 46 ------------------------------ 1 file changed, 46 deletions(-) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index 53d35e4af86ba..1dc165b8906fe 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -37,26 +37,6 @@ ApplicationWindow { close.accepted = true } - ApplyGovernanceLabel { - id: applyGovernanceLabel - - account: governanceLabelsDialog.account - - labelId: 'labelId' - labelType: GovernanceNetworkJob.Sensitivity - entityId: governanceLabelsDialog.fileId - } - - DeleteGovernanceLabel { - id: deleteGovernanceLabel - - account: governanceLabelsDialog.account - - labelId: 'labelId' - labelType: GovernanceNetworkJob.Sensitivity - entityId: governanceLabelsDialog.fileId - } - GetAvailableGovernanceLabels { id: getAvailableGovernanceLabelsForSensitivity @@ -70,32 +50,6 @@ ApplicationWindow { } } - GetAvailableGovernanceLabels { - id: getAvailableGovernanceLabelsForHold - - account: governanceLabelsDialog.account - - labelType: GovernanceNetworkJob.Hold - entityId: governanceLabelsDialog.fileId - } - - GetAvailableGovernanceLabels { - id: getAvailableGovernanceLabelsForRetention - - account: governanceLabelsDialog.account - - labelType: GovernanceNetworkJob.Retention - entityId: governanceLabelsDialog.fileId - } - - GetGovernanceLabels { - id: getGovernanceLabels - - account: governanceLabelsDialog.account - - entityId: governanceLabelsDialog.fileId - } - GovernanceLabelsListModel { id: labelsModel From 76587dfa0044327d28f1c2ae2b33c63a18ba99f8 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 24 Jun 2026 18:20:03 +0200 Subject: [PATCH 11/26] feat(governance): now the ComboBox displays the labels we explicitely define which roles need to be used Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index 1dc165b8906fe..90230e23bf64c 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -84,6 +84,8 @@ ApplicationWindow { Accessible.name: qsTr("Select sensitivity label") model: labelsModel + textRole: 'name' + valueRole: 'id' } DialogButtonBox { From 971e21779edbd1aa4d7f932b58d63637762ac8e1 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 30 Jun 2026 16:34:15 +0200 Subject: [PATCH 12/26] feat(governance): add all label types Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 118 +++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 14 deletions(-) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index 90230e23bf64c..fe2521ee645da 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -37,6 +37,17 @@ ApplicationWindow { close.accepted = true } + GovernanceLabelsListModel { + id: sensitivityLabelsModel + + entityId: governanceLabelsDialog.fileId + labelType: GovernanceNetworkJob.Sensitivity + + onRefreshData: function(labelType, entityId) { + getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + } + } + GetAvailableGovernanceLabels { id: getAvailableGovernanceLabelsForSensitivity @@ -46,21 +57,58 @@ ApplicationWindow { entityId: governanceLabelsDialog.fileId onFinished: function(reply) { - labelsModel.setAvailableLabelsJsonData(reply) + sensitivityLabelsModel.setAvailableLabelsJsonData(reply) } } GovernanceLabelsListModel { - id: labelsModel + id: retentionLabelsModel entityId: governanceLabelsDialog.fileId - labelType: GovernanceNetworkJob.Sensitivity + labelType: GovernanceNetworkJob.Retention onRefreshData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) } } + GetAvailableGovernanceLabels { + id: getAvailableGovernanceLabelsForRetention + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Retention + entityId: governanceLabelsDialog.fileId + + onFinished: function(reply) { + retentionLabelsModel.setAvailableLabelsJsonData(reply) + } + } + + GovernanceLabelsListModel { + id: legalHoldLabelsModel + + entityId: governanceLabelsDialog.fileId + labelType: GovernanceNetworkJob.Retention + + onRefreshData: function(labelType, entityId) { + getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + } + } + + GetAvailableGovernanceLabels { + id: getAvailableGovernanceLabelsForLegalHold + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Retention + entityId: governanceLabelsDialog.fileId + + onFinished: function(reply) { + legalHoldLabelsModel.setAvailableLabelsJsonData(reply) + } + } + ColumnLayout { anchors.fill: parent anchors.leftMargin: 10 @@ -70,22 +118,64 @@ ApplicationWindow { spacing: 15 z: 2 - EnforcedPlainTextLabel { - text: 'Sensitivity label:' + RowLayout { + EnforcedPlainTextLabel { + text: 'Sensitivity label:' + + font.pixelSize: Style.pixelSize + 2 + } + + ComboBox { + id: selectedNewSensitivityLabel - font.pixelSize: Style.pixelSize + 2 + font.pixelSize: Style.pixelSize + 2 + Accessible.role: Accessible.ComboBox + Accessible.name: qsTr("Select sensitivity label") + + model: sensitivityLabelsModel + textRole: 'name' + valueRole: 'id' + } + } + + RowLayout { + EnforcedPlainTextLabel { + text: 'Retention labels:' + + font.pixelSize: Style.pixelSize + 2 + } + + ComboBox { + id: selectedNewRetentionLabel + + font.pixelSize: Style.pixelSize + 2 + Accessible.role: Accessible.ComboBox + Accessible.name: qsTr("Select retention labels") + + model: retentionLabelsModel + textRole: 'name' + valueRole: 'id' + } } - ComboBox { - id: selectedNewSensitivityLabel + RowLayout { + EnforcedPlainTextLabel { + text: 'Legal hold labels:' - font.pixelSize: Style.pixelSize + 2 - Accessible.role: Accessible.ComboBox - Accessible.name: qsTr("Select sensitivity label") + font.pixelSize: Style.pixelSize + 2 + } - model: labelsModel - textRole: 'name' - valueRole: 'id' + ComboBox { + id: selectedNewLegalHoldLabel + + font.pixelSize: Style.pixelSize + 2 + Accessible.role: Accessible.ComboBox + Accessible.name: qsTr("Select legal hold labels") + + model: legalHoldLabelsModel + textRole: 'name' + valueRole: 'id' + } } DialogButtonBox { From 96e34e6992accb291d0837a2ed045b760ddd1e8d Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 30 Jun 2026 18:07:18 +0200 Subject: [PATCH 13/26] feat(governance): apply labels and fix issues Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 23 +++++++++++++++---- .../governance/typedgovernancenetworkjob.cpp | 6 ++--- .../typedwithlabelidgovernancenetworkjob.cpp | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index fe2521ee645da..27b0b34bf5894 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -61,6 +61,19 @@ ApplicationWindow { } } + ApplyGovernanceLabel { + id: applySensitivityLabel + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Sensitivity + entityId: governanceLabelsDialog.fileId + + onFinished: function(reply) { + sensitivityLabelsModel.setAvailableLabelsJsonData(reply) + } + } + GovernanceLabelsListModel { id: retentionLabelsModel @@ -135,6 +148,11 @@ ApplicationWindow { model: sensitivityLabelsModel textRole: 'name' valueRole: 'id' + + onActivated: function() { + applySensitivityLabel.labelId = currentValue + applySensitivityLabel.start() + } } } @@ -181,11 +199,6 @@ ApplicationWindow { DialogButtonBox { Layout.fillWidth: true - Button { - text: qsTr("Apply") - DialogButtonBox.buttonRole: DialogButtonBox.ApplyRole - } - Button { text: qsTr("Cancel") DialogButtonBox.buttonRole: DialogButtonBox.RejectRole diff --git a/src/gui/governance/typedgovernancenetworkjob.cpp b/src/gui/governance/typedgovernancenetworkjob.cpp index d9f2b091fcd56..b43569128a38b 100644 --- a/src/gui/governance/typedgovernancenetworkjob.cpp +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -37,13 +37,13 @@ QString TypedGovernanceNetworkJob::labelTypeAsString() const switch (_labelType) { case Governance::LabelType::Sensitivity: - result = u"sensitivity"_s; + result = u"SENSITIVITY"_s; break; case Governance::LabelType::Retention: - result = u"retention"_s; + result = u"RETENTION"_s; break; case Governance::LabelType::Hold: - result = u"hold"_s; + result = u"HOLD"_s; break; case Governance::LabelType::InvalidLabelType: result = u"invalid"_s; diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp index 8d35ca055dcd1..391a0c7dfd827 100644 --- a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp @@ -32,7 +32,7 @@ void TypedWithLabelIdGovernanceNetworkJob::setLabelId(const QString &newLabelId) QString TypedWithLabelIdGovernanceNetworkJob::buildPath() const { - return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelId(), labelTypeAsString()); + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelTypeAsString(), labelId()); } bool TypedWithLabelIdGovernanceNetworkJob::checkParameters() const From 230e876b9e38aa72d8d681d8389b61fc40cf47f4 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 1 Jul 2026 09:57:46 +0200 Subject: [PATCH 14/26] feat(governance): apply wizard visual style to GovernanceLabelsDialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restyle GovernanceLabelsDialog to match the design system used by AccountWizardWindow and its sub-components in wizard/qml/: - Set explicit wizard palette and background on the ApplicationWindow for full light/dark mode support - Replace stock ComboBox, Button and DialogButtonBox with WizardComboBox and WizardButton for consistent Nextcloud branding - Add bold dialog title heading, update margins to 24 px, fix spacing - Wrap all label strings in qsTr() and apply wizard text colors - Fix window title typo ("Applys labels" → "Apply labels") - Add WizardComboBox.qml, a reusable styled combo box following the WizardButton/WizardTextField component pattern Signed-off-by: Matthieu Gallien Assisted-by: ClaudeCode:claude-sonnet-4-6 --- resources.qrc | 1 + src/gui/GovernanceLabelsDialog.qml | 114 +++++++++++++++----------- src/gui/wizard/qml/WizardComboBox.qml | 109 ++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 46 deletions(-) create mode 100644 src/gui/wizard/qml/WizardComboBox.qml diff --git a/resources.qrc b/resources.qrc index 5496ebdf8afed..940b9cc0ff4bf 100644 --- a/resources.qrc +++ b/resources.qrc @@ -77,6 +77,7 @@ src/gui/wizard/qml/ServerPage.qml src/gui/wizard/qml/SyncOptionsPage.qml src/gui/wizard/qml/WizardButton.qml + src/gui/wizard/qml/WizardComboBox.qml src/gui/wizard/qml/WizardDialogFrame.qml src/gui/wizard/qml/WizardTextField.qml src/gui/macOS/ui/FileProviderSettings.qml diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index 27b0b34bf5894..25fc184da97f6 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -12,6 +12,7 @@ import QtQml.Models import Style import com.nextcloud.desktopclient import "./tray" +import "./wizard/qml" ApplicationWindow { id: governanceLabelsDialog @@ -20,6 +21,9 @@ ApplicationWindow { required property var fileId required property var account + readonly property color primaryTextColor: Style.wizardPrimaryText + readonly property color hintTextColor: Style.wizardSecondaryText + flags: Qt.Window | Qt.Dialog visible: true @@ -30,7 +34,21 @@ ApplicationWindow { height: Style.minimumHeightResolveConflictsDialog minimumWidth: Style.minimumWidthResolveConflictsDialog minimumHeight: Style.minimumHeightResolveConflictsDialog - title: qsTr('Applys labels') + title: qsTr("Apply labels") + + color: Style.wizardWindowBackground + palette.window: Style.wizardWindowBackground + palette.windowText: Style.wizardPrimaryText + palette.base: Style.wizardFieldBackground + palette.text: Style.wizardPrimaryText + palette.button: Style.wizardFieldBackground + palette.buttonText: Style.wizardPrimaryText + palette.mid: Style.wizardDisabledText + palette.placeholderText: Style.wizardPlaceholderText + + background: Rectangle { + color: Style.wizardWindowBackground + } onClosing: function(close) { Systray.destroyDialog(self); @@ -124,30 +142,33 @@ ApplicationWindow { ColumnLayout { anchors.fill: parent - anchors.leftMargin: 10 - anchors.rightMargin: 10 - anchors.bottomMargin: 5 - anchors.topMargin: 10 - spacing: 15 - z: 2 + anchors.leftMargin: 24 + anchors.rightMargin: 24 + anchors.bottomMargin: 24 + anchors.topMargin: 24 + spacing: 14 RowLayout { - EnforcedPlainTextLabel { - text: 'Sensitivity label:' + Layout.fillWidth: true + spacing: 8 - font.pixelSize: Style.pixelSize + 2 + EnforcedPlainTextLabel { + text: qsTr("Sensitivity:") + color: governanceLabelsDialog.hintTextColor + font.pixelSize: Style.pixelSize } - ComboBox { + WizardComboBox { id: selectedNewSensitivityLabel - font.pixelSize: Style.pixelSize + 2 Accessible.role: Accessible.ComboBox Accessible.name: qsTr("Select sensitivity label") model: sensitivityLabelsModel - textRole: 'name' - valueRole: 'id' + textRole: "name" + valueRole: "id" + + Layout.fillWidth: true onActivated: function() { applySensitivityLabel.labelId = currentValue @@ -157,66 +178,67 @@ ApplicationWindow { } RowLayout { - EnforcedPlainTextLabel { - text: 'Retention labels:' + Layout.fillWidth: true + spacing: 8 - font.pixelSize: Style.pixelSize + 2 + EnforcedPlainTextLabel { + text: qsTr("Retention:") + color: governanceLabelsDialog.hintTextColor + font.pixelSize: Style.pixelSize } - ComboBox { + WizardComboBox { id: selectedNewRetentionLabel - font.pixelSize: Style.pixelSize + 2 Accessible.role: Accessible.ComboBox - Accessible.name: qsTr("Select retention labels") + Accessible.name: qsTr("Select retention label") model: retentionLabelsModel - textRole: 'name' - valueRole: 'id' + textRole: "name" + valueRole: "id" + Layout.fillWidth: true } } RowLayout { - EnforcedPlainTextLabel { - text: 'Legal hold labels:' + Layout.fillWidth: true + spacing: 8 - font.pixelSize: Style.pixelSize + 2 + EnforcedPlainTextLabel { + text: qsTr("Legal hold:") + color: governanceLabelsDialog.hintTextColor + font.pixelSize: Style.pixelSize } - ComboBox { + WizardComboBox { id: selectedNewLegalHoldLabel - font.pixelSize: Style.pixelSize + 2 Accessible.role: Accessible.ComboBox - Accessible.name: qsTr("Select legal hold labels") + Accessible.name: qsTr("Select legal hold label") model: legalHoldLabelsModel - textRole: 'name' - valueRole: 'id' + textRole: "name" + valueRole: "id" + Layout.fillWidth: true } } - DialogButtonBox { - Layout.fillWidth: true + Item { + Layout.fillHeight: true + } - Button { - text: qsTr("Cancel") - DialogButtonBox.buttonRole: DialogButtonBox.RejectRole - } + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 8 - onAccepted: function() { - Systray.destroyDialog(governanceLabelsDialog) + Item { + Layout.fillWidth: true } - onRejected: function() { - Systray.destroyDialog(governanceLabelsDialog) + WizardButton { + text: qsTr("Cancel") + onClicked: Systray.destroyDialog(governanceLabelsDialog) } } } - - Rectangle { - color: palette.base - anchors.fill: parent - z: 1 - } } diff --git a/src/gui/wizard/qml/WizardComboBox.qml b/src/gui/wizard/qml/WizardComboBox.qml new file mode 100644 index 0000000000000..fde303970c911 --- /dev/null +++ b/src/gui/wizard/qml/WizardComboBox.qml @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick +import QtQuick.Controls.Basic as BasicControls +import Style + +BasicControls.ComboBox { + id: root + + implicitHeight: Style.standardPrimaryButtonHeight + leftPadding: 12 + rightPadding: 40 + topPadding: 0 + bottomPadding: 0 + font.pixelSize: Style.pixelSize + 3 + + contentItem: Text { + text: root.displayText + font: root.font + color: root.enabled ? Style.wizardPrimaryText : Style.wizardDisabledText + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + indicator: Image { + x: root.width - width - root.leftPadding + y: Math.round((root.height - height) / 2) + width: 16 + height: 16 + source: "image://svgimage-custom-color/caret-down.svg/" + Style.wizardPrimaryText + rotation: root.popup.visible ? 180 : 0 + opacity: root.enabled ? 1 : 0.45 + fillMode: Image.PreserveAspectFit + + Behavior on rotation { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + } + + background: Rectangle { + radius: 8 + color: Style.wizardFieldBackground + border.width: 1 + border.color: root.activeFocus || root.popup.visible + ? Style.ncBlue + : Style.wizardFieldBorder + } + + delegate: BasicControls.ItemDelegate { + id: delegateItem + + required property int index + required property var model + + width: ListView.view ? ListView.view.width : root.width - 8 + height: Style.standardPrimaryButtonHeight + highlighted: root.highlightedIndex === index + + contentItem: Text { + text: delegateItem.model.name + font: root.font + color: root.currentIndex === delegateItem.index + ? Style.wizardSelectedText + : Style.wizardPrimaryText + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + radius: 6 + color: { + if (root.currentIndex === delegateItem.index) { + return Style.ncBlue + } + if (delegateItem.highlighted) { + return Style.wizardSecondaryButtonBackground + } + return Style.wizardFieldBackground + } + } + } + + popup: BasicControls.Popup { + y: root.height + 4 + width: root.width + implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding + padding: 4 + + contentItem: ListView { + clip: true + implicitHeight: Math.min(contentHeight, Style.standardPrimaryButtonHeight * 6) + model: root.popup.visible ? root.delegateModel : null + currentIndex: root.highlightedIndex + } + + background: Rectangle { + radius: 8 + color: Style.wizardFieldBackground + border.width: 1 + border.color: Style.wizardSecondaryButtonBorder + } + } +} From d7c01621d66fac107bb5223103d13d1515639b4c Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 1 Jul 2026 10:18:52 +0200 Subject: [PATCH 15/26] feat(governance): fix governance API requests syntax Signed-off-by: Matthieu Gallien --- src/gui/governance/applygovernancelabel.cpp | 7 +++ src/gui/governance/applygovernancelabel.h | 3 ++ src/gui/governance/deletegovernancelabel.cpp | 7 +++ src/gui/governance/deletegovernancelabel.h | 3 ++ .../getavailablegovernancelabels.cpp | 2 +- src/gui/governance/getgovernancelabels.cpp | 7 +++ src/gui/governance/getgovernancelabels.h | 3 ++ src/gui/governance/governancenetworkjob.cpp | 20 ++++++-- src/gui/governance/governancenetworkjob.h | 4 +- .../governance/typedgovernancenetworkjob.cpp | 46 ++++++++++++++----- .../governance/typedgovernancenetworkjob.h | 9 +++- .../typedwithlabelidgovernancenetworkjob.cpp | 5 -- .../typedwithlabelidgovernancenetworkjob.h | 2 - 13 files changed, 91 insertions(+), 27 deletions(-) diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp index 0df7b42f68284..62b6057af7ade 100644 --- a/src/gui/governance/applygovernancelabel.cpp +++ b/src/gui/governance/applygovernancelabel.cpp @@ -7,6 +7,8 @@ #include "ocsgovernancejob.h" +using namespace Qt::StringLiterals; + namespace OCC { @@ -42,6 +44,11 @@ void ApplyGovernanceLabel::start() ocsGovernanceJob()->start(); } +QString ApplyGovernanceLabel::buildPath() const +{ + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString(), labelTypeAsString(TypedGovernanceNetworkJob::Capitalization::UpCase), labelId()); +} + void ApplyGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) { Q_EMIT finished(reply); diff --git a/src/gui/governance/applygovernancelabel.h b/src/gui/governance/applygovernancelabel.h index 5a7afab1b8e4f..af0890a637d67 100644 --- a/src/gui/governance/applygovernancelabel.h +++ b/src/gui/governance/applygovernancelabel.h @@ -25,6 +25,9 @@ class ApplyGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob public Q_SLOTS: void start(); +protected: + [[nodiscard]] QString buildPath() const override; + private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp index d3320e3475f67..ee221f277a8d6 100644 --- a/src/gui/governance/deletegovernancelabel.cpp +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -7,6 +7,8 @@ #include "ocsgovernancejob.h" +using namespace Qt::StringLiterals; + namespace OCC { @@ -42,6 +44,11 @@ void DeleteGovernanceLabel::start() ocsGovernanceJob()->start(); } +QString DeleteGovernanceLabel::buildPath() const +{ + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString(), labelTypeAsString(TypedGovernanceNetworkJob::Capitalization::UpCase), labelId()); +} + void DeleteGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) { Q_EMIT finished(reply); diff --git a/src/gui/governance/deletegovernancelabel.h b/src/gui/governance/deletegovernancelabel.h index 8be8c1a22bc5f..c4be9bef39261 100644 --- a/src/gui/governance/deletegovernancelabel.h +++ b/src/gui/governance/deletegovernancelabel.h @@ -25,6 +25,9 @@ class DeleteGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob public Q_SLOTS: void start(); +protected: + [[nodiscard]] QString buildPath() const override; + private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp index 535d453b738d4..be4dd20cc5b62 100644 --- a/src/gui/governance/getavailablegovernancelabels.cpp +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -65,7 +65,7 @@ void GetAvailableGovernanceLabels::initialize() QString GetAvailableGovernanceLabels::buildPath() const { - return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/available"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelTypeAsString()); + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/available"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelTypeAsString(TypedGovernanceNetworkJob::Capitalization::LowCase)); } } // namespace OCC diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp index 04503fe2e1aca..2b6da65941a41 100644 --- a/src/gui/governance/getgovernancelabels.cpp +++ b/src/gui/governance/getgovernancelabels.cpp @@ -7,6 +7,8 @@ #include "ocsgovernancejob.h" +using namespace Qt::StringLiterals; + namespace OCC { @@ -40,6 +42,11 @@ void GetGovernanceLabels::start() ocsGovernanceJob()->start(); } +QString GetGovernanceLabels::buildPath() const +{ + return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString()); +} + void GetGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) { Q_EMIT finished(reply); diff --git a/src/gui/governance/getgovernancelabels.h b/src/gui/governance/getgovernancelabels.h index 176944efbd30b..1a32ea4288b0a 100644 --- a/src/gui/governance/getgovernancelabels.h +++ b/src/gui/governance/getgovernancelabels.h @@ -25,6 +25,9 @@ class GetGovernanceLabels : public OCC::GovernanceNetworkJob public Q_SLOTS: void start(); +protected: + [[nodiscard]] QString buildPath() const override; + private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp index b2ce60d48b397..9a04ab377f311 100644 --- a/src/gui/governance/governancenetworkjob.cpp +++ b/src/gui/governance/governancenetworkjob.cpp @@ -67,6 +67,21 @@ QString GovernanceNetworkJob::entityId() const return _entityId; } +QString GovernanceNetworkJob::integerEntityIdAsString() const +{ + auto result = u"-1"_s; + + const auto entityIdView = QStringView{_entityId}; + const auto idView = entityIdView.left(8); + auto conversionStatus = false; + const auto fileId = idView.toInt(&conversionStatus); + if (conversionStatus) { + result = QString::number(fileId); + } + + return result; +} + void GovernanceNetworkJob::setEntityId(const QString &newEntityId) { if (_entityId == newEntityId) { @@ -77,11 +92,6 @@ void GovernanceNetworkJob::setEntityId(const QString &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{}; diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h index 25707d3f407da..38d8a2cb34c14 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -53,6 +53,8 @@ class GovernanceNetworkJob : public QObject [[nodiscard]] QString entityId() const; + [[nodiscard]] QString integerEntityIdAsString() const; + void setEntityId(const QString &newEntityId); void setAccount(AccountPtr newAccount); @@ -88,7 +90,7 @@ class GovernanceNetworkJob : public QObject return _account; } - [[nodiscard]] virtual QString buildPath() const; + [[nodiscard]] virtual QString buildPath() const = 0; [[nodiscard]] QString apiVersionAsString() const; diff --git a/src/gui/governance/typedgovernancenetworkjob.cpp b/src/gui/governance/typedgovernancenetworkjob.cpp index b43569128a38b..e19b2cd886ef7 100644 --- a/src/gui/governance/typedgovernancenetworkjob.cpp +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -30,23 +30,45 @@ void TypedGovernanceNetworkJob::setLabelType(Governance::LabelType newLabelType) Q_EMIT labelTypeChanged(); } -QString TypedGovernanceNetworkJob::labelTypeAsString() const +QString TypedGovernanceNetworkJob::labelTypeAsString(Capitalization capitalization) const { auto result = QString{}; - switch (_labelType) + switch (capitalization) { - case Governance::LabelType::Sensitivity: - result = u"SENSITIVITY"_s; + case Capitalization::LowCase: + 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; + } 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; + case Capitalization::UpCase: + 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; + } break; } diff --git a/src/gui/governance/typedgovernancenetworkjob.h b/src/gui/governance/typedgovernancenetworkjob.h index 1b20e2eef80b0..35e9282da3f17 100644 --- a/src/gui/governance/typedgovernancenetworkjob.h +++ b/src/gui/governance/typedgovernancenetworkjob.h @@ -21,6 +21,13 @@ class TypedGovernanceNetworkJob : public GovernanceNetworkJob Q_PROPERTY(Governance::LabelType labelType READ labelType WRITE setLabelType NOTIFY labelTypeChanged FINAL) public: + enum class Capitalization { + UpCase, + LowCase, + }; + + Q_ENUM(Capitalization) + TypedGovernanceNetworkJob(QObject *parent = nullptr); [[nodiscard]] Governance::LabelType labelType() const; @@ -31,7 +38,7 @@ class TypedGovernanceNetworkJob : public GovernanceNetworkJob void labelTypeChanged(); protected: - [[nodiscard]] QString labelTypeAsString() const; + [[nodiscard]] QString labelTypeAsString(Capitalization capitalization) const; [[nodiscard]] bool checkParameters() const override; diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp index 391a0c7dfd827..81102ff0007ad 100644 --- a/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.cpp @@ -30,11 +30,6 @@ void TypedWithLabelIdGovernanceNetworkJob::setLabelId(const QString &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(), labelTypeAsString(), labelId()); -} - bool TypedWithLabelIdGovernanceNetworkJob::checkParameters() const { auto result = TypedGovernanceNetworkJob::checkParameters(); diff --git a/src/gui/governance/typedwithlabelidgovernancenetworkjob.h b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h index 428578e88e96a..88ef0682d1d61 100644 --- a/src/gui/governance/typedwithlabelidgovernancenetworkjob.h +++ b/src/gui/governance/typedwithlabelidgovernancenetworkjob.h @@ -31,8 +31,6 @@ class TypedWithLabelIdGovernanceNetworkJob : public OCC::TypedGovernanceNetworkJ void labelIdChanged(); protected: - [[nodiscard]] QString buildPath() const override; - [[nodiscard]] bool checkParameters() const override; private: From 304de16aba32c0c4e31bd0d46abd1417eaa7d155 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 1 Jul 2026 12:31:43 +0200 Subject: [PATCH 16/26] feat(governance): improve QML bindings to the governance jobs should make it easier and more predictable to use from QML Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 39 +++++++++++++++---- src/gui/governance/applygovernancelabel.cpp | 32 ++++++++------- src/gui/governance/applygovernancelabel.h | 10 ++++- src/gui/governance/deletegovernancelabel.cpp | 32 ++++++++------- src/gui/governance/deletegovernancelabel.h | 10 ++++- .../getavailablegovernancelabels.cpp | 24 ++++++------ .../governance/getavailablegovernancelabels.h | 7 +++- src/gui/governance/getgovernancelabels.cpp | 29 ++++++++------ src/gui/governance/getgovernancelabels.h | 10 ++++- src/gui/governance/governancenetworkjob.h | 4 +- src/gui/governance/governancetypes.h | 2 +- .../governance/typedgovernancenetworkjob.cpp | 4 +- 12 files changed, 130 insertions(+), 73 deletions(-) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index 25fc184da97f6..3a8991bf5d9d9 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -86,10 +86,6 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Sensitivity entityId: governanceLabelsDialog.fileId - - onFinished: function(reply) { - sensitivityLabelsModel.setAvailableLabelsJsonData(reply) - } } GovernanceLabelsListModel { @@ -116,11 +112,20 @@ ApplicationWindow { } } + ApplyGovernanceLabel { + id: applyRetentionLabel + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Retention + entityId: governanceLabelsDialog.fileId + } + GovernanceLabelsListModel { id: legalHoldLabelsModel entityId: governanceLabelsDialog.fileId - labelType: GovernanceNetworkJob.Retention + labelType: GovernanceNetworkJob.LegalHold onRefreshData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) @@ -132,7 +137,7 @@ ApplicationWindow { account: governanceLabelsDialog.account - labelType: GovernanceNetworkJob.Retention + labelType: GovernanceNetworkJob.LegalHold entityId: governanceLabelsDialog.fileId onFinished: function(reply) { @@ -140,6 +145,15 @@ ApplicationWindow { } } + ApplyGovernanceLabel { + id: applyLegalHoldLabel + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.LegalHold + entityId: governanceLabelsDialog.fileId + } + ColumnLayout { anchors.fill: parent anchors.leftMargin: 24 @@ -171,8 +185,7 @@ ApplicationWindow { Layout.fillWidth: true onActivated: function() { - applySensitivityLabel.labelId = currentValue - applySensitivityLabel.start() + applySensitivityLabel.start(currentValue) } } } @@ -196,7 +209,12 @@ ApplicationWindow { model: retentionLabelsModel textRole: "name" valueRole: "id" + Layout.fillWidth: true + + onActivated: function() { + applyRetentionLabel.start(currentValue) + } } } @@ -219,7 +237,12 @@ ApplicationWindow { model: legalHoldLabelsModel textRole: "name" valueRole: "id" + Layout.fillWidth: true + + onActivated: function() { + applyLegalHoldLabel.start(currentValue) + } } } diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp index 62b6057af7ade..a620fc253b04b 100644 --- a/src/gui/governance/applygovernancelabel.cpp +++ b/src/gui/governance/applygovernancelabel.cpp @@ -15,13 +15,17 @@ 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::classBegin() +{ +} + +void ApplyGovernanceLabel::componentComplete() +{ + if (checkParameters()) { + start(); + } } void ApplyGovernanceLabel::start() @@ -44,6 +48,13 @@ void ApplyGovernanceLabel::start() ocsGovernanceJob()->start(); } +void ApplyGovernanceLabel::start(const QString &labelId) +{ + setLabelId(labelId); + + start(); +} + QString ApplyGovernanceLabel::buildPath() const { return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString(), labelTypeAsString(TypedGovernanceNetworkJob::Capitalization::UpCase), labelId()); @@ -54,11 +65,4 @@ void ApplyGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int sta 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 index af0890a637d67..e955302c1a936 100644 --- a/src/gui/governance/applygovernancelabel.h +++ b/src/gui/governance/applygovernancelabel.h @@ -19,19 +19,25 @@ class ApplyGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob { Q_OBJECT QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) + public: explicit ApplyGovernanceLabel(QObject *parent = nullptr); + void classBegin() override; + + void componentComplete() override; + public Q_SLOTS: void start(); + void start(const QString &labelId); + protected: [[nodiscard]] QString buildPath() const override; private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); - - void initialize(); }; } // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp index ee221f277a8d6..0b3b58d846148 100644 --- a/src/gui/governance/deletegovernancelabel.cpp +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -15,13 +15,17 @@ 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::classBegin() +{ +} + +void DeleteGovernanceLabel::componentComplete() +{ + if (checkParameters()) { + start(); + } } void DeleteGovernanceLabel::start() @@ -44,6 +48,13 @@ void DeleteGovernanceLabel::start() ocsGovernanceJob()->start(); } +void DeleteGovernanceLabel::start(const QString &labelId) +{ + setLabelId(labelId); + + start(); +} + QString DeleteGovernanceLabel::buildPath() const { return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString(), labelTypeAsString(TypedGovernanceNetworkJob::Capitalization::UpCase), labelId()); @@ -54,11 +65,4 @@ void DeleteGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int st 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 index c4be9bef39261..a19cb1612e0e6 100644 --- a/src/gui/governance/deletegovernancelabel.h +++ b/src/gui/governance/deletegovernancelabel.h @@ -19,19 +19,25 @@ class DeleteGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob { Q_OBJECT QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) + public: explicit DeleteGovernanceLabel(QObject *parent = nullptr); + void classBegin() override; + + void componentComplete() override; + public Q_SLOTS: void start(); + void start(const QString &labelId); + protected: [[nodiscard]] QString buildPath() const override; private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); - - void initialize(); }; } // namespace OCC diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp index be4dd20cc5b62..f61aaa74bf645 100644 --- a/src/gui/governance/getavailablegovernancelabels.cpp +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -15,12 +15,17 @@ 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::classBegin() +{ +} + +void GetAvailableGovernanceLabels::componentComplete() +{ + if (checkParameters()) { + start(); + } } void GetAvailableGovernanceLabels::start(Governance::LabelType labelType, const QString &entityId) @@ -56,13 +61,6 @@ void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] 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(TypedGovernanceNetworkJob::Capitalization::LowCase)); diff --git a/src/gui/governance/getavailablegovernancelabels.h b/src/gui/governance/getavailablegovernancelabels.h index 9bbf0242bffce..b5db8559e3067 100644 --- a/src/gui/governance/getavailablegovernancelabels.h +++ b/src/gui/governance/getavailablegovernancelabels.h @@ -19,10 +19,15 @@ class GetAvailableGovernanceLabels : public OCC::TypedGovernanceNetworkJob { Q_OBJECT QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) public: explicit GetAvailableGovernanceLabels(QObject *parent = nullptr); + void classBegin() override; + + void componentComplete() override; + Q_SIGNALS: public Q_SLOTS: @@ -35,8 +40,6 @@ public Q_SLOTS: private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); - - void initialize(); }; } // namespace OCC diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp index 2b6da65941a41..4933722becb7c 100644 --- a/src/gui/governance/getgovernancelabels.cpp +++ b/src/gui/governance/getgovernancelabels.cpp @@ -15,11 +15,17 @@ 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::classBegin() +{ +} + +void GetGovernanceLabels::componentComplete() +{ + if (checkParameters()) { + start(); + } } void GetGovernanceLabels::start() @@ -42,6 +48,12 @@ void GetGovernanceLabels::start() ocsGovernanceJob()->start(); } +void GetGovernanceLabels::start(const QString &entityId) +{ + setEntityId(entityId); + start(); +} + QString GetGovernanceLabels::buildPath() const { return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString()); @@ -52,11 +64,4 @@ void GetGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int stat 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 index 1a32ea4288b0a..6c6d6f5ba664a 100644 --- a/src/gui/governance/getgovernancelabels.h +++ b/src/gui/governance/getgovernancelabels.h @@ -19,19 +19,25 @@ class GetGovernanceLabels : public OCC::GovernanceNetworkJob { Q_OBJECT QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) + public: explicit GetGovernanceLabels(QObject *parent = nullptr); + void classBegin() override; + + void componentComplete() override; + public Q_SLOTS: void start(); + void start(const QString &entityId); + protected: [[nodiscard]] QString buildPath() const override; private Q_SLOTS: void jobDone(QJsonDocument reply, int statusCode); - - void initialize(); }; } // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h index 38d8a2cb34c14..8674450a20bee 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -21,10 +22,11 @@ Q_DECLARE_LOGGING_CATEGORY(lcGovernanceNetwork) class OcsGovernanceJob; -class GovernanceNetworkJob : public QObject +class GovernanceNetworkJob : public QObject, public QQmlParserStatus { Q_OBJECT QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(AccountPtr account READ account WRITE setAccount NOTIFY accountChanged FINAL) diff --git a/src/gui/governance/governancetypes.h b/src/gui/governance/governancetypes.h index b95d5eae8b530..35775b96e9738 100644 --- a/src/gui/governance/governancetypes.h +++ b/src/gui/governance/governancetypes.h @@ -25,7 +25,7 @@ Q_ENUM_NS(EntityType) enum class LabelType { Sensitivity, Retention, - Hold, + LegalHold, InvalidLabelType, }; diff --git a/src/gui/governance/typedgovernancenetworkjob.cpp b/src/gui/governance/typedgovernancenetworkjob.cpp index e19b2cd886ef7..7ac3ce6be5240 100644 --- a/src/gui/governance/typedgovernancenetworkjob.cpp +++ b/src/gui/governance/typedgovernancenetworkjob.cpp @@ -45,7 +45,7 @@ QString TypedGovernanceNetworkJob::labelTypeAsString(Capitalization capitalizati case Governance::LabelType::Retention: result = u"retention"_s; break; - case Governance::LabelType::Hold: + case Governance::LabelType::LegalHold: result = u"hold"_s; break; case Governance::LabelType::InvalidLabelType: @@ -62,7 +62,7 @@ QString TypedGovernanceNetworkJob::labelTypeAsString(Capitalization capitalizati case Governance::LabelType::Retention: result = u"RETENTION"_s; break; - case Governance::LabelType::Hold: + case Governance::LabelType::LegalHold: result = u"HOLD"_s; break; case Governance::LabelType::InvalidLabelType: From 9dd8e55bd1bfa4295694eaf3d20e6583494bd5ec Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 1 Jul 2026 12:54:18 +0200 Subject: [PATCH 17/26] feat(governance): manage label selection inside the model the model is responsible to provide data to the UI and manage the set of selected labels by taking UI input and delegating the modifications via emitted signals Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 60 ++++++++++++- .../governance/governancelabelslistmodel.cpp | 90 ++++++++++++++----- .../governance/governancelabelslistmodel.h | 26 ++++++ test/testgovernance.cpp | 3 +- 4 files changed, 152 insertions(+), 27 deletions(-) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index 3a8991bf5d9d9..e8adbafa4920e 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -60,10 +60,19 @@ ApplicationWindow { entityId: governanceLabelsDialog.fileId labelType: GovernanceNetworkJob.Sensitivity + labelBehavior: GovernanceLabelsListModel.UniqueLabel onRefreshData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) } + + onAddLabel: function(labelId) { + applySensitivityLabel.start(labelId) + } + + onRemoveLabel: function(labelId) { + deleteSensitivityLabel.start(labelId) + } } GetAvailableGovernanceLabels { @@ -88,15 +97,33 @@ ApplicationWindow { entityId: governanceLabelsDialog.fileId } + DeleteGovernanceLabel { + id: deleteSensitivityLabel + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Sensitivity + entityId: governanceLabelsDialog.fileId + } + GovernanceLabelsListModel { id: retentionLabelsModel entityId: governanceLabelsDialog.fileId labelType: GovernanceNetworkJob.Retention + labelBehavior: GovernanceLabelsListModel.MultipleLabels onRefreshData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) } + + onAddLabel: function(labelId) { + applyRetentionLabel.start(labelId) + } + + onRemoveLabel: function(labelId) { + deleteRetentionLabel.start(labelId) + } } GetAvailableGovernanceLabels { @@ -121,15 +148,33 @@ ApplicationWindow { entityId: governanceLabelsDialog.fileId } + DeleteGovernanceLabel { + id: deleteRetentionLabel + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.Retention + entityId: governanceLabelsDialog.fileId + } + GovernanceLabelsListModel { id: legalHoldLabelsModel entityId: governanceLabelsDialog.fileId labelType: GovernanceNetworkJob.LegalHold + labelBehavior: GovernanceLabelsListModel.MultipleLabels onRefreshData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) } + + onAddLabel: function(labelId) { + applyLegalHoldLabel.start(labelId) + } + + onRemoveLabel: function(labelId) { + deleteLegalHoldLabel.start(labelId) + } } GetAvailableGovernanceLabels { @@ -154,6 +199,15 @@ ApplicationWindow { entityId: governanceLabelsDialog.fileId } + DeleteGovernanceLabel { + id: deleteLegalHoldLabel + + account: governanceLabelsDialog.account + + labelType: GovernanceNetworkJob.LegalHold + entityId: governanceLabelsDialog.fileId + } + ColumnLayout { anchors.fill: parent anchors.leftMargin: 24 @@ -185,7 +239,7 @@ ApplicationWindow { Layout.fillWidth: true onActivated: function() { - applySensitivityLabel.start(currentValue) + sensitivityLabelsModel.toggleLabel(currentValue) } } } @@ -213,7 +267,7 @@ ApplicationWindow { Layout.fillWidth: true onActivated: function() { - applyRetentionLabel.start(currentValue) + retentionLabelsModel.toggleLabel(currentValue) } } } @@ -241,7 +295,7 @@ ApplicationWindow { Layout.fillWidth: true onActivated: function() { - applyLegalHoldLabel.start(currentValue) + legalHoldLabelsModel.toggleLabel(currentValue) } } } diff --git a/src/gui/governance/governancelabelslistmodel.cpp b/src/gui/governance/governancelabelslistmodel.cpp index 02684e37b334c..4cf1b5941652e 100644 --- a/src/gui/governance/governancelabelslistmodel.cpp +++ b/src/gui/governance/governancelabelslistmodel.cpp @@ -29,7 +29,7 @@ int GovernanceLabelsListModel::rowCount(const QModelIndex &parent) const return result; } - result = _data.count(); + result = _data.count() + 1; return result; } @@ -45,33 +45,54 @@ QVariant GovernanceLabelsListModel::data(const QModelIndex &index, int role) con return result; } - if (index.row() < 0 || index.row() >= _data.count()) { + if (index.row() < 0 || index.row() >= _data.count() + 1) { 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; + [[unlikely]] if (index.row() == _data.count()) { + switch (convertedRole) + { + case LabelsListModelRoles::IdRole: + result = -1; + break; + case LabelsListModelRoles::NameRole: + result = tr("None"); + break; + case LabelsListModelRoles::PriorityRole: + result = -1; + break; + case LabelsListModelRoles::DescriptionRole: + result = tr("No label"); + break; + case LabelsListModelRoles::ColorRole: + case LabelsListModelRoles::ScopesRole: + break; + } + } else { + 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; + } } } @@ -175,6 +196,16 @@ void GovernanceLabelsListModel::setExistingLabelsJsonData(const QJsonDocument &d qCInfo(lcGovernanceLabelsListModel()) << data.toJson(QJsonDocument::JsonFormat::Compact); } +void GovernanceLabelsListModel::toggleLabel(const QString &labelId) +{ + Q_EMIT addLabel(labelId); +} + +void GovernanceLabelsListModel::toggleLabel(const QString &labelId) +{ + Q_EMIT addLabel(labelId); +} + void OCC::GovernanceLabelsListModel::etagChanged() { Q_EMIT refreshData(_labelType, _entityId); @@ -189,4 +220,19 @@ void GovernanceLabelsListModel::emitRefreshData() Q_EMIT refreshData(_labelType, _entityId); } +GovernanceLabelsListModel::LabelBehavior GovernanceLabelsListModel::labelBehavior() const +{ + return _labelBehavior; +} + +void GovernanceLabelsListModel::setLabelBehavior(LabelBehavior newLabelBehavior) +{ + if (_labelBehavior == newLabelBehavior) { + return; + } + + _labelBehavior = newLabelBehavior; + Q_EMIT labelBehaviorChanged(); +} + } // namespace OCC diff --git a/src/gui/governance/governancelabelslistmodel.h b/src/gui/governance/governancelabelslistmodel.h index d4b20b7028b31..45f09e7ee6b76 100644 --- a/src/gui/governance/governancelabelslistmodel.h +++ b/src/gui/governance/governancelabelslistmodel.h @@ -25,6 +25,8 @@ class GovernanceLabelsListModel : public QAbstractListModel Q_PROPERTY(QString entityId READ entityId WRITE setEntityId NOTIFY entityIdChanged FINAL) + Q_PROPERTY(LabelBehavior labelBehavior READ labelBehavior WRITE setLabelBehavior NOTIFY labelBehaviorChanged FINAL) + public: enum class LabelsListModelRoles { IdRole = Qt::UserRole + 1, @@ -37,6 +39,14 @@ class GovernanceLabelsListModel : public QAbstractListModel Q_ENUM(LabelsListModelRoles) + enum class LabelBehavior { + UniqueLabel, + MultipleLabels, + UnknownLabelbehavior, + }; + + Q_ENUM(LabelBehavior) + explicit GovernanceLabelsListModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -53,11 +63,19 @@ class GovernanceLabelsListModel : public QAbstractListModel void setEntityId(const QString &newEntityId); + [[nodiscard]] LabelBehavior labelBehavior() const; + + void setLabelBehavior(LabelBehavior newLabelBehavior); + public Q_SLOTS: void setAvailableLabelsJsonData(const QJsonDocument &reply); void setExistingLabelsJsonData(const QJsonDocument &data); + void toggleLabel(const QString &labelId); + + void toggleLabel(const QString &labelId); + void etagChanged(); Q_SIGNALS: @@ -67,6 +85,12 @@ public Q_SLOTS: void refreshData(OCC::Governance::LabelType labelType, const QString &entityId); + void addLabel(const QString &labelId); + + void removeLabel(const QString &labelId); + + void labelBehaviorChanged(); + private: void emitRefreshData(); @@ -75,6 +99,8 @@ public Q_SLOTS: QString _entityId; QList _data; + + LabelBehavior _labelBehavior = LabelBehavior::UnknownLabelbehavior; }; } // namespace OCC diff --git a/test/testgovernance.cpp b/test/testgovernance.cpp index 94557c4f949cf..526192178e152 100644 --- a/test/testgovernance.cpp +++ b/test/testgovernance.cpp @@ -562,7 +562,7 @@ private slots: myModel.setAvailableLabelsJsonData(replyData); - QCOMPARE(myModel.rowCount(), 1); + QCOMPARE(myModel.rowCount(), 2); const auto labelIndex = myModel.index(0); QVERIFY(labelIndex.isValid()); QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::IdRole)), u"91785883351310337"_s); @@ -582,7 +582,6 @@ private slots: 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); From 21e468219a27b4748136a2805e76cc3fee83235b Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 7 Jul 2026 11:05:40 +0200 Subject: [PATCH 18/26] feat(governance): add or remove labels by selecting them, display state the item in the combobox have the ability to show if they are selected or not refresh state as soon as one label is selected select a selected one will remove it select an unselected one will apply it selecting none should remove all labels Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 47 ++++- src/gui/governance/governancelabelinfo.h | 8 + .../governance/governancelabelslistmodel.cpp | 165 ++++++++++++++++-- .../governance/governancelabelslistmodel.h | 9 +- src/gui/wizard/qml/WizardComboBox.qml | 3 +- 5 files changed, 207 insertions(+), 25 deletions(-) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index e8adbafa4920e..b9512c5a3c053 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -55,6 +55,20 @@ ApplicationWindow { close.accepted = true } + GetGovernanceLabels { + id: getGovernanceLabels + + account: governanceLabelsDialog.account + + entityId: governanceLabelsDialog.fileId + + onFinished: function(reply) { + sensitivityLabelsModel.setExistingLabelsJsonData(reply) + retentionLabelsModel.setExistingLabelsJsonData(reply) + legalHoldLabelsModel.setExistingLabelsJsonData(reply) + } + } + GovernanceLabelsListModel { id: sensitivityLabelsModel @@ -64,6 +78,7 @@ ApplicationWindow { onRefreshData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + getGovernanceLabels.start(entityId) } onAddLabel: function(labelId) { @@ -95,6 +110,10 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Sensitivity entityId: governanceLabelsDialog.fileId + + onFinished: function() { + sensitivityLabelsModel.labelWasModified() + } } DeleteGovernanceLabel { @@ -104,6 +123,10 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Sensitivity entityId: governanceLabelsDialog.fileId + + onFinished: function() { + sensitivityLabelsModel.labelWasModified() + } } GovernanceLabelsListModel { @@ -115,6 +138,7 @@ ApplicationWindow { onRefreshData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + getGovernanceLabels.start(entityId) } onAddLabel: function(labelId) { @@ -146,6 +170,10 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Retention entityId: governanceLabelsDialog.fileId + + onFinished: function() { + retentionLabelsModel.labelWasModified() + } } DeleteGovernanceLabel { @@ -155,6 +183,10 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Retention entityId: governanceLabelsDialog.fileId + + onFinished: function() { + retentionLabelsModel.labelWasModified() + } } GovernanceLabelsListModel { @@ -166,6 +198,7 @@ ApplicationWindow { onRefreshData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + getGovernanceLabels.start(entityId) } onAddLabel: function(labelId) { @@ -197,6 +230,10 @@ ApplicationWindow { labelType: GovernanceNetworkJob.LegalHold entityId: governanceLabelsDialog.fileId + + onFinished: function() { + legalHoldLabelsModel.labelWasModified() + } } DeleteGovernanceLabel { @@ -206,6 +243,10 @@ ApplicationWindow { labelType: GovernanceNetworkJob.LegalHold entityId: governanceLabelsDialog.fileId + + onFinished: function() { + legalHoldLabelsModel.labelWasModified() + } } ColumnLayout { @@ -238,8 +279,8 @@ ApplicationWindow { Layout.fillWidth: true - onActivated: function() { - sensitivityLabelsModel.toggleLabel(currentValue) + onActivated: function(index) { + sensitivityLabelsModel.toggleLabel(index, currentValue) } } } @@ -313,7 +354,7 @@ ApplicationWindow { } WizardButton { - text: qsTr("Cancel") + text: qsTr("Done") onClicked: Systray.destroyDialog(governanceLabelsDialog) } } diff --git a/src/gui/governance/governancelabelinfo.h b/src/gui/governance/governancelabelinfo.h index 9ee7e311792c2..7c72fc01e0a10 100644 --- a/src/gui/governance/governancelabelinfo.h +++ b/src/gui/governance/governancelabelinfo.h @@ -15,6 +15,12 @@ namespace OCC struct GovernanceLabelInfo { public: + enum class Status { + Selected, + Available, + UnknownStatus, + }; + QString _id; QString _name; @@ -26,6 +32,8 @@ struct GovernanceLabelInfo QString _color; QStringList _scopes; + + Status _status = Status::UnknownStatus; }; } // namespace OCC diff --git a/src/gui/governance/governancelabelslistmodel.cpp b/src/gui/governance/governancelabelslistmodel.cpp index 4cf1b5941652e..b08808ce00dc5 100644 --- a/src/gui/governance/governancelabelslistmodel.cpp +++ b/src/gui/governance/governancelabelslistmodel.cpp @@ -70,6 +70,9 @@ QVariant GovernanceLabelsListModel::data(const QModelIndex &index, int role) con case LabelsListModelRoles::ColorRole: case LabelsListModelRoles::ScopesRole: break; + case LabelsListModelRoles::SelectedRole: + result = false; + break; } } else { switch (convertedRole) @@ -92,6 +95,9 @@ QVariant GovernanceLabelsListModel::data(const QModelIndex &index, int role) con case LabelsListModelRoles::ScopesRole: result = _data[index.row()]._scopes; break; + case LabelsListModelRoles::SelectedRole: + result = _data[index.row()]._status == GovernanceLabelInfo::Status::Selected; + break; } } } @@ -108,6 +114,7 @@ QHash GovernanceLabelsListModel::roleNames() const {static_cast(LabelsListModelRoles::DescriptionRole), "description"_ba}, {static_cast(LabelsListModelRoles::ColorRole), "color"_ba}, {static_cast(LabelsListModelRoles::ScopesRole), "scopes"_ba}, + {static_cast(LabelsListModelRoles::SelectedRole), "isSelected"_ba}, }; return result; @@ -149,21 +156,7 @@ void GovernanceLabelsListModel::setEntityId(const QString &newEntityId) 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 dataArray = readOcsReply(reply).toArray(); const auto convertToStringList = [] (const QJsonArray &scopesList) -> QStringList { @@ -191,14 +184,120 @@ void GovernanceLabelsListModel::setAvailableLabelsJsonData(const QJsonDocument & endResetModel(); } -void GovernanceLabelsListModel::setExistingLabelsJsonData(const QJsonDocument &data) +void GovernanceLabelsListModel::setExistingLabelsJsonData(const QJsonDocument &reply) { - qCInfo(lcGovernanceLabelsListModel()) << data.toJson(QJsonDocument::JsonFormat::Compact); + const auto dataReply = readOcsReply(reply).toObject(); + qCInfo(lcGovernanceLabelsListModel()) << reply; + qCInfo(lcGovernanceLabelsListModel()) << dataReply; + + switch (_labelType) + { + case Governance::LabelType::Sensitivity: + { + if (!dataReply.contains(u"sensitivity"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "missing sensitivity data in OCS reply"; + return; + } + + const auto sensitivityLabel = dataReply[u"sensitivity"_s].toObject(); + for (auto rowIndex = 0; rowIndex < _data.size(); ++rowIndex) { + if (_data[rowIndex]._id == sensitivityLabel[u"id"_s]) { + if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Selected) { + _data[rowIndex]._status = GovernanceLabelInfo::Status::Selected; + Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); + } + } else { + if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Available) { + _data[rowIndex]._status = GovernanceLabelInfo::Status::Available; + Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); + } + } + } + + break; + } + case Governance::LabelType::Retention: + { + if (!dataReply.contains(u"retention"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "missing retention data in OCS reply"; + return; + } + + const auto retentionLabels = dataReply[u"retention"_s].toArray(); + + for (const auto &oneLabel : retentionLabels) { + const auto oneLabelObject = oneLabel.toObject(); + for (auto rowIndex = 0; rowIndex < _data.size(); ++rowIndex) { + if (_data[rowIndex]._id == oneLabelObject[u"id"_s]) { + if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Selected) { + _data[rowIndex]._status = GovernanceLabelInfo::Status::Selected; + Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); + } + } else { + if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Available) { + _data[rowIndex]._status = GovernanceLabelInfo::Status::Available; + Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); + } + } + } + } + + break; + } + case Governance::LabelType::LegalHold: + { + if (!dataReply.contains(u"hold"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "missing legal hold data in OCS reply"; + return; + } + + const auto legalHoldLabels = dataReply[u"hold"_s].toArray(); + + for (const auto &oneLabel : legalHoldLabels) { + const auto oneLabelObject = oneLabel.toObject(); + for (auto rowIndex = 0; rowIndex < _data.size(); ++rowIndex) { + if (_data[rowIndex]._id == oneLabelObject[u"id"_s]) { + if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Selected) { + _data[rowIndex]._status = GovernanceLabelInfo::Status::Selected; + Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); + } + } else { + if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Available) { + _data[rowIndex]._status = GovernanceLabelInfo::Status::Available; + Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); + } + } + } + } + + break; + } + case Governance::LabelType::InvalidLabelType: + break; + } } -void GovernanceLabelsListModel::toggleLabel(const QString &labelId) +void GovernanceLabelsListModel::toggleLabel(const int index, const QString &labelId) { - Q_EMIT addLabel(labelId); + if (_data.size() == index) { + for (const auto &oneLabel : std::as_const(_data)) { + if (oneLabel._status == GovernanceLabelInfo::Status::Selected) { + Q_EMIT removeLabel(oneLabel._id); + } + } + } else if (index <_data.size() && index >= 0) { + switch (_data[index]._status) + { + case GovernanceLabelInfo::Status::Selected: + Q_EMIT removeLabel(labelId); + break; + case GovernanceLabelInfo::Status::Available: + Q_EMIT addLabel(labelId); + break; + case GovernanceLabelInfo::Status::UnknownStatus: + break; + } + } } void GovernanceLabelsListModel::toggleLabel(const QString &labelId) @@ -211,6 +310,11 @@ void OCC::GovernanceLabelsListModel::etagChanged() Q_EMIT refreshData(_labelType, _entityId); } +void GovernanceLabelsListModel::labelWasModified() +{ + Q_EMIT refreshData(_labelType, _entityId); +} + void GovernanceLabelsListModel::emitRefreshData() { if (_entityId.isEmpty() || _labelType == Governance::LabelType::InvalidLabelType) { @@ -220,6 +324,29 @@ void GovernanceLabelsListModel::emitRefreshData() Q_EMIT refreshData(_labelType, _entityId); } +QJsonValue GovernanceLabelsListModel::readOcsReply(const QJsonDocument &reply) +{ + auto result = QJsonValue{}; + + const auto replyObject = reply.object(); + + if (!replyObject.contains(u"ocs"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "wrong format for reply" << reply.toJson(QJsonDocument::JsonFormat::Compact); + return result; + } + + const auto ocsObject = replyObject.value(u"ocs"_s).toObject(); + + if (!ocsObject.contains(u"data"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "wrong format for reply" << ocsObject; + return result; + } + + result = ocsObject.value(u"data"_s); + + return result; +} + GovernanceLabelsListModel::LabelBehavior GovernanceLabelsListModel::labelBehavior() const { return _labelBehavior; diff --git a/src/gui/governance/governancelabelslistmodel.h b/src/gui/governance/governancelabelslistmodel.h index 45f09e7ee6b76..c98a8abf0ff75 100644 --- a/src/gui/governance/governancelabelslistmodel.h +++ b/src/gui/governance/governancelabelslistmodel.h @@ -35,6 +35,7 @@ class GovernanceLabelsListModel : public QAbstractListModel DescriptionRole, ColorRole, ScopesRole, + SelectedRole, }; Q_ENUM(LabelsListModelRoles) @@ -70,14 +71,16 @@ class GovernanceLabelsListModel : public QAbstractListModel public Q_SLOTS: void setAvailableLabelsJsonData(const QJsonDocument &reply); - void setExistingLabelsJsonData(const QJsonDocument &data); + void setExistingLabelsJsonData(const QJsonDocument &reply); - void toggleLabel(const QString &labelId); + void toggleLabel(int index, const QString &labelId); void toggleLabel(const QString &labelId); void etagChanged(); + void labelWasModified(); + Q_SIGNALS: void labelTypeChanged(); @@ -94,6 +97,8 @@ public Q_SLOTS: private: void emitRefreshData(); + QJsonValue readOcsReply(const QJsonDocument &reply); + Governance::LabelType _labelType = Governance::LabelType::InvalidLabelType; QString _entityId; diff --git a/src/gui/wizard/qml/WizardComboBox.qml b/src/gui/wizard/qml/WizardComboBox.qml index fde303970c911..903f04fcc50bf 100644 --- a/src/gui/wizard/qml/WizardComboBox.qml +++ b/src/gui/wizard/qml/WizardComboBox.qml @@ -52,7 +52,7 @@ BasicControls.ComboBox { : Style.wizardFieldBorder } - delegate: BasicControls.ItemDelegate { + delegate: BasicControls.CheckDelegate { id: delegateItem required property int index @@ -61,6 +61,7 @@ BasicControls.ComboBox { width: ListView.view ? ListView.view.width : root.width - 8 height: Style.standardPrimaryButtonHeight highlighted: root.highlightedIndex === index + checkState: model.isSelected ? Qt.Checked : Qt.Unchecked contentItem: Text { text: delegateItem.model.name From 085bf5facfbf81c332e670587632ef7fb770ffc1 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 9 Jul 2026 13:44:28 +0200 Subject: [PATCH 19/26] feat(governance): display network errors when using governance labels Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 97 +++++++++++++++++++ src/gui/governance/applygovernancelabel.cpp | 1 + src/gui/governance/deletegovernancelabel.cpp | 1 + .../getavailablegovernancelabels.cpp | 1 + src/gui/governance/getgovernancelabels.cpp | 1 + src/gui/governance/governancenetworkjob.h | 2 + 6 files changed, 103 insertions(+) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index b9512c5a3c053..aee2fda822479 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -23,6 +23,17 @@ ApplicationWindow { readonly property color primaryTextColor: Style.wizardPrimaryText readonly property color hintTextColor: Style.wizardSecondaryText + readonly property color networkErrorTextColor: Style.wizardErrorText + + property string lastError: '' + + function displayError(networkError) { + lastError = networkError + } + + function clearError() { + lastError = '' + } flags: Qt.Window | Qt.Dialog visible: true @@ -62,11 +73,19 @@ ApplicationWindow { entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function(reply) { sensitivityLabelsModel.setExistingLabelsJsonData(reply) retentionLabelsModel.setExistingLabelsJsonData(reply) legalHoldLabelsModel.setExistingLabelsJsonData(reply) } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } GovernanceLabelsListModel { @@ -98,9 +117,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Sensitivity entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function(reply) { sensitivityLabelsModel.setAvailableLabelsJsonData(reply) } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } ApplyGovernanceLabel { @@ -111,9 +138,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Sensitivity entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function() { sensitivityLabelsModel.labelWasModified() } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } DeleteGovernanceLabel { @@ -124,9 +159,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Sensitivity entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function() { sensitivityLabelsModel.labelWasModified() } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } GovernanceLabelsListModel { @@ -158,9 +201,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Retention entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function(reply) { retentionLabelsModel.setAvailableLabelsJsonData(reply) } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } ApplyGovernanceLabel { @@ -171,9 +222,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Retention entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function() { retentionLabelsModel.labelWasModified() } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } DeleteGovernanceLabel { @@ -184,9 +243,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Retention entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function() { retentionLabelsModel.labelWasModified() } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } GovernanceLabelsListModel { @@ -218,9 +285,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.LegalHold entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function(reply) { legalHoldLabelsModel.setAvailableLabelsJsonData(reply) } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } ApplyGovernanceLabel { @@ -231,9 +306,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.LegalHold entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function() { legalHoldLabelsModel.labelWasModified() } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } DeleteGovernanceLabel { @@ -244,9 +327,17 @@ ApplicationWindow { labelType: GovernanceNetworkJob.LegalHold entityId: governanceLabelsDialog.fileId + onStarted: function() { + clearError() + } + onFinished: function() { legalHoldLabelsModel.labelWasModified() } + + onFinishedWithError: function(errorCode, errorMessage) { + displayError(errorMessage) + } } ColumnLayout { @@ -257,6 +348,12 @@ ApplicationWindow { anchors.topMargin: 24 spacing: 14 + EnforcedPlainTextLabel { + text: lastError + color: governanceLabelsDialog.networkErrorTextColor + font.pixelSize: Style.pixelSize + } + RowLayout { Layout.fillWidth: true spacing: 8 diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp index a620fc253b04b..3c3c5c1e5001c 100644 --- a/src/gui/governance/applygovernancelabel.cpp +++ b/src/gui/governance/applygovernancelabel.cpp @@ -46,6 +46,7 @@ void ApplyGovernanceLabel::start() ocsGovernanceJob()->setMethod("POST"); ocsGovernanceJob()->start(); + Q_EMIT started(); } void ApplyGovernanceLabel::start(const QString &labelId) diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp index 0b3b58d846148..b56d633104a9e 100644 --- a/src/gui/governance/deletegovernancelabel.cpp +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -46,6 +46,7 @@ void DeleteGovernanceLabel::start() ocsGovernanceJob()->setMethod("DELETE"); ocsGovernanceJob()->start(); + Q_EMIT started(); } void DeleteGovernanceLabel::start(const QString &labelId) diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp index f61aaa74bf645..8e681423a5f86 100644 --- a/src/gui/governance/getavailablegovernancelabels.cpp +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -54,6 +54,7 @@ void GetAvailableGovernanceLabels::start() ocsGovernanceJob()->setMethod("GET"); ocsGovernanceJob()->start(); + Q_EMIT started(); } void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp index 4933722becb7c..66f2f0700e622 100644 --- a/src/gui/governance/getgovernancelabels.cpp +++ b/src/gui/governance/getgovernancelabels.cpp @@ -46,6 +46,7 @@ void GetGovernanceLabels::start() ocsGovernanceJob()->setMethod("GET"); ocsGovernanceJob()->start(); + Q_EMIT started(); } void GetGovernanceLabels::start(const QString &entityId) diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h index 8674450a20bee..1e9200d0a4a5c 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -70,6 +70,8 @@ class GovernanceNetworkJob : public QObject, public QQmlParserStatus void entityIdChanged(); + void started(); + void finished(QJsonDocument reply); void finishedWithError(int errorCode, const QString &errorMessage); From c172efae1e67f4cf29657b05de03d54fdf971aee Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 9 Jul 2026 15:08:38 +0200 Subject: [PATCH 20/26] feat(governance): apply design review on the new dialog Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 116 +++++++++++++---------------- theme/Style/Style.qml | 3 + 2 files changed, 55 insertions(+), 64 deletions(-) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index aee2fda822479..3b5f24c280e47 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -41,10 +41,11 @@ ApplicationWindow { LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true - width: Style.minimumWidthResolveConflictsDialog - height: Style.minimumHeightResolveConflictsDialog - minimumWidth: Style.minimumWidthResolveConflictsDialog - minimumHeight: Style.minimumHeightResolveConflictsDialog + minimumWidth: mainContent.implicitWidth + minimumHeight: mainContent.implicitHeight + 40 + width: Style.defaultWidthGovernanceLabelsDialog + height: Style.defaultHeightGovernanceLabelsDialog + title: qsTr("Apply labels") color: Style.wizardWindowBackground @@ -341,6 +342,8 @@ ApplicationWindow { } ColumnLayout { + id: mainContent + anchors.fill: parent anchors.leftMargin: 24 anchors.rightMargin: 24 @@ -354,87 +357,72 @@ ApplicationWindow { font.pixelSize: Style.pixelSize } - RowLayout { - Layout.fillWidth: true - spacing: 8 - - EnforcedPlainTextLabel { - text: qsTr("Sensitivity:") - color: governanceLabelsDialog.hintTextColor - font.pixelSize: Style.pixelSize - } + EnforcedPlainTextLabel { + text: qsTr("Sensitivity:") + color: governanceLabelsDialog.hintTextColor + font.pixelSize: Style.pixelSize + } - WizardComboBox { - id: selectedNewSensitivityLabel + WizardComboBox { + id: selectedNewSensitivityLabel - Accessible.role: Accessible.ComboBox - Accessible.name: qsTr("Select sensitivity label") + Accessible.role: Accessible.ComboBox + Accessible.name: qsTr("Select sensitivity label") - model: sensitivityLabelsModel - textRole: "name" - valueRole: "id" + model: sensitivityLabelsModel + textRole: "name" + valueRole: "id" - Layout.fillWidth: true + Layout.fillWidth: true - onActivated: function(index) { - sensitivityLabelsModel.toggleLabel(index, currentValue) - } + onActivated: function(index) { + sensitivityLabelsModel.toggleLabel(index, currentValue) } } - RowLayout { - Layout.fillWidth: true - spacing: 8 - - EnforcedPlainTextLabel { - text: qsTr("Retention:") - color: governanceLabelsDialog.hintTextColor - font.pixelSize: Style.pixelSize - } + EnforcedPlainTextLabel { + text: qsTr("Retention:") + color: governanceLabelsDialog.hintTextColor + font.pixelSize: Style.pixelSize + } - WizardComboBox { - id: selectedNewRetentionLabel + WizardComboBox { + id: selectedNewRetentionLabel - Accessible.role: Accessible.ComboBox - Accessible.name: qsTr("Select retention label") + Accessible.role: Accessible.ComboBox + Accessible.name: qsTr("Select retention label") - model: retentionLabelsModel - textRole: "name" - valueRole: "id" + model: retentionLabelsModel + textRole: "name" + valueRole: "id" - Layout.fillWidth: true + Layout.fillWidth: true - onActivated: function() { - retentionLabelsModel.toggleLabel(currentValue) - } + onActivated: function() { + retentionLabelsModel.toggleLabel(currentValue) } } - RowLayout { - Layout.fillWidth: true - spacing: 8 - - EnforcedPlainTextLabel { - text: qsTr("Legal hold:") - color: governanceLabelsDialog.hintTextColor - font.pixelSize: Style.pixelSize - } + EnforcedPlainTextLabel { + text: qsTr("Legal hold:") + color: governanceLabelsDialog.hintTextColor + font.pixelSize: Style.pixelSize + } - WizardComboBox { - id: selectedNewLegalHoldLabel + WizardComboBox { + id: selectedNewLegalHoldLabel - Accessible.role: Accessible.ComboBox - Accessible.name: qsTr("Select legal hold label") + Accessible.role: Accessible.ComboBox + Accessible.name: qsTr("Select legal hold label") - model: legalHoldLabelsModel - textRole: "name" - valueRole: "id" + model: legalHoldLabelsModel + textRole: "name" + valueRole: "id" - Layout.fillWidth: true + Layout.fillWidth: true - onActivated: function() { - legalHoldLabelsModel.toggleLabel(currentValue) - } + onActivated: function() { + legalHoldLabelsModel.toggleLabel(currentValue) } } diff --git a/theme/Style/Style.qml b/theme/Style/Style.qml index b73d60e9a7cfb..18d6de8f41a5b 100644 --- a/theme/Style/Style.qml +++ b/theme/Style/Style.qml @@ -246,6 +246,9 @@ QtObject { readonly property int minimumWidthResolveConflictsDialog: 600 readonly property int minimumHeightResolveConflictsDialog: 300 + readonly property int defaultWidthGovernanceLabelsDialog: 400 + readonly property int defaultHeightGovernanceLabelsDialog: 300 + readonly property double smallIconScaleFactor: 0.6 readonly property double trayFolderListButtonWidthScaleFactor: 1.75 From af07f65046dd0c4f674dcf9bf15c8edfe3398401 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 9 Jul 2026 15:59:10 +0200 Subject: [PATCH 21/26] feat(governance): ensure labels model is properly populated we need to request available and selected labels in a proper sequence or we cannot guarantee that the model state is correct Signed-off-by: Matthieu Gallien --- src/gui/GovernanceLabelsDialog.qml | 17 +- .../governance/governancelabelslistmodel.cpp | 231 +++++++++--------- .../governance/governancelabelslistmodel.h | 21 +- test/testgovernance.cpp | 2 +- 4 files changed, 143 insertions(+), 128 deletions(-) diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index 3b5f24c280e47..3a560035daa58 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -96,8 +96,11 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Sensitivity labelBehavior: GovernanceLabelsListModel.UniqueLabel - onRefreshData: function(labelType, entityId) { + onRefreshAvailableLabelsData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + } + + onRefreshExistingLabelsData: function(labelType, entityId) { getGovernanceLabels.start(entityId) } @@ -180,8 +183,11 @@ ApplicationWindow { labelType: GovernanceNetworkJob.Retention labelBehavior: GovernanceLabelsListModel.MultipleLabels - onRefreshData: function(labelType, entityId) { + onRefreshAvailableLabelsData: function(labelType, entityId) { getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + } + + onRefreshExistingLabelsData: function(labelType, entityId) { getGovernanceLabels.start(entityId) } @@ -264,8 +270,11 @@ ApplicationWindow { labelType: GovernanceNetworkJob.LegalHold labelBehavior: GovernanceLabelsListModel.MultipleLabels - onRefreshData: function(labelType, entityId) { - getAvailableGovernanceLabelsForSensitivity.start(labelType, entityId) + onRefreshAvailableLabelsData: function(labelType, entityId) { + getAvailableGovernanceLabelsForLegalHold.start(labelType, entityId) + } + + onRefreshExistingLabelsData: function(labelType, entityId) { getGovernanceLabels.start(entityId) } diff --git a/src/gui/governance/governancelabelslistmodel.cpp b/src/gui/governance/governancelabelslistmodel.cpp index b08808ce00dc5..0baf28ab6fba9 100644 --- a/src/gui/governance/governancelabelslistmodel.cpp +++ b/src/gui/governance/governancelabelslistmodel.cpp @@ -156,124 +156,19 @@ void GovernanceLabelsListModel::setEntityId(const QString &newEntityId) void GovernanceLabelsListModel::setAvailableLabelsJsonData(const QJsonDocument &reply) { - const auto dataArray = readOcsReply(reply).toArray(); + _availableLabelsReply = reply.object(); - 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()) - ); + if (hasReceivedAllData()) { + refreshModel(); } - endResetModel(); } void GovernanceLabelsListModel::setExistingLabelsJsonData(const QJsonDocument &reply) { - const auto dataReply = readOcsReply(reply).toObject(); - qCInfo(lcGovernanceLabelsListModel()) << reply; - qCInfo(lcGovernanceLabelsListModel()) << dataReply; - - switch (_labelType) - { - case Governance::LabelType::Sensitivity: - { - if (!dataReply.contains(u"sensitivity"_s)) { - qCWarning(lcGovernanceLabelsListModel()) << "missing sensitivity data in OCS reply"; - return; - } - - const auto sensitivityLabel = dataReply[u"sensitivity"_s].toObject(); - for (auto rowIndex = 0; rowIndex < _data.size(); ++rowIndex) { - if (_data[rowIndex]._id == sensitivityLabel[u"id"_s]) { - if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Selected) { - _data[rowIndex]._status = GovernanceLabelInfo::Status::Selected; - Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); - } - } else { - if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Available) { - _data[rowIndex]._status = GovernanceLabelInfo::Status::Available; - Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); - } - } - } - - break; - } - case Governance::LabelType::Retention: - { - if (!dataReply.contains(u"retention"_s)) { - qCWarning(lcGovernanceLabelsListModel()) << "missing retention data in OCS reply"; - return; - } - - const auto retentionLabels = dataReply[u"retention"_s].toArray(); - - for (const auto &oneLabel : retentionLabels) { - const auto oneLabelObject = oneLabel.toObject(); - for (auto rowIndex = 0; rowIndex < _data.size(); ++rowIndex) { - if (_data[rowIndex]._id == oneLabelObject[u"id"_s]) { - if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Selected) { - _data[rowIndex]._status = GovernanceLabelInfo::Status::Selected; - Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); - } - } else { - if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Available) { - _data[rowIndex]._status = GovernanceLabelInfo::Status::Available; - Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); - } - } - } - } - - break; - } - case Governance::LabelType::LegalHold: - { - if (!dataReply.contains(u"hold"_s)) { - qCWarning(lcGovernanceLabelsListModel()) << "missing legal hold data in OCS reply"; - return; - } - - const auto legalHoldLabels = dataReply[u"hold"_s].toArray(); - - for (const auto &oneLabel : legalHoldLabels) { - const auto oneLabelObject = oneLabel.toObject(); - for (auto rowIndex = 0; rowIndex < _data.size(); ++rowIndex) { - if (_data[rowIndex]._id == oneLabelObject[u"id"_s]) { - if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Selected) { - _data[rowIndex]._status = GovernanceLabelInfo::Status::Selected; - Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); - } - } else { - if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Available) { - _data[rowIndex]._status = GovernanceLabelInfo::Status::Available; - Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); - } - } - } - } + _existingLabelsReply = reply.object(); - break; - } - case Governance::LabelType::InvalidLabelType: - break; + if (hasReceivedAllData()) { + refreshModel(); } } @@ -307,12 +202,12 @@ void GovernanceLabelsListModel::toggleLabel(const QString &labelId) void OCC::GovernanceLabelsListModel::etagChanged() { - Q_EMIT refreshData(_labelType, _entityId); + emitRefreshData(); } void GovernanceLabelsListModel::labelWasModified() { - Q_EMIT refreshData(_labelType, _entityId); + emitRefreshData(); } void GovernanceLabelsListModel::emitRefreshData() @@ -321,17 +216,18 @@ void GovernanceLabelsListModel::emitRefreshData() return; } - Q_EMIT refreshData(_labelType, _entityId); + _availableLabelsReply = {}; + _existingLabelsReply = {}; + Q_EMIT refreshAvailableLabelsData(_labelType, _entityId); + Q_EMIT refreshExistingLabelsData(_entityId); } -QJsonValue GovernanceLabelsListModel::readOcsReply(const QJsonDocument &reply) +QJsonValue GovernanceLabelsListModel::readOcsReply(const QJsonObject &replyObject) { auto result = QJsonValue{}; - const auto replyObject = reply.object(); - if (!replyObject.contains(u"ocs"_s)) { - qCWarning(lcGovernanceLabelsListModel()) << "wrong format for reply" << reply.toJson(QJsonDocument::JsonFormat::Compact); + qCWarning(lcGovernanceLabelsListModel()) << "wrong format for reply" << replyObject; return result; } @@ -347,6 +243,105 @@ QJsonValue GovernanceLabelsListModel::readOcsReply(const QJsonDocument &reply) return result; } +bool GovernanceLabelsListModel::hasReceivedAllData() const +{ + return _availableLabelsReply.contains(u"ocs"_s) && _existingLabelsReply.contains(u"ocs"_s); +} + +void GovernanceLabelsListModel::refreshModel() +{ + const auto dataArray = readOcsReply(_availableLabelsReply).toArray(); + const auto dataReply = readOcsReply(_existingLabelsReply).toObject(); + + const auto convertToStringList = [] (const QJsonArray &scopesList) -> QStringList + { + auto result = QStringList{}; + + for (const auto &oneScope : scopesList) { + result << oneScope.toString(); + } + + return result; + }; + + const auto parseExistingSingleLabel = [this] (const int rowIndex, const QJsonObject &oneLabel) -> void{ + if (_data[rowIndex]._id == oneLabel[u"id"_s]) { + if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Selected) { + _data[rowIndex]._status = GovernanceLabelInfo::Status::Selected; + Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); + } + } else { + if (_data[rowIndex]._status != GovernanceLabelInfo::Status::Available) { + _data[rowIndex]._status = GovernanceLabelInfo::Status::Available; + Q_EMIT dataChanged(index(rowIndex), index(rowIndex)); + } + } + }; + + beginResetModel(); + _data.clear(); + for (const auto oneLabel : dataArray) { + const auto rowIndex = _data.size(); + 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()) + ); + + switch (_labelType) + { + case Governance::LabelType::Sensitivity: + { + if (!dataReply.contains(u"sensitivity"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "missing sensitivity data in OCS reply"; + break; + } + + const auto &sensitivityLabel = dataReply[u"sensitivity"_s].toObject(); + + parseExistingSingleLabel(rowIndex, sensitivityLabel); + break; + } + case Governance::LabelType::Retention: + { + if (!dataReply.contains(u"retention"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "missing retention data in OCS reply"; + break; + } + + const auto retentionLabels = dataReply[u"retention"_s].toArray(); + + for (const auto &oneLabel : retentionLabels) { + const auto &oneLabelObject = oneLabel.toObject(); + parseExistingSingleLabel(rowIndex, oneLabelObject); + } + break; + } + case Governance::LabelType::LegalHold: + { + if (!dataReply.contains(u"hold"_s)) { + qCWarning(lcGovernanceLabelsListModel()) << "missing legal hold data in OCS reply"; + break; + } + + const auto legalHoldLabels = dataReply[u"hold"_s].toArray(); + + for (const auto &oneLabel : legalHoldLabels) { + const auto &oneLabelObject = oneLabel.toObject(); + parseExistingSingleLabel(rowIndex, oneLabelObject); + } + break; + } + case Governance::LabelType::InvalidLabelType: + break; + } + } + endResetModel(); +} + GovernanceLabelsListModel::LabelBehavior GovernanceLabelsListModel::labelBehavior() const { return _labelBehavior; diff --git a/src/gui/governance/governancelabelslistmodel.h b/src/gui/governance/governancelabelslistmodel.h index c98a8abf0ff75..a78d93c487e83 100644 --- a/src/gui/governance/governancelabelslistmodel.h +++ b/src/gui/governance/governancelabelslistmodel.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace OCC { @@ -50,11 +51,11 @@ class GovernanceLabelsListModel : public QAbstractListModel explicit GovernanceLabelsListModel(QObject *parent = nullptr); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; + [[nodiscard]] QHash roleNames() const override; [[nodiscard]] Governance::LabelType labelType() const; @@ -86,7 +87,9 @@ public Q_SLOTS: void entityIdChanged(); - void refreshData(OCC::Governance::LabelType labelType, const QString &entityId); + void refreshAvailableLabelsData(OCC::Governance::LabelType labelType, const QString &entityId); + + void refreshExistingLabelsData(const QString &entityId); void addLabel(const QString &labelId); @@ -97,7 +100,11 @@ public Q_SLOTS: private: void emitRefreshData(); - QJsonValue readOcsReply(const QJsonDocument &reply); + static QJsonValue readOcsReply(const QJsonObject &reply); + + [[nodiscard]] bool hasReceivedAllData() const; + + void refreshModel(); Governance::LabelType _labelType = Governance::LabelType::InvalidLabelType; @@ -106,6 +113,10 @@ public Q_SLOTS: QList _data; LabelBehavior _labelBehavior = LabelBehavior::UnknownLabelbehavior; + + QJsonObject _availableLabelsReply; + + QJsonObject _existingLabelsReply; }; } // namespace OCC diff --git a/test/testgovernance.cpp b/test/testgovernance.cpp index 526192178e152..d10018b322932 100644 --- a/test/testgovernance.cpp +++ b/test/testgovernance.cpp @@ -577,7 +577,7 @@ private slots: { GovernanceLabelsListModel myModel; QAbstractItemModelTester myModelTester(&myModel); - QSignalSpy modelRefreshDataSignalSpy(&myModel, &GovernanceLabelsListModel::refreshData); + QSignalSpy modelRefreshDataSignalSpy(&myModel, &GovernanceLabelsListModel::refreshAvailableLabelsData); myModel.setEntityId(u"117"_s); myModel.setLabelType(Governance::LabelType::Sensitivity); From 98d7296a5f32ab543af7bb9b1b86bafa7eef3ef0 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 9 Jul 2026 16:00:16 +0200 Subject: [PATCH 22/26] fix: improve logging of network requests Signed-off-by: Matthieu Gallien --- src/libsync/accessmanager.cpp | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/libsync/accessmanager.cpp b/src/libsync/accessmanager.cpp index 0ffa3f50bb13b..08a1dc91702d3 100644 --- a/src/libsync/accessmanager.cpp +++ b/src/libsync/accessmanager.cpp @@ -50,6 +50,35 @@ QByteArray AccessManager::generateRequestId() QNetworkReply *AccessManager::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) { + const auto opToText = [] (QNetworkAccessManager::Operation op) -> QByteArray { + auto result = QByteArray{}; + switch (op) { + case QNetworkAccessManager::HeadOperation: + result = "HEAD"; + break; + case QNetworkAccessManager::GetOperation: + result = "GET"; + break; + case QNetworkAccessManager::PutOperation: + result = "PUT"; + break; + case QNetworkAccessManager::PostOperation: + result = "POST"; + break; + case QNetworkAccessManager::DeleteOperation: + result = "DELETE"; + break; + case QNetworkAccessManager::CustomOperation: + result = "CUSTOM"; + break; + case QNetworkAccessManager::UnknownOperation: + result = "UNKNOWN"; + break; + }; + + return result; + }; + QNetworkRequest newRequest(request); // Respect request specific user agent if any @@ -69,7 +98,7 @@ QNetworkReply *AccessManager::createRequest(QNetworkAccessManager::Operation op, // Generate a new request id QByteArray requestId = generateRequestId(); - qInfo(lcAccessManager) << op << verb << newRequest.url().toString() << "has X-Request-ID" << requestId; + qInfo(lcAccessManager) << opToText(op) << verb << newRequest.url().toString() << "has X-Request-ID" << requestId; newRequest.setRawHeader("X-Request-ID", requestId); if (newRequest.url().scheme() == "https") { // Not for "http": QTBUG-61397 From 01c8ec3e72b09bd86f33a24beb555aefd5f6e012 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 9 Jul 2026 18:12:13 +0200 Subject: [PATCH 23/26] feat(governance): add icons before Sensitivity and Retention labels Add mdiSecurity and mdiFileClockOutline SVG icons from Material Design Authors (Apache-2.0) to the theme folder and display them inline before the Sensitivity and Retention section labels in GovernanceLabelsDialog. Assisted-by: ClaudeCode:claude-sonnet-4-6 Signed-off-by: Matthieu Gallien --- REUSE.toml | 6 ++++ src/gui/GovernanceLabelsDialog.qml | 44 ++++++++++++++++++++++++------ theme.qrc.in | 2 ++ theme/file-clock-outline.svg | 1 + theme/security.svg | 1 + 5 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 theme/file-clock-outline.svg create mode 100644 theme/security.svg diff --git a/REUSE.toml b/REUSE.toml index 96fb81fdf1d13..016babf5c6f1c 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -125,6 +125,12 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2018-2025 Google LLC" SPDX-License-Identifier = "Apache-2.0" +[[annotations]] +path = ["theme/security.svg", "theme/file-clock-outline.svg"] +precedence = "aggregate" +SPDX-FileCopyrightText = "Material Design Authors" +SPDX-License-Identifier = "Apache-2.0" + [[annotations]] path = [ "Nextcloud Desktop Client.xcworkspace/**", diff --git a/src/gui/GovernanceLabelsDialog.qml b/src/gui/GovernanceLabelsDialog.qml index 3a560035daa58..27883243fd7a6 100644 --- a/src/gui/GovernanceLabelsDialog.qml +++ b/src/gui/GovernanceLabelsDialog.qml @@ -366,10 +366,24 @@ ApplicationWindow { font.pixelSize: Style.pixelSize } - EnforcedPlainTextLabel { - text: qsTr("Sensitivity:") - color: governanceLabelsDialog.hintTextColor - font.pixelSize: Style.pixelSize + RowLayout { + spacing: 4 + + Image { + source: "image://svgimage-custom-color/security.svg/" + governanceLabelsDialog.hintTextColor + sourceSize.width: Style.smallIconSize + sourceSize.height: Style.smallIconSize + fillMode: Image.PreserveAspectFit + Layout.alignment: Qt.AlignVCenter + opacity: governanceLabelsDialog.hintTextColor.a + } + + EnforcedPlainTextLabel { + text: qsTr("Sensitivity:") + color: governanceLabelsDialog.hintTextColor + font.pixelSize: Style.pixelSize + Layout.alignment: Qt.AlignVCenter + } } WizardComboBox { @@ -389,10 +403,24 @@ ApplicationWindow { } } - EnforcedPlainTextLabel { - text: qsTr("Retention:") - color: governanceLabelsDialog.hintTextColor - font.pixelSize: Style.pixelSize + RowLayout { + spacing: 4 + + Image { + source: "image://svgimage-custom-color/file-clock-outline.svg/" + governanceLabelsDialog.hintTextColor + sourceSize.width: Style.smallIconSize + sourceSize.height: Style.smallIconSize + fillMode: Image.PreserveAspectFit + Layout.alignment: Qt.AlignVCenter + opacity: governanceLabelsDialog.hintTextColor.a + } + + EnforcedPlainTextLabel { + text: qsTr("Retention:") + color: governanceLabelsDialog.hintTextColor + font.pixelSize: Style.pixelSize + Layout.alignment: Qt.AlignVCenter + } } WizardComboBox { diff --git a/theme.qrc.in b/theme.qrc.in index f3c3259bc455d..501cae84d3d83 100644 --- a/theme.qrc.in +++ b/theme.qrc.in @@ -297,8 +297,10 @@ theme/chevron-double-up.svg @CALL_NOTIFICATION_WAV_FILE_ENTRY@ theme/info.svg + theme/file-clock-outline.svg theme/file-open.svg theme/backup.svg theme/convert_to_text.svg + theme/security.svg diff --git a/theme/file-clock-outline.svg b/theme/file-clock-outline.svg new file mode 100644 index 0000000000000..e794eeb6322fe --- /dev/null +++ b/theme/file-clock-outline.svg @@ -0,0 +1 @@ + diff --git a/theme/security.svg b/theme/security.svg new file mode 100644 index 0000000000000..3313ff8cf6b90 --- /dev/null +++ b/theme/security.svg @@ -0,0 +1 @@ + From 00e710fb9eb77cce53504e7172ee561e18c999e6 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 9 Jul 2026 15:59:10 +0200 Subject: [PATCH 24/26] feat(governance): ensure labels model is properly populated we need to request available and selected labels in a proper sequence or we cannot guarantee that the model state is correct Signed-off-by: Matthieu Gallien --- test/testgovernance.cpp | 48 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/test/testgovernance.cpp b/test/testgovernance.cpp index d10018b322932..c910ac72d713e 100644 --- a/test/testgovernance.cpp +++ b/test/testgovernance.cpp @@ -534,7 +534,7 @@ private slots: GovernanceLabelsListModel myModel; QAbstractItemModelTester myModelTester(&myModel); - const auto replyJson = R"json( + const auto availableReplyJson = R"json( { "ocs": { "meta": { @@ -557,10 +557,51 @@ private slots: } } )json"_ba; + const auto availableReplyData = QJsonDocument::fromJson(availableReplyJson); - const auto replyData = QJsonDocument::fromJson(replyJson); + const auto existingReplyJson = R"json( +{ + "ocs": { + "meta": { + "status": "string", + "statuscode": 0, + "message": "string", + "totalitems": "string", + "itemsperpage": "string" + }, + "data": { + "sensitivity": { + "id": "91785883351310337", + "name": "Test Sensitivity", + "priority": 0, + "description": "This is a long description", + "color": "bf4040", + "canRemove": "yes", + "canAssign": "yes", + "isAssigned": true + }, + "retention": [ + { + "id": "string", + "name": "string", + "priority": 0, + "description": "string", + "color": "string", + "canRemove": "yes", + "canAssign": "yes", + "isAssigned": true + } + ] + } + } +} +)json"_ba; + const auto existingReplyData = QJsonDocument::fromJson(existingReplyJson); + + myModel.setAvailableLabelsJsonData(availableReplyData); + QCOMPARE(myModel.rowCount(), 1); - myModel.setAvailableLabelsJsonData(replyData); + myModel.setExistingLabelsJsonData(existingReplyData); QCOMPARE(myModel.rowCount(), 2); const auto labelIndex = myModel.index(0); @@ -571,6 +612,7 @@ private slots: 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}); + QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::SelectedRole)).toBool(), true); } void testGovernanceLabelListModel_refreshData() From 1b87098e8724a5982fe84853a213c41d8f5b5f16 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Fri, 10 Jul 2026 12:22:39 +0200 Subject: [PATCH 25/26] feat(governance): apply changes from API updated development state the reply from OCS API calls of governance app have small changes needing an update of our code to be aligned and up to date Signed-off-by: Matthieu Gallien --- src/gui/governance/governancelabelinfo.h | 2 - .../governance/governancelabelslistmodel.cpp | 57 ++++++------ .../governance/governancelabelslistmodel.h | 3 +- test/testgovernance.cpp | 87 ++++++++++++------- 4 files changed, 85 insertions(+), 64 deletions(-) diff --git a/src/gui/governance/governancelabelinfo.h b/src/gui/governance/governancelabelinfo.h index 7c72fc01e0a10..8cfbb0d43fbd9 100644 --- a/src/gui/governance/governancelabelinfo.h +++ b/src/gui/governance/governancelabelinfo.h @@ -31,8 +31,6 @@ struct GovernanceLabelInfo QString _color; - QStringList _scopes; - Status _status = Status::UnknownStatus; }; diff --git a/src/gui/governance/governancelabelslistmodel.cpp b/src/gui/governance/governancelabelslistmodel.cpp index 0baf28ab6fba9..203cca598d94a 100644 --- a/src/gui/governance/governancelabelslistmodel.cpp +++ b/src/gui/governance/governancelabelslistmodel.cpp @@ -68,7 +68,6 @@ QVariant GovernanceLabelsListModel::data(const QModelIndex &index, int role) con result = tr("No label"); break; case LabelsListModelRoles::ColorRole: - case LabelsListModelRoles::ScopesRole: break; case LabelsListModelRoles::SelectedRole: result = false; @@ -92,9 +91,6 @@ QVariant GovernanceLabelsListModel::data(const QModelIndex &index, int role) con case LabelsListModelRoles::ColorRole: result = _data[index.row()]._color; break; - case LabelsListModelRoles::ScopesRole: - result = _data[index.row()]._scopes; - break; case LabelsListModelRoles::SelectedRole: result = _data[index.row()]._status == GovernanceLabelInfo::Status::Selected; break; @@ -113,7 +109,6 @@ QHash GovernanceLabelsListModel::roleNames() const {static_cast(LabelsListModelRoles::PriorityRole), "priority"_ba}, {static_cast(LabelsListModelRoles::DescriptionRole), "description"_ba}, {static_cast(LabelsListModelRoles::ColorRole), "color"_ba}, - {static_cast(LabelsListModelRoles::ScopesRole), "scopes"_ba}, {static_cast(LabelsListModelRoles::SelectedRole), "isSelected"_ba}, }; @@ -158,6 +153,11 @@ void GovernanceLabelsListModel::setAvailableLabelsJsonData(const QJsonDocument & { _availableLabelsReply = reply.object(); + if (!validState()) { + qCWarning(lcGovernanceLabelsListModel()) << "setting data in an invalid model state"; + return; + } + if (hasReceivedAllData()) { refreshModel(); } @@ -167,6 +167,11 @@ void GovernanceLabelsListModel::setExistingLabelsJsonData(const QJsonDocument &r { _existingLabelsReply = reply.object(); + if (!validState()) { + qCWarning(lcGovernanceLabelsListModel()) << "setting data in an invalid model state"; + return; + } + if (hasReceivedAllData()) { refreshModel(); } @@ -212,7 +217,7 @@ void GovernanceLabelsListModel::labelWasModified() void GovernanceLabelsListModel::emitRefreshData() { - if (_entityId.isEmpty() || _labelType == Governance::LabelType::InvalidLabelType) { + if (!validState()) { return; } @@ -248,21 +253,15 @@ bool GovernanceLabelsListModel::hasReceivedAllData() const return _availableLabelsReply.contains(u"ocs"_s) && _existingLabelsReply.contains(u"ocs"_s); } -void GovernanceLabelsListModel::refreshModel() +bool GovernanceLabelsListModel::validState() const { - const auto dataArray = readOcsReply(_availableLabelsReply).toArray(); - const auto dataReply = readOcsReply(_existingLabelsReply).toObject(); - - const auto convertToStringList = [] (const QJsonArray &scopesList) -> QStringList - { - auto result = QStringList{}; - - for (const auto &oneScope : scopesList) { - result << oneScope.toString(); - } + return !_entityId.isEmpty() && _labelType != Governance::LabelType::InvalidLabelType; +} - return result; - }; +void GovernanceLabelsListModel::refreshModel() +{ + const auto availableLabelsArray = readOcsReply(_availableLabelsReply).toArray(); + const auto existingLabelsObject = readOcsReply(_existingLabelsReply).toObject(); const auto parseExistingSingleLabel = [this] (const int rowIndex, const QJsonObject &oneLabel) -> void{ if (_data[rowIndex]._id == oneLabel[u"id"_s]) { @@ -280,39 +279,40 @@ void GovernanceLabelsListModel::refreshModel() beginResetModel(); _data.clear(); - for (const auto oneLabel : dataArray) { + for (const auto oneLabel : availableLabelsArray) { const auto rowIndex = _data.size(); 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()) + oneLabelObject.value(u"color"_s).toString() ); switch (_labelType) { case Governance::LabelType::Sensitivity: { - if (!dataReply.contains(u"sensitivity"_s)) { + qCDebug(lcGovernanceLabelsListModel()) << "checking existing sensitivity labels"; + if (!existingLabelsObject.contains(u"sensitivity"_s)) { qCWarning(lcGovernanceLabelsListModel()) << "missing sensitivity data in OCS reply"; break; } - const auto &sensitivityLabel = dataReply[u"sensitivity"_s].toObject(); + const auto &sensitivityLabel = existingLabelsObject[u"sensitivity"_s].toObject(); parseExistingSingleLabel(rowIndex, sensitivityLabel); break; } case Governance::LabelType::Retention: { - if (!dataReply.contains(u"retention"_s)) { + qCDebug(lcGovernanceLabelsListModel()) << "checking existing retention labels"; + if (!existingLabelsObject.contains(u"retention"_s)) { qCWarning(lcGovernanceLabelsListModel()) << "missing retention data in OCS reply"; break; } - const auto retentionLabels = dataReply[u"retention"_s].toArray(); + const auto retentionLabels = existingLabelsObject[u"retention"_s].toArray(); for (const auto &oneLabel : retentionLabels) { const auto &oneLabelObject = oneLabel.toObject(); @@ -322,12 +322,13 @@ void GovernanceLabelsListModel::refreshModel() } case Governance::LabelType::LegalHold: { - if (!dataReply.contains(u"hold"_s)) { + qCDebug(lcGovernanceLabelsListModel()) << "checking existing legal hold labels"; + if (!existingLabelsObject.contains(u"hold"_s)) { qCWarning(lcGovernanceLabelsListModel()) << "missing legal hold data in OCS reply"; break; } - const auto legalHoldLabels = dataReply[u"hold"_s].toArray(); + const auto legalHoldLabels = existingLabelsObject[u"hold"_s].toArray(); for (const auto &oneLabel : legalHoldLabels) { const auto &oneLabelObject = oneLabel.toObject(); diff --git a/src/gui/governance/governancelabelslistmodel.h b/src/gui/governance/governancelabelslistmodel.h index a78d93c487e83..f777deb48b8e9 100644 --- a/src/gui/governance/governancelabelslistmodel.h +++ b/src/gui/governance/governancelabelslistmodel.h @@ -35,7 +35,6 @@ class GovernanceLabelsListModel : public QAbstractListModel PriorityRole, DescriptionRole, ColorRole, - ScopesRole, SelectedRole, }; @@ -104,6 +103,8 @@ public Q_SLOTS: [[nodiscard]] bool hasReceivedAllData() const; + [[nodiscard]] bool validState() const; + void refreshModel(); Governance::LabelType _labelType = Governance::LabelType::InvalidLabelType; diff --git a/test/testgovernance.cpp b/test/testgovernance.cpp index c910ac72d713e..5d5b1f1e296da 100644 --- a/test/testgovernance.cpp +++ b/test/testgovernance.cpp @@ -316,6 +316,8 @@ private slots: { OCC::Logger::instance()->setLogFlush(true); OCC::Logger::instance()->setLogDebug(true); + OCC::Logger::instance()->setLogFile(QStringLiteral("-")); + OCC::Logger::instance()->addLogRule({ QStringLiteral("sync.httplogger=true") }); QStandardPaths::setTestModeEnabled(true); } @@ -549,9 +551,19 @@ private slots: "priority": 0, "description": "This is a long description", "color": "bf4040", - "scopes": [ - "FILES" - ] + "canAssign": "no", + "canRemove": "no", + "isAssigned": false + }, + { + "id": "91345129959310149", + "name": "High Sensitivity", + "priority": 1, + "description": "This is a long description for high sensitive label", + "color": "56cd40", + "canAssign": "no", + "canRemove": "no", + "isAssigned": false } ] } @@ -563,56 +575,65 @@ private slots: { "ocs": { "meta": { - "status": "string", - "statuscode": 0, - "message": "string", - "totalitems": "string", - "itemsperpage": "string" + "status": "ok", + "statuscode": 200, + "message": "OK" }, "data": { + "retention": [], "sensitivity": { "id": "91785883351310337", "name": "Test Sensitivity", "priority": 0, "description": "This is a long description", "color": "bf4040", - "canRemove": "yes", - "canAssign": "yes", + "canAssign": "no", + "canRemove": "no", "isAssigned": true - }, - "retention": [ - { - "id": "string", - "name": "string", - "priority": 0, - "description": "string", - "color": "string", - "canRemove": "yes", - "canAssign": "yes", - "isAssigned": true - } - ] + } } } } )json"_ba; const auto existingReplyData = QJsonDocument::fromJson(existingReplyJson); + myModel.setEntityId(u"123456"_s); + myModel.setLabelBehavior(GovernanceLabelsListModel::LabelBehavior::UniqueLabel); + myModel.setLabelType(Governance::LabelType::Sensitivity); + myModel.setAvailableLabelsJsonData(availableReplyData); QCOMPARE(myModel.rowCount(), 1); myModel.setExistingLabelsJsonData(existingReplyData); - QCOMPARE(myModel.rowCount(), 2); - 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}); - QCOMPARE(myModel.data(labelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::SelectedRole)).toBool(), true); + QCOMPARE(myModel.rowCount(), 3); + + const auto firstLabelIndex = myModel.index(0); + QVERIFY(firstLabelIndex.isValid()); + QCOMPARE(myModel.data(firstLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::IdRole)), u"91785883351310337"_s); + QCOMPARE(myModel.data(firstLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::NameRole)), u"Test Sensitivity"_s); + QCOMPARE(myModel.data(firstLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::PriorityRole)), 0); + QCOMPARE(myModel.data(firstLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::DescriptionRole)), u"This is a long description"_s); + QCOMPARE(myModel.data(firstLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::ColorRole)), u"bf4040"_s); + QCOMPARE(myModel.data(firstLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::SelectedRole)).toBool(), true); + + const auto secondLabelIndex = myModel.index(1); + QVERIFY(secondLabelIndex.isValid()); + QCOMPARE(myModel.data(secondLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::IdRole)), u"91345129959310149"_s); + QCOMPARE(myModel.data(secondLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::NameRole)), u"High Sensitivity"_s); + QCOMPARE(myModel.data(secondLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::PriorityRole)), 1); + QCOMPARE(myModel.data(secondLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::DescriptionRole)), u"This is a long description for high sensitive label"_s); + QCOMPARE(myModel.data(secondLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::ColorRole)), u"56cd40"_s); + QCOMPARE(myModel.data(secondLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::SelectedRole)).toBool(), false); + + const auto noneLabelIndex = myModel.index(2); + QVERIFY(noneLabelIndex.isValid()); + QCOMPARE(myModel.data(noneLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::IdRole)), -1); + QCOMPARE(myModel.data(noneLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::NameRole)), u"None"_s); + QCOMPARE(myModel.data(noneLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::PriorityRole)), -1); + QCOMPARE(myModel.data(noneLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::DescriptionRole)), u"No label"_s); + QCOMPARE(myModel.data(noneLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::ColorRole)), {}); + QCOMPARE(myModel.data(noneLabelIndex, static_cast(GovernanceLabelsListModel::LabelsListModelRoles::SelectedRole)).toBool(), false); } void testGovernanceLabelListModel_refreshData() From 6030027af24103de1786c5682643f03501a26d68 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Fri, 10 Jul 2026 15:46:53 +0200 Subject: [PATCH 26/26] feat(governance): deduplicate some code Signed-off-by: Matthieu Gallien --- src/gui/governance/applygovernancelabel.cpp | 16 ---------------- src/gui/governance/applygovernancelabel.h | 10 +--------- src/gui/governance/deletegovernancelabel.cpp | 16 ---------------- src/gui/governance/deletegovernancelabel.h | 10 +--------- .../governance/getavailablegovernancelabels.cpp | 16 ---------------- .../governance/getavailablegovernancelabels.h | 10 +--------- src/gui/governance/getgovernancelabels.cpp | 16 ---------------- src/gui/governance/getgovernancelabels.h | 10 +--------- src/gui/governance/governancenetworkjob.cpp | 16 ++++++++++++++++ src/gui/governance/governancenetworkjob.h | 10 ++++++++++ 10 files changed, 30 insertions(+), 100 deletions(-) diff --git a/src/gui/governance/applygovernancelabel.cpp b/src/gui/governance/applygovernancelabel.cpp index 3c3c5c1e5001c..88dc726d7896d 100644 --- a/src/gui/governance/applygovernancelabel.cpp +++ b/src/gui/governance/applygovernancelabel.cpp @@ -17,17 +17,6 @@ ApplyGovernanceLabel::ApplyGovernanceLabel(QObject *parent) { } -void ApplyGovernanceLabel::classBegin() -{ -} - -void ApplyGovernanceLabel::componentComplete() -{ - if (checkParameters()) { - start(); - } -} - void ApplyGovernanceLabel::start() { if (!checkParameters()) { @@ -61,9 +50,4 @@ QString ApplyGovernanceLabel::buildPath() const return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString(), labelTypeAsString(TypedGovernanceNetworkJob::Capitalization::UpCase), labelId()); } -void ApplyGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) -{ - Q_EMIT finished(reply); -} - } // namespace OCC diff --git a/src/gui/governance/applygovernancelabel.h b/src/gui/governance/applygovernancelabel.h index e955302c1a936..86a1e235bfe95 100644 --- a/src/gui/governance/applygovernancelabel.h +++ b/src/gui/governance/applygovernancelabel.h @@ -19,25 +19,17 @@ class ApplyGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob { Q_OBJECT QML_ELEMENT - Q_INTERFACES(QQmlParserStatus) public: explicit ApplyGovernanceLabel(QObject *parent = nullptr); - void classBegin() override; - - void componentComplete() override; + void start() override; public Q_SLOTS: - void start(); - void start(const QString &labelId); protected: [[nodiscard]] QString buildPath() const override; - -private Q_SLOTS: - void jobDone(QJsonDocument reply, int statusCode); }; } // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.cpp b/src/gui/governance/deletegovernancelabel.cpp index b56d633104a9e..56032e2a64ddb 100644 --- a/src/gui/governance/deletegovernancelabel.cpp +++ b/src/gui/governance/deletegovernancelabel.cpp @@ -17,17 +17,6 @@ DeleteGovernanceLabel::DeleteGovernanceLabel(QObject *parent) { } -void DeleteGovernanceLabel::classBegin() -{ -} - -void DeleteGovernanceLabel::componentComplete() -{ - if (checkParameters()) { - start(); - } -} - void DeleteGovernanceLabel::start() { if (!checkParameters()) { @@ -61,9 +50,4 @@ QString DeleteGovernanceLabel::buildPath() const return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/%5"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString(), labelTypeAsString(TypedGovernanceNetworkJob::Capitalization::UpCase), labelId()); } -void DeleteGovernanceLabel::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) -{ - Q_EMIT finished(reply); -} - } // namespace OCC diff --git a/src/gui/governance/deletegovernancelabel.h b/src/gui/governance/deletegovernancelabel.h index a19cb1612e0e6..4a300cc8bfbad 100644 --- a/src/gui/governance/deletegovernancelabel.h +++ b/src/gui/governance/deletegovernancelabel.h @@ -19,25 +19,17 @@ class DeleteGovernanceLabel : public OCC::TypedWithLabelIdGovernanceNetworkJob { Q_OBJECT QML_ELEMENT - Q_INTERFACES(QQmlParserStatus) public: explicit DeleteGovernanceLabel(QObject *parent = nullptr); - void classBegin() override; - - void componentComplete() override; + void start() override; public Q_SLOTS: - void start(); - void start(const QString &labelId); protected: [[nodiscard]] QString buildPath() const override; - -private Q_SLOTS: - void jobDone(QJsonDocument reply, int statusCode); }; } // namespace OCC diff --git a/src/gui/governance/getavailablegovernancelabels.cpp b/src/gui/governance/getavailablegovernancelabels.cpp index 8e681423a5f86..cf76f74f7ac0c 100644 --- a/src/gui/governance/getavailablegovernancelabels.cpp +++ b/src/gui/governance/getavailablegovernancelabels.cpp @@ -17,17 +17,6 @@ GetAvailableGovernanceLabels::GetAvailableGovernanceLabels(QObject *parent) { } -void GetAvailableGovernanceLabels::classBegin() -{ -} - -void GetAvailableGovernanceLabels::componentComplete() -{ - if (checkParameters()) { - start(); - } -} - void GetAvailableGovernanceLabels::start(Governance::LabelType labelType, const QString &entityId) { setLabelType(labelType); @@ -57,11 +46,6 @@ void GetAvailableGovernanceLabels::start() Q_EMIT started(); } -void GetAvailableGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) -{ - Q_EMIT finished(reply); -} - QString GetAvailableGovernanceLabels::buildPath() const { return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3/%4/available"_s.arg(apiVersionAsString(), entityTypeAsString(), entityId(), labelTypeAsString(TypedGovernanceNetworkJob::Capitalization::LowCase)); diff --git a/src/gui/governance/getavailablegovernancelabels.h b/src/gui/governance/getavailablegovernancelabels.h index b5db8559e3067..891f3ab3c7c39 100644 --- a/src/gui/governance/getavailablegovernancelabels.h +++ b/src/gui/governance/getavailablegovernancelabels.h @@ -19,27 +19,19 @@ class GetAvailableGovernanceLabels : public OCC::TypedGovernanceNetworkJob { Q_OBJECT QML_ELEMENT - Q_INTERFACES(QQmlParserStatus) public: explicit GetAvailableGovernanceLabels(QObject *parent = nullptr); - void classBegin() override; - - void componentComplete() override; + void start() override; 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); }; } // namespace OCC diff --git a/src/gui/governance/getgovernancelabels.cpp b/src/gui/governance/getgovernancelabels.cpp index 66f2f0700e622..ac95577a50f6c 100644 --- a/src/gui/governance/getgovernancelabels.cpp +++ b/src/gui/governance/getgovernancelabels.cpp @@ -17,17 +17,6 @@ GetGovernanceLabels::GetGovernanceLabels(QObject *parent) { } -void GetGovernanceLabels::classBegin() -{ -} - -void GetGovernanceLabels::componentComplete() -{ - if (checkParameters()) { - start(); - } -} - void GetGovernanceLabels::start() { if (!checkParameters()) { @@ -60,9 +49,4 @@ QString GetGovernanceLabels::buildPath() const return u"/ocs/v2.php/apps/governance/%1/labels/%2/%3"_s.arg(apiVersionAsString(), entityTypeAsString(), integerEntityIdAsString()); } -void GetGovernanceLabels::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) -{ - Q_EMIT finished(reply); -} - } // namespace OCC diff --git a/src/gui/governance/getgovernancelabels.h b/src/gui/governance/getgovernancelabels.h index 6c6d6f5ba664a..ba7eff01bcfea 100644 --- a/src/gui/governance/getgovernancelabels.h +++ b/src/gui/governance/getgovernancelabels.h @@ -19,25 +19,17 @@ class GetGovernanceLabels : public OCC::GovernanceNetworkJob { Q_OBJECT QML_ELEMENT - Q_INTERFACES(QQmlParserStatus) public: explicit GetGovernanceLabels(QObject *parent = nullptr); - void classBegin() override; - - void componentComplete() override; + void start() override; public Q_SLOTS: - void start(); - void start(const QString &entityId); protected: [[nodiscard]] QString buildPath() const override; - -private Q_SLOTS: - void jobDone(QJsonDocument reply, int statusCode); }; } // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.cpp b/src/gui/governance/governancenetworkjob.cpp index 9a04ab377f311..e43d2824c8932 100644 --- a/src/gui/governance/governancenetworkjob.cpp +++ b/src/gui/governance/governancenetworkjob.cpp @@ -171,4 +171,20 @@ void GovernanceNetworkJob::setAccount(AccountPtr newAccount) Q_EMIT accountChanged(); } +void GovernanceNetworkJob::classBegin() +{ +} + +void GovernanceNetworkJob::componentComplete() +{ + if (checkParameters()) { + start(); + } +} + +void GovernanceNetworkJob::jobDone(QJsonDocument reply, [[maybe_unused]] int statusCode) +{ + Q_EMIT finished(reply); +} + } // namespace OCC diff --git a/src/gui/governance/governancenetworkjob.h b/src/gui/governance/governancenetworkjob.h index 1e9200d0a4a5c..75c7ee24efec0 100644 --- a/src/gui/governance/governancenetworkjob.h +++ b/src/gui/governance/governancenetworkjob.h @@ -61,6 +61,10 @@ class GovernanceNetworkJob : public QObject, public QQmlParserStatus void setAccount(AccountPtr newAccount); + void classBegin() override; + + void componentComplete() override; + Q_SIGNALS: void apiVersionChanged(); @@ -78,6 +82,12 @@ class GovernanceNetworkJob : public QObject, public QQmlParserStatus void accountChanged(); +public Q_SLOTS: + virtual void start() = 0; + +protected Q_SLOTS: + void jobDone(QJsonDocument reply, int statusCode); + protected: void setOcsGovernanceJob(QPointer newJob) {