Skip to content

Feature/feature drafts - #4652

Open
xkello wants to merge 4 commits into
masterfrom
feature/feature-drafts
Open

xkello wants to merge 4 commits into
masterfrom
feature/feature-drafts

Conversation

@xkello

@xkello xkello commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Adds automatic recovery of in-progress feature edits. If the app closes unexpectedly while you're editing a feature - a crash, an incoming call, a dead battery - your unsaved geometry and attribute changes are no longer lost. On the next launch you're offered the chance to resume exactly where you left off, or discard the changes.

Fixes: #4585

What changed

  • New FeatureDraftStorage / FeatureDraftController (app/drafts/) persist an in-progress edit (geometry + attributes) as a JSON blob via QSettings, debounced by ~1s so it isn't written on every keystroke/vertex.
  • AttributeController and RecordingMapTool write a draft whenever attributes or geometry are edited, and clear it once the feature is saved, deleted, or the edit is cancelled.
  • Before a draft is offered, it's validated: not older than 10 days, the layer/schema still matches, and (for edits) the feature still exists.
  • A new notification, plus resume/discard drawers, let the user act on a pending draft.
  • New banners on the Layers list and Features list screens surface a draft on the affected layer, reusing the existing "active filters" banner styling.
  • ActiveProject exposes the draft controller to QML.

Behaviour

  • Editing a feature's attributes or geometry saves a draft in the background about a second after the last change.
  • If the app is closed and reopened with a pending draft, a notification appears: "You have unsaved changes. Tap here to open them."
  • Tapping it - or trying to start a new "Add"/"Edit" while a draft exists - opens a drawer offering to Resume or Discard.
  • Resuming puts you back exactly where you left off: mid geometry capture, or on the form with the in-progress attributes.
  • Saving, deleting, or cancelling an edit normally clears its draft; a draft is silently discarded instead of offered if its layer was removed, its schema changed, its feature no longer exists, or it's over 10 days old.
Screen_Recording_20260818-082140_One.UI.Home.mp4
Screen_Recording_20260818-082018_One.UI.Home.mp4
Screen_Recording_20260818-082039.mp4

TLDR @Withalion

Feature edits are now drafted automatically so a crash, call, or dead battery doesn't cost you your unsaved work - surfaced via a notification and resume/discard drawers.

@xkello
xkello requested a review from Withalion August 18, 2026 06:34
@github-actions

github-actions Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 35886137946

Coverage increased (+0.4%) to 59.91%

Details

  • Coverage increased (+0.4%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 607 coverage regressions across 8 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

607 previously-covered lines in 8 files lost coverage.

File Lines Losing Coverage Coverage
mm/app/maptools/recordingmaptool.cpp 216 70.63%
mm/app/attributes/attributecontroller.cpp 193 76.21%
mm/app/main.cpp 99 36.29%
mm/app/activeproject.cpp 67 70.52%
mm/app/notificationmodel.cpp 21 48.45%
mm/app/maptools/recordingmaptool.h 4 50.0%
mm/core/merginuserinfo.cpp 4 77.12%
mm/app/notificationmodel.h 3 50.0%

Coverage Stats

Coverage Status
Relevant Lines: 16014
Covered Lines: 9594
Line Coverage: 59.91%
Coverage Strength: 92.8 hits per line

💛 - Coveralls

@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build ❌ Build failed or not found. #7166
linux Build 📬 Mergin Maps 71921 x86_64 Expires: 16/11/2026 #7192
win64 Build 📬 Mergin Maps 63681 win64 Expires: 16/11/2026 #6368
Android Build 📬 Mergin Maps 847751 APK [arm64-v8a] Expires: 16/11/2026 #8477
📬 Mergin Maps 847751 APK [arm64-v8a] Google Play Store #8477
Android Build 📬 Mergin Maps 847711 APK [armeabi-v7a] Expires: 16/11/2026 #8477
📬 Mergin Maps 847711 APK [armeabi-v7a] Google Play Store #8477
iOS Build 📬 Build number: 26.08.941811 #9418

@Withalion Withalion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job, it looks very promising! There are some trivial things, a bit more refactoring is needed, but mainly unit tests.

UI/UX

  • It would be nice to navigate to the geometry you will be recording further after you open the draft
  • After user is done recording geometry and doesn't fill out anything in the form yet, application crashes. We should open the form right away and not the geometry recording
  • When a draft is available and an existing feature is clicked to edit the warning drawer opens and closes right away
  • When a draft is available and "add" button is clicked, the drawer opens correctly, but discarding draft doesn't start recording mode, but stays in "view" mode of map

Comment thread app/notificationmodel.h Outdated
Comment thread app/qml/dialogs/MMDiscardDraftDialog.qml
Comment thread app/qml/dialogs/MMDiscardDraftDialog.qml Outdated
Comment thread app/qml/dialogs/MMDiscardDraftDialog.qml Outdated
Comment thread app/qml/dialogs/MMResumeDraftDialog.qml Outdated
Comment thread app/drafts/featuredraftcontroller.cpp Outdated
Comment thread app/drafts/featuredraftstorage.h Outdated
Comment thread app/drafts/featuredraftstorage.cpp Outdated
Comment thread app/drafts/featuredraftstorage.cpp Outdated
Comment thread app/drafts/featuredraftstorage.h

@Withalion Withalion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job, it looks very promising! There are some trivial things, a bit more refactoring is needed, but mainly unit tests.

UI/UX

  • It would be nice to navigate to the geometry you will be recording further after you open the draft
  • After user is done recording geometry and doesn't fill out anything in the form yet, application crashes. We should open the form right away and not the geometry recording
  • When a draft is available and an existing feature is clicked to edit the warning drawer opens and closes right away
  • When a draft is available and "add" button is clicked, the drawer opens correctly, but discarding draft doesn't start recording mode, but stays in "view" mode of map

@xkello
xkello force-pushed the feature/feature-drafts branch from b242e2a to d02dcfb Compare September 23, 2026 16:00
@xkello
xkello requested a review from Withalion September 23, 2026 16:05
@github-actions

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build 📬 Mergin Maps 73291 dmg Expires: 22/12/2026 #7329
linux Build 📬 Mergin Maps 73551 x86_64 Expires: 22/12/2026 #7355
win64 Build 📬 Mergin Maps 65311 win64 Expires: 22/12/2026 #6531
Android Build 📬 Mergin Maps 864151 APK [arm64-v8a] Expires: 22/12/2026 #8641
📬 QR code Google Play Store #8641
Android Build 📬 Mergin Maps 864111 APK [armeabi-v7a] Expires: 22/12/2026 #8641
📬 QR code Google Play Store #8641
iOS Build 📬 Build number: 26.09.958111 #9581

@tomasMizera tomasMizera left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! :)

When testing, let's try it out on some low-end devices to see if the 1 second interval is not causing too much slowness in UI. If so, we might need to move the saving part to another thread.

In general, this is going in a very good direction, I believe we will have this ready for testing soon.

// geometry edits round-trip back into this same setter (via the live QML
// binding once the geometry-editing map tool hands the feature back) - that
// must not wipe attribute changes already tracked for this same feature
bool isSameFeature = !hasLayerChanged && mFeatureLayerPair.feature().id() == pair.feature().id();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? Can't we simply store draft immediately? What is the problem with it?

{
mTouchedFieldIndices.clear();

// draft immediately so a crash before the first keystroke still resumes into the form

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually do that here :)

Comment on lines +690 to +691
// only touched fields are drafted - an untouched one falls back to its
// default value expression on resume rather than a stale recorded value

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer if we stored everything, not just selected fields. Imagine there are automatic field values that are changed when a different field changes. They would not get stored. Let's instead try storing everything and when opening the feature again, do not evaluate anything, just present what we stored.

Comment on lines +48 to +51
connect( this, &RecordingMapTool::recordedGeometryChanged, this, [ this ]()
{
mDraftSaveTimer->start();
} );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signal sounds good to me too for now.

mActiveLayer->endEditCommand();

mRecordedGeometry = geometry;
mActiveLayer->beginEditCommand( QStringLiteral( "Resume feature" ) );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we need edit command for resuming. It also does not have any end command associated.

QTimer mDraftSaveTimer; // debounces saveDraft()

//! Indices of fields the user has actually changed this session, used for drafting
QSet<int> mTouchedFieldIndices;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not need this. Simply store everything whenever an attribute is changed


void FeatureDraftController::checkForDraft()
{
const QString projectId = QgsProject::instance()->homePath();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one's not resolved and I am +1 to use project IDs instead of homepath.


// push into the layer too, so the map shows the resumed shape right away
// instead of the stale committed one until the next vertex edit
layer->startEditing();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this action may fail

setDraft( true, layer, toQmlStage( draft.stage ), draft.isExistingFeature(), featureTitle );
}

FeatureLayerPair FeatureDraftController::resumeDraft()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe loadDraft would better say what this method is doing

featureTitle: __activeProject.featureDraftController.draftFeatureTitle
layerName: __activeProject.featureDraftController.draftLayerName

onResumeClicked: () => root.resumeDraft()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we ignoring the returned feature? We need to open form of the drafted feature, the one that is currently open might not be the one..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resume unsaved feature edits after unexpected app closure

3 participants