Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving UX by hiding editing buttons for readers of a project #3682

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
35 changes: 35 additions & 0 deletions app/activeproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ ActiveProject::ActiveProject( AppSettings &appSettings
, ActiveLayer &activeLayer
, LayersProxyModel &recordingLayerPM
, LocalProjectsManager &localProjectsManager
, MerginApi *merginApi
, QObject *parent ) :

QObject( parent )
, mAppSettings( appSettings )
, mActiveLayer( activeLayer )
, mRecordingLayerPM( recordingLayerPM )
, mLocalProjectsManager( localProjectsManager )
, mMerginApi( merginApi )
, mProjectLoadingLog( "" )
{
// we used to have our own QgsProject instance, but unfortunately few pieces of qgis_core
Expand Down Expand Up @@ -74,6 +76,17 @@ ActiveProject::ActiveProject( AppSettings &appSettings
setAutosyncEnabled( mAppSettings.autosyncAllowed() );

QObject::connect( &mAppSettings, &AppSettings::autosyncAllowedChanged, this, &ActiveProject::setAutosyncEnabled );

QObject::connect(
mMerginApi,
&MerginApi::projectMetadataRoleUpdated,
this, [this]( const QString & projectFullName, const QString & role )
{
if ( projectFullName == this->projectFullName() )
{
setProjectRole( role );
}
} );
}

ActiveProject::~ActiveProject() = default;
Expand Down Expand Up @@ -188,6 +201,7 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force )
updateRecordingLayers();
updateActiveLayer();
updateMapSettingsLayers();
updateUserRoleInActiveProject();

emit localProjectChanged( mLocalProject );
emit projectReloaded( mQgsProject );
Expand Down Expand Up @@ -553,3 +567,24 @@ bool ActiveProject::positionTrackingSupported() const

return mQgsProject->readBoolEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PositionTracking/Enabled" ), false );
}

QString ActiveProject::projectRole() const
{
return mProjectRole;
}

void ActiveProject::setProjectRole( const QString &role )
{
if ( mProjectRole != role )
{
mProjectRole = role;

emit projectRoleChanged();
}
}

void ActiveProject::updateUserRoleInActiveProject()
{
// update user's role each time a project is opened, following #3174
mMerginApi->updateProjectMetadataRole( projectFullName() );
}
19 changes: 19 additions & 0 deletions app/activeproject.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "localprojectsmanager.h"
#include "autosynccontroller.h"
#include "inputmapsettings.h"
#include "merginapi.h"

/**
* \brief The ActiveProject class can load a QGIS project and holds its data.
Expand All @@ -33,6 +34,7 @@ class ActiveProject: public QObject
Q_PROPERTY( QgsProject *qgsProject READ qgsProject NOTIFY qgsProjectChanged ) // QgsProject instance of active project, never changes
Q_PROPERTY( AutosyncController *autosyncController READ autosyncController NOTIFY autosyncControllerChanged )
Q_PROPERTY( InputMapSettings *mapSettings READ mapSettings WRITE setMapSettings NOTIFY mapSettingsChanged )
Q_PROPERTY( QString projectRole READ projectRole WRITE setProjectRole NOTIFY projectRoleChanged )

Q_PROPERTY( QString mapTheme READ mapTheme WRITE setMapTheme NOTIFY mapThemeChanged )
Q_PROPERTY( bool positionTrackingSupported READ positionTrackingSupported NOTIFY positionTrackingSupportedChanged )
Expand All @@ -43,6 +45,7 @@ class ActiveProject: public QObject
, ActiveLayer &activeLayer
, LayersProxyModel &recordingLayerPM
, LocalProjectsManager &localProjectsManager
, MerginApi *mMerginApi
, QObject *parent = nullptr );

virtual ~ActiveProject();
Expand Down Expand Up @@ -118,6 +121,18 @@ class ActiveProject: public QObject

bool positionTrackingSupported() const;

/**
* Returns role/permission level of current user for this project
*/
Q_INVOKABLE QString projectRole() const;

void setProjectRole( const QString &role );

/**
* Calls Mergin API to update current project’s role
*/
void updateUserRoleInActiveProject();

signals:
void qgsProjectChanged();
void localProjectChanged( LocalProject project );
Expand Down Expand Up @@ -145,6 +160,8 @@ class ActiveProject: public QObject
// Emited when the app (UI) should show tracking because there is a running tracking service
void startPositionTracking();

void projectRoleChanged();

public slots:
// Reloads project if current project path matches given path (its the same project)
bool reloadProject( QString projectDir );
Expand Down Expand Up @@ -182,10 +199,12 @@ class ActiveProject: public QObject
LayersProxyModel &mRecordingLayerPM;
LocalProjectsManager &mLocalProjectsManager;
InputMapSettings *mMapSettings = nullptr;
MerginApi *mMerginApi = nullptr;

std::unique_ptr<AutosyncController> mAutosyncController;

QString mProjectLoadingLog;
QString mProjectRole;

/**
* Reloads project.
Expand Down
2 changes: 1 addition & 1 deletion app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ int main( int argc, char *argv[] )
LayersProxyModel recordingLpm( &lm, LayerModelTypes::ActiveLayerSelection );

ActiveLayer al;
ActiveProject activeProject( as, al, recordingLpm, localProjectsManager );
ActiveProject activeProject( as, al, recordingLpm, localProjectsManager, ma.get() );
std::unique_ptr<VariablesManager> vm( new VariablesManager( ma.get() ) );
vm->registerInputExpressionFunctions();

Expand Down
4 changes: 2 additions & 2 deletions app/qml/form/MMFormPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Page {

footer: MMComponents.MMToolbar {

visible: !root.layerIsReadOnly
visible: !root.layerIsReadOnly && __activeProject.projectRole !== "reader"

ObjectModel {
id: readStateButtons
Expand Down Expand Up @@ -231,7 +231,7 @@ Page {
id: editGeometry
text: qsTr( "Edit geometry" )
iconSource: __style.editIcon
visible: root.layerIsSpatial
visible: root.layerIsSpatial && __activeProject.projectRole !== "reader"
onClicked: root.editGeometryRequested( root.controller.featureLayerPair )
}
}
Expand Down
2 changes: 1 addition & 1 deletion app/qml/form/MMPreviewDrawer.qml
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ Item {
property bool isHTMLType: root.controller.type === MM.AttributePreviewController.HTML
property bool isEmptyType: root.controller.type === MM.AttributePreviewController.Empty

property bool showEditButton: !root.layerIsReadOnly
property bool showEditButton: !root.layerIsReadOnly && __activeProject.projectRole !== "reader"
property bool showStakeoutButton: __inputUtils.isPointLayerFeature( controller.featureLayerPair )
property bool showButtons: showEditButton || showStakeoutButton

Expand Down
1 change: 1 addition & 0 deletions app/qml/form/components/MMFeaturesListPageDrawer.qml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Drawer {
}

text: qsTr( "Add feature" )
visible: __activeProject.projectRole !== "reader"

onClicked: root.buttonClicked()
}
Expand Down
2 changes: 1 addition & 1 deletion app/qml/layers/MMFeaturesListPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ MMComponents.MMPage {
anchors.bottom: parent.bottom
anchors.bottomMargin: root.hasToolbar ? __style.margin20 : ( __style.safeAreaBottom + __style.margin8 )

visible: __inputUtils.isNoGeometryLayer( root.selectedLayer ) && !root.layerIsReadOnly
visible: __inputUtils.isNoGeometryLayer( root.selectedLayer ) && !root.layerIsReadOnly && __activeProject.projectRole !== "reader"

text: qsTr("Add feature")

Expand Down
1 change: 1 addition & 0 deletions app/qml/main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ ApplicationWindow {
MMToolbarButton {
text: qsTr("Add")
iconSource: __style.addIcon
visible: __activeProject.projectRole !== "reader"
onClicked: {
if ( __recordingLayersModel.rowCount() > 0 ) {
stateManager.state = "map"
Expand Down
2 changes: 2 additions & 0 deletions app/qml/project/MMProjectController.qml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Item {
function setupProjectOpen( projectPath ) {
if ( projectPath === __activeProject.localProject.qgisProjectFilePath )
{
// update user's role in project ( in case user has changed )
__activeProject.updateUserRoleInActiveProject()
// just hide the panel - project already loaded
hidePanel()
}
Expand Down
6 changes: 3 additions & 3 deletions app/test/testactiveproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ void TestActiveProject::testProjectValidations()
ActiveLayer al;
LayersModel lm;
LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection );
ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() );
ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager(), mApi );

QSignalSpy spyReportIssues( &activeProject, &ActiveProject::reportIssue );
QSignalSpy spyErrorsFound( &activeProject, &ActiveProject::loadingErrorFound );
Expand All @@ -66,7 +66,7 @@ void TestActiveProject::testProjectLoadFailure()
ActiveLayer al;
LayersModel lm;
LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection );
ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() );
ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager(), mApi );

mApi->localProjectsManager().addLocalProject( projectdir, projectname );

Expand All @@ -88,7 +88,7 @@ void TestActiveProject::testPositionTrackingFlag()
ActiveLayer al;
LayersModel lm;
LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection );
ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() );
ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager(), mApi );

// project "planes" - tracking not enabled
QString projectDir = TestUtils::testDataDir() + "/planes/";
Expand Down
2 changes: 1 addition & 1 deletion app/test/testmerginapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2369,7 +2369,7 @@ void TestMerginApi::testAutosync()

MapThemesModel mtm; AppSettings as; ActiveLayer al;
LayersModel lm; LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection );
ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() );
ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager(), mApi );

mApi->localProjectsManager().addLocalProject( projectdir, projectname );

Expand Down
95 changes: 95 additions & 0 deletions core/merginapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3456,6 +3456,45 @@ bool MerginApi::writeData( const QByteArray &data, const QString &path )
return true;
}

bool MerginApi::updateCachedProjectRole( const QString &projectFullName, const QString &newRole )
{
LocalProject project = mLocalProjects.projectFromMerginName( projectFullName );
if ( !project.isValid() )
{
return false;
}

QString metadataPath = project.projectDir + "/" + sMetadataFile;

QFile file( metadataPath );
if ( !file.open( QIODevice::ReadOnly ) )
{
return false;
}

QByteArray data = file.readAll();
file.close();

QJsonDocument doc = QJsonDocument::fromJson( data );
if ( !doc.isObject() )
{
return false;
}

QJsonObject obj = doc.object();
obj["role"] = newRole;
doc.setObject( obj );

if ( !file.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
{
return false;
}

bool success = ( file.write( doc.toJson() ) != -1 );
file.close();

return success;
}

void MerginApi::createPathIfNotExists( const QString &filePath )
{
Expand Down Expand Up @@ -3952,3 +3991,59 @@ DownloadQueueItem::DownloadQueueItem( const QString &fp, qint64 s, int v, qint64
tempFileName = CoreUtils::uuidWithoutBraces( QUuid::createUuid() );
}

void MerginApi::updateProjectMetadataRole( const QString &projectFullName )
{
if ( projectFullName.isEmpty() )
{
return;
}

QString projectDir = mLocalProjects.projectFromMerginName( projectFullName ).projectDir;
MerginProjectMetadata cachedProjectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile );
QString cachedRole = cachedProjectMetadata.role;

QNetworkReply *reply = getProjectInfo( projectFullName );
if ( !reply )
{
emit projectMetadataRoleUpdated( projectFullName, cachedRole );
return;
}

reply->request().setAttribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ), projectFullName );
reply->request().setAttribute( static_cast<QNetworkRequest::Attribute>( AttrCachedRole ), cachedRole );
connect( reply, &QNetworkReply::finished, this, &MerginApi::updateProjectMetadataRoleReplyFinished );
}

void MerginApi::updateProjectMetadataRoleReplyFinished()
{
QNetworkReply *r = qobject_cast<QNetworkReply *>( sender() );
Q_ASSERT( r );

QString projectFullName = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrProjectFullName ) ).toString();
QString cachedRole = r->request().attribute( static_cast<QNetworkRequest::Attribute>( AttrCachedRole ) ).toString();

if ( r->error() == QNetworkReply::NoError )
{
QByteArray data = r->readAll();
MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data );
QString role = serverProject.role;

if ( role != cachedRole )
{
if ( updateCachedProjectRole( projectFullName, role ) )
{
emit projectMetadataRoleUpdated( projectFullName, role );
}
else
{
CoreUtils::log( "metadata", QString( "Failed to update cached role for project %1" ).arg( projectFullName ) );
}
}
}
else
{
emit projectMetadataRoleUpdated( projectFullName, cachedRole );
}

r->deleteLater();
}
11 changes: 11 additions & 0 deletions core/merginapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,11 @@ class MerginApi: public QObject
*/
bool apiSupportsWorkspaces();

/**
* Updates project metadata role by fetching latest information from server.
*/
Q_INVOKABLE void updateProjectMetadataRole( const QString &projectFullName );

signals:
void apiSupportsSubscriptionsChanged();
void supportsSelectiveSyncChanged();
Expand Down Expand Up @@ -652,6 +657,7 @@ class MerginApi: public QObject
void apiSupportsWorkspacesChanged();

void serverWasUpgraded();
void projectMetadataRoleUpdated( const QString &projectFullName, const QString &role );

private slots:
void listProjectsReplyFinished( QString requestId );
Expand Down Expand Up @@ -791,6 +797,10 @@ class MerginApi: public QObject

bool projectFileHasBeenUpdated( const ProjectDiff &diff );

void updateProjectMetadataRoleReplyFinished();

bool updateCachedProjectRole( const QString &projectFullName, const QString &newRole );

QNetworkAccessManager mManager;
QString mApiRoot;
LocalProjectsManager &mLocalProjects;
Expand All @@ -807,6 +817,7 @@ class MerginApi: public QObject
AttrTempFileName = QNetworkRequest::User + 1,
AttrWorkspaceName = QNetworkRequest::User + 2,
AttrAcceptFlag = QNetworkRequest::User + 3,
AttrCachedRole = QNetworkRequest::User + 4
};

Transactions mTransactionalStatus; //projectFullname -> transactionStatus
Expand Down
1 change: 1 addition & 0 deletions core/merginprojectmetadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ MerginProjectMetadata MerginProjectMetadata::fromJson( const QByteArray &data )

project.name = docObj.value( QStringLiteral( "name" ) ).toString();
project.projectNamespace = docObj.value( QStringLiteral( "namespace" ) ).toString();
project.role = docObj.value( QStringLiteral( "role" ) ).toString();

QString versionStr = docObj.value( QStringLiteral( "version" ) ).toString();
if ( versionStr.isEmpty() )
Expand Down
2 changes: 2 additions & 0 deletions core/merginprojectmetadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ struct MerginProjectMetadata
{
QString name;
QString projectNamespace;
QString role;
QList<QString> writersnames;
int version = -1;
QList<MerginFile> files;
QString projectId; //!< unique project ID (only available in API that supports project IDs)
Expand Down
Loading