From 7fd510e840371d5e80e99bcd0b3e97deabed2e25 Mon Sep 17 00:00:00 2001 From: emilhammarstedtst <89985309+emilhammarstedtst@users.noreply.github.com> Date: Thu, 23 Feb 2023 20:58:11 +0100 Subject: [PATCH] Save changes before closing interaction (#12166) - Question to save all changes, cancel or discard all changes before closing - Display what is not yet saved. - Reorder options and reword the question and options to be clear of the outcome of each action - Implemented using ConfirmDialogSave extending ConfirmDialog Contributed by STMicroelectronics Signed-off-by: Emil Hammarstedt --- .../browser/common-frontend-contribution.ts | 12 +++- packages/core/src/browser/dialogs.ts | 72 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 909d73490cc1f..0253b43852b0b 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -56,7 +56,7 @@ import { QuickInputService, QuickPickItem, QuickPickItemOrSeparator } from './qu import { AsyncLocalizationProvider } from '../common/i18n/localization'; import { nls } from '../common/nls'; import { CurrentWidgetCommandAdapter } from './shell/current-widget-command-adapter'; -import { ConfirmDialog, confirmExit, Dialog } from './dialogs'; +import { ConfirmDialog, confirmExitWithOrWithoutSaving, Dialog } from './dialogs'; import { WindowService } from './window/window-service'; import { FrontendApplicationConfigProvider } from './frontend-application-config-provider'; import { DecorationStyle } from './decoration-style'; @@ -1144,13 +1144,19 @@ export class CommonFrontendContribution implements FrontendApplicationContributi onWillStop(): OnWillStopAction | undefined { try { if (this.shouldPreventClose || this.shell.canSaveAll()) { - return { reason: 'Dirty editors present', action: () => confirmExit() }; + const captionsToSave = this.unsavedTabsCaptions(); + + return { reason: 'Dirty editors present', action: async () => confirmExitWithOrWithoutSaving(captionsToSave, async () => this.shell.saveAll()) }; } } finally { this.shouldPreventClose = false; } } - + protected unsavedTabsCaptions(): string[] { + return this.shell.widgets + .filter(widget => this.saveResourceService.canSave(widget)) + .map(widget => widget.title.label); + } protected async configureDisplayLanguage(): Promise { const languageInfo = await this.languageQuickPickService.pickDisplayLanguage(); if (languageInfo && !nls.isSelectedLocale(languageInfo.languageId) && await this.confirmRestart( diff --git a/packages/core/src/browser/dialogs.ts b/packages/core/src/browser/dialogs.ts index 885055fe18e11..0a5a4786bc8de 100644 --- a/packages/core/src/browser/dialogs.ts +++ b/packages/core/src/browser/dialogs.ts @@ -401,6 +401,78 @@ export async function confirmExit(): Promise { return safeToExit === true; } +export class ConfirmSaveDialogProps extends ConfirmDialogProps { + readonly save: string; + performSave: () => Promise; +} + +export class ConfirmSaveDialog extends ConfirmDialog { + + protected saveButton: HTMLButtonElement | undefined; + constructor( + @inject(ConfirmSaveDialogProps) protected override readonly props: ConfirmSaveDialogProps + ) { + super(props); + this.contentNode.appendChild(this.createMessageNode(this.props.msg)); + // reorder buttons + this.controlPanel.childNodes.forEach(child => this.controlPanel.removeChild(child)); + [this.acceptButton, this.closeButton].forEach(child => { + if (typeof child !== 'undefined') { + this.controlPanel.appendChild(child); + } + }); + this.appendSaveButton(props.save).addEventListener('click', async () => { + await props.performSave(); + this.acceptButton?.click(); + }); + } + + protected appendSaveButton(text: string = Dialog.OK): HTMLButtonElement { + this.saveButton = this.createButton(text); + this.controlPanel.appendChild(this.saveButton); + this.saveButton.classList.add('main'); + return this.saveButton; + } + + protected override onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + if (this.saveButton) { + this.saveButton.focus(); + } + } + +} + +export async function confirmExitWithOrWithoutSaving(captionsToSave: string[], performSave: () => Promise): Promise { + const div: HTMLElement = document.createElement('div'); + div.innerText = nls.localizeByDefault("Your changes will be lost if you don't save them."); + + if (captionsToSave.length > 0) { + const span = document.createElement('span'); + span.appendChild(document.createElement('br')); + captionsToSave.forEach(cap => { + const b = document.createElement('b'); + b.innerText = cap; + span.appendChild(b); + span.appendChild(document.createElement('br')); + }); + span.appendChild(document.createElement('br')); + div.appendChild(span); + const safeToExit = await new ConfirmSaveDialog({ + title: nls.localizeByDefault('Do you want to save the changes to the following {0} files?', captionsToSave.length), + msg: div, + ok: nls.localizeByDefault("Don't Save"), + save: nls.localizeByDefault('Save All'), + cancel: Dialog.CANCEL, + performSave: performSave + }).open(); + return safeToExit === true; + } else { + // fallback if not passed with an empty caption-list. + return confirmExit(); + } + +} @injectable() export class SingleTextInputDialogProps extends DialogProps { readonly confirmButtonLabel?: string;