Page MenuHomePhabricator

Add LeaveEditorDialog component to AbstractContent: ask for exit confirmation when editor tries to leave but there are unsaved changes
Closed, ResolvedPublic

Description

Description

Generally MW links are never target=_blank, so they will often try to open in the same tab. This means that when accidentally clicked, editors might lose any unsaved changes.

In Wikifunctions, we track the page-wide dirty state (are there any unsaved changes) and, when clicking an outgoing link or trying to close or exit the window, we show a Leave dialog:

Screenshot from 2026-02-11 12-38-41.png (664×287 px, 24 KB)

We can use the same dialog, and the state is already been tracked, so store.isDirty can be used the same way in AW than in Wikifunctions.

Desired behavior/Acceptance criteria

  • While editing Abstract Content and there are unsaved changes, any exit attempt will be followed by an explicit prompt to the user that prevents accidental loss of work

Completion checklist

Event Timeline

DSmit-WMF changed the task status from Open to In Progress.Feb 11 2026, 3:18 PM
DSmit-WMF claimed this task.

When adding the LeaveEditorDialog I realized both files ares starting to look very similar when it comes to publish logic.
So I did a compare + recommendation, here are the results:

Investigation: Making Publish.vue and AbstractContent.vue more DRY

Overlap

ConcernPublish.vueAbstractContent.vueMatch
Leave editor dialogLines 36-40, 88-89, 114-116, 191-286Lines 31-36, 154-235~95%
Publish dialogLines 28-34, 90, 107-109, 124-139, 298-321Lines 25-30, 99-101, 107-118, 128-144, 150Same structure
handleClickAway191-220171-188Identical
handleUnload235-240196-201Same (different dirty source)
leaveTo250-269209-222Same structure; Publish adds event logging
addListeners / removeListeners275-286225-235Identical

Differences

AspectPublish.vueAbstractContent.vue
LayoutSidebar widget with Cancel + PublishContent area with only Publish in header
isDirty sourceFrom props.isDirtyFrom store.isDirty
Cancel buttonHas handleCancel + Cancel buttonNone
On leaveCalls submitInteraction('cancel', ...)Plain navigation only
When listeners activeAlways on mountOnly when props.edit
ValidationvalidateZObject, raisePublishWarningsvalidateAbstractWikiContent
SubmitsubmitZObjectsubmitAbstractWikiContent
Success callbackZObject view URLAbstract title URL
Publish button disabled logic!isDirty && !revertToEdit!isDirty

Refactoring Options

Option 1: useLeaveEditorConfirmation composable (recommended)

Extract the leave-editor logic (~80 duplicated lines) into a composable at composables/useLeaveEditorConfirmation.js.

API:

useLeaveEditorConfirmation({
  isDirty, // Ref<boolean> or () => boolean
  onBeforeLeave, // optional: (targetUrl) => void - e.g. for event logging
})

Option 2: Single configurable EditorActionsWidget (alternative to composable)

Instead of extracting a composable, extend the existing Publish widget into a single EditorActionsWidget (or PublishActionsWidget) that handles both use cases via props.

Props:

PropTypeDefaultPurpose
showCancelButtonBooleantrueShow/hide Cancel button
isDirtyBoolean(required)Enables Publish, triggers leave confirmation
isPublishEnabledBooleanisDirtyExtra condition (e.g. revertToEdit)
submitActionFunction(required)({ summary }) => Promise
successCallbackFunction(required)(response) => void
getCancelUrlFunctionundefined() => string when Cancel exists
beforePublishFunctionundefinedValidation/warnings before opening dialog
onBeforeLeaveFunctionundefinedCalled before nav (e.g. event logging)
shouldListenForExitBooleantrueWhether to add click/beforeunload listeners

Usage:

Publish.vue / Default view (ZObject editor):

<wl-editor-actions-widget
  :show-cancel-button="true"
  :is-dirty="isDirty"
  :get-cancel-url="getCancelUrl"
  :before-publish="raisePublishWarnings"
  :on-before-leave="logCancel"
  ...
/>

AbstractContent.vue:

<wl-editor-actions-widget
  :show-cancel-button="false"
  :is-dirty="isDirty"
  :submit-action="submitAction"
  :success-callback="successCallback"
  :should-listen-for-exit="edit"
/>

Pros: Single component, shared logic, no composable. Cons: Prop surface can grow; AbstractContent layout may need to change to host the widget in header-action.

Recommendation

  1. Implement useLeaveEditorConfirmation and refactor both components to use it
  2. Optionally add findNavigationLink for link-detection reuse
// utils/navigationUtils.js or similar
function findNavigationLink( clickTarget ) {
    let target = clickTarget;
    while ( target && target.tagName !== 'A' ) {
        target = target.parentNode;
        if ( !target ) return null;
    }
    if (
        !target?.href ||
        target.target === '_blank' ||
        urlUtils.isLinkCurrentPath( target.href ) ||
        target.role === 'button'
    ) {
        return null;
    }
    return target;  // the <a> element, or null
}

Why the composable

  1. Matches the duplication – The leave-editor logic is ~95% identical between the two components. The publish flow differs more; unifying those would be a larger refactor.
  2. Lower risk – No structural changes. Publish.vue and AbstractContent.vue keep their current templates; we only move shared logic into a composable.

Thanks. What an AI doesn't take into account is that these components are also used differently in About widget, and that this is part of an MVP.

I'd say lets take these recommendations with caution and not refactor unnecessarily.

Change #1238783 had a related patch set uploaded (by Daphne Smit; author: Daphne Smit):

[mediawiki/extensions/WikiLambda@master] LeaveEditorDialog: add composable and also use in AbstractContent

https://gerrit.wikimedia.org/r/1238783

Yeah it did a quick analysis and i agree 95% is duplicate. I made a very very simple hook to reuse for publish and abstract. Works great

Change #1238783 merged by jenkins-bot:

[mediawiki/extensions/WikiLambda@master] LeaveEditorDialog: add composable and also use in AbstractContent

https://gerrit.wikimedia.org/r/1238783

Jdforrester-WMF triaged this task as Medium priority.